// 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.Preconditions; 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.RuleContext; import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode; 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.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 STRIP_FRAMEWORK_DIRS = ImmutableSet.of("Headers", "PrivateHeaders", "Modules"); static final class Builder { private String name; private String bundleDirFormat; private ImmutableList.Builder bundleFilesBuilder = ImmutableList.builder(); private ObjcProvider objcProvider; private NestedSetBuilder infoplistInputs = NestedSetBuilder.stableOrder(); private Artifact automaticEntriesInfoplistInput; private IntermediateArtifacts intermediateArtifacts; private String primaryBundleId; private String fallbackBundleId; private String architecture; private DottedVersion minimumOsVersion; private ImmutableSet 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 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 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 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 families) { this.families = families; return this; } public Builder setArtifactPrefix(String artifactPrefix) { this.artifactPrefix = artifactPrefix; return this; } private static NestedSet nestedBundleContentArtifacts(Iterable bundles) { NestedSetBuilder artifacts = NestedSetBuilder.stableOrder(); for (Bundling bundle : bundles) { artifacts.addTransitive(bundle.getBundleContentArtifacts()); } return artifacts.build(); } private NestedSet mergeZips(Optional actoolzipOutput) { NestedSetBuilder mergeZipBuilder = NestedSetBuilder.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 rootMergeZips() { NestedSetBuilder rootMergeZipsBuilder = NestedSetBuilder.stableOrder().addTransitive(objcProvider.get(ROOT_MERGE_ZIP)); if (objcProvider.is(USES_SWIFT)) { rootMergeZipsBuilder.add(intermediateArtifacts.swiftSupportZip()); } return rootMergeZipsBuilder.build(); } private NestedSet bundleInfoplistInputs() { if (objcProvider.hasAssetCatalogs()) { infoplistInputs.add(intermediateArtifacts.actoolPartialInfoplist()); } return infoplistInputs.build(); } private Optional bundleInfoplist(NestedSet 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 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 actoolzipOutput() { Optional actoolzipOutput = Optional.absent(); if (!Iterables.isEmpty(objcProvider.get(ASSET_CATALOG))) { actoolzipOutput = Optional.of(intermediateArtifacts.actoolzipOutput()); } return actoolzipOutput; } private NestedSet binaryStringsFiles() { NestedSetBuilder 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 dynamicFrameworkFiles() { NestedSetBuilder 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. * *

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 deduplicateByBundlePaths( ImmutableList bundleFiles) { ImmutableList.Builder deduplicated = ImmutableList.builder(); Set 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 bundleInfoplistInputs = bundleInfoplistInputs(); Optional bundleInfoplist = bundleInfoplist(bundleInfoplistInputs); Optional actoolzipOutput = actoolzipOutput(); Optional combinedArchitectureBinary = combinedArchitectureBinary(); NestedSet binaryStringsFiles = binaryStringsFiles(); NestedSet dynamicFrameworks = dynamicFrameworkFiles(); NestedSet mergeZips = mergeZips(actoolzipOutput); NestedSet rootMergeZips = rootMergeZips(); bundleFilesBuilder .addAll(binaryStringsFiles) .addAll(dynamicFrameworks) .addAll(objcProvider.get(BUNDLE_FILE)); ImmutableList bundleFiles = deduplicateByBundlePaths(bundleFilesBuilder.build()); NestedSetBuilder bundleContentArtifactsBuilder = NestedSetBuilder.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 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 combinedArchitectureBinary; private final ImmutableList bundleFiles; private final Optional bundleInfoplist; private final Optional actoolzipOutput; private final NestedSet bundleContentArtifacts; private final NestedSet mergeZips; private final NestedSet rootMergeZips; private final String primaryBundleId; private final String fallbackBundleId; private final DottedVersion minimumOsVersion; private final NestedSet infoplistInputs; private final NestedSet nestedBundlings; private Artifact automaticEntriesInfoplistInput; private final ImmutableSet families; private final IntermediateArtifacts intermediateArtifacts; private final String artifactPrefix; private Bundling( String name, String executableName, String bundleDirFormat, Optional combinedArchitectureBinary, ImmutableList bundleFiles, Optional bundleInfoplist, Optional actoolzipOutput, NestedSet bundleContentArtifacts, NestedSet mergeZips, NestedSet rootMergeZips, String primaryBundleId, String fallbackBundleId, String architecture, DottedVersion minimumOsVersion, NestedSet infoplistInputs, Artifact automaticEntriesInfoplistInput, NestedSet nestedBundlings, ImmutableSet 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 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 getBundleFiles() { return bundleFiles; } /** * Returns any bundles nested in this one. */ public NestedSet 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 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 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 getMergingContentArtifacts() { NestedSetBuilder 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 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: *

    *
  • {bundleDir}/a/b *
  • {bundleDir}/c/d *
*/ public NestedSet 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: *
    *
  • dir1/file1 *
  • {bundleDir}/other_files *
*/ public NestedSet getRootMergeZips() { return rootMergeZips; } /** * Returns the variable substitutions that should be used when merging the plist info file of * this bundle. */ public Map variableSubstitutions() { return ImmutableMap.of( "EXECUTABLE_NAME", Strings.nullToEmpty(executableName), "BUNDLE_NAME", PathFragment.create(getBundleDir()).getBaseName(), "PRODUCT_NAME", name); } /** * Returns the artifacts that are required to generate this bundle. */ public NestedSet 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 not 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 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; } }