// Copyright 2014 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.BUNDLE_IMPORT_DIR; import static com.google.devtools.build.lib.rules.objc.ObjcProvider.DEFINE; import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FORCE_LOAD_FOR_XCODEGEN; import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_DIR; import static com.google.devtools.build.lib.rules.objc.ObjcProvider.GENERAL_RESOURCE_FILE; import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_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.ObjcProvider.XCASSETS_DIR; import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCDATAMODEL; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag; import com.google.devtools.build.lib.syntax.Label; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.xcode.util.Interspersing; import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.DependencyControl; import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.TargetControl; import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.Set; /** * Provider which provides transitive dependency information that is specific to Xcodegen. In * particular, it provides a sequence of targets which can be used to create a self-contained * {@code .xcodeproj} file. */ @Immutable public final class XcodeProvider implements TransitiveInfoProvider { /** * A builder for instances of {@link XcodeProvider}. */ public static final class Builder { private Label label; private final NestedSetBuilder userHeaderSearchPaths = NestedSetBuilder.stableOrder(); private final NestedSetBuilder headerSearchPaths = NestedSetBuilder.stableOrder(); private Optional infoplistMerging = Optional.absent(); private final NestedSetBuilder dependencies = NestedSetBuilder.stableOrder(); private final ImmutableList.Builder xcodeprojBuildSettings = new ImmutableList.Builder<>(); private final ImmutableList.Builder copts = new ImmutableList.Builder<>(); private final ImmutableList.Builder compilationModeCopts = new ImmutableList.Builder<>(); private XcodeProductType productType; private final ImmutableList.Builder headers = new ImmutableList.Builder<>(); private Optional compilationArtifacts = Optional.absent(); private ObjcProvider objcProvider; private Optional testHost = Optional.absent(); private final NestedSetBuilder inputsToXcodegen = NestedSetBuilder.stableOrder(); /** * Sets the label of the build target which corresponds to this Xcode target. */ public Builder setLabel(Label label) { this.label = label; return this; } /** * Adds user header search paths for this target. */ public Builder addUserHeaderSearchPaths(Iterable userHeaderSearchPaths) { this.userHeaderSearchPaths.addAll(rootEach("$(WORKSPACE_ROOT)", userHeaderSearchPaths)); return this; } /** * Adds header search paths for this target. Each path is interpreted relative to the given * root, such as {@code "$(WORKSPACE_ROOT)"}. */ public Builder addHeaderSearchPaths(String root, Iterable paths) { this.headerSearchPaths.addAll(rootEach(root, paths)); return this; } /** * Sets the Info.plist merging information. Used for applications. May be * absent for other bundles. */ public Builder setInfoplistMerging(InfoplistMerging infoplistMerging) { this.infoplistMerging = Optional.of(infoplistMerging); return this; } /** * Adds items in the {@link NestedSet}s of the given target to the corresponding sets in this * builder. This is useful if the given target is a dependency or like a dependency * (e.g. a test host). The given provider is not registered as a dependency with this provider. */ private void addTransitiveSets(XcodeProvider dependencyish) { inputsToXcodegen.addTransitive(dependencyish.inputsToXcodegen); userHeaderSearchPaths.addTransitive(dependencyish.userHeaderSearchPaths); headerSearchPaths.addTransitive(dependencyish.headerSearchPaths); } /** * Adds {@link XcodeProvider}s corresponding to direct dependencies of this target which should * be added in the {@code .xcodeproj} file. */ public Builder addDependencies(Iterable dependencies) { for (XcodeProvider dependency : dependencies) { this.dependencies.add(dependency); this.dependencies.addTransitive(dependency.dependencies); this.addTransitiveSets(dependency); } return this; } /** * Adds additional build settings of this target. */ public Builder addXcodeprojBuildSettings( Iterable xcodeprojBuildSettings) { this.xcodeprojBuildSettings.addAll(xcodeprojBuildSettings); return this; } /** * Sets the copts to use when compiling the Xcode target. */ public Builder addCopts(Iterable copts) { this.copts.addAll(copts); return this; } /** * Sets the copts derived from compilation mode to use when compiling the Xcode target. These * will be included before the DEFINE options. */ public Builder addCompilationModeCopts(Iterable copts) { this.compilationModeCopts.addAll(copts); return this; } /** * Sets the product type for the PBXTarget in the .xcodeproj file. */ public Builder setProductType(XcodeProductType productType) { this.productType = productType; return this; } /** * Adds to the header files of this target. It needs not to include the header files of * dependencies. */ public Builder addHeaders(Iterable headers) { this.headers.addAll(headers); return this; } /** * The compilation artifacts for this target. */ public Builder setCompilationArtifacts(CompilationArtifacts compilationArtifacts) { this.compilationArtifacts = Optional.of(compilationArtifacts); return this; } /** * Sets the {@link ObjcProvider} corresponding to this target. */ public Builder setObjcProvider(ObjcProvider objcProvider) { this.objcProvider = objcProvider; return this; } /** * Sets the test host. This is used for xctest targets. */ public Builder setTestHost(XcodeProvider testHost) { Preconditions.checkState(!this.testHost.isPresent()); this.testHost = Optional.of(testHost); this.addTransitiveSets(testHost); return this; } /** * Adds inputs that are passed to Xcodegen when generating the project file. */ public Builder addInputsToXcodegen(Iterable inputsToXcodegen) { this.inputsToXcodegen.addAll(inputsToXcodegen); return this; } public XcodeProvider build() { Preconditions.checkArgument( !testHost.isPresent() || (productType == XcodeProductType.UNIT_TEST), "%s product types cannot have a test host (test host: %s).", productType, testHost); return new XcodeProvider(this); } } /** * A collection of top-level targets that can be used to create a complete project. */ public static final class Project { private final NestedSet inputsToXcodegen; private final ImmutableList topLevelTargets; private Project( NestedSet inputsToXcodegen, ImmutableList topLevelTargets) { this.inputsToXcodegen = inputsToXcodegen; this.topLevelTargets = topLevelTargets; } public static Project fromTopLevelTarget(XcodeProvider topLevelTarget) { return fromTopLevelTargets(ImmutableList.of(topLevelTarget)); } public static Project fromTopLevelTargets(Iterable topLevelTargets) { NestedSetBuilder inputsToXcodegen = NestedSetBuilder.stableOrder(); for (XcodeProvider target : topLevelTargets) { inputsToXcodegen.addTransitive(target.inputsToXcodegen); } return new Project(inputsToXcodegen.build(), ImmutableList.copyOf(topLevelTargets)); } /** * Returns artifacts that are passed to the Xcodegen action when generating a project file that * contains all of the given targets. */ public NestedSet getInputsToXcodegen() { return inputsToXcodegen; } public ImmutableList getTopLevelTargets() { return topLevelTargets; } /** * Returns all the target controls that must be added to the xcodegen control. No other target * controls are needed to generate a functional project file. This method creates a new list * whenever it is called. */ public ImmutableList targets() { // Collect all the dependencies of all the providers, filtering out duplicates. Set providerSet = new LinkedHashSet<>(); for (XcodeProvider target : topLevelTargets) { Iterables.addAll(providerSet, target.providers()); } ImmutableList.Builder controls = new ImmutableList.Builder<>(); for (XcodeProvider provider : providerSet) { controls.add(provider.targetControl()); } return controls.build(); } } private final Label label; private final NestedSet userHeaderSearchPaths; private final NestedSet headerSearchPaths; private final Optional infoplistMerging; private final NestedSet dependencies; private final ImmutableList xcodeprojBuildSettings; private final ImmutableList copts; private final ImmutableList compilationModeCopts; private final XcodeProductType productType; private final ImmutableList headers; private final Optional compilationArtifacts; private final ObjcProvider objcProvider; private final Optional testHost; private final NestedSet inputsToXcodegen; private XcodeProvider(Builder builder) { this.label = Preconditions.checkNotNull(builder.label); this.userHeaderSearchPaths = builder.userHeaderSearchPaths.build(); this.headerSearchPaths = builder.headerSearchPaths.build(); this.infoplistMerging = builder.infoplistMerging; this.dependencies = builder.dependencies.build(); this.xcodeprojBuildSettings = builder.xcodeprojBuildSettings.build(); this.copts = builder.copts.build(); this.compilationModeCopts = builder.compilationModeCopts.build(); this.productType = Preconditions.checkNotNull(builder.productType); this.headers = builder.headers.build(); this.compilationArtifacts = builder.compilationArtifacts; this.objcProvider = Preconditions.checkNotNull(builder.objcProvider); this.testHost = Preconditions.checkNotNull(builder.testHost); this.inputsToXcodegen = builder.inputsToXcodegen.build(); } /** * Creates a builder whose values are all initialized to this provider. */ public Builder toBuilder() { Builder builder = new Builder(); builder.label = label; builder.userHeaderSearchPaths.addAll(userHeaderSearchPaths); builder.headerSearchPaths.addTransitive(headerSearchPaths); builder.infoplistMerging = infoplistMerging; builder.dependencies.addTransitive(dependencies); builder.xcodeprojBuildSettings.addAll(xcodeprojBuildSettings); builder.copts.addAll(copts); builder.productType = productType; builder.headers.addAll(headers); builder.compilationArtifacts = compilationArtifacts; builder.objcProvider = objcProvider; builder.testHost = testHost; builder.inputsToXcodegen.addTransitive(inputsToXcodegen); return builder; } /** * Returns a list of this provider and all its transitive dependencies. */ private Iterable providers() { Set providers = new LinkedHashSet<>(); providers.add(this); Iterables.addAll(providers, dependencies); for (XcodeProvider justTestHost : testHost.asSet()) { providers.add(justTestHost); Iterables.addAll(providers, justTestHost.dependencies); } return ImmutableList.copyOf(providers); } private static final EnumSet CAN_LINK_PRODUCT_TYPES = EnumSet.of( XcodeProductType.APPLICATION, XcodeProductType.BUNDLE, XcodeProductType.UNIT_TEST); private TargetControl targetControl() { String buildFilePath = label.getPackageFragment().getSafePathString() + "/BUILD"; // TODO(bazel-team): Add provisioning profile information when Xcodegen supports it. TargetControl.Builder targetControl = TargetControl.newBuilder() .setName(label.getName()) .setLabel(label.toString()) .setProductType(productType.getIdentifier()) .addAllImportedLibrary(Artifact.toExecPaths(objcProvider.get(IMPORTED_LIBRARY))) .addAllUserHeaderSearchPath(userHeaderSearchPaths) .addAllHeaderSearchPath(headerSearchPaths) .addAllSupportFile(Artifact.toExecPaths(headers)) .addAllCopt(compilationModeCopts) .addAllCopt(Interspersing.prependEach("-D", objcProvider.get(DEFINE))) .addAllCopt(copts) .addAllLinkopt( Interspersing.beforeEach("-force_load", objcProvider.get(FORCE_LOAD_FOR_XCODEGEN))) .addAllLinkopt(IosSdkCommands.DEFAULT_LINKER_FLAGS) .addAllLinkopt(Interspersing.beforeEach( "-weak_framework", SdkFramework.names(objcProvider.get(WEAK_SDK_FRAMEWORK)))) .addAllBuildSetting(xcodeprojBuildSettings) .addAllBuildSetting(IosSdkCommands.defaultWarningsForXcode()) .addAllSdkFramework(SdkFramework.names(objcProvider.get(SDK_FRAMEWORK))) .addAllFramework(PathFragment.safePathStrings(objcProvider.get(FRAMEWORK_DIR))) .addAllXcassetsDir(PathFragment.safePathStrings(objcProvider.get(XCASSETS_DIR))) .addAllXcdatamodel(PathFragment.safePathStrings( Xcdatamodel.xcdatamodelDirs(objcProvider.get(XCDATAMODEL)))) .addAllBundleImport(PathFragment.safePathStrings(objcProvider.get(BUNDLE_IMPORT_DIR))) .addAllSdkDylib(objcProvider.get(SDK_DYLIB)) .addAllGeneralResourceFile(Artifact.toExecPaths(objcProvider.get(GENERAL_RESOURCE_FILE))) .addSupportFile(buildFilePath); if (CAN_LINK_PRODUCT_TYPES.contains(productType)) { for (XcodeProvider dependency : dependencies) { // Only add a library target to a binary's dependencies if it has source files to compile. // Xcode cannot build targets without a source file in the PBXSourceFilesBuildPhase, so if // such a target is present in the control file, it is only to get Xcodegen to put headers // and resources not used by the final binary in the Project Navigator. // // The exception to this rule is the objc_bundle_library target. Bundles are generally used // for resources and can lack a PBXSourceFilesBuildPhase in the project file and still be // considered valid by Xcode. boolean hasSources = dependency.compilationArtifacts.isPresent() && dependency.compilationArtifacts.get().getArchive().isPresent(); if (hasSources || (dependency.productType == XcodeProductType.BUNDLE)) { targetControl.addDependency(DependencyControl.newBuilder() .setTargetLabel(dependency.label.toString()) .build()); } } for (XcodeProvider justTestHost : testHost.asSet()) { targetControl.addDependency(DependencyControl.newBuilder() .setTargetLabel(justTestHost.label.toString()) .setTestHost(true) .build()); } } for (InfoplistMerging merging : infoplistMerging.asSet()) { for (Artifact infoplist : merging.getPlistWithEverything().asSet()) { targetControl.setInfoplist(infoplist.getExecPathString()); } } for (CompilationArtifacts artifacts : compilationArtifacts.asSet()) { targetControl .addAllSourceFile(Artifact.toExecPaths(artifacts.getSrcs())) .addAllNonArcSourceFile(Artifact.toExecPaths(artifacts.getNonArcSrcs())); for (Artifact pchFile : artifacts.getPchFile().asSet()) { targetControl .setPchPath(pchFile.getExecPathString()) .addSupportFile(pchFile.getExecPathString()); } } if (objcProvider.is(Flag.USES_CPP)) { targetControl.addSdkDylib("libc++"); } return targetControl.build(); } /** * Prepends the given path to each path in {@code paths}. Empty paths are * transformed to the value of {@code variable} rather than {@code variable + "/."} */ @VisibleForTesting static Iterable rootEach(final String prefix, Iterable paths) { Preconditions.checkArgument(prefix.startsWith("$"), "prefix should start with a build setting variable like '$(NAME)': %s", prefix); Preconditions.checkArgument(!prefix.endsWith("/"), "prefix should not end with '/': %s", prefix); return Iterables.transform(paths, new Function() { @Override public String apply(PathFragment input) { if (input.getSafePathString().equals(".")) { return prefix; } else { return prefix + "/" + input.getSafePathString(); } } }); } }