com.google.devtools.build.lib.rules.objc.BundleSupport.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.rules.objc.BundleSupport.java

Source

// 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.Predicate;
import com.google.common.base.Verify;
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.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.BinaryFileWriteAction;
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.NestedSet;
import com.google.devtools.build.lib.rules.apple.AppleConfiguration;
import com.google.devtools.build.lib.rules.apple.AppleToolchain;
import com.google.devtools.build.lib.rules.apple.Platform;
import com.google.devtools.build.lib.rules.apple.Platform.PlatformType;
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;

/**
 * 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 Platform 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, Platform 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;
    }

    /**
     * Adds any Xcode settings related to this bundle to the given provider builder.
     *
     * @return this bundle support
     */
    BundleSupport addXcodeSettings(Builder xcodeProviderBuilder) {
        if (bundling.getBundleInfoplist().isPresent()) {
            xcodeProviderBuilder.setBundleInfoplist(bundling.getBundleInfoplist().get());
        }
        return this;
    }

    private void validatePlatform() {
        Platform platform = null;
        for (String architecture : appleConfiguration.getIosMultiCpus()) {
            if (platform == null) {
                platform = Platform.forTarget(PlatformType.IOS, architecture);
            } else if (platform != Platform.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()));
            }
        }
    }

    private void 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.
    }

    /**
     * Validates bundle support.
     * <ul>
     * <li>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)
     * <li>Validates the platform for this build is either simulator or device, and does not
     *     contain architectures for both platforms
     * </ul>
     *
     * @return this bundle support
     */
    BundleSupport validate(ObjcProvider objcProvider) {
        validatePlatform();
        validateResources(objcProvider);

        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 Iterables.any(targetDeviceFamilies(), new Predicate<TargetDeviceFamily>() {
            @Override
            public boolean apply(TargetDeviceFamily targetDeviceFamily) {
                return 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(appleConfiguration, platform)
                    .setMnemonic("StoryboardCompile").setExecutable(attributes.ibtoolWrapper())
                    .setCommandLine(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()
                // The next three arguments are positional, i.e. they don't have flags before them.
                .addPath(zipOutput.getExecPath()).add(archiveRoot).add("--minimum-deployment-target")
                .add(bundling.getMinimumOsVersion().toString()).add("--module")
                .add(ruleContext.getLabel().getName());

        for (TargetDeviceFamily targetDeviceFamily : targetDeviceFamiliesForResources()) {
            commandLine.add("--target-device").add(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(appleConfiguration, platform)
                    .setMnemonic("MomCompile").setExecutable(attributes.momcWrapper()).addOutput(outputZip)
                    .addInputs(datamodel.getInputs())
                    .setCommandLine(CustomCommandLine.builder().addPath(outputZip.getExecPath())
                            .add(datamodel.archiveRootForMomczip())
                            .add("-XD_MOMC_SDKROOT=" + AppleToolchain.sdkDir())
                            .add("-XD_MOMC_IOS_TARGET_VERSION=" + bundling.getMinimumOsVersion())
                            .add("-MOMC_PLATFORMS")
                            .add(appleConfiguration.getMultiArchPlatform(PlatformType.IOS)
                                    .getLowerCaseNameInPlist())
                            .add("-XD_MOMC_TARGET_VERSION=10.6").add(datamodel.getContainer().getSafePathString())
                            .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(appleConfiguration, platform)
                    .setMnemonic("XibCompile").setExecutable(attributes.ibtoolWrapper())
                    .setCommandLine(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(appleConfiguration, platform)
                    .setMnemonic("ConvertStringsPlist").setExecutable(new PathFragment("/usr/bin/plutil"))
                    .setCommandLine(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()).addArgument("--control")
                .addInputArgument(plMergeControlArtifact).addTransitiveInputs(mergingContentArtifacts)
                .addOutput(bundling.getIntermediateArtifacts().mergedInfoplist()).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(appleConfiguration, platform)
                .setMnemonic("AssetCatalogCompile").setExecutable(attributes.actoolWrapper())
                .addTransitiveInputs(objcProvider.get(ASSET_CATALOG)).addOutput(zipOutput)
                .addOutput(actoolPartialInfoplist)
                .setCommandLine(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()
                // The next three arguments are positional, i.e. they don't have flags before them.
                .addPath(zipOutput.getExecPath()).add("--platform")
                .add(appleConfiguration.getMultiArchPlatform(platformType).getLowerCaseNameInPlist())
                .addExecPath("--output-partial-info-plist", partialInfoPlist).add("--minimum-deployment-target")
                .add(bundling.getMinimumOsVersion().toString());

        for (TargetDeviceFamily targetDeviceFamily : targetDeviceFamiliesForResources()) {
            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.
     *
     * <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);
        }
    }
}