// 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", PathFragment.create(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; } }