// Copyright 2014 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.xcode.xcodegen;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
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.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.escape.Escaper;
import com.google.common.escape.Escapers;
import com.google.devtools.build.xcode.common.XcodeprojPath;
import com.google.devtools.build.xcode.util.Containing;
import com.google.devtools.build.xcode.util.Equaling;
import com.google.devtools.build.xcode.util.Mapping;
import com.google.devtools.build.xcode.xcodegen.LibraryObjects.BuildPhaseBuilder;
import com.google.devtools.build.xcode.xcodegen.SourceFile.BuildType;
import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.Control;
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 com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSObject;
import com.dd.plist.NSString;
import com.facebook.buck.apple.xcode.GidGenerator;
import com.facebook.buck.apple.xcode.XcodeprojSerializer;
import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildFile;
import com.facebook.buck.apple.xcode.xcodeproj.PBXContainerItemProxy.ProxyType;
import com.facebook.buck.apple.xcode.xcodeproj.PBXCopyFilesBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXFileReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXFrameworksBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXNativeTarget;
import com.facebook.buck.apple.xcode.xcodeproj.PBXProject;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference.SourceTree;
import com.facebook.buck.apple.xcode.xcodeproj.PBXResourcesBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXShellScriptBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXSourcesBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget.ProductType;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTargetDependency;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
/**
* Utility code for generating Xcode project files.
*/
public class XcodeprojGeneration {
public static final String FILE_TYPE_ARCHIVE_LIBRARY = "archive.ar";
public static final String FILE_TYPE_WRAPPER_APPLICATION = "wrapper.application";
public static final String FILE_TYPE_WRAPPER_BUNDLE = "wrapper.cfbundle";
public static final String FILE_TYPE_APP_EXTENSION = "wrapper.app-extension";
public static final String FILE_TYPE_FRAMEWORK = "wrapper.frawework";
private static final String DEFAULT_OPTIONS_NAME = "Debug";
private static final Escaper QUOTE_ESCAPER = Escapers.builder().addEscape('"', "\\\"").build();
@VisibleForTesting
static final String APP_NEEDS_SOURCE_ERROR =
"Due to limitations in Xcode, application projects must have at least one source file.";
private XcodeprojGeneration() {
throw new UnsupportedOperationException("static-only");
}
/**
* Determines the relative path to the workspace root from the path of the project.pbxproj output
* file. An absolute path is preferred if available.
*/
static Path relativeWorkspaceRoot(Path pbxproj) {
int levelsToExecRoot = pbxproj.getParent().getParent().getNameCount();
return pbxproj.getFileSystem().getPath(Joiner
.on('/')
.join(Collections.nCopies(levelsToExecRoot, "..")));
}
/**
* Writes a project to an {@code OutputStream} in the correct encoding.
*/
public static void write(OutputStream out, PBXProject project) throws IOException {
XcodeprojSerializer ser = new XcodeprojSerializer(
new GidGenerator(ImmutableSet.<String>of()), project);
Writer outWriter = new OutputStreamWriter(out, StandardCharsets.UTF_8);
// toXMLPropertyList includes an XML encoding specification (UTF-8), which we specify above.
// Standard Xcodeproj files use the toASCIIPropertyList format, but Xcode will rewrite
// XML-encoded project files automatically when first opening them. We use XML to prevent
// encoding issues, since toASCIIPropertyList does not include the UTF-8 encoding comment, and
// Xcode by default apparently uses MacRoman.
// This encoding concern is probably why Buck also generates XML project files as well.
outWriter.write(ser.toPlist().toXMLPropertyList());
outWriter.flush();
}
private static final EnumSet<ProductType> SUPPORTED_PRODUCT_TYPES = EnumSet.of(
ProductType.STATIC_LIBRARY,
ProductType.APPLICATION,
ProductType.BUNDLE,
ProductType.UNIT_TEST,
ProductType.APP_EXTENSION,
ProductType.FRAMEWORK,
ProductType.WATCH_OS1_APPLICATION,
ProductType.WATCH_OS1_EXTENSION);
private static final EnumSet<ProductType> PRODUCT_TYPES_THAT_HAVE_A_BINARY = EnumSet.of(
ProductType.APPLICATION,
ProductType.BUNDLE,
ProductType.UNIT_TEST,
ProductType.APP_EXTENSION,
ProductType.FRAMEWORK,
ProductType.WATCH_OS1_APPLICATION,
ProductType.WATCH_OS1_EXTENSION);
/**
* Detects the product type of the given target based on multiple fields in {@code targetControl}.
* {@code productType} is set as a field on {@code PBXNativeTarget} objects in Xcode project
* files, and we support three values: {@link ProductType#APPLICATION},
* {@link ProductType#STATIC_LIBRARY}, and {@link ProductType#BUNDLE}. The product type is not
* only what xcodegen sets the {@code productType} field to - it also dictates what can be built
* with this target (e.g. a library cannot be built with resources), what build phase it should be
* added to of its dependers, and the name and shape of its build output.
*/
public static ProductType productType(TargetControl targetControl) {
if (targetControl.hasProductType()) {
for (ProductType supportedType : SUPPORTED_PRODUCT_TYPES) {
if (targetControl.getProductType().equals(supportedType.identifier)) {
return supportedType;
}
}
throw new IllegalArgumentException(
"Unsupported product type: " + targetControl.getProductType());
}
return targetControl.hasInfoplist() ? ProductType.APPLICATION : ProductType.STATIC_LIBRARY;
}
private static String productName(TargetControl targetControl) {
if (Equaling.of(ProductType.STATIC_LIBRARY, productType(targetControl))) {
// The product names for static libraries must be unique since the final
// binary is linked with "clang -l${LIBRARY_PRODUCT_NAME}" for each static library.
// Unlike other product types, a full application may have dozens of static libraries,
// so rather than just use the target name, we use the full label to generate the product
// name.
return targetControl.getLabel();
} else {
return targetControl.getName();
}
}
/**
* Returns the file reference corresponding to the {@code productReference} of the given target.
* The {@code productReference} is the build output of a target, and its name and file type
* (stored in the {@link FileReference}) change based on the product type.
*/
private static FileReference productReference(TargetControl targetControl) {
ProductType type = productType(targetControl);
String productName = productName(targetControl);
switch (type) {
case APPLICATION:
case WATCH_OS1_APPLICATION:
return FileReference.of(String.format("%s.app", productName), SourceTree.BUILT_PRODUCTS_DIR)
.withExplicitFileType(FILE_TYPE_WRAPPER_APPLICATION);
case STATIC_LIBRARY:
return FileReference.of(
String.format("lib%s.a", productName), SourceTree.BUILT_PRODUCTS_DIR)
.withExplicitFileType(FILE_TYPE_ARCHIVE_LIBRARY);
case BUNDLE:
return FileReference.of(
String.format("%s.bundle", productName), SourceTree.BUILT_PRODUCTS_DIR)
.withExplicitFileType(FILE_TYPE_WRAPPER_BUNDLE);
case UNIT_TEST:
return FileReference.of(
String.format("%s.xctest", productName), SourceTree.BUILT_PRODUCTS_DIR)
.withExplicitFileType(FILE_TYPE_WRAPPER_BUNDLE);
case APP_EXTENSION:
case WATCH_OS1_EXTENSION:
return FileReference.of(
String.format("%s.appex", productName), SourceTree.BUILT_PRODUCTS_DIR)
.withExplicitFileType(FILE_TYPE_APP_EXTENSION);
case FRAMEWORK:
return FileReference.of(
String.format("%s.framework", productName), SourceTree.BUILT_PRODUCTS_DIR)
.withExplicitFileType(FILE_TYPE_FRAMEWORK);
default:
throw new IllegalArgumentException("unknown: " + type);
}
}
private static class TargetInfo {
final TargetControl control;
final PBXNativeTarget nativeTarget;
final PBXFrameworksBuildPhase frameworksPhase;
final PBXResourcesBuildPhase resourcesPhase;
final PBXBuildFile productBuildFile;
final PBXTargetDependency targetDependency;
final NSDictionary buildConfig;
TargetInfo(TargetControl control,
PBXNativeTarget nativeTarget,
PBXFrameworksBuildPhase frameworksPhase,
PBXResourcesBuildPhase resourcesPhase,
PBXBuildFile productBuildFile,
PBXTargetDependency targetDependency,
NSDictionary buildConfig) {
this.control = control;
this.nativeTarget = nativeTarget;
this.frameworksPhase = frameworksPhase;
this.resourcesPhase = resourcesPhase;
this.productBuildFile = productBuildFile;
this.targetDependency = targetDependency;
this.buildConfig = buildConfig;
}
/**
* Returns the path to the built, statically-linked binary for this target. The path contains
* build-setting variables and may be used in a build setting such as {@code TEST_HOST}.
*
* <p>One example return value is {@code $(BUILT_PRODUCTS_DIR)/Foo.app/Foo}.
*/
String staticallyLinkedBinary() {
ProductType type = productType(control);
Preconditions.checkArgument(
Containing.item(PRODUCT_TYPES_THAT_HAVE_A_BINARY, type),
"This product type (%s) is not known to have a binary.", type);
FileReference productReference = productReference(control);
return String.format("$(%s)/%s/%s",
productReference.sourceTree().name(),
productReference.path().or(productReference.name()),
control.getName());
}
/**
* Adds the given dependency to the list of dependencies, the
* appropriate build phase if applicable, and the appropriate build setting values if
* applicable, of this target.
*/
void addDependencyInfo(
DependencyControl dependencyControl, Map<String, TargetInfo> targetInfoByLabel) {
TargetInfo dependencyInfo =
Mapping.of(targetInfoByLabel, dependencyControl.getTargetLabel()).get();
if (dependencyControl.getTestHost()) {
buildConfig.put("TEST_HOST", dependencyInfo.staticallyLinkedBinary());
buildConfig.put("BUNDLE_LOADER", dependencyInfo.staticallyLinkedBinary());
} else if (productType(dependencyInfo.control) == ProductType.BUNDLE
|| productType(dependencyInfo.control) == ProductType.WATCH_OS1_APPLICATION) {
resourcesPhase.getFiles().add(dependencyInfo.productBuildFile);
} else if (productType(dependencyInfo.control) == ProductType.APP_EXTENSION
|| productType(dependencyInfo.control) == ProductType.WATCH_OS1_EXTENSION) {
PBXCopyFilesBuildPhase copyFilesPhase = new PBXCopyFilesBuildPhase(
PBXCopyFilesBuildPhase.Destination.PLUGINS, /*path=*/"");
copyFilesPhase.getFiles().add(dependencyInfo.productBuildFile);
nativeTarget.getBuildPhases().add(copyFilesPhase);
} else {
frameworksPhase.getFiles().add(dependencyInfo.productBuildFile);
}
nativeTarget.getDependencies().add(dependencyInfo.targetDependency);
}
}
private static NSDictionary nonArcCompileSettings() {
NSDictionary result = new NSDictionary();
result.put("COMPILER_FLAGS", "-fno-objc-arc");
return result;
}
private static boolean hasAtLeastOneCompilableSource(TargetControl control) {
return (control.getSourceFileCount() != 0) || (control.getNonArcSourceFileCount() != 0);
}
private static <E> Iterable<E> plus(Iterable<E> before, E... rest) {
return Iterables.concat(before, ImmutableList.copyOf(rest));
}
/**
* Returns the final header search paths to be placed in a build configuration.
*/
private static NSArray headerSearchPaths(Iterable<String> paths) {
ImmutableList.Builder<String> result = new ImmutableList.Builder<>();
for (String path : paths) {
// TODO(bazel-team): Remove this hack once the released version of Bazel is prepending
// "$(WORKSPACE_ROOT)/" to every "source rooted" path.
if (!path.startsWith("$")) {
path = "$(WORKSPACE_ROOT)/" + path;
}
result.add(path);
}
return (NSArray) NSObject.wrap(result.build());
}
/**
* Returns the {@code FRAMEWORK_SEARCH_PATHS} array for a target's build config given the list of
* {@code .framework} directory paths.
*/
private static NSArray frameworkSearchPaths(Iterable<String> frameworks) {
ImmutableSet.Builder<NSString> result = new ImmutableSet.Builder<>();
for (String framework : frameworks) {
result.add(new NSString("$(WORKSPACE_ROOT)/" + Paths.get(framework).getParent()));
}
// This is needed by XcTest targets (and others, just in case) for SenTestingKit.framework.
result.add(new NSString("$(SDKROOT)/Developer/Library/Frameworks"));
// This is needed by non-XcTest targets that use XcTest.framework, for instance for test
// utility libraries packaged as an objc_library.
result.add(new NSString("$(PLATFORM_DIR)/Developer/Library/Frameworks"));
return (NSArray) NSObject.wrap(result.build().asList());
}
/**
* Returns the {@code ARCHS} array for a target's build config given the list of architecture
* strings. If none is given, an array with default architectures "armv7" and "arm64" will be
* returned.
*/
private static NSArray cpuArchitectures(Iterable<String> architectures) {
if (Iterables.isEmpty(architectures)) {
return new NSArray(new NSString("armv7"), new NSString("arm64"));
} else {
ImmutableSet.Builder<NSString> result = new ImmutableSet.Builder<>();
for (String architecture : architectures) {
result.add(new NSString(architecture));
}
return (NSArray) NSObject.wrap(result.build().asList());
}
}
private static PBXFrameworksBuildPhase buildFrameworksInfo(
LibraryObjects libraryObjects, TargetControl target) {
BuildPhaseBuilder builder = libraryObjects.newBuildPhase();
for (String sdkFramework : target.getSdkFrameworkList()) {
builder.addSdkFramework(sdkFramework);
}
for (String framework : target.getFrameworkList()) {
builder.addFramework(framework);
}
return builder.build();
}
private static ImmutableList<String> otherLdflags(TargetControl targetControl) {
Iterable<String> givenFlags = targetControl.getLinkoptList();
ImmutableList.Builder<String> flags = new ImmutableList.Builder<>();
flags.addAll(givenFlags);
if (Containing.item(PRODUCT_TYPES_THAT_HAVE_A_BINARY, productType(targetControl))) {
for (String dylib : targetControl.getSdkDylibList()) {
if (dylib.startsWith("lib")) {
dylib = dylib.substring(3);
}
flags.add("-l" + dylib);
}
}
return flags.build();
}
/**
* Returns a unique name for the given imported library path, scoped by both the base name and
* the parent directories. For example, with "foo/bar/lib.a", "lib_bar_foo.a" will be returned.
*/
private static String uniqueImportedLibraryName(String importedLibrary) {
String extension = "";
String pathWithoutExtension = "";
int i = importedLibrary.lastIndexOf('.');
if (i > 0) {
extension = importedLibrary.substring(i);
pathWithoutExtension = importedLibrary.substring(0, i);
} else {
pathWithoutExtension = importedLibrary;
}
String[] pathFragments = pathWithoutExtension.replace("-", "_").split("/");
return Joiner.on("_").join(Lists.reverse(Arrays.asList(pathFragments))) + extension;
}
/** Generates a project file. */
public static PBXProject xcodeproj(Path workspaceRoot, Control control,
Iterable<PbxReferencesProcessor> postProcessors) {
checkArgument(control.hasPbxproj(), "Must set pbxproj field on control proto.");
FileSystem fileSystem = workspaceRoot.getFileSystem();
XcodeprojPath<Path> outputPath = XcodeprojPath.converter().fromPath(
RelativePaths.fromString(fileSystem, control.getPbxproj()));
NSDictionary projBuildConfigMap = new NSDictionary();
projBuildConfigMap.put("ARCHS", cpuArchitectures(control.getCpuArchitectureList()));
projBuildConfigMap.put("VALID_ARCHS",
new NSArray(
new NSString("armv7"),
new NSString("armv7s"),
new NSString("arm64"),
new NSString("i386"),
new NSString("x86_64")));
projBuildConfigMap.put("CLANG_ENABLE_OBJC_ARC", "YES");
projBuildConfigMap.put("SDKROOT", "iphoneos");
projBuildConfigMap.put("IPHONEOS_DEPLOYMENT_TARGET", "7.0");
projBuildConfigMap.put("GCC_VERSION", "com.apple.compilers.llvm.clang.1_0");
projBuildConfigMap.put("CODE_SIGN_IDENTITY[sdk=iphoneos*]", "iPhone Developer");
// Disable bitcode for now.
// TODO(bazel-team): Need to re-enable once we have real Xcode 7 support.
projBuildConfigMap.put("ENABLE_BITCODE", "NO");
for (XcodeprojBuildSetting projectSetting : control.getBuildSettingList()) {
projBuildConfigMap.put(projectSetting.getName(), projectSetting.getValue());
}
PBXProject project = new PBXProject(outputPath.getProjectName());
project.getMainGroup().setPath(workspaceRoot.toString());
if (workspaceRoot.isAbsolute()) {
project.getMainGroup().setSourceTree(SourceTree.ABSOLUTE);
}
try {
project
.getBuildConfigurationList()
.getBuildConfigurationsByName()
.get(DEFAULT_OPTIONS_NAME)
.setBuildSettings(projBuildConfigMap);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
Map<String, TargetInfo> targetInfoByLabel = new HashMap<>();
List<String> usedTargetNames = new ArrayList<>();
PBXFileReferences fileReferences = new PBXFileReferences();
LibraryObjects libraryObjects = new LibraryObjects(fileReferences);
PBXBuildFiles pbxBuildFiles = new PBXBuildFiles(fileReferences);
Resources resources =
Resources.fromTargetControls(fileSystem, pbxBuildFiles, control.getTargetList());
Xcdatamodels xcdatamodels =
Xcdatamodels.fromTargetControls(fileSystem, pbxBuildFiles, control.getTargetList());
// We use a hash set for the Project Navigator files so that the same PBXFileReference does not
// get added twice. Because PBXFileReference uses equality-by-identity semantics, this requires
// the PBXFileReferences cache to properly return the same reference for functionally-equivalent
// files.
Set<PBXReference> projectNavigatorFiles = new LinkedHashSet<>();
for (TargetControl targetControl : control.getTargetList()) {
checkArgument(targetControl.hasName(), "TargetControl requires a name: %s", targetControl);
checkArgument(targetControl.hasLabel(), "TargetControl requires a label: %s", targetControl);
ProductType productType = productType(targetControl);
Preconditions.checkArgument(
(productType != ProductType.APPLICATION) || hasAtLeastOneCompilableSource(targetControl),
APP_NEEDS_SOURCE_ERROR);
PBXSourcesBuildPhase sourcesBuildPhase = new PBXSourcesBuildPhase();
for (SourceFile source : SourceFile.allSourceFiles(fileSystem, targetControl)) {
PBXFileReference fileRef =
fileReferences.get(FileReference.of(source.path().toString(), SourceTree.GROUP));
projectNavigatorFiles.add(fileRef);
if (Equaling.of(source.buildType(), BuildType.NO_BUILD)) {
continue;
}
PBXBuildFile buildFile = new PBXBuildFile(fileRef);
if (Equaling.of(source.buildType(), BuildType.NON_ARC_BUILD)) {
buildFile.setSettings(Optional.of(nonArcCompileSettings()));
}
sourcesBuildPhase.getFiles().add(buildFile);
}
sourcesBuildPhase.getFiles().addAll(xcdatamodels.buildFiles().get(targetControl));
PBXFileReference productReference = fileReferences.get(productReference(targetControl));
projectNavigatorFiles.add(productReference);
NSDictionary targetBuildConfigMap = new NSDictionary();
// TODO(bazel-team): Stop adding the workspace root automatically once the
// released version of Bazel starts passing it.
targetBuildConfigMap.put("USER_HEADER_SEARCH_PATHS",
headerSearchPaths(
plus(targetControl.getUserHeaderSearchPathList(), "$(WORKSPACE_ROOT)")));
targetBuildConfigMap.put(
"HEADER_SEARCH_PATHS",
headerSearchPaths(plus(targetControl.getHeaderSearchPathList(), "$(inherited)")));
targetBuildConfigMap.put(
"FRAMEWORK_SEARCH_PATHS",
frameworkSearchPaths(
Iterables.concat(
targetControl.getFrameworkList(),
targetControl.getFrameworkSearchPathOnlyList())));
targetBuildConfigMap.put("WORKSPACE_ROOT", workspaceRoot.toString());
if (targetControl.hasPchPath()) {
targetBuildConfigMap.put(
"GCC_PREFIX_HEADER", "$(WORKSPACE_ROOT)/" + targetControl.getPchPath());
}
targetBuildConfigMap.put("PRODUCT_NAME", productName(targetControl));
if (targetControl.hasInfoplist()) {
targetBuildConfigMap.put(
"INFOPLIST_FILE", "$(WORKSPACE_ROOT)/" + targetControl.getInfoplist());
}
// Double-quotes in copt strings need to be escaped for XCode.
if (targetControl.getCoptCount() > 0) {
List<String> escapedCopts = Lists.transform(
targetControl.getCoptList(), QUOTE_ESCAPER.asFunction());
targetBuildConfigMap.put("OTHER_CFLAGS", NSObject.wrap(escapedCopts));
}
targetBuildConfigMap.put("OTHER_LDFLAGS", NSObject.wrap(otherLdflags(targetControl)));
for (XcodeprojBuildSetting setting : targetControl.getBuildSettingList()) {
String name = setting.getName();
String value = setting.getValue();
// TODO(bazel-team): Remove this hack after next Bazel release.
if (name.equals("CODE_SIGN_ENTITLEMENTS") && !value.startsWith("$")) {
value = "$(WORKSPACE_ROOT)/" + value;
}
targetBuildConfigMap.put(name, value);
}
// Note that HFS+ (the Mac filesystem) is usually case insensitive, so we cast all target
// names to lower case before checking for duplication because otherwise users may end up
// having duplicated intermediate build directories that can interfere with the build.
String targetName = targetControl.getName();
String targetNameInLowerCase = targetName.toLowerCase();
if (usedTargetNames.contains(targetNameInLowerCase)) {
// Use the label in the odd case where we have two targets with the same name.
targetName = targetControl.getLabel();
targetNameInLowerCase = targetName.toLowerCase();
}
checkState(!usedTargetNames.contains(targetNameInLowerCase),
"Name (case-insensitive) already exists for target with label/name %s/%s in list: %s",
targetControl.getLabel(), targetControl.getName(), usedTargetNames);
usedTargetNames.add(targetNameInLowerCase);
PBXNativeTarget target = new PBXNativeTarget(targetName, productType);
try {
target
.getBuildConfigurationList()
.getBuildConfigurationsByName()
.get(DEFAULT_OPTIONS_NAME)
.setBuildSettings(targetBuildConfigMap);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
target.setProductReference(productReference);
// We only add frameworks here and not dylibs because of differences in how
// Xcode 6 and Xcode 7 specify dylibs in the project organizer.
// (Xcode 6 -> *.dylib, Xcode 7 -> *.tbd)
PBXFrameworksBuildPhase frameworksPhase = buildFrameworksInfo(libraryObjects, targetControl);
PBXResourcesBuildPhase resourcesPhase = resources.resourcesBuildPhase(targetControl);
for (String importedArchive : targetControl.getImportedLibraryList()) {
PBXFileReference fileReference = fileReferences.get(
FileReference.of(importedArchive, SourceTree.GROUP)
.withExplicitFileType(FILE_TYPE_ARCHIVE_LIBRARY));
projectNavigatorFiles.add(fileReference);
}
project.getTargets().add(target);
target.getBuildPhases().add(frameworksPhase);
target.getBuildPhases().add(sourcesBuildPhase);
target.getBuildPhases().add(resourcesPhase);
checkState(!Mapping.of(targetInfoByLabel, targetControl.getLabel()).isPresent(),
"Mapping already exists for target with label %s in map: %s",
targetControl.getLabel(), targetInfoByLabel);
targetInfoByLabel.put(
targetControl.getLabel(),
new TargetInfo(
targetControl,
target,
frameworksPhase,
resourcesPhase,
new PBXBuildFile(productReference),
new LocalPBXTargetDependency(
new LocalPBXContainerItemProxy(
project, target, ProxyType.TARGET_REFERENCE)),
targetBuildConfigMap));
}
for (HasProjectNavigatorFiles references : ImmutableList.of(pbxBuildFiles, libraryObjects)) {
Iterables.addAll(projectNavigatorFiles, references.mainGroupReferences());
}
Iterable<PBXReference> processedProjectFiles = projectNavigatorFiles;
for (PbxReferencesProcessor postProcessor : postProcessors) {
processedProjectFiles = postProcessor.process(processedProjectFiles);
}
Iterables.addAll(project.getMainGroup().getChildren(), processedProjectFiles);
for (TargetInfo targetInfo : targetInfoByLabel.values()) {
TargetControl targetControl = targetInfo.control;
for (DependencyControl dependency : targetControl.getDependencyList()) {
targetInfo.addDependencyInfo(dependency, targetInfoByLabel);
}
if (!Equaling.of(ProductType.STATIC_LIBRARY, productType(targetControl))
&& !targetControl.getImportedLibraryList().isEmpty()) {
// We add a script build phase to copy the imported libraries to BUILT_PRODUCT_DIR with
// unique names before linking them to work around an Xcode issue where imported libraries
// with duplicated names lead to link errors.
//
// Internally Xcode uses linker flag -l{LIBRARY_NAME} to link a particular library and
// delegates to the linker to locate the actual library using library search paths. So given
// two imported libraries with the same name: a/b/libfoo.a, c/d/libfoo.a, Xcode uses
// duplicate linker flag -lfoo to link both of the libraries. Depending on the order of
// the library search paths, the linker will only be able to locate and link one of the
// libraries.
//
// With this workaround using a script build phase, all imported libraries to link have
// unique names. For the previous example with a/b/libfoo.a and c/d/libfoo.a, the script
// build phase will copy them to BUILT_PRODUCTS_DIR with unique names libfoo_b_a.a and
// libfoo_d_c.a, respectively. The linker flags Xcode uses to link them will be
// -lfoo_d_c and -lfoo_b_a, with no duplication.
PBXShellScriptBuildPhase scriptBuildPhase = new PBXShellScriptBuildPhase();
scriptBuildPhase.setShellScript(
"for ((i=0; i < ${SCRIPT_INPUT_FILE_COUNT}; i++)) do\n"
+ " INPUT_FILE=\"SCRIPT_INPUT_FILE_${i}\"\n"
+ " OUTPUT_FILE=\"SCRIPT_OUTPUT_FILE_${i}\"\n"
+ " cp -v -f \"${!INPUT_FILE}\" \"${!OUTPUT_FILE}\"\n"
+ "done");
for (String importedLibrary : targetControl.getImportedLibraryList()) {
String uniqueImportedLibrary = uniqueImportedLibraryName(importedLibrary);
scriptBuildPhase.getInputPaths().add("$(WORKSPACE_ROOT)/" + importedLibrary);
scriptBuildPhase.getOutputPaths().add("$(BUILT_PRODUCTS_DIR)/" + uniqueImportedLibrary);
FileReference fileReference = FileReference.of(uniqueImportedLibrary,
SourceTree.BUILT_PRODUCTS_DIR).withExplicitFileType(FILE_TYPE_ARCHIVE_LIBRARY);
targetInfo.frameworksPhase.getFiles().add(pbxBuildFiles.getStandalone(fileReference));
}
targetInfo.nativeTarget.getBuildPhases().add(scriptBuildPhase);
}
}
return project;
}
}