// Copyright 2016 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.collect.nestedset.Order.STABLE_ORDER; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; 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.ActionEnvironment; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionExecutionException; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionOwner; import com.google.devtools.build.lib.actions.ActionResult; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.ArtifactResolver; import com.google.devtools.build.lib.actions.CommandLineExpansionException; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.ResourceSet; import com.google.devtools.build.lib.actions.RunfilesSupplier; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.extra.SpawnInfo; import com.google.devtools.build.lib.analysis.actions.CommandLine; import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.ProfilerTask; import com.google.devtools.build.lib.rules.apple.ApplePlatform; import com.google.devtools.build.lib.rules.apple.XcodeConfigProvider; import com.google.devtools.build.lib.rules.cpp.CppCompileAction.DotdFile; import com.google.devtools.build.lib.rules.cpp.CppFileTypes; import com.google.devtools.build.lib.rules.cpp.HeaderDiscovery; import com.google.devtools.build.lib.rules.cpp.IncludeScanningContext; import com.google.devtools.build.lib.util.DependencySet; import com.google.devtools.build.lib.util.Fingerprint; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Nullable; /** * An action that compiles objc or objc++ source. * *

We don't use a plain SpawnAction here because we implement .d input pruning, which requires * post-execution filtering of input artifacts. * *

Additionally the header thinning feature is implemented here which, like .d input pruning, * reduces the action inputs to just the required set of headers for improved performance. Unlike * dotd it does so via the discoverInputs mechanism before execution. * *

We don't use a CppCompileAction because the ObjcCompileAction uses custom logic instead of the * CROSSTOOL to construct its command line. */ public class ObjcCompileAction extends SpawnAction { /** * A spawn that provides all headers to sandboxed execution to allow pruned headers to be * re-introduced into action inputs. */ public class ObjcCompileActionSpawn extends ActionSpawn { public ObjcCompileActionSpawn(ImmutableList arguments, Map clientEnv) { super(arguments, clientEnv); } @Override public Iterable getInputFiles() { ImmutableList.Builder listBuilder = ImmutableList.builder().addAll(super.getInputFiles()); // Normally discoveredInputs should not be null when this is called, however that may occur if // the extra action feature is used if (discoveredInputs != null) { listBuilder.addAll(discoveredInputs); } return listBuilder.build(); } } private final DotdFile dotdFile; private final Artifact sourceFile; private final NestedSet mandatoryInputs; private final HeaderDiscovery.DotdPruningMode dotdPruningPlan; private final NestedSet headers; private final Artifact headersListFile; private Iterable discoveredInputs; private static final String GUID = "a00d5bac-a72c-4f0f-99a7-d5fdc6072137"; private ObjcCompileAction( ActionOwner owner, Iterable tools, Iterable inputs, Iterable outputs, ResourceSet resourceSet, CommandLine argv, boolean isShellCommand, ActionEnvironment env, ImmutableMap executionInfo, CharSequence progressMessage, RunfilesSupplier runfilesSupplier, String mnemonic, boolean executeUnconditionally, ExtraActionInfoSupplier extraActionInfoSupplier, DotdFile dotdFile, Artifact sourceFile, NestedSet mandatoryInputs, HeaderDiscovery.DotdPruningMode dotdPruningPlan, NestedSet headers, @Nullable Artifact headersListFile) { super( owner, tools, headersListFile == null ? inputs : mandatoryInputs, outputs, resourceSet, argv, isShellCommand, env, executionInfo, progressMessage, runfilesSupplier, mnemonic, executeUnconditionally, extraActionInfoSupplier); this.dotdFile = dotdFile; this.sourceFile = sourceFile; this.mandatoryInputs = mandatoryInputs; this.dotdPruningPlan = dotdPruningPlan; this.headers = headers; this.headersListFile = headersListFile; } private Iterable filterHeaderFiles() { ImmutableList.Builder inputs = ImmutableList.builder(); for (Artifact headerArtifact : headers) { if (CppFileTypes.OBJC_HEADER.matches(headerArtifact.getFilename()) // C++ headers can be extensionless || (!headerArtifact.isFileset() && headerArtifact.getExtension().isEmpty())) { inputs.add(headerArtifact); } } return inputs.build(); } /** Returns the DotdPruningPlan for this compile */ @VisibleForTesting public HeaderDiscovery.DotdPruningMode getDotdPruningPlan() { return dotdPruningPlan; } @Override public final Spawn getSpawn(Map clientEnv) { try { return new ObjcCompileActionSpawn( ImmutableList.copyOf(getCommandLine().arguments()), clientEnv); } catch (CommandLineExpansionException e) { throw new AssertionError("ObjcCompileAction command line expansion cannot fail"); } } @Override public boolean discoversInputs() { return true; } @Override public synchronized Iterable discoverInputs( ActionExecutionContext actionExecutionContext) throws ActionExecutionException, InterruptedException { if (headersListFile != null) { try { discoveredInputs = HeaderThinning.findRequiredHeaderInputs( sourceFile, headersListFile, getAllowedDerivedInputsMap(false)); } catch (ExecException e) { throw e.toActionExecutionException( "Header thinning of rule '" + getOwner().getLabel() + "'", actionExecutionContext.getVerboseFailures(), this); } } else { discoveredInputs = filterHeaderFiles(); } return discoveredInputs; } @Override public ImmutableSet getMandatoryOutputs() { return ImmutableSet.of(dotdFile.artifact()); } @Override public ActionResult execute(ActionExecutionContext actionExecutionContext) throws ActionExecutionException, InterruptedException { ActionResult actionResult = super.execute(actionExecutionContext); if (dotdPruningPlan == HeaderDiscovery.DotdPruningMode.USE) { IncludeScanningContext scanningContext = actionExecutionContext.getContext(IncludeScanningContext.class); NestedSet discoveredInputs = discoverInputsFromDotdFiles( actionExecutionContext.getExecRoot(), scanningContext.getArtifactResolver()); updateActionInputs(discoveredInputs); } else { // TODO(lberki): This is awkward, but necessary since updateInputs() must be called when // input discovery is in effect. I *think* it's possible to avoid setting discoversInputs() // to true if the header list file is null and then we'd not need to have this here, but I // haven't quite managed to get that right yet. updateActionInputs(getInputs()); } return actionResult; } @VisibleForTesting public NestedSet discoverInputsFromDotdFiles( Path execRoot, ArtifactResolver artifactResolver) throws ActionExecutionException { if (dotdFile == null) { return NestedSetBuilder.emptySet(Order.STABLE_ORDER); } return new HeaderDiscovery.Builder() .setAction(this) .setSourceFile(sourceFile) .setDependencies(processDepset(execRoot).getDependencies()) .setPermittedSystemIncludePrefixes(ImmutableList.of()) .setAllowedDerivedinputsMap(getAllowedDerivedInputsMap(true)) .build() .discoverInputsFromDependencies(execRoot, artifactResolver); } private DependencySet processDepset(Path execRoot) throws ActionExecutionException { try { DependencySet depSet = new DependencySet(execRoot); return depSet.read(dotdFile.getPath()); } catch (IOException e) { // Some kind of IO or parse exception--wrap & rethrow it to stop the build. throw new ActionExecutionException("error while parsing .d file", e, this, false); } } /** * Returns a map of input and header artifacts for this action. * * @param excludeSourceArtifacts If true artifacts where {@link Artifact#isSourceArtifact()} is * true are excluded from the map */ private Map getAllowedDerivedInputsMap(boolean excludeSourceArtifacts) { // LinkedHashMap required here as it is not guaranteed that entries are unique and to preserve // insertion order Map allowedDerivedInputMap = new LinkedHashMap<>(); for (Artifact artifact : Iterables.concat(mandatoryInputs, headers)) { if (!excludeSourceArtifacts || !artifact.isSourceArtifact()) { allowedDerivedInputMap.put(artifact.getExecPath(), artifact); } } return allowedDerivedInputMap; } @Override public Iterable getAllowedDerivedInputs() { return getAllowedDerivedInputsMap(true).values(); } /** * Recalculates this action's live input collection, including sources, middlemen. * * @throws ActionExecutionException iff any errors happen during update. */ @VisibleForTesting // productionVisibility = Visibility.PRIVATE @ThreadCompatible final void updateActionInputs(Iterable discoveredInputs) throws ActionExecutionException { Profiler.instance().startTask(ProfilerTask.ACTION_UPDATE, this); try { updateInputs(Iterables.concat(mandatoryInputs, discoveredInputs)); } finally { Profiler.instance().completeTask(ProfilerTask.ACTION_UPDATE); } } @Override protected SpawnInfo getExtraActionSpawnInfo() { SpawnInfo.Builder info = null; try { info = SpawnInfo.newBuilder(super.getExtraActionSpawnInfo()); } catch (CommandLineExpansionException e) { throw new AssertionError("ObjcCompileAction command line expansion cannot fail"); } if (!inputsDiscovered()) { for (Artifact headerArtifact : filterHeaderFiles()) { // As in SpawnAction#getExtraActionSpawnInfo explicitly ignore middleman artifacts here. if (!headerArtifact.isMiddlemanArtifact()) { info.addInputFile(headerArtifact.getExecPathString()); } } } return info.build(); } @Override public String computeKey() { Fingerprint f = new Fingerprint(); f.addString(GUID); try { f.addString(super.computeKey()); } catch (CommandLineExpansionException e) { throw new AssertionError("ObjcCompileAction command line expansion cannot fail"); } f.addBoolean(dotdFile == null || dotdFile.artifact() == null); f.addBoolean(dotdPruningPlan == HeaderDiscovery.DotdPruningMode.USE); f.addBoolean(headersListFile == null); if (dotdFile != null) { f.addPath(dotdFile.getSafeExecPath()); } return f.hexDigestAndReset(); } /** A Builder for ObjcCompileAction */ public static class Builder extends SpawnAction.Builder { private DotdFile dotdFile; private Artifact sourceFile; private Artifact headersListFile; private final NestedSetBuilder mandatoryInputs = new NestedSetBuilder<>(STABLE_ORDER); private HeaderDiscovery.DotdPruningMode dotdPruningPlan; private final NestedSetBuilder headers = NestedSetBuilder.stableOrder(); /** * Creates a new compile action builder with apple environment variables set that are typically * needed by the apple toolchain. */ public static ObjcCompileAction.Builder createObjcCompileActionBuilderWithAppleEnv( XcodeConfigProvider xcodeConfigProvider, ApplePlatform targetPlatform) { return (Builder) new ObjcCompileAction.Builder() .setExecutionInfo(ObjcRuleClasses.darwinActionExecutionRequirement()) .setEnvironment( ObjcRuleClasses.appleToolchainEnvironment(xcodeConfigProvider, targetPlatform)); } @Override public Builder addTools(Iterable artifacts) { super.addTools(artifacts); mandatoryInputs.addAll(artifacts); return this; } @Override public Builder addTransitiveTools(NestedSet artifacts) { super.addTransitiveTools(artifacts); mandatoryInputs.addTransitive(artifacts); return this; } /** Sets a .d file that will used to prune input headers */ public Builder setDotdFile(DotdFile dotdFile) { Preconditions.checkNotNull(dotdFile); this.dotdFile = dotdFile; return this; } /** * Sets a .headers_list file that is generated for the header thinning feature. File is used to * discover required inputs to compile action and update action inputs. */ public Builder setHeadersListFile(Artifact headersListFile) { Preconditions.checkNotNull(headersListFile); this.headersListFile = headersListFile; this.addMandatoryInput(headersListFile); return this; } /** Sets the source file that is being compiled in this action */ public Builder setSourceFile(Artifact sourceFile) { Preconditions.checkNotNull(sourceFile); this.sourceFile = sourceFile; this.mandatoryInputs.add(sourceFile); this.addInput(sourceFile); return this; } /** Add an input that cannot be pruned */ public Builder addMandatoryInput(Artifact input) { Preconditions.checkNotNull(input); this.mandatoryInputs.add(input); this.addInput(input); return this; } /** Add inputs that cannot be pruned */ public Builder addMandatoryInputs(Iterable input) { Preconditions.checkNotNull(input); this.mandatoryInputs.addAll(input); this.addInputs(input); return this; } /** Add inputs that cannot be pruned */ public Builder addTransitiveMandatoryInputs(NestedSet input) { Preconditions.checkNotNull(input); this.mandatoryInputs.addTransitive(input); this.addTransitiveInputs(input); return this; } /** Indicates that this compile action should perform .d pruning */ public Builder setDotdPruningPlan(HeaderDiscovery.DotdPruningMode dotdPruningPlan) { Preconditions.checkNotNull(dotdPruningPlan); this.dotdPruningPlan = dotdPruningPlan; return this; } /** Adds to the set of all possible headers that could be required by this compile action. */ public Builder addTransitiveHeaders(NestedSet headers) { this.headers.addTransitive(Preconditions.checkNotNull(headers)); this.addTransitiveInputs(headers); return this; } /** Adds to the set of all possible headers that could be required by this compile action. */ public Builder addHeaders(Iterable headers) { this.headers.addAll(Preconditions.checkNotNull(headers)); this.addInputs(headers); return this; } @Override protected SpawnAction createSpawnAction( ActionOwner owner, NestedSet tools, NestedSet inputsAndTools, ImmutableList outputs, ResourceSet resourceSet, CommandLine actualCommandLine, boolean isShellCommand, ActionEnvironment env, ImmutableMap executionInfo, CharSequence progressMessage, RunfilesSupplier runfilesSupplier, String mnemonic) { return new ObjcCompileAction( owner, tools, inputsAndTools, outputs, resourceSet, actualCommandLine, isShellCommand, // TODO(#3320): This is missing the inherited action env from --action_env. ActionEnvironment.create(env.getFixedEnv()), executionInfo, progressMessage, runfilesSupplier, mnemonic, executeUnconditionally, extraActionInfoSupplier, dotdFile, sourceFile, mandatoryInputs.build(), dotdPruningPlan, headers.build(), headersListFile); } } }