// Copyright 2015 Google Inc. 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.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.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.FilesToRunProvider; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.actions.CommandLine; import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraActoolArgs; import com.google.devtools.build.lib.rules.objc.XcodeProvider.Builder; 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; import java.util.Set; /** * 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. * *

Methods on this class can be called in any order without impacting the result. */ final class BundleSupport { static class ExtraMergePlists extends IterableWrapper { ExtraMergePlists(Artifact... inputs) { super(inputs); } } private final RuleContext ruleContext; private final Set targetDeviceFamilies; private final ExtraActoolArgs extraActoolArgs; private final Bundling bundling; private final Attributes attributes; /** * Returns merging instructions for a bundle's {@code Info.plist}. * * @param ruleContext context this bundle is constructed in * @param objcProvider provider containing all dependencies' information as well as some of this * rule's * @param optionsProvider provider containing options and plist settings for this rule and its * dependencies * @param extraMergePlists additional plist files to merge */ static InfoplistMerging infoPlistMerging( RuleContext ruleContext, ObjcProvider objcProvider, OptionsProvider optionsProvider, ExtraMergePlists extraMergePlists) { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); return new InfoplistMerging.Builder(ruleContext) .setIntermediateArtifacts(intermediateArtifacts) .setInputPlists(NestedSetBuilder.stableOrder() .addTransitive(optionsProvider.getInfoplists()) .addAll(actoolPartialInfoplist(ruleContext, objcProvider).asSet()) .addAll(extraMergePlists) .build()) .setPlmerge(ruleContext.getExecutablePrerequisite("$plmerge", Mode.HOST)) .build(); } /** * Creates a new bundle support with no special {@code actool} arguments. * * @param ruleContext context this bundle is constructed in * @param targetDeviceFamilies device families used in asset catalogue construction * @param bundling bundle information as configured for this rule */ public BundleSupport( RuleContext ruleContext, Set targetDeviceFamilies, Bundling bundling) { this(ruleContext, targetDeviceFamilies, bundling, new ExtraActoolArgs()); } /** * Creates a new bundle support. * * @param ruleContext context this bundle is constructed in * @param targetDeviceFamilies device families used in asset catalogue construction * @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, Set targetDeviceFamilies, Bundling bundling, ExtraActoolArgs extraActoolArgs) { this.ruleContext = ruleContext; this.targetDeviceFamilies = targetDeviceFamilies; 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); registerMergeInfoplistAction(); registerActoolActionIfNecessary(objcProvider); return this; } /** * Adds any Xcode settings related to this bundle to the given provider builder. * * @return this bundle support */ BundleSupport addXcodeSettings(Builder xcodeProviderBuilder) { xcodeProviderBuilder.setInfoplistMerging(bundling.getInfoplistMerging()); 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 */ BundleSupport validateResources(ObjcProvider objcProvider) { Map bundlePathToFile = new HashMap<>(); for (Artifact stringsFile : objcProvider.get(STRINGS)) { String bundlePath = BundleableFile.flatBundlePath(stringsFile.getExecPath()); // 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(), stringsFile.getOwner())) { ruleContext.ruleError(String.format( "Two string files map to the same path [%s] in this bundle but come from different " + "locations: %s and %s", bundlePath, original.getOwner(), stringsFile.getOwner())); } else { Verify.verify(!original.getRoot().equals(stringsFile.getRoot()), "%s and %s should have different roots but have %s and %s", original, stringsFile, original.getRoot(), stringsFile.getRoot()); } } else { bundlePathToFile.put(bundlePath, stringsFile); } } // TODO(bazel-team): Do the same validation for storyboards and datamodels which could also be // generated by genrules or doubly defined. return this; } private void registerInterfaceBuilderActions(ObjcProvider objcProvider) { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext); String minimumOs = objcConfiguration.getMinimumOs(); for (Artifact storyboardInput : objcProvider.get(ObjcProvider.STORYBOARD)) { String archiveRoot = BundleableFile.flatBundlePath(storyboardInput.getExecPath()) + "c"; Artifact zipOutput = intermediateArtifacts.compiledStoryboardZip(storyboardInput); ruleContext.registerAction( ObjcActionsBuilder.spawnJavaOnDarwinActionBuilder(attributes.ibtoolzipDeployJar()) .setMnemonic("StoryboardCompile") .setCommandLine(CustomCommandLine.builder() // The next three arguments are positional, // i.e. they don't have flags before them. .addPath(zipOutput.getExecPath()) .add(archiveRoot) .addPath(ObjcActionsBuilder.IBTOOL) .add("--minimum-deployment-target").add(minimumOs) .addPath(storyboardInput.getExecPath()) .build()) .addOutput(zipOutput) .addInput(storyboardInput) .build(ruleContext)); } } private void registerMomczipActions(ObjcProvider objcProvider) { ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext); IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); Iterable xcdatamodels = Xcdatamodels.xcdatamodels( intermediateArtifacts, objcProvider.get(ObjcProvider.XCDATAMODEL)); for (Xcdatamodel datamodel : xcdatamodels) { Artifact outputZip = datamodel.getOutputZip(); ruleContext.registerAction( ObjcActionsBuilder.spawnJavaOnDarwinActionBuilder(attributes.momczipDeployJar()) .setMnemonic("MomCompile") .addOutput(outputZip) .addInputs(datamodel.getInputs()) .setCommandLine(CustomCommandLine.builder() .addPath(outputZip.getExecPath()) .add(datamodel.archiveRootForMomczip()) .add(IosSdkCommands.MOMC_PATH) .add(commonMomczipArguments(objcConfiguration)) .add(datamodel.getContainer().getSafePathString()) .build()) .build(ruleContext)); } } static Iterable commonMomczipArguments(ObjcConfiguration configuration) { return ImmutableList.of( "-XD_MOMC_SDKROOT=" + IosSdkCommands.sdkDir(configuration), "-XD_MOMC_IOS_TARGET_VERSION=" + configuration.getMinimumOs(), "-MOMC_PLATFORMS", configuration.getBundlingPlatform().getLowerCaseNameInPlist(), "-XD_MOMC_TARGET_VERSION=10.6"); } private void registerConvertXibsActions(ObjcProvider objcProvider) { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext); for (Artifact original : objcProvider.get(ObjcProvider.XIB)) { Artifact zipOutput = intermediateArtifacts.compiledXibFileZip(original); String archiveRoot = BundleableFile.flatBundlePath( FileSystemUtils.replaceExtension(original.getExecPath(), ".nib")); ruleContext.registerAction( ObjcActionsBuilder.spawnJavaOnDarwinActionBuilder(attributes.ibtoolzipDeployJar()) .setMnemonic("XibCompile") .setCommandLine(CustomCommandLine.builder() // The next three arguments are positional, // i.e. they don't have flags before them. .addPath(zipOutput.getExecPath()) .add(archiveRoot) .addPath(ObjcActionsBuilder.IBTOOL) .add("--minimum-deployment-target").add(objcConfiguration.getMinimumOs()) .addPath(original.getExecPath()) .build()) .addOutput(zipOutput) .addInput(original) .build(ruleContext)); } } private void registerConvertStringsActions(ObjcProvider objcProvider) { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); for (Artifact strings : objcProvider.get(ObjcProvider.STRINGS)) { Artifact bundled = intermediateArtifacts.convertedStringsFile(strings); ruleContext.registerAction(new SpawnAction.Builder() .setMnemonic("ConvertStringsPlist") .setExecutable(attributes.plmerge()) .setCommandLine(CustomCommandLine.builder() .addExecPath("--source_file", strings) .addExecPath("--out_file", bundled) .build()) .addInput(strings) .addOutput(bundled) .build(ruleContext)); } } /** * Validates any rule attributes and dependencies related to this bundle. * * @return this bundle support */ BundleSupport validateAttributes() { return this; } private void registerMergeInfoplistAction() { // TODO(bazel-team): Move action implementation from InfoplistMerging to this class. ruleContext.registerAction(bundling.getInfoplistMerging().getMergeAction()); } private void registerActoolActionIfNecessary(ObjcProvider objcProvider) { Optional actoolzipOutput = bundling.getActoolzipOutput(); if (!actoolzipOutput.isPresent()) { return; } Artifact actoolPartialInfoplist = actoolPartialInfoplist(ruleContext, 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( ObjcActionsBuilder.spawnJavaOnDarwinActionBuilder(attributes.actoolzipDeployJar()) .setMnemonic("AssetCatalogCompile") .addTransitiveInputs(objcProvider.get(ASSET_CATALOG)) .addOutput(zipOutput) .addOutput(actoolPartialInfoplist) .setCommandLine(actoolzipCommandLine( objcProvider, zipOutput, actoolPartialInfoplist)) .build(ruleContext)); } private CommandLine actoolzipCommandLine( final ObjcProvider provider, final Artifact zipOutput, final Artifact partialInfoPlist) { ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext); CustomCommandLine.Builder commandLine = CustomCommandLine.builder() // The next three arguments are positional, i.e. they don't have flags before them. .addPath(zipOutput.getExecPath()) .add("") // archive root .add(IosSdkCommands.ACTOOL_PATH) .add("--platform").add(objcConfiguration.getBundlingPlatform().getLowerCaseNameInPlist()) .addExecPath("--output-partial-info-plist", partialInfoPlist) .add("--minimum-deployment-target").add(objcConfiguration.getMinimumOs()); for (TargetDeviceFamily targetDeviceFamily : targetDeviceFamilies) { commandLine.add("--target-device").add(targetDeviceFamily.name().toLowerCase(Locale.US)); } return commandLine .add(PathFragment.safePathStrings(provider.get(XCASSETS_DIR))) .add(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. * *

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 static Optional actoolPartialInfoplist( RuleContext ruleContext, ObjcProvider objcProvider) { if (objcProvider.hasAssetCatalogs()) { IntermediateArtifacts intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext); return Optional.of(intermediateArtifacts.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 ibtoolzip deploy jar. */ Artifact ibtoolzipDeployJar() { return ruleContext.getPrerequisiteArtifact("$ibtoolzip_deploy", Mode.HOST); } /** * Returns the location of the momczip deploy jar. */ Artifact momczipDeployJar() { return ruleContext.getPrerequisiteArtifact("$momczip_deploy", Mode.HOST); } /** * Returns the location of the actoolzip deploy jar. */ Artifact actoolzipDeployJar() { return ruleContext.getPrerequisiteArtifact("$actoolzip_deploy", Mode.HOST); } } }