| // Copyright 2015 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.STRINGS; |
| import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCASSETS_DIR; |
| |
| import com.google.common.base.Optional; |
| import com.google.common.base.Verify; |
| import com.google.common.collect.ImmutableList; |
| 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.actions.CommandLine; |
| import com.google.devtools.build.lib.analysis.FilesToRunProvider; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction; |
| import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; |
| import com.google.devtools.build.lib.analysis.actions.SpawnAction; |
| 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.rules.apple.AppleConfiguration; |
| import com.google.devtools.build.lib.rules.apple.ApplePlatform; |
| import com.google.devtools.build.lib.rules.apple.ApplePlatform.PlatformType; |
| import com.google.devtools.build.lib.rules.apple.AppleToolchain; |
| import com.google.devtools.build.lib.rules.apple.XcodeConfigProvider; |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * Support for generating iOS bundles which contain metadata (a plist file), assets, resources and |
| * optionally a binary: registers actions that assemble resources and merge plists, provides data |
| * to providers and validates bundle-related attributes. |
| * |
| * <p>Methods on this class can be called in any order without impacting the result. |
| */ |
| final class BundleSupport { |
| |
| /** |
| * Iterable wrapper used to strongly type arguments eventually passed to {@code actool}. |
| */ |
| static final class ExtraActoolArgs extends IterableWrapper<String> { |
| ExtraActoolArgs(Iterable<String> args) { |
| super(args); |
| } |
| |
| ExtraActoolArgs(String... args) { |
| super(args); |
| } |
| } |
| |
| // TODO(cparsons): Take restricted interfaces of RuleContext instead of RuleContext (such as |
| // RuleErrorConsumer). |
| private final RuleContext ruleContext; |
| private final AppleConfiguration appleConfiguration; |
| private final ApplePlatform platform; |
| private final ExtraActoolArgs extraActoolArgs; |
| private final Bundling bundling; |
| private final Attributes attributes; |
| |
| /** |
| * Creates a new bundle support. |
| * |
| * @param ruleContext context this bundle is constructed in |
| * @param appleConfiguration the configuration this bundle is constructed in |
| * @param platform the platform this bundle is built for |
| * @param bundling bundle information as configured for this rule |
| * @param extraActoolArgs any additional parameters to be used for invoking {@code actool} |
| */ |
| public BundleSupport( |
| RuleContext ruleContext, |
| AppleConfiguration appleConfiguration, |
| ApplePlatform platform, |
| Bundling bundling, |
| ExtraActoolArgs extraActoolArgs) { |
| this.ruleContext = ruleContext; |
| this.appleConfiguration = appleConfiguration; |
| this.platform = platform; |
| this.extraActoolArgs = extraActoolArgs; |
| this.bundling = bundling; |
| this.attributes = new Attributes(ruleContext); |
| } |
| |
| /** |
| * Registers actions required for constructing this bundle, namely merging all involved {@code |
| * Info.plist} files and generating asset catalogues. |
| * |
| * @param objcProvider source of information from this rule's attributes and its dependencies |
| * @return this bundle support |
| */ |
| BundleSupport registerActions(ObjcProvider objcProvider) { |
| registerConvertStringsActions(objcProvider); |
| registerConvertXibsActions(objcProvider); |
| registerMomczipActions(objcProvider); |
| registerInterfaceBuilderActions(objcProvider); |
| registerActoolActionIfNecessary(objcProvider); |
| |
| if (bundling.needsToMergeInfoplist()) { |
| NestedSet<Artifact> mergingContentArtifacts = bundling.getMergingContentArtifacts(); |
| Artifact mergedPlist = bundling.getBundleInfoplist().get(); |
| registerMergeInfoplistAction( |
| mergingContentArtifacts, PlMergeControlBytes.fromBundling(bundling, mergedPlist)); |
| } |
| return this; |
| } |
| |
| /** |
| * Validates the platform for this build is either simulator or device, and does not |
| * contain architectures for both platforms. |
| * |
| * @return this bundle support |
| */ |
| public BundleSupport validatePlatform() { |
| ApplePlatform platform = null; |
| for (String architecture : appleConfiguration.getIosMultiCpus()) { |
| if (platform == null) { |
| platform = ApplePlatform.forTarget(PlatformType.IOS, architecture); |
| } else if (platform != ApplePlatform.forTarget(PlatformType.IOS, architecture)) { |
| ruleContext.ruleError( |
| String.format("In builds which require bundling, --ios_multi_cpus does not currently " |
| + "allow values for both simulator and device builds. Flag was %s", |
| appleConfiguration.getIosMultiCpus())); |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * Validates that resources defined in this rule and its dependencies and written to this |
| * bundle are legal (for example that they are not mapped to the same bundle location). |
| * |
| * @return this bundle support |
| */ |
| public BundleSupport validateResources(ObjcProvider objcProvider) { |
| Map<String, Artifact> bundlePathToFile = new HashMap<>(); |
| NestedSet<Artifact> artifacts = objcProvider.get(STRINGS); |
| |
| Iterable<BundleableFile> bundleFiles = |
| Iterables.concat( |
| objcProvider.get(BUNDLE_FILE), BundleableFile.flattenedRawResourceFiles(artifacts)); |
| for (BundleableFile bundleFile : bundleFiles) { |
| String bundlePath = bundleFile.getBundlePath(); |
| Artifact bundled = bundleFile.getBundled(); |
| |
| // Normally any two resources mapped to the same path in the bundle are illegal. However, we |
| // currently don't have a good solution for resources generated by a genrule in |
| // multi-architecture builds: They map to the same bundle path but have different owners (the |
| // genrules targets in the various configurations) and roots (one for each architecture). |
| // Since we know that architecture shouldn't matter for strings file generation we silently |
| // ignore cases like this and pick one of the outputs at random to put in the bundle (see also |
| // related filtering code in Bundling.Builder.build()). |
| if (bundlePathToFile.containsKey(bundlePath)) { |
| Artifact original = bundlePathToFile.get(bundlePath); |
| if (!Objects.equals(original.getOwner(), bundled.getOwner())) { |
| ruleContext.ruleError( |
| String.format( |
| "Two files map to the same path [%s] in this bundle but come from different " |
| + "locations: %s and %s", |
| bundlePath, |
| original.getOwner(), |
| bundled.getOwner())); |
| } else { |
| Verify.verify( |
| !original.getRoot().equals(bundled.getRoot()), |
| "%s and %s should have different roots but have %s and %s", |
| original, |
| bundleFile, |
| original.getRoot(), |
| bundled.getRoot()); |
| } |
| |
| } else { |
| bundlePathToFile.put(bundlePath, bundled); |
| } |
| } |
| // TODO(bazel-team): Do the same validation for storyboards and datamodels which could also be |
| // generated by genrules or doubly defined. |
| |
| return this; |
| } |
| |
| /** |
| * Returns a set containing the {@link TargetDeviceFamily} values which this bundle is targeting. |
| * Returns an empty set for any invalid value of the target device families attribute. |
| */ |
| ImmutableSet<TargetDeviceFamily> targetDeviceFamilies() { |
| return bundling.getTargetDeviceFamilies(); |
| } |
| |
| /** |
| * Returns true if this bundle is targeted to {@link TargetDeviceFamily#WATCH}, false otherwise. |
| */ |
| boolean isBuildingForWatch() { |
| return targetDeviceFamilies() |
| .stream() |
| .anyMatch( |
| targetDeviceFamily -> |
| targetDeviceFamily |
| .name() |
| .equalsIgnoreCase(TargetDeviceFamily.WATCH.getNameInRule())); |
| } |
| |
| /** |
| * Returns a set containing the {@link TargetDeviceFamily} values the resources in this bundle |
| * are targeting. When watch is included as one of the families, (for example [iphone, watch] for |
| * simulator builds, assets should always be compiled for {@link TargetDeviceFamily#WATCH}. |
| */ |
| private ImmutableSet<TargetDeviceFamily> targetDeviceFamiliesForResources() { |
| if (isBuildingForWatch()) { |
| return ImmutableSet.of(TargetDeviceFamily.WATCH); |
| } else { |
| return targetDeviceFamilies(); |
| } |
| } |
| |
| private void registerInterfaceBuilderActions(ObjcProvider objcProvider) { |
| for (Artifact storyboardInput : objcProvider.get(ObjcProvider.STORYBOARD)) { |
| String archiveRoot = storyboardArchiveRoot(storyboardInput); |
| Artifact zipOutput = bundling.getIntermediateArtifacts() |
| .compiledStoryboardZip(storyboardInput); |
| |
| ruleContext.registerAction( |
| ObjcRuleClasses.spawnAppleEnvActionBuilder( |
| XcodeConfigProvider.fromRuleContext(ruleContext), platform) |
| .setMnemonic("StoryboardCompile") |
| .setExecutable(attributes.ibtoolWrapper()) |
| .addCommandLine(ibActionsCommandLine(archiveRoot, zipOutput, storyboardInput)) |
| .addOutput(zipOutput) |
| .addInput(storyboardInput) |
| .build(ruleContext)); |
| } |
| } |
| |
| /** |
| * Returns the root file path to which storyboard interfaces are compiled. |
| */ |
| protected String storyboardArchiveRoot(Artifact storyboardInput) { |
| // When storyboards are compiled for {@link TargetDeviceFamily#WATCH}, return the containing |
| // directory if it ends with .lproj to account for localization or "." representing the bundle |
| // root otherwise. Examples: Payload/Foo.app/Base.lproj/<compiled_file>, |
| // Payload/Foo.app/<compile_file_1> |
| if (isBuildingForWatch()) { |
| String containingDir = storyboardInput.getExecPath().getParentDirectory().getBaseName(); |
| return containingDir.endsWith(".lproj") ? (containingDir + "/") : "."; |
| } else { |
| return BundleableFile.flatBundlePath(storyboardInput.getExecPath()) + "c"; |
| } |
| } |
| |
| private CommandLine ibActionsCommandLine(String archiveRoot, Artifact zipOutput, |
| Artifact storyboardInput) { |
| CustomCommandLine.Builder commandLine = |
| CustomCommandLine.builder() |
| .addPath(zipOutput.getExecPath()) |
| .addDynamicString(archiveRoot) |
| .add("--minimum-deployment-target", bundling.getMinimumOsVersion().toString()) |
| .add("--module", ruleContext.getLabel().getName()); |
| |
| for (TargetDeviceFamily targetDeviceFamily : targetDeviceFamiliesForResources()) { |
| commandLine.add("--target-device", targetDeviceFamily.name().toLowerCase(Locale.US)); |
| } |
| |
| return commandLine.addPath(storyboardInput.getExecPath()).build(); |
| } |
| |
| private void registerMomczipActions(ObjcProvider objcProvider) { |
| Iterable<Xcdatamodel> xcdatamodels = Xcdatamodels.xcdatamodels( |
| bundling.getIntermediateArtifacts(), objcProvider.get(ObjcProvider.XCDATAMODEL)); |
| for (Xcdatamodel datamodel : xcdatamodels) { |
| Artifact outputZip = datamodel.getOutputZip(); |
| ruleContext.registerAction( |
| ObjcRuleClasses.spawnAppleEnvActionBuilder( |
| XcodeConfigProvider.fromRuleContext(ruleContext), platform) |
| .setMnemonic("MomCompile") |
| .setExecutable(attributes.momcWrapper()) |
| .addOutput(outputZip) |
| .addInputs(datamodel.getInputs()) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .addExecPath(outputZip) |
| .addDynamicString(datamodel.archiveRootForMomczip()) |
| .addDynamicString("-XD_MOMC_SDKROOT=" + AppleToolchain.sdkDir()) |
| .addDynamicString( |
| "-XD_MOMC_IOS_TARGET_VERSION=" + bundling.getMinimumOsVersion()) |
| .add("-MOMC_PLATFORMS") |
| .addDynamicString( |
| appleConfiguration |
| .getMultiArchPlatform(PlatformType.IOS) |
| .getLowerCaseNameInPlist()) |
| .add("-XD_MOMC_TARGET_VERSION=10.6") |
| .addPath(datamodel.getContainer()) |
| .build()) |
| .build(ruleContext)); |
| } |
| } |
| |
| private void registerConvertXibsActions(ObjcProvider objcProvider) { |
| for (Artifact original : objcProvider.get(ObjcProvider.XIB)) { |
| Artifact zipOutput = bundling.getIntermediateArtifacts().compiledXibFileZip(original); |
| String archiveRoot = BundleableFile.flatBundlePath( |
| FileSystemUtils.replaceExtension(original.getExecPath(), ".nib")); |
| |
| ruleContext.registerAction( |
| ObjcRuleClasses.spawnAppleEnvActionBuilder( |
| XcodeConfigProvider.fromRuleContext(ruleContext), platform) |
| .setMnemonic("XibCompile") |
| .setExecutable(attributes.ibtoolWrapper()) |
| .addCommandLine(ibActionsCommandLine(archiveRoot, zipOutput, original)) |
| .addOutput(zipOutput) |
| .addInput(original) |
| // Disable sandboxing due to Bazel issue #2189. |
| .disableSandboxing() |
| .build(ruleContext)); |
| } |
| } |
| |
| private void registerConvertStringsActions(ObjcProvider objcProvider) { |
| for (Artifact strings : objcProvider.get(ObjcProvider.STRINGS)) { |
| Artifact bundled = bundling.getIntermediateArtifacts().convertedStringsFile(strings); |
| ruleContext.registerAction( |
| ObjcRuleClasses.spawnAppleEnvActionBuilder( |
| XcodeConfigProvider.fromRuleContext(ruleContext), platform) |
| .setMnemonic("ConvertStringsPlist") |
| .setExecutable(PathFragment.create("/usr/bin/plutil")) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .add("-convert") |
| .add("binary1") |
| .addExecPath("-o", bundled) |
| .add("--") |
| .addPath(strings.getExecPath()) |
| .build()) |
| .addInput(strings) |
| .addInput(CompilationSupport.xcrunwrapper(ruleContext).getExecutable()) |
| .addOutput(bundled) |
| .build(ruleContext)); |
| } |
| } |
| |
| /** |
| * Creates action to merge multiple Info.plist files of a bundle into a single Info.plist. The |
| * merge action is necessary if there are more than one input plist files or we have a bundle ID |
| * to stamp on the merged plist. |
| */ |
| private void registerMergeInfoplistAction( |
| NestedSet<Artifact> mergingContentArtifacts, PlMergeControlBytes controlBytes) { |
| if (!bundling.needsToMergeInfoplist()) { |
| return; // Nothing to do here. |
| } |
| |
| Artifact plMergeControlArtifact = baseNameArtifact(ruleContext, ".plmerge-control"); |
| |
| ruleContext.registerAction( |
| new BinaryFileWriteAction( |
| ruleContext.getActionOwner(), |
| plMergeControlArtifact, |
| controlBytes, |
| /*makeExecutable=*/ false)); |
| |
| ruleContext.registerAction( |
| new SpawnAction.Builder() |
| .setMnemonic("MergeInfoPlistFiles") |
| .setExecutable(attributes.plmerge()) |
| .addTransitiveInputs(mergingContentArtifacts) |
| .addOutput(bundling.getIntermediateArtifacts().mergedInfoplist()) |
| .addInput(plMergeControlArtifact) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .addExecPath("--control", plMergeControlArtifact) |
| .build()) |
| .build(ruleContext)); |
| } |
| |
| /** |
| * Returns an {@link Artifact} with name prefixed with prefix given in {@link Bundling} if |
| * available. This helps in creating unique artifact name when multiple bundles are created |
| * with a different name than the target name. |
| */ |
| private Artifact baseNameArtifact(RuleContext ruleContext, String artifactName) { |
| String prefixedArtifactName; |
| if (bundling.getArtifactPrefix() != null) { |
| prefixedArtifactName = String.format("-%s%s", bundling.getArtifactPrefix(), artifactName); |
| } else { |
| prefixedArtifactName = artifactName; |
| } |
| return ObjcRuleClasses.artifactByAppendingToBaseName(ruleContext, prefixedArtifactName); |
| } |
| |
| private void registerActoolActionIfNecessary(ObjcProvider objcProvider) { |
| Optional<Artifact> actoolzipOutput = bundling.getActoolzipOutput(); |
| if (!actoolzipOutput.isPresent()) { |
| return; |
| } |
| |
| Artifact actoolPartialInfoplist = actoolPartialInfoplist(objcProvider).get(); |
| Artifact zipOutput = actoolzipOutput.get(); |
| |
| // TODO(bazel-team): Do not use the deploy jar explicitly here. There is currently a bug where |
| // we cannot .setExecutable({java_binary target}) and set REQUIRES_DARWIN in the execution info. |
| // Note that below we set the archive root to the empty string. This means that the generated |
| // zip file will be rooted at the bundle root, and we have to prepend the bundle root to each |
| // entry when merging it with the final .ipa file. |
| ruleContext.registerAction( |
| ObjcRuleClasses.spawnAppleEnvActionBuilder( |
| XcodeConfigProvider.fromRuleContext(ruleContext), platform) |
| .setMnemonic("AssetCatalogCompile") |
| .setExecutable(attributes.actoolWrapper()) |
| .addTransitiveInputs(objcProvider.get(ASSET_CATALOG)) |
| .addOutput(zipOutput) |
| .addOutput(actoolPartialInfoplist) |
| .addCommandLine(actoolzipCommandLine(objcProvider, zipOutput, actoolPartialInfoplist)) |
| .disableSandboxing() |
| .build(ruleContext)); |
| } |
| |
| private CommandLine actoolzipCommandLine(ObjcProvider provider, Artifact zipOutput, |
| Artifact partialInfoPlist) { |
| PlatformType platformType = PlatformType.IOS; |
| // watchOS 1 and 2 use different platform arguments. It is likely that versions 2 and later will |
| // use the watchos platform whereas watchOS 1 uses the iphone platform. |
| if (isBuildingForWatch() && bundling.getBundleDir().startsWith("Watch")) { |
| platformType = PlatformType.WATCHOS; |
| } |
| CustomCommandLine.Builder commandLine = |
| CustomCommandLine.builder() |
| .addPath(zipOutput.getExecPath()) |
| .add( |
| "--platform", |
| appleConfiguration.getMultiArchPlatform(platformType).getLowerCaseNameInPlist()) |
| .addExecPath("--output-partial-info-plist", partialInfoPlist) |
| .add("--minimum-deployment-target", bundling.getMinimumOsVersion().toString()); |
| |
| for (TargetDeviceFamily targetDeviceFamily : targetDeviceFamiliesForResources()) { |
| commandLine.add("--target-device", targetDeviceFamily.name().toLowerCase(Locale.US)); |
| } |
| |
| return commandLine |
| .addAll( |
| ImmutableList.copyOf( |
| Iterables.transform(provider.get(XCASSETS_DIR), PathFragment::getSafePathString))) |
| .addAll(ImmutableList.copyOf(extraActoolArgs)) |
| .build(); |
| } |
| |
| |
| /** |
| * Returns the artifact that is a plist file generated by an invocation of {@code actool} or |
| * {@link Optional#absent()} if no asset catalogues are present in this target and its |
| * dependencies. |
| * |
| * <p>All invocations of {@code actool} generate this kind of plist file, which contains metadata |
| * about the {@code app_icon} and {@code launch_image} if supplied. If neither an app icon or a |
| * launch image was supplied, the plist file generated is empty. |
| */ |
| private Optional<Artifact> actoolPartialInfoplist(ObjcProvider objcProvider) { |
| if (objcProvider.hasAssetCatalogs()) { |
| return Optional.of(bundling.getIntermediateArtifacts().actoolPartialInfoplist()); |
| } else { |
| return Optional.absent(); |
| } |
| } |
| |
| /** |
| * Common rule attributes used by a bundle support. |
| */ |
| private static class Attributes { |
| private final RuleContext ruleContext; |
| |
| private Attributes(RuleContext ruleContext) { |
| this.ruleContext = ruleContext; |
| } |
| |
| /** |
| * Returns a reference to the plmerge executable. |
| */ |
| FilesToRunProvider plmerge() { |
| return ruleContext.getExecutablePrerequisite("$plmerge", Mode.HOST); |
| } |
| |
| /** |
| * Returns the location of the ibtoolwrapper tool. |
| */ |
| FilesToRunProvider ibtoolWrapper() { |
| return ruleContext.getExecutablePrerequisite("$ibtoolwrapper", Mode.HOST); |
| } |
| |
| /** |
| * Returns the location of the momcwrapper. |
| */ |
| FilesToRunProvider momcWrapper() { |
| return ruleContext.getExecutablePrerequisite("$momcwrapper", Mode.HOST); |
| } |
| |
| /** |
| * Returns the location of the actoolwrapper. |
| */ |
| FilesToRunProvider actoolWrapper() { |
| return ruleContext.getExecutablePrerequisite("$actoolwrapper", Mode.HOST); |
| } |
| } |
| } |