// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.build.lib.rules.objc;

import static com.google.devtools.build.lib.rules.objc.ObjcProvider.ASSET_CATALOG;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_FILE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.DYNAMIC_FRAMEWORK_FILE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag.USES_SWIFT;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LIBRARY;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.MERGE_ZIP;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.MULTI_ARCH_LINKED_BINARIES;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.NESTED_BUNDLE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.ROOT_MERGE_ZIP;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.STORYBOARD;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.STRINGS;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCDATAMODEL;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XIB;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.BundlingRule.FAMILIES_ATTR;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.BundlingRule.INFOPLIST_ATTR;

import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
import com.google.devtools.build.lib.rules.apple.DottedVersion;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Contains information regarding the creation of an iOS bundle.
 */
@Immutable
final class Bundling {

  /**
   * Names of top-level directories in dynamic frameworks (i.e. directly under the
   * {@code *.framework} directory) that should not be copied into the final bundle.
   */
  private static final ImmutableSet<String> STRIP_FRAMEWORK_DIRS =
      ImmutableSet.of("Headers", "PrivateHeaders", "Modules");

  static final class Builder {
    private String name;
    private String bundleDirFormat;
    private ImmutableList.Builder<BundleableFile> bundleFilesBuilder = ImmutableList.builder();
    private ObjcProvider objcProvider;
    private NestedSetBuilder<Artifact> infoplistInputs = NestedSetBuilder.stableOrder();
    private Artifact automaticEntriesInfoplistInput;
    private IntermediateArtifacts intermediateArtifacts;
    private String primaryBundleId;
    private String fallbackBundleId;
    private String architecture;
    private DottedVersion minimumOsVersion;
    private ImmutableSet<TargetDeviceFamily> families;
    private String artifactPrefix;
    @Nullable private String executableName;

    public Builder setName(String name) {
      this.name = name;
      return this;
    }

    /** Sets the name of the bundle's executable. */
    public Builder setExecutableName(String executableName) {
      this.executableName = executableName;
      return this;
    }

    /**
     * Sets the CPU architecture this bundling was constructed for. Legal value are any that may be
     * set on {@link AppleConfiguration#getIosCpu()}.
     */
    public Builder setArchitecture(String architecture) {
      this.architecture = architecture;
      return this;
    }

    public Builder setBundleDirFormat(String bundleDirFormat) {
      this.bundleDirFormat = bundleDirFormat;
      return this;
    }

    public Builder addExtraBundleFiles(ImmutableList<BundleableFile> extraBundleFiles) {
      this.bundleFilesBuilder.addAll(extraBundleFiles);
      return this;
    }

    public Builder setObjcProvider(ObjcProvider objcProvider) {
      this.objcProvider = objcProvider;
      return this;
    }

    /**
     * Adds an artifact representing an {@code Info.plist} as an input to this bundle's
     * {@code Info.plist} (which is merged from any such added plists plus the generated
     * automatic entries plist).
     */
    public Builder addInfoplistInput(Artifact infoplist) {
      this.infoplistInputs.add(infoplist);
      return this;
    }

    /**
     * Adds the given list of artifacts representing {@code Info.plist}s that are to be merged into
     * this bundle's {@code Info.plist}.
     */
    public Builder addInfoplistInputs(Iterable<Artifact> infoplists) {
      this.infoplistInputs.addAll(infoplists);
      return this;
    }

    /**
     * Adds an artifact representing an {@code Info.plist} that contains automatic entries
     * generated by xcode.
     */
    public Builder setAutomaticEntriesInfoplistInput(Artifact automaticEntriesInfoplist) {
      this.automaticEntriesInfoplistInput = automaticEntriesInfoplist;
      return this;
    }

    /**
     * Adds any info plists specified in the given rule's {@code infoplist}  or {@code infoplists}
     * attribute as well as from its {@code options} as inputs to this bundle's {@code Info.plist}
     * (which is merged from any such added plists plus some additional information).
     */
    public Builder addInfoplistInputFromRule(RuleContext ruleContext) {
      Artifact infoplist =
          ruleContext.getPrerequisiteArtifact(INFOPLIST_ATTR, Mode.TARGET);
      if (infoplist != null) {
        infoplistInputs.add(infoplist);
      }

      Iterable<Artifact> infoplists =
          ruleContext.getPrerequisiteArtifacts("infoplists", Mode.TARGET).list();
      if (infoplists != null) {
        infoplistInputs.addAll(infoplists);
      }

      return this;
    }

    public Builder setIntermediateArtifacts(IntermediateArtifacts intermediateArtifacts) {
      this.intermediateArtifacts = intermediateArtifacts;
      return this;
    }

    public Builder setPrimaryBundleId(String primaryId) {
      this.primaryBundleId = primaryId;
      return this;
    }

    public Builder setFallbackBundleId(String fallbackId) {
      this.fallbackBundleId = fallbackId;
      return this;
    }

    /**
     * Sets the minimum OS version for this bundle which will be used when constructing the bundle's
     * plist.
     */
    public Builder setMinimumOsVersion(DottedVersion minimumOsVersion) {
      this.minimumOsVersion = minimumOsVersion;
      return this;
    }

    public Builder setTargetDeviceFamilies(ImmutableSet<TargetDeviceFamily> families) {
      this.families = families;
      return this;
    }

    public Builder setArtifactPrefix(String artifactPrefix) {
      this.artifactPrefix = artifactPrefix;
      return this;
    }

    private static NestedSet<Artifact> nestedBundleContentArtifacts(Iterable<Bundling> bundles) {
      NestedSetBuilder<Artifact> artifacts = NestedSetBuilder.stableOrder();
      for (Bundling bundle : bundles) {
        artifacts.addTransitive(bundle.getBundleContentArtifacts());
      }
      return artifacts.build();
    }

    private NestedSet<Artifact> mergeZips(Optional<Artifact> actoolzipOutput) {
      NestedSetBuilder<Artifact> mergeZipBuilder =
          NestedSetBuilder.<Artifact>stableOrder()
              .addAll(actoolzipOutput.asSet())
              .addAll(
                  Xcdatamodel.outputZips(
                      Xcdatamodels.xcdatamodels(
                          intermediateArtifacts, objcProvider.get(XCDATAMODEL))))
              .addTransitive(objcProvider.get(MERGE_ZIP));
      for (Artifact xibFile : objcProvider.get(XIB)) {
        mergeZipBuilder.add(intermediateArtifacts.compiledXibFileZip(xibFile));
      }
      for (Artifact storyboard : objcProvider.get(STORYBOARD)) {
        mergeZipBuilder.add(intermediateArtifacts.compiledStoryboardZip(storyboard));
      }

      if (objcProvider.is(USES_SWIFT)) {
        mergeZipBuilder.add(intermediateArtifacts.swiftFrameworksFileZip());
      }

      return mergeZipBuilder.build();
    }

    private NestedSet<Artifact> rootMergeZips() {
      NestedSetBuilder<Artifact> rootMergeZipsBuilder =
          NestedSetBuilder.<Artifact>stableOrder().addTransitive(objcProvider.get(ROOT_MERGE_ZIP));

      if (objcProvider.is(USES_SWIFT)) {
        rootMergeZipsBuilder.add(intermediateArtifacts.swiftSupportZip());
      }

      return rootMergeZipsBuilder.build();
    }

    private NestedSet<Artifact> bundleInfoplistInputs() {
      if (objcProvider.hasAssetCatalogs()) {
        infoplistInputs.add(intermediateArtifacts.actoolPartialInfoplist());
      }
      return infoplistInputs.build();
    }

    private Optional<Artifact> bundleInfoplist(NestedSet<Artifact> bundleInfoplistInputs) {
      if (bundleInfoplistInputs.isEmpty()) {
        return Optional.absent();
      }
      if (needsToMerge(bundleInfoplistInputs, primaryBundleId, fallbackBundleId)) {
        return Optional.of(intermediateArtifacts.mergedInfoplist());
      }
      return Optional.of(Iterables.getOnlyElement(bundleInfoplistInputs));
    }

    private Optional<Artifact> combinedArchitectureBinary() {
      if (!Iterables.isEmpty(objcProvider.get(MULTI_ARCH_LINKED_BINARIES))) {
        return Optional.of(Iterables.getOnlyElement(objcProvider.get(MULTI_ARCH_LINKED_BINARIES)));
      } else if (!Iterables.isEmpty(objcProvider.get(LIBRARY))
          || !Iterables.isEmpty(objcProvider.get(IMPORTED_LIBRARY))) {
        return Optional.of(intermediateArtifacts.combinedArchitectureBinary());
      }
      return Optional.absent();
    }

    private Optional<Artifact> actoolzipOutput() {
      Optional<Artifact> actoolzipOutput = Optional.absent();
      if (!Iterables.isEmpty(objcProvider.get(ASSET_CATALOG))) {
        actoolzipOutput = Optional.of(intermediateArtifacts.actoolzipOutput());
      }
      return actoolzipOutput;
    }

    private NestedSet<BundleableFile> binaryStringsFiles() {
      NestedSetBuilder<BundleableFile> binaryStringsBuilder = NestedSetBuilder.stableOrder();
      for (Artifact stringsFile : objcProvider.get(STRINGS)) {
        BundleableFile bundleFile =
            new BundleableFile(
                intermediateArtifacts.convertedStringsFile(stringsFile),
                BundleableFile.flatBundlePath(stringsFile.getExecPath()));
        binaryStringsBuilder.add(bundleFile);
      }
      return binaryStringsBuilder.build();
    }

    private NestedSet<BundleableFile> dynamicFrameworkFiles() {
      NestedSetBuilder<BundleableFile> frameworkFilesBuilder = NestedSetBuilder.stableOrder();
      for (Artifact frameworkFile : objcProvider.get(DYNAMIC_FRAMEWORK_FILE)) {
        PathFragment frameworkDir =
            ObjcCommon.nearestContainerMatching(ObjcCommon.FRAMEWORK_CONTAINER_TYPE, frameworkFile)
                .get();
        String frameworkName = frameworkDir.getBaseName();
        PathFragment inFrameworkPath = frameworkFile.getExecPath().relativeTo(frameworkDir);
        if (inFrameworkPath.getFirstSegment(STRIP_FRAMEWORK_DIRS) == 0) {
          continue;
        }
        // If this is a top-level file in the framework set the executable bit (to make sure we set
        // the bit on the actual dylib binary - other files may also get it but we have no way to
        // distinguish them).
        int permissions =
            inFrameworkPath.segmentCount() == 1
                ? BundleableFile.EXECUTABLE_EXTERNAL_FILE_ATTRIBUTE
                : BundleableFile.DEFAULT_EXTERNAL_FILE_ATTRIBUTE;
        BundleableFile bundleFile =
            new BundleableFile(
                frameworkFile,
                "Frameworks/" + frameworkName + "/" + inFrameworkPath.getPathString(),
                permissions);
        frameworkFilesBuilder.add(bundleFile);
      }
      return frameworkFilesBuilder.build();
    }

    /**
     * Filters files that would map to the same location in the bundle, adding only one copy to the
     * set of files returned.
     *
     * <p>Files can have the same bundle path for various illegal reasons and errors are raised for
     * that separately (see {@link BundleSupport#validateResources}). There are situations though
     * where the same file exists multiple times (for example in multi-architecture builds) and
     * would conflict when creating the bundle. In all these cases it shouldn't matter which one is
     * included and this class will select the first one.
     */
    ImmutableList<BundleableFile> deduplicateByBundlePaths(
        ImmutableList<BundleableFile> bundleFiles) {
      ImmutableList.Builder<BundleableFile> deduplicated = ImmutableList.builder();
      Set<String> bundlePaths = new HashSet<>();
      for (BundleableFile bundleFile : bundleFiles) {
        if (bundlePaths.add(bundleFile.getBundlePath())) {
          deduplicated.add(bundleFile);
        }
      }
      return deduplicated.build();
    }

    public Bundling build() {
      Preconditions.checkNotNull(intermediateArtifacts, "intermediateArtifacts");
      Preconditions.checkNotNull(families, FAMILIES_ATTR);
      NestedSet<Artifact> bundleInfoplistInputs = bundleInfoplistInputs();
      Optional<Artifact> bundleInfoplist = bundleInfoplist(bundleInfoplistInputs);
      Optional<Artifact> actoolzipOutput = actoolzipOutput();
      Optional<Artifact> combinedArchitectureBinary = combinedArchitectureBinary();
      NestedSet<BundleableFile> binaryStringsFiles = binaryStringsFiles();
      NestedSet<BundleableFile> dynamicFrameworks = dynamicFrameworkFiles();
      NestedSet<Artifact> mergeZips = mergeZips(actoolzipOutput);
      NestedSet<Artifact> rootMergeZips = rootMergeZips();

      bundleFilesBuilder
          .addAll(binaryStringsFiles)
          .addAll(dynamicFrameworks)
          .addAll(objcProvider.get(BUNDLE_FILE));
      ImmutableList<BundleableFile> bundleFiles =
          deduplicateByBundlePaths(bundleFilesBuilder.build());

      NestedSetBuilder<Artifact> bundleContentArtifactsBuilder =
          NestedSetBuilder.<Artifact>stableOrder()
              .addTransitive(nestedBundleContentArtifacts(objcProvider.get(NESTED_BUNDLE)))
              .addAll(combinedArchitectureBinary.asSet())
              .addAll(bundleInfoplist.asSet())
              .addTransitive(mergeZips)
              .addTransitive(rootMergeZips)
              .addAll(BundleableFile.toArtifacts(bundleFiles));

      return new Bundling(
          name,
          executableName,
          bundleDirFormat,
          combinedArchitectureBinary,
          bundleFiles,
          bundleInfoplist,
          actoolzipOutput,
          bundleContentArtifactsBuilder.build(),
          mergeZips,
          rootMergeZips,
          primaryBundleId,
          fallbackBundleId,
          architecture,
          minimumOsVersion,
          bundleInfoplistInputs,
          automaticEntriesInfoplistInput,
          objcProvider.get(NESTED_BUNDLE),
          families,
          intermediateArtifacts,
          artifactPrefix);
    }
  }

  private static boolean needsToMerge(
      NestedSet<Artifact> bundleInfoplistInputs, String primaryBundleId, String fallbackBundleId) {
    return primaryBundleId != null || fallbackBundleId != null
        || Iterables.size(bundleInfoplistInputs) > 1;
  }

  private final String name;
  @Nullable private final String executableName;
  private final String architecture;
  private final String bundleDirFormat;
  private final Optional<Artifact> combinedArchitectureBinary;
  private final ImmutableList<BundleableFile> bundleFiles;
  private final Optional<Artifact> bundleInfoplist;
  private final Optional<Artifact> actoolzipOutput;
  private final NestedSet<Artifact> bundleContentArtifacts;
  private final NestedSet<Artifact> mergeZips;
  private final NestedSet<Artifact> rootMergeZips;
  private final String primaryBundleId;
  private final String fallbackBundleId;
  private final DottedVersion minimumOsVersion;
  private final NestedSet<Artifact> infoplistInputs;
  private final NestedSet<Bundling> nestedBundlings;
  private Artifact automaticEntriesInfoplistInput;
  private final ImmutableSet<TargetDeviceFamily> families;
  private final IntermediateArtifacts intermediateArtifacts;
  private final String artifactPrefix;

  private Bundling(
      String name,
      String executableName,
      String bundleDirFormat,
      Optional<Artifact> combinedArchitectureBinary,
      ImmutableList<BundleableFile> bundleFiles,
      Optional<Artifact> bundleInfoplist,
      Optional<Artifact> actoolzipOutput,
      NestedSet<Artifact> bundleContentArtifacts,
      NestedSet<Artifact> mergeZips,
      NestedSet<Artifact> rootMergeZips,
      String primaryBundleId,
      String fallbackBundleId,
      String architecture,
      DottedVersion minimumOsVersion,
      NestedSet<Artifact> infoplistInputs,
      Artifact automaticEntriesInfoplistInput,
      NestedSet<Bundling> nestedBundlings,
      ImmutableSet<TargetDeviceFamily> families,
      IntermediateArtifacts intermediateArtifacts,
      String artifactPrefix) {
    this.nestedBundlings = Preconditions.checkNotNull(nestedBundlings);
    this.name = Preconditions.checkNotNull(name);
    this.executableName = executableName;
    this.bundleDirFormat = Preconditions.checkNotNull(bundleDirFormat);
    this.combinedArchitectureBinary = Preconditions.checkNotNull(combinedArchitectureBinary);
    this.bundleFiles = Preconditions.checkNotNull(bundleFiles);
    this.bundleInfoplist = Preconditions.checkNotNull(bundleInfoplist);
    this.actoolzipOutput = Preconditions.checkNotNull(actoolzipOutput);
    this.bundleContentArtifacts = Preconditions.checkNotNull(bundleContentArtifacts);
    this.mergeZips = Preconditions.checkNotNull(mergeZips);
    this.rootMergeZips = Preconditions.checkNotNull(rootMergeZips);
    this.fallbackBundleId = fallbackBundleId;
    this.primaryBundleId = primaryBundleId;
    this.architecture = Preconditions.checkNotNull(architecture);
    this.minimumOsVersion = Preconditions.checkNotNull(minimumOsVersion);
    this.infoplistInputs = Preconditions.checkNotNull(infoplistInputs);
    this.automaticEntriesInfoplistInput = automaticEntriesInfoplistInput;
    this.families = Preconditions.checkNotNull(families);
    this.intermediateArtifacts = intermediateArtifacts;
    this.artifactPrefix = artifactPrefix;
  }

  /**
   * The bundle directory. For apps, this would be {@code "Payload/TARGET_NAME.app"}, which is where
   * in the bundle zip archive every file is found, including the linked binary, nested bundles, and
   * everything returned by {@link #getBundleFiles()}.
   */
  public String getBundleDir() {
    return String.format(bundleDirFormat, name);
  }

  /**
   * The name of the bundle, from which the bundle root and the path of the linked binary in the
   * bundle archive are derived.
   */
  public String getName() {
    return name;
  }
  
  /** The name of the bundle's executable, or null if the bundle has no executable. */
  @Nullable public String getExecutableName() {
    return executableName;
  }

  /**
   * An {@link Optional} with the linked binary artifact, or {@link Optional#absent()} if it is
   * empty and should not be included in the bundle.
   */
  public Optional<Artifact> getCombinedArchitectureBinary() {
    return combinedArchitectureBinary;
  }

  /**
   * Bundle files to include in the bundle. These files are placed under the bundle root (possibly
   * nested, of course, depending on the bundle path of the files).
   */
  public ImmutableList<BundleableFile> getBundleFiles() {
    return bundleFiles;
  }

  /**
   * Returns any bundles nested in this one.
   */
  public NestedSet<Bundling> getNestedBundlings() {
    return nestedBundlings;
  }

  /**
   * Returns an artifact representing this bundle's {@code Info.plist} or {@link Optional#absent()}
   * if this bundle has no info plist inputs.
   */
  public Optional<Artifact> getBundleInfoplist() {
    return bundleInfoplist;
  }

  /**
   * Returns all info plists that need to be merged into this bundle's {@link #getBundleInfoplist()
   * info plist}, other than that plist that contains blaze-generated automatic entires.
   */
  public NestedSet<Artifact> getBundleInfoplistInputs() {
    return infoplistInputs;
  }

  /**
   * Returns an artifact representing a plist containing automatic entries generated by bazel.
   */
  public Artifact getAutomaticInfoPlist() {
    return automaticEntriesInfoplistInput;
  }

  /**
   * Returns all artifacts that are required as input to the merging of the final plist.
   */
  public NestedSet<Artifact> getMergingContentArtifacts() {
    NestedSetBuilder<Artifact> result = NestedSetBuilder.stableOrder();
    result.addTransitive(infoplistInputs);
    if (automaticEntriesInfoplistInput != null) {
      result.add(automaticEntriesInfoplistInput);
    }
    return result.build();
  }

  /**
   * Returns {@code true} if this bundle requires merging of its {@link #getBundleInfoplist() info
   * plist}.
   */
  public boolean needsToMergeInfoplist() {
    return needsToMerge(infoplistInputs, primaryBundleId, fallbackBundleId);
  }

  /**
   * The location of the actoolzip output for this bundle. This is non-absent only included in the
   * bundle if there is at least one asset catalog artifact supplied by
   * {@link ObjcProvider#ASSET_CATALOG}.
   */
  public Optional<Artifact> getActoolzipOutput() {
    return actoolzipOutput;
  }

  /**
   * Returns all zip files whose contents should be merged into this bundle under the main bundle
   * directory. For instance, if a merge zip contains files a/b and c/d, then the resulting bundling
   * would have additional files at:
   * <ul>
   *   <li>{bundleDir}/a/b
   *   <li>{bundleDir}/c/d
   * </ul>
   */
  public NestedSet<Artifact> getMergeZips() {
    return mergeZips;
  }

  /**
   * Returns all zip files whose contents should be merged into final ipa and outside the
   * main bundle. For instance, if a merge zip contains files dir1/file1, then the resulting
   * bundling would have additional files at:
   * <ul>
   *   <li>dir1/file1
   *   <li>{bundleDir}/other_files
   * </ul>
   */
  public NestedSet<Artifact> getRootMergeZips() {
    return rootMergeZips;
  }

  /**
   * Returns the variable substitutions that should be used when merging the plist info file of
   * this bundle.
   */
  public Map<String, String> variableSubstitutions() {
    return ImmutableMap.of(
        "EXECUTABLE_NAME", Strings.nullToEmpty(executableName),
        "BUNDLE_NAME", new PathFragment(getBundleDir()).getBaseName(),
        "PRODUCT_NAME", name);
  }

  /**
   * Returns the artifacts that are required to generate this bundle.
   */
  public NestedSet<Artifact> getBundleContentArtifacts() {
    return bundleContentArtifacts;
  }

  /**
   * Returns primary bundle ID to use, can be null.
   */
  public String getPrimaryBundleId() {
    return primaryBundleId;
  }
  
  /**
   * Returns fallback bundle ID to use when primary isn't set.
   */
  public String getFallbackBundleId() {
    return fallbackBundleId;
  }

  /**
   * Returns the iOS CPU architecture this bundle was constructed for.
   */
  public String getArchitecture() {
    return architecture;
  }

  /**
   * Returns the minimum iOS version this bundle's plist and resources should be generated for
   * (does <b>not</b> affect the minimum OS version its binary is compiled with).
   */
  public DottedVersion getMinimumOsVersion() {
    return minimumOsVersion;
  }

  /**
   * Returns the list of {@link TargetDeviceFamily} values this bundle is targeting.
   * If empty, the default values specified by {@link FAMILIES_ATTR} will be used.
   */
  public ImmutableSet<TargetDeviceFamily> getTargetDeviceFamilies() {
    return families;
  }

  /**
   * Returns {@link IntermediateArtifacts} required to create this bundle.
   */
  public IntermediateArtifacts getIntermediateArtifacts() {
    return intermediateArtifacts;
  }

  /**
   * Returns the prefix to be added to all generated artifact names, can be null. This is useful
   * to disambiguate artifacts for multiple bundles created with different names withing same rule. 
   */
  public String getArtifactPrefix() {
    return artifactPrefix;
  }
}
