// 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.DEFINE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FORCE_LOAD_LIBRARY;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_DIR;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_FILE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag.USES_CPP;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.HEADER;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.INCLUDE;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LIBRARY;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_DYLIB;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_FRAMEWORK;
import static com.google.devtools.build.lib.rules.objc.ObjcProvider.WEAK_SDK_FRAMEWORK;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.CLANG;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.CLANG_PLUSPLUS;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.DSYMUTIL;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.NON_ARC_SRCS_TYPE;
import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.SRCS_TYPE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Action;
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.analysis.actions.ActionConstructionContext;
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.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.rules.objc.ObjcCommon.CompilationAttributes;
import com.google.devtools.build.lib.rules.objc.XcodeProvider.Builder;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.List;
/**
* Support for rules that compile sources. Provides ways to determine files that should be output,
* registering Xcode settings and generating the various actions that might be needed for
* compilation.
*
*
Methods on this class can be called in any order without impacting the result.
*/
final class CompilationSupport {
@VisibleForTesting
static final String ABSOLUTE_INCLUDES_PATH_FORMAT =
"The path '%s' is absolute, but only relative paths are allowed.";
@VisibleForTesting
static final ImmutableList LINKER_COVERAGE_FLAGS = ImmutableList.of(
"-ftest-coverage", "-fprofile-arcs");
@VisibleForTesting
static final ImmutableList CLANG_COVERAGE_FLAGS =
ImmutableList.of("-fprofile-arcs", "-ftest-coverage", "-fprofile-dir=./coverage_output");
/**
* Iterable wrapper providing strong type safety for arguments to binary linking.
*/
static final class ExtraLinkArgs extends IterableWrapper {
ExtraLinkArgs(String... args) {
super(args);
}
}
/**
* Returns information about the given rule's compilation artifacts.
*/
// TODO(bazel-team): Remove this information from ObjcCommon and move it internal to this class.
static CompilationArtifacts compilationArtifacts(RuleContext ruleContext) {
return new CompilationArtifacts.Builder()
.addSrcs(ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET)
.errorsForNonMatching(SRCS_TYPE)
.list())
.addNonArcSrcs(ruleContext.getPrerequisiteArtifacts("non_arc_srcs", Mode.TARGET)
.errorsForNonMatching(NON_ARC_SRCS_TYPE)
.list())
.setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext))
.setPchFile(Optional.fromNullable(ruleContext.getPrerequisiteArtifact("pch", Mode.TARGET)))
.build();
}
private final RuleContext ruleContext;
private final CompilationAttributes attributes;
/**
* Creates a new compilation support for the given rule.
*/
CompilationSupport(RuleContext ruleContext) {
this.ruleContext = ruleContext;
this.attributes = new CompilationAttributes(ruleContext);
}
/**
* Registers all actions necessary to compile this rule's sources and archive them.
*
* @param common common information about this rule and its dependencies
* @param optionsProvider option and plist information about this rule and its dependencies
*
* @return this compilation support
*/
CompilationSupport registerCompileAndArchiveActions(
ObjcCommon common, OptionsProvider optionsProvider) {
if (common.getCompilationArtifacts().isPresent()) {
registerCompileAndArchiveActions(
common.getCompilationArtifacts().get(),
ObjcRuleClasses.intermediateArtifacts(ruleContext),
common.getObjcProvider(), optionsProvider,
ruleContext.getConfiguration().isCodeCoverageEnabled());
}
return this;
}
/**
* Creates actions to compile each source file individually, and link all the compiled object
* files into a single archive library.
*/
private void registerCompileAndArchiveActions(CompilationArtifacts compilationArtifacts,
IntermediateArtifacts intermediateArtifacts,
ObjcProvider objcProvider, OptionsProvider optionsProvider, boolean isCodeCoverageEnabled) {
ImmutableList.Builder objFiles = new ImmutableList.Builder<>();
for (Artifact sourceFile : compilationArtifacts.getSrcs()) {
Artifact objFile = intermediateArtifacts.objFile(sourceFile);
objFiles.add(objFile);
registerCompileAction(sourceFile, objFile, compilationArtifacts.getPchFile(),
objcProvider, intermediateArtifacts, ImmutableList.of("-fobjc-arc"), optionsProvider,
isCodeCoverageEnabled);
}
for (Artifact nonArcSourceFile : compilationArtifacts.getNonArcSrcs()) {
Artifact objFile = intermediateArtifacts.objFile(nonArcSourceFile);
objFiles.add(objFile);
registerCompileAction(nonArcSourceFile, objFile, compilationArtifacts.getPchFile(),
objcProvider, intermediateArtifacts, ImmutableList.of("-fno-objc-arc"), optionsProvider,
isCodeCoverageEnabled);
}
for (Artifact archive : compilationArtifacts.getArchive().asSet()) {
registerArchiveActions(intermediateArtifacts, objFiles, archive);
}
}
private void registerCompileAction(
Artifact sourceFile,
Artifact objFile,
Optional pchFile,
ObjcProvider objcProvider,
IntermediateArtifacts intermediateArtifacts,
Iterable otherFlags,
OptionsProvider optionsProvider,
boolean isCodeCoverageEnabled) {
ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
ImmutableList.Builder coverageFlags = new ImmutableList.Builder<>();
ImmutableList.Builder gcnoFiles = new ImmutableList.Builder<>();
if (isCodeCoverageEnabled) {
coverageFlags.addAll(CLANG_COVERAGE_FLAGS);
gcnoFiles.add(intermediateArtifacts.gcnoFile(sourceFile));
}
CustomCommandLine.Builder commandLine = new CustomCommandLine.Builder();
if (ObjcRuleClasses.CPP_SOURCES.matches(sourceFile.getExecPath())) {
commandLine.add("-stdlib=libc++");
}
commandLine
.add(IosSdkCommands.compileFlagsForClang(objcConfiguration))
.add(IosSdkCommands.commonLinkAndCompileFlagsForClang(
objcProvider, objcConfiguration))
.add(objcConfiguration.getCoptsForCompilationMode())
.addBeforeEachPath(
"-iquote", ObjcCommon.userHeaderSearchPaths(ruleContext.getConfiguration()))
.addBeforeEachExecPath("-include", pchFile.asSet())
.addBeforeEachPath("-I", objcProvider.get(INCLUDE))
.add(otherFlags)
.addFormatEach("-D%s", objcProvider.get(DEFINE))
.add(coverageFlags.build())
.add(objcConfiguration.getCopts())
.add(optionsProvider.getCopts())
.addExecPath("-c", sourceFile)
.addExecPath("-o", objFile);
ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("ObjcCompile")
.setExecutable(CLANG)
.setCommandLine(commandLine.build())
.addInput(sourceFile)
.addOutput(objFile)
.addOutputs(gcnoFiles.build())
.addTransitiveInputs(objcProvider.get(HEADER))
.addTransitiveInputs(objcProvider.get(FRAMEWORK_FILE))
.addInputs(pchFile.asSet())
.build(ruleContext));
}
private void registerArchiveActions(IntermediateArtifacts intermediateArtifacts,
ImmutableList.Builder objFiles, Artifact archive) {
for (Action action : archiveActions(ruleContext, objFiles.build(), archive,
ObjcRuleClasses.objcConfiguration(ruleContext),
intermediateArtifacts.objList())) {
ruleContext.registerAction(action);
}
}
private static Iterable archiveActions(
ActionConstructionContext context,
Iterable objFiles,
Artifact archive,
ObjcConfiguration objcConfiguration,
Artifact objList) {
ImmutableList.Builder actions = new ImmutableList.Builder<>();
actions.add(new FileWriteAction(
context.getActionOwner(),
objList,
Artifact.joinExecPaths("\n", objFiles),
/*makeExecutable=*/ false));
actions.add(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("ObjcLink")
.setExecutable(ObjcRuleClasses.LIBTOOL)
.setCommandLine(new CustomCommandLine.Builder()
.add("-static")
.add("-filelist").add(objList.getExecPathString())
.add("-arch_only").add(objcConfiguration.getIosCpu())
.add("-syslibroot").add(IosSdkCommands.sdkDir(objcConfiguration))
.add("-o").add(archive.getExecPathString())
.build())
.addInputs(objFiles)
.addInput(objList)
.addOutput(archive)
.build(context));
return actions.build();
}
/**
* Registers any actions necessary to link this rule and its dependencies. Debug symbols are
* generated if {@link ObjcConfiguration#generateDebugSymbols()} is set.
*
* @param objcProvider common information about this rule's attributes and its dependencies
* @param extraLinkArgs any additional arguments to pass to the linker
* @param extraLinkInputs any additional input artifacts to pass to the link action
*
* @return this compilation support
*/
CompilationSupport registerLinkActions(ObjcProvider objcProvider, ExtraLinkArgs extraLinkArgs,
Iterable extraLinkInputs) {
IntermediateArtifacts intermediateArtifacts =
ObjcRuleClasses.intermediateArtifacts(ruleContext);
Optional dsymBundle;
if (ObjcRuleClasses.objcConfiguration(ruleContext).generateDebugSymbols()) {
registerDsymActions();
dsymBundle = Optional.of(intermediateArtifacts.dsymBundle());
} else {
dsymBundle = Optional.absent();
}
registerLinkAction(objcProvider, extraLinkArgs, extraLinkInputs, dsymBundle);
return this;
}
private void registerLinkAction(ObjcProvider objcProvider, ExtraLinkArgs extraLinkArgs,
Iterable extraLinkInputs, Optional dsymBundle) {
Artifact linkedBinary =
ObjcRuleClasses.intermediateArtifacts(ruleContext).singleArchitectureBinary();
ruleContext.registerAction(
ObjcActionsBuilder.spawnOnDarwinActionBuilder()
.setMnemonic("ObjcLink")
.setShellCommand(ImmutableList.of("/bin/bash", "-c"))
.setCommandLine(linkCommandLine(extraLinkArgs, objcProvider, linkedBinary, dsymBundle))
.addOutput(linkedBinary)
.addOutputs(dsymBundle.asSet())
.addTransitiveInputs(objcProvider.get(LIBRARY))
.addTransitiveInputs(objcProvider.get(IMPORTED_LIBRARY))
.addTransitiveInputs(objcProvider.get(FRAMEWORK_FILE))
.addInputs(extraLinkInputs)
.build(ruleContext));
}
private static final String FRAMEWORK_SUFFIX = ".framework";
private CommandLine linkCommandLine(ExtraLinkArgs extraLinkArgs,
ObjcProvider objcProvider, Artifact linkedBinary, Optional dsymBundle) {
ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
final CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
if (objcProvider.is(USES_CPP)) {
commandLine
.addPath(CLANG_PLUSPLUS)
.add("-stdlib=libc++");
} else {
commandLine.addPath(CLANG);
}
commandLine
.add(IosSdkCommands.commonLinkAndCompileFlagsForClang(objcProvider, objcConfiguration))
.add("-Xlinker").add("-objc_abi_version")
.add("-Xlinker").add("2")
.add("-fobjc-link-runtime")
.add(IosSdkCommands.DEFAULT_LINKER_FLAGS)
.addBeforeEach("-framework", frameworkNames(objcProvider))
.addBeforeEach("-weak_framework", SdkFramework.names(objcProvider.get(WEAK_SDK_FRAMEWORK)))
.addExecPath("-o", linkedBinary)
.addExecPaths(objcProvider.get(LIBRARY))
.addExecPaths(objcProvider.get(IMPORTED_LIBRARY))
.add(dylibPaths(objcProvider))
.addBeforeEach("-force_load", Artifact.toExecPaths(objcProvider.get(FORCE_LOAD_LIBRARY)))
.add(extraLinkArgs)
.build();
if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
commandLine.add(LINKER_COVERAGE_FLAGS);
}
// Call to dsymutil for debug symbol generation must happen in the link action.
// All debug symbol information is encoded in object files inside archive files. To generate
// the debug symbol bundle, dsymutil will look inside the linked binary for the encoded
// absolute paths to archive files, which are only valid in the link action.
if (dsymBundle.isPresent()) {
commandLine
.add("&&")
.addPath(DSYMUTIL)
.add(linkedBinary.getExecPathString())
.addExecPath("-o", dsymBundle.get());
}
return new SingleArgCommandLine(commandLine.build());
}
/**
* Command line that converts its input's arg array to a single input.
*
* Required as a hack to the link command line because that may contain two commands, which are
* then passed to {@code /bin/bash -c}, and accordingly need to be a single argument.
*/
private static class SingleArgCommandLine extends CommandLine {
private final CommandLine original;
private SingleArgCommandLine(CommandLine original) {
this.original = original;
}
@Override
public Iterable arguments() {
return ImmutableList.of(Joiner.on(' ').join(original.arguments()));
}
}
private Iterable dylibPaths(ObjcProvider objcProvider) {
ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
ImmutableList.Builder args = new ImmutableList.Builder<>();
for (String dylib : objcProvider.get(SDK_DYLIB)) {
args.add(String.format(
"%s/usr/lib/%s.dylib", IosSdkCommands.sdkDir(objcConfiguration), dylib));
}
return args.build();
}
/**
* All framework names to pass to the linker using {@code -framework} flags. For a framework in
* the directory foo/bar.framework, the name is "bar". Each framework is found without using the
* full path by means of the framework search paths. The search paths are added by
* {@link IosSdkCommands#commonLinkAndCompileFlagsForClang(ObjcProvider, ObjcConfiguration)}).
*
* It's awful that we can't pass the full path to the framework and avoid framework search
* paths, but this is imposed on us by clang. clang does not support passing the full path to the
* framework, so Bazel cannot do it either.
*/
private Iterable frameworkNames(ObjcProvider provider) {
List names = new ArrayList<>();
Iterables.addAll(names, SdkFramework.names(provider.get(SDK_FRAMEWORK)));
for (PathFragment frameworkDir : provider.get(FRAMEWORK_DIR)) {
String segment = frameworkDir.getBaseName();
Preconditions.checkState(segment.endsWith(FRAMEWORK_SUFFIX),
"expect %s to end with %s, but it does not", segment, FRAMEWORK_SUFFIX);
names.add(segment.substring(0, segment.length() - FRAMEWORK_SUFFIX.length()));
}
return names;
}
/**
* Registers actions that compile and archive j2Objc dependencies of this rule.
*
* @param optionsProvider option and plist information about this rule and its dependencies
* @param objcProvider common information about this rule's attributes and its dependencies
*
* @return this compilation support
*/
CompilationSupport registerJ2ObjcCompileAndArchiveActions(
OptionsProvider optionsProvider, ObjcProvider objcProvider) {
for (J2ObjcSource j2ObjcSource : ObjcRuleClasses.j2ObjcSrcsProvider(ruleContext).getSrcs()) {
IntermediateArtifacts intermediateArtifacts =
ObjcRuleClasses.j2objcIntermediateArtifacts(ruleContext, j2ObjcSource);
CompilationArtifacts compilationArtifact = new CompilationArtifacts.Builder()
.addNonArcSrcs(j2ObjcSource.getObjcSrcs())
.setIntermediateArtifacts(intermediateArtifacts)
.setPchFile(Optional.absent())
.build();
registerCompileAndArchiveActions(compilationArtifact, intermediateArtifacts, objcProvider,
optionsProvider, ruleContext.getConfiguration().isCodeCoverageEnabled());
}
return this;
}
/**
* Sets compilation-related Xcode project information on the given provider builder.
*
* @param common common information about this rule's attributes and its dependencies
* @param optionsProvider option and plist information about this rule and its dependencies
* @return this compilation support
*/
CompilationSupport addXcodeSettings(Builder xcodeProviderBuilder,
ObjcCommon common, OptionsProvider optionsProvider) {
ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
for (CompilationArtifacts artifacts : common.getCompilationArtifacts().asSet()) {
xcodeProviderBuilder.setCompilationArtifacts(artifacts);
}
xcodeProviderBuilder
.addHeaders(attributes.hdrs())
.addUserHeaderSearchPaths(ObjcCommon.userHeaderSearchPaths(ruleContext.getConfiguration()))
.addHeaderSearchPaths("$(WORKSPACE_ROOT)", attributes.headerSearchPaths())
.addHeaderSearchPaths("$(SDKROOT)/usr/include", attributes.sdkIncludes())
.addCompilationModeCopts(objcConfiguration.getCoptsForCompilationMode())
.addCopts(objcConfiguration.getCopts())
.addCopts(optionsProvider.getCopts());
return this;
}
/**
* Validates compilation-related attributes on this rule.
*
* @return this compilation support
*/
CompilationSupport validateAttributes() {
for (PathFragment absoluteInclude :
Iterables.filter(attributes.includes(), PathFragment.IS_ABSOLUTE)) {
ruleContext.attributeError(
"includes", String.format(ABSOLUTE_INCLUDES_PATH_FORMAT, absoluteInclude));
}
return this;
}
private CompilationSupport registerDsymActions() {
IntermediateArtifacts intermediateArtifacts =
ObjcRuleClasses.intermediateArtifacts(ruleContext);
Artifact dsymBundle = intermediateArtifacts.dsymBundle();
Artifact debugSymbolFile = intermediateArtifacts.dsymSymbol();
ruleContext.registerAction(new SpawnAction.Builder()
.setMnemonic("UnzipDsym")
.setProgressMessage("Unzipping dSYM file: " + ruleContext.getLabel())
.setExecutable(new PathFragment("/usr/bin/unzip"))
.addInput(dsymBundle)
.setCommandLine(CustomCommandLine.builder()
.add(dsymBundle.getExecPathString())
.add("-d")
.add(stripSuffix(dsymBundle.getExecPathString(),
IntermediateArtifacts.TMP_DSYM_BUNDLE_SUFFIX) + ".app.dSYM")
.build())
.addOutput(intermediateArtifacts.dsymPlist())
.addOutput(debugSymbolFile)
.build(ruleContext));
Artifact dumpsyms = ruleContext.getPrerequisiteArtifact(":dumpsyms", Mode.HOST);
Artifact breakpadFile = intermediateArtifacts.breakpadSym();
ruleContext.registerAction(new SpawnAction.Builder()
.setMnemonic("GenBreakpad")
.setProgressMessage("Generating breakpad file: " + ruleContext.getLabel())
.setShellCommand(ImmutableList.of("/bin/bash", "-c"))
.setExecutionInfo(ImmutableMap.of(ExecutionRequirements.REQUIRES_DARWIN, ""))
.addInput(dumpsyms)
.addInput(debugSymbolFile)
.addArgument(String.format("%s %s > %s",
ShellUtils.shellEscape(dumpsyms.getExecPathString()),
ShellUtils.shellEscape(debugSymbolFile.getExecPathString()),
ShellUtils.shellEscape(breakpadFile.getExecPathString())))
.addOutput(breakpadFile)
.build(ruleContext));
return this;
}
private String stripSuffix(String str, String suffix) {
// TODO(bazel-team): Throw instead of returning null?
return str.endsWith(suffix) ? str.substring(0, str.length() - suffix.length()) : null;
}
}