// 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; } }