Java tutorial
/* * Copyright 2013-present Facebook, Inc. * * 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.facebook.buck.apple.xcode; import com.dd.plist.NSArray; import com.dd.plist.NSDictionary; import com.dd.plist.NSString; import com.facebook.buck.apple.AppleAssetCatalog; import com.facebook.buck.apple.AppleAssetCatalogDescription; import com.facebook.buck.apple.AppleBuildable; import com.facebook.buck.apple.AppleResource; import com.facebook.buck.apple.GroupedSource; import com.facebook.buck.apple.HeaderVisibility; import com.facebook.buck.apple.IosBinary; import com.facebook.buck.apple.IosBinaryDescription; import com.facebook.buck.apple.IosLibrary; import com.facebook.buck.apple.IosLibraryDescription; import com.facebook.buck.apple.IosResourceDescription; import com.facebook.buck.apple.IosTest; import com.facebook.buck.apple.IosTestDescription; import com.facebook.buck.apple.IosTestType; import com.facebook.buck.apple.MacosxBinary; import com.facebook.buck.apple.MacosxBinaryDescription; import com.facebook.buck.apple.MacosxFramework; import com.facebook.buck.apple.MacosxFrameworkDescription; import com.facebook.buck.apple.OsxResourceDescription; import com.facebook.buck.apple.XcodeNative; import com.facebook.buck.apple.XcodeNativeDescription; import com.facebook.buck.apple.XcodeRuleConfiguration; import com.facebook.buck.apple.xcode.xcconfig.XcconfigStack; import com.facebook.buck.apple.xcode.xcodeproj.PBXAggregateTarget; import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildFile; import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildPhase; import com.facebook.buck.apple.xcode.xcodeproj.PBXContainerItemProxy; 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.PBXGroup; import com.facebook.buck.apple.xcode.xcodeproj.PBXHeadersBuildPhase; 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.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; import com.facebook.buck.apple.xcode.xcodeproj.PBXTargetDependency; import com.facebook.buck.apple.xcode.xcodeproj.SourceTreePath; import com.facebook.buck.apple.xcode.xcodeproj.XCBuildConfiguration; import com.facebook.buck.apple.xcode.xcodeproj.XCConfigurationList; import com.facebook.buck.codegen.SourceSigner; import com.facebook.buck.graph.AbstractAcyclicDepthFirstPostOrderTraversal; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.parser.PartialGraph; import com.facebook.buck.rules.AbstractBuildable; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleType; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePaths; import com.facebook.buck.shell.Genrule; import com.facebook.buck.shell.GenruleDescription; import com.facebook.buck.shell.ShellStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.util.BuckConstant; import com.facebook.buck.util.Escaper; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.ProjectFilesystem; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.google.common.util.concurrent.UncheckedExecutionException; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; /** * Generator for xcode project and associated files from a set of xcode/ios rules. */ public class ProjectGenerator { public enum Option { /** * Generate a build scheme */ GENERATE_SCHEME, /** * generate native xcode targets for dependent build targets. */ GENERATE_TARGETS_FOR_DEPENDENCIES, /** * Generate a workspace */ GENERATE_WORKSPACE, /** * Attempt to generate projects with configurations in the standard xcode configuration layout. * * Checks that the rules declare their configurations in either * - 4 layers: file-project inline-project file-target inline-target * - 2 layers: file-project file-target * * Additionally, all project-level layers should be identical amongst all targets in the * project. */ REFERENCE_EXISTING_XCCONFIGS, /** Use short BuildTarget name instead of full name for targets */ USE_SHORT_NAMES_FOR_TARGETS,; } /** * Standard options for generating a combined project */ public static final ImmutableSet<Option> COMBINED_PROJECT_OPTIONS = ImmutableSet.of(Option.GENERATE_SCHEME, Option.GENERATE_TARGETS_FOR_DEPENDENCIES, Option.GENERATE_WORKSPACE); public static final ImmutableSet<Option> SEPARATED_PROJECT_OPTIONS = ImmutableSet .of(Option.REFERENCE_EXISTING_XCCONFIGS, Option.USE_SHORT_NAMES_FOR_TARGETS); private static final ImmutableSet<String> HEADER_FILE_EXTENSIONS = ImmutableSet.of("h", "hh", "hpp"); public static final String PATH_TO_ASSET_CATALOG_COMPILER = System.getProperty( "buck.path_to_compile_asset_catalogs_py", "src/com/facebook/buck/apple/compile_asset_catalogs.py"); public static final String PATH_TO_ASSET_CATALOG_BUILD_PHASE_SCRIPT = System.getProperty( "buck.path_to_compile_asset_catalogs_build_phase_sh", "src/com/facebook/buck/apple/compile_asset_catalogs_build_phase.sh"); private final PartialGraph partialGraph; private final ProjectFilesystem projectFilesystem; private final ExecutionContext executionContext; private final Path outputDirectory; private final String projectName; private final ImmutableSet<BuildTarget> initialTargets; private final Path projectPath; private final Path repoRootRelativeToOutputDirectory; private final Path placedAssetCatalogBuildPhaseScript; private final ImmutableSet<Option> options; // These fields are created/filled when creating the projects. private final PBXProject project; private final LoadingCache<BuildRule, Optional<PBXTarget>> buildRuleToXcodeTarget; private XCScheme scheme = null; private Document workspace = null; private boolean shouldPlaceAssetCatalogCompiler = false; /** * Populated while generating project configurations, in order to collect the possible * project-level configurations to set when operation with * {@link Option#REFERENCE_EXISTING_XCCONFIGS}. */ private final ImmutableMultimap.Builder<String, ConfigInXcodeLayout> xcodeConfigurationLayersMultimapBuilder; private ImmutableMap<String, String> targetNameToGIDMap; public ProjectGenerator(PartialGraph partialGraph, Set<BuildTarget> initialTargets, ProjectFilesystem projectFilesystem, ExecutionContext executionContext, Path outputDirectory, String projectName, Set<Option> options) { this.partialGraph = Preconditions.checkNotNull(partialGraph); this.initialTargets = ImmutableSet.copyOf(initialTargets); this.projectFilesystem = Preconditions.checkNotNull(projectFilesystem); this.executionContext = Preconditions.checkNotNull(executionContext); this.outputDirectory = Preconditions.checkNotNull(outputDirectory); this.projectName = Preconditions.checkNotNull(projectName); this.options = ImmutableSet.copyOf(options); this.placedAssetCatalogBuildPhaseScript = this.projectFilesystem.getPathForRelativePath( BuckConstant.BIN_PATH.resolve("xcode-scripts/compile_asset_catalogs_build_phase.sh")); this.projectPath = outputDirectory.resolve(projectName + ".xcodeproj"); this.repoRootRelativeToOutputDirectory = this.outputDirectory.normalize().toAbsolutePath() .relativize(projectFilesystem.getRootPath().toAbsolutePath()); this.project = new PBXProject(projectName); this.buildRuleToXcodeTarget = CacheBuilder.newBuilder() .build(new CacheLoader<BuildRule, Optional<PBXTarget>>() { @Override public Optional<PBXTarget> load(BuildRule key) throws Exception { return generateTargetForBuildRule(key); } }); xcodeConfigurationLayersMultimapBuilder = ImmutableMultimap.builder(); } @Nullable @VisibleForTesting XCScheme getGeneratedScheme() { return scheme; } @VisibleForTesting PBXProject getGeneratedProject() { return project; } @Nullable @VisibleForTesting Document getGeneratedWorkspace() { return workspace; } public Path getProjectPath() { return projectPath; } public void createXcodeProjects() throws IOException { try { targetNameToGIDMap = buildTargetNameToGIDMap(); Iterable<BuildRule> allRules = RuleDependencyFinder.getAllRules(partialGraph, initialTargets); ImmutableMap.Builder<BuildRule, PBXTarget> ruleToTargetMapBuilder = ImmutableMap.builder(); for (BuildRule rule : allRules) { if (isBuiltByCurrentProject(rule)) { // Trigger the loading cache to call the generateTargetForBuildRule function. Optional<PBXTarget> target = buildRuleToXcodeTarget.getUnchecked(rule); if (target.isPresent()) { ruleToTargetMapBuilder.put(rule, target.get()); } } } if (options.contains(Option.REFERENCE_EXISTING_XCCONFIGS)) { setProjectLevelConfigs(project, repoRootRelativeToOutputDirectory, collectProjectLevelConfigsIfIdenticalOrFail( xcodeConfigurationLayersMultimapBuilder.build())); } addGeneratedSignedSourceTarget(project); writeProjectFile(project); if (options.contains(Option.GENERATE_WORKSPACE)) { writeWorkspace(projectPath); } if (options.contains(Option.GENERATE_SCHEME)) { scheme = SchemeGenerator.createScheme(partialGraph, projectPath, ruleToTargetMapBuilder.build()); SchemeGenerator.writeScheme(projectFilesystem, scheme, projectPath); } if (shouldPlaceAssetCatalogCompiler) { Path placedAssetCatalogCompilerPath = projectFilesystem.getPathForRelativePath( BuckConstant.BIN_PATH.resolve("xcode-scripts/compile_asset_catalogs.py")); projectFilesystem.createParentDirs(placedAssetCatalogCompilerPath); projectFilesystem.createParentDirs(placedAssetCatalogBuildPhaseScript); projectFilesystem.copyFile(Paths.get(PATH_TO_ASSET_CATALOG_COMPILER), placedAssetCatalogCompilerPath); projectFilesystem.copyFile(Paths.get(PATH_TO_ASSET_CATALOG_BUILD_PHASE_SCRIPT), placedAssetCatalogBuildPhaseScript); } } catch (UncheckedExecutionException e) { // if any code throws an exception, they tend to get wrapped in LoadingCache's // UncheckedExecutionException. Unwrap it if its cause is HumanReadable. if (e.getCause() instanceof HumanReadableException) { throw (HumanReadableException) e.getCause(); } else { throw e; } } } private Optional<PBXTarget> generateTargetForBuildRule(BuildRule rule) throws IOException { Preconditions.checkState(isBuiltByCurrentProject(rule), "should not generate rule if it shouldn't be built by current project"); Preconditions.checkNotNull(targetNameToGIDMap); Optional<PBXTarget> result; if (rule.getType().equals(IosLibraryDescription.TYPE)) { result = Optional .of((PBXTarget) generateIosLibraryTarget(project, rule, (IosLibrary) rule.getBuildable())); } else if (rule.getType().equals(XcodeNativeDescription.TYPE)) { result = Optional .of((PBXTarget) generateXcodeNativeTarget(project, rule, (XcodeNative) rule.getBuildable())); } else if (rule.getType().equals(IosTestDescription.TYPE)) { result = Optional.of((PBXTarget) generateIosTestTarget(project, rule, (IosTest) rule.getBuildable())); } else if (rule.getType().equals(IosBinaryDescription.TYPE)) { result = Optional .of((PBXTarget) generateIOSBinaryTarget(project, rule, (IosBinary) rule.getBuildable())); } else if (rule.getType().equals(MacosxFrameworkDescription.TYPE)) { result = Optional.of((PBXTarget) generateMacosxFrameworkTarget(project, rule, (MacosxFramework) rule.getBuildable())); } else if (rule.getType().equals(MacosxBinaryDescription.TYPE)) { result = Optional .of((PBXTarget) generateMacosxBinaryTarget(project, rule, (MacosxBinary) rule.getBuildable())); } else { result = Optional.absent(); } if (result.isPresent()) { setTargetGIDIfNameInMap(result.get(), targetNameToGIDMap); } return result; } private PBXNativeTarget generateIosLibraryTarget(PBXProject project, BuildRule rule, IosLibrary buildable) throws IOException { PBXNativeTarget target = new PBXNativeTarget(getXcodeTargetName(rule)); target.setProductType(PBXTarget.ProductType.IOS_LIBRARY); PBXGroup targetGroup = project.getMainGroup().getOrCreateChildGroupByName(target.getName()); // -- configurations setTargetBuildConfigurations(rule.getBuildTarget(), target, targetGroup, buildable.getConfigurations(), ImmutableMap.<String, String>of()); // -- build phases // TODO(Task #3772930): Go through all dependencies of the rule // and add any shell script rules here addRunScriptBuildPhasesForDependencies(rule, target); addSourcesAndHeadersBuildPhases(target, targetGroup, buildable.getSrcs(), buildable.getPerFileFlags()); // -- products PBXGroup productsGroup = project.getMainGroup().getOrCreateChildGroupByName("Products"); String libraryName = "lib" + getProductName(rule.getBuildTarget()) + ".a"; PBXFileReference productReference = new PBXFileReference(libraryName, libraryName, PBXReference.SourceTree.BUILT_PRODUCTS_DIR); productsGroup.getChildren().add(productReference); target.setProductReference(productReference); project.getTargets().add(target); return target; } private PBXNativeTarget generateIosTestTarget(PBXProject project, BuildRule rule, IosTest buildable) throws IOException { PBXNativeTarget target = new PBXNativeTarget(getXcodeTargetName(rule)); target.setProductType(testTypeToTargetProductType(buildable.getTestType())); PBXGroup targetGroup = project.getMainGroup().getOrCreateChildGroupByName(target.getName()); // -- configurations Path infoPlistPath = this.repoRootRelativeToOutputDirectory.resolve(buildable.getInfoPlist()); setTargetBuildConfigurations(rule.getBuildTarget(), target, targetGroup, buildable.getConfigurations(), ImmutableMap.of("INFOPLIST_FILE", infoPlistPath.toString())); // -- phases // TODO(Task #3772930): Go through all dependencies of the rule // and add any shell script rules here addRunScriptBuildPhasesForDependencies(rule, target); addSourcesAndHeadersBuildPhases(target, targetGroup, buildable.getSrcs(), buildable.getPerFileFlags()); ImmutableSet.Builder<String> frameworksBuilder = ImmutableSet.builder(); frameworksBuilder.addAll(buildable.getFrameworks()); collectRecursiveFrameworkDependencies(rule, frameworksBuilder); addFrameworksBuildPhase(rule.getBuildTarget(), target, project.getMainGroup().getOrCreateChildGroupByName("Frameworks"), frameworksBuilder.build(), collectRecursiveLibraryDependencies(rule)); addResourcesBuildPhase(target, targetGroup, collectRecursiveResources(rule, IosResourceDescription.TYPE)); addAssetCatalogBuildPhase(target, targetGroup, collectRecursiveAssetCatalogs(rule)); // -- products PBXGroup productsGroup = project.getMainGroup().getOrCreateChildGroupByName("Products"); String productName = getProductName(rule.getBuildTarget()); String productOutputName = Joiner.on(".").join(productName, buildable.getTestType().toFileExtension()); PBXFileReference productReference = new PBXFileReference(productOutputName, productOutputName, PBXReference.SourceTree.BUILT_PRODUCTS_DIR); productsGroup.getChildren().add(productReference); target.setProductName(productName); target.setProductReference(productReference); project.getTargets().add(target); return target; } private PBXNativeTarget generateIOSBinaryTarget(PBXProject project, BuildRule rule, IosBinary buildable) throws IOException { PBXNativeTarget target = generateBinaryTarget(project, rule, buildable, PBXTarget.ProductType.IOS_BINARY, IosResourceDescription.TYPE); project.getTargets().add(target); return target; } private PBXNativeTarget generateMacosxFrameworkTarget(PBXProject project, BuildRule rule, MacosxFramework buildable) throws IOException { PBXNativeTarget target = new PBXNativeTarget(getXcodeTargetName(rule)); target.setProductType(PBXTarget.ProductType.MACOSX_FRAMEWORK); PBXGroup targetGroup = project.getMainGroup().getOrCreateChildGroupByName(target.getName()); // -- configurations setTargetBuildConfigurations(rule.getBuildTarget(), target, targetGroup, buildable.getConfigurations(), ImmutableMap.<String, String>of()); // -- build phases // TODO(Task #3772930): Go through all dependencies of the rule // and add any shell script rules here addRunScriptBuildPhasesForDependencies(rule, target); addSourcesAndHeadersBuildPhases(target, targetGroup, buildable.getSrcs(), buildable.getPerFileFlags()); // MacOSX frameworks actually link with libraries and other frameworks. ImmutableSet.Builder<String> frameworksBuilder = ImmutableSet.builder(); frameworksBuilder.addAll(buildable.getFrameworks()); collectRecursiveFrameworkDependencies(rule, frameworksBuilder); addFrameworksBuildPhase(rule.getBuildTarget(), target, project.getMainGroup().getOrCreateChildGroupByName("Frameworks"), frameworksBuilder.build(), collectRecursiveLibraryDependencies(rule)); addResourcesBuildPhase(target, targetGroup, collectRecursiveResources(rule, OsxResourceDescription.TYPE)); addAssetCatalogBuildPhase(target, targetGroup, collectRecursiveAssetCatalogs(rule)); // -- products PBXGroup productsGroup = project.getMainGroup().getOrCreateChildGroupByName("Products"); String frameworkName = getProductName(rule.getBuildTarget()) + ".framework"; PBXFileReference productReference = new PBXFileReference(frameworkName, frameworkName, PBXReference.SourceTree.BUILT_PRODUCTS_DIR); productsGroup.getChildren().add(productReference); target.setProductReference(productReference); project.getTargets().add(target); return target; } private <BuildableBinary extends AbstractBuildable & AppleBuildable> PBXNativeTarget generateBinaryTarget( PBXProject project, BuildRule rule, BuildableBinary buildable, PBXTarget.ProductType productType, BuildRuleType resourceRuleType) throws IOException { PBXNativeTarget target = new PBXNativeTarget(getXcodeTargetName(rule)); target.setProductType(productType); PBXGroup targetGroup = project.getMainGroup().getOrCreateChildGroupByName(target.getName()); // -- configurations Path infoPlistPath = this.repoRootRelativeToOutputDirectory.resolve(buildable.getInfoPlist()); setTargetBuildConfigurations(rule.getBuildTarget(), target, targetGroup, buildable.getConfigurations(), ImmutableMap.of("INFOPLIST_FILE", infoPlistPath.toString())); // -- phases // TODO(Task #3772930): Go through all dependencies of the rule // and add any shell script rules here addRunScriptBuildPhasesForDependencies(rule, target); addSourcesAndHeadersBuildPhases(target, targetGroup, buildable.getSrcs(), buildable.getPerFileFlags()); ImmutableSet.Builder<String> frameworksBuilder = ImmutableSet.builder(); frameworksBuilder.addAll(buildable.getFrameworks()); collectRecursiveFrameworkDependencies(rule, frameworksBuilder); addFrameworksBuildPhase(rule.getBuildTarget(), target, project.getMainGroup().getOrCreateChildGroupByName("Frameworks"), frameworksBuilder.build(), collectRecursiveLibraryDependencies(rule)); addResourcesBuildPhase(target, targetGroup, collectRecursiveResources(rule, resourceRuleType)); addAssetCatalogBuildPhase(target, targetGroup, collectRecursiveAssetCatalogs(rule)); // -- products PBXGroup productsGroup = project.getMainGroup().getOrCreateChildGroupByName("Products"); String productName = getProductName(rule.getBuildTarget()); String productOutputName = productName + ".app"; PBXFileReference productReference = new PBXFileReference(productOutputName, productOutputName, PBXReference.SourceTree.BUILT_PRODUCTS_DIR); productsGroup.getChildren().add(productReference); target.setProductName(productName); target.setProductReference(productReference); return target; } private PBXNativeTarget generateMacosxBinaryTarget(PBXProject project, BuildRule rule, MacosxBinary buildable) throws IOException { PBXNativeTarget target = generateBinaryTarget(project, rule, buildable, PBXTarget.ProductType.MACOSX_BINARY, OsxResourceDescription.TYPE); // Unlike an ios target, macosx targets collect their frameworks and copy them in. ImmutableSet.Builder<String> frameworksBuilder = ImmutableSet.builder(); frameworksBuilder.addAll(buildable.getFrameworks()); collectRecursiveFrameworkDependencies(rule, frameworksBuilder); addCopyFrameworksBuildPhase(rule.getBuildTarget(), target, project.getMainGroup().getOrCreateChildGroupByName("Frameworks"), frameworksBuilder.build()); project.getTargets().add(target); return target; } private PBXAggregateTarget generateXcodeNativeTarget(PBXProject project, BuildRule rule, XcodeNative buildable) { Path referencedProjectPath = buildable.getProjectContainerPath().resolve(); PBXFileReference referencedProject = project.getMainGroup() .getOrCreateChildGroupByName("Project References").getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT, this.outputDirectory.normalize() .toAbsolutePath().relativize(referencedProjectPath.toAbsolutePath()))); PBXContainerItemProxy proxy = new PBXContainerItemProxy(referencedProject, buildable.getTargetGid(), PBXContainerItemProxy.ProxyType.TARGET_REFERENCE); PBXAggregateTarget target = new PBXAggregateTarget(getXcodeTargetName(rule)); target.getDependencies().add(new PBXTargetDependency(proxy)); project.getTargets().add(target); return target; } /** * Create project level (if it does not exist) and target level configuration entries. * * Each configuration should have an empty entry at the project level. The target level entries * combine the configuration values of every layer into a single configuration file that is * effectively laid out in layers. */ private void setTargetBuildConfigurations(BuildTarget buildTarget, PBXTarget target, PBXGroup targetGroup, ImmutableSet<XcodeRuleConfiguration> configurations, ImmutableMap<String, String> extraBuildSettings) throws IOException { ImmutableMap.Builder<String, String> extraConfigsBuilder = ImmutableMap.builder(); extraConfigsBuilder.putAll(extraBuildSettings).put("TARGET_NAME", getProductName(buildTarget)) .put("SRCROOT", relativizeBuckRelativePathToGeneratedProject(buildTarget, "").toString()); // HACK: GCC_PREFIX_HEADER needs to be modified because the path is referenced relative to // project root, so if the project is generated in a different place from the BUCK file, it // would break. This forces it to be based off of SRCROOT, which is overriden to point to the // BUCK file location. // However, when using REFERENCE_EXISTING_XCCONFIGS, this setting is not put into another layer, // and therefore may override an existing setting in the target-inline-config level. // Fortunately, this option is only set when we are generating separated projects, which are // placed next to the BUCK files, so avoiding this is OK. // In the long run, setting should be written relative to SRCROOT everywhere, and this entire // hack can be deleted. if (!options.contains(Option.REFERENCE_EXISTING_XCCONFIGS)) { extraConfigsBuilder.put("GCC_PREFIX_HEADER", "$(SRCROOT)/$(inherited)"); } ImmutableMap<String, String> extraConfigs = extraConfigsBuilder.build(); PBXGroup configurationsGroup = targetGroup.getOrCreateChildGroupByName("Configurations"); for (XcodeRuleConfiguration configuration : configurations) { if (options.contains(Option.REFERENCE_EXISTING_XCCONFIGS)) { ConfigInXcodeLayout layers = extractXcodeConfigurationLayers(buildTarget, configuration); xcodeConfigurationLayersMultimapBuilder.put(configuration.getName(), layers); XCBuildConfiguration outputConfiguration = target.getBuildConfigurationList() .getBuildConfigurationsByName().getUnchecked(configuration.getName()); if (layers.targetLevelConfigFile.isPresent()) { PBXFileReference fileReference = configurationsGroup.getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT, this.repoRootRelativeToOutputDirectory .resolve(layers.targetLevelConfigFile.get()).normalize())); outputConfiguration.setBaseConfigurationReference(fileReference); NSDictionary inlineSettings = new NSDictionary(); Iterable<Map.Entry<String, String>> entries = Iterables .concat(layers.targetLevelInlineSettings.entrySet(), extraConfigs.entrySet()); for (Map.Entry<String, String> entry : entries) { inlineSettings.put(entry.getKey(), entry.getValue()); } outputConfiguration.setBuildSettings(inlineSettings); } } else { Path outputConfigurationDirectory = outputDirectory.resolve("Configurations"); projectFilesystem.mkdirs(outputConfigurationDirectory); Path originalProjectPath = projectFilesystem .getPathForRelativePath(Paths.get(buildTarget.getBasePathWithSlash())); // XCConfig search path is relative to the xcode project and the file itself. ImmutableList<Path> searchPaths = ImmutableList.of(originalProjectPath); // Call for effect to create a stub configuration entry at project level. project.getBuildConfigurationList().getBuildConfigurationsByName() .getUnchecked(configuration.getName()); // Write an xcconfig that embodies all the config levels, and set that as the target config. Path configurationFilePath = outputConfigurationDirectory .resolve(mangledBuildTargetName(buildTarget) + "-" + configuration.getName() + ".xcconfig"); String serializedConfiguration = serializeBuildConfiguration(configuration, searchPaths, extraConfigs); projectFilesystem.writeContentsToPath(serializedConfiguration, configurationFilePath); PBXFileReference fileReference = configurationsGroup.getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT, this.repoRootRelativeToOutputDirectory.resolve(configurationFilePath))); XCBuildConfiguration outputConfiguration = target.getBuildConfigurationList() .getBuildConfigurationsByName().getUnchecked(configuration.getName()); outputConfiguration.setBaseConfigurationReference(fileReference); } } } private void addRunScriptBuildPhase(PBXNativeTarget target, Genrule rule) { // TODO(user): Check and validate dependencies of the script. If it depends on libraries etc. // we can't handle it currently. PBXShellScriptBuildPhase shellScriptBuildPhase = new PBXShellScriptBuildPhase(); target.getBuildPhases().add(shellScriptBuildPhase); for (Path path : rule.getSrcs()) { shellScriptBuildPhase.getInputPaths().add(path.toString()); } StringBuilder bashCommandBuilder = new StringBuilder(); ShellStep genruleStep = rule.createGenruleStep(); for (String commandElement : genruleStep.getShellCommand(executionContext)) { if (bashCommandBuilder.length() > 0) { bashCommandBuilder.append(' '); } bashCommandBuilder.append(Escaper.escapeAsBashString(commandElement)); } shellScriptBuildPhase.setShellScript(bashCommandBuilder.toString()); } private void addRunScriptBuildPhasesForDependencies(BuildRule rule, PBXNativeTarget target) { for (BuildRule dependency : rule.getDeps()) { if (dependency.getType().equals(GenruleDescription.TYPE)) { addRunScriptBuildPhase(target, (Genrule) dependency.getBuildable()); } } } /** * Add sources and headers build phases to a target, and add references to the target's group. * * @param target Target to add the build phases to. * @param targetGroup Group to link the source files to. * @param groupedSources Grouped sources and headers to include in the build * phase, path relative to project root. * @param sourceFlags Source path to flag mapping. */ private void addSourcesAndHeadersBuildPhases(PBXNativeTarget target, PBXGroup targetGroup, Iterable<GroupedSource> groupedSources, ImmutableMap<SourcePath, String> sourceFlags) { PBXGroup sourcesGroup = targetGroup.getOrCreateChildGroupByName("Sources"); // Sources groups stay in the order in which they're declared in the BUCK file. sourcesGroup.setSortPolicy(PBXGroup.SortPolicy.UNSORTED); PBXSourcesBuildPhase sourcesBuildPhase = new PBXSourcesBuildPhase(); PBXHeadersBuildPhase headersBuildPhase = new PBXHeadersBuildPhase(); addGroupedSourcesToBuildPhases(sourcesGroup, sourcesBuildPhase, Optional.of(headersBuildPhase), groupedSources, sourceFlags); if (!sourcesBuildPhase.getFiles().isEmpty()) { target.getBuildPhases().add(sourcesBuildPhase); } if (!headersBuildPhase.getFiles().isEmpty()) { target.getBuildPhases().add(headersBuildPhase); } } private static boolean isHeaderSourcePath(SourcePath sourcePath) { return HEADER_FILE_EXTENSIONS.contains(Files.getFileExtension(sourcePath.toString())); } private void addGroupedSourcesToBuildPhases(PBXGroup sourcesGroup, PBXSourcesBuildPhase sourcesBuildPhase, Optional<PBXHeadersBuildPhase> headersBuildPhase, Iterable<GroupedSource> groupedSources, ImmutableMap<SourcePath, String> sourceFlags) { for (GroupedSource groupedSource : groupedSources) { switch (groupedSource.getType()) { case SOURCE_PATH: if (isHeaderSourcePath(groupedSource.getSourcePath())) { if (headersBuildPhase.isPresent()) { addSourcePathToHeadersBuildPhase(groupedSource.getSourcePath(), sourcesGroup, headersBuildPhase.get(), sourceFlags); } } else { addSourcePathToSourcesBuildPhase(groupedSource.getSourcePath(), sourcesGroup, sourcesBuildPhase, sourceFlags); } break; case SOURCE_GROUP: PBXGroup newSourceGroup = sourcesGroup .getOrCreateChildGroupByName(groupedSource.getSourceGroupName()); // Sources groups stay in the order in which they're declared in the BUCK file. newSourceGroup.setSortPolicy(PBXGroup.SortPolicy.UNSORTED); addGroupedSourcesToBuildPhases(newSourceGroup, sourcesBuildPhase, headersBuildPhase, groupedSource.getSourceGroup(), sourceFlags); break; default: throw new RuntimeException("Unhandled grouped source type: " + groupedSource.getType()); } } } private void addSourcePathToSourcesBuildPhase(SourcePath sourcePath, PBXGroup sourcesGroup, PBXSourcesBuildPhase sourcesBuildPhase, ImmutableMap<SourcePath, String> sourceFlags) { Path path = sourcePath.resolve(); PBXFileReference fileReference = sourcesGroup.getOrCreateFileReferenceBySourceTreePath(new SourceTreePath( PBXReference.SourceTree.SOURCE_ROOT, this.repoRootRelativeToOutputDirectory.resolve(path))); PBXBuildFile buildFile = new PBXBuildFile(fileReference); sourcesBuildPhase.getFiles().add(buildFile); String customFlags = sourceFlags.get(sourcePath); if (customFlags != null) { NSDictionary settings = new NSDictionary(); settings.put("COMPILER_FLAGS", customFlags); buildFile.setSettings(Optional.of(settings)); } } private void addSourcePathToHeadersBuildPhase(SourcePath headerPath, PBXGroup headersGroup, PBXHeadersBuildPhase headersBuildPhase, ImmutableMap<SourcePath, String> sourceFlags) { Path path = headerPath.resolve(); PBXFileReference fileReference = headersGroup.getOrCreateFileReferenceBySourceTreePath(new SourceTreePath( PBXReference.SourceTree.SOURCE_ROOT, this.repoRootRelativeToOutputDirectory.resolve(path))); PBXBuildFile buildFile = new PBXBuildFile(fileReference); NSDictionary settings = new NSDictionary(); String headerFlags = sourceFlags.get(headerPath); if (headerFlags != null) { // If we specify nothing, Xcode will use "project" visibility. settings.put("ATTRIBUTES", new NSArray(new NSString(HeaderVisibility.fromString(headerFlags).toXcodeAttribute()))); buildFile.setSettings(Optional.of(settings)); } else { buildFile.setSettings(Optional.<NSDictionary>absent()); } headersBuildPhase.getFiles().add(buildFile); } private void addResourcesBuildPhase(PBXNativeTarget target, PBXGroup targetGroup, Iterable<Path> resources) { PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources"); PBXBuildPhase phase = new PBXResourcesBuildPhase(); target.getBuildPhases().add(phase); for (Path resource : resources) { PBXFileReference fileReference = resourcesGroup.getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT, this.repoRootRelativeToOutputDirectory.resolve(resource))); PBXBuildFile buildFile = new PBXBuildFile(fileReference); phase.getFiles().add(buildFile); } } private void addAssetCatalogBuildPhase(PBXNativeTarget target, PBXGroup targetGroup, final Iterable<AppleAssetCatalog> assetCatalogs) { // Asset catalogs go in the resources group also. PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources"); // Some asset catalogs should be copied to their sibling bundles, while others use the default // output format (which may be to copy individual files to the root resource output path or to // be archived in Assets.car if it is supported by the target platform version). ImmutableList.Builder<String> commonAssetCatalogsBuilder = ImmutableList.builder(); ImmutableList.Builder<String> assetCatalogsToSplitIntoBundlesBuilder = ImmutableList.builder(); for (AppleAssetCatalog assetCatalog : assetCatalogs) { List<String> scriptArguments = Lists.newArrayList(); for (Path dir : assetCatalog.getDirs()) { resourcesGroup.getOrCreateFileReferenceBySourceTreePath(new SourceTreePath( PBXReference.SourceTree.SOURCE_ROOT, this.repoRootRelativeToOutputDirectory.resolve(dir))); Path pathRelativeToProjectRoot = outputDirectory.relativize(dir); scriptArguments.add("$PROJECT_DIR/" + pathRelativeToProjectRoot.toString()); } if (assetCatalog.getCopyToBundles()) { assetCatalogsToSplitIntoBundlesBuilder.addAll(scriptArguments); } else { commonAssetCatalogsBuilder.addAll(scriptArguments); } } ImmutableList<String> commonAssetCatalogs = commonAssetCatalogsBuilder.build(); ImmutableList<String> assetCatalogsToSplitIntoBundles = assetCatalogsToSplitIntoBundlesBuilder.build(); // If there are no asset catalogs, don't add the build phase if (commonAssetCatalogs.size() == 0 && assetCatalogsToSplitIntoBundles.size() == 0) { return; } // In order for the script to run, it must be accessible by Xcode and deserves to be part of the // generated output. shouldPlaceAssetCatalogCompiler = true; Path assetCatalogBuildPhaseScriptRelativeToProjectRoot = outputDirectory .relativize(placedAssetCatalogBuildPhaseScript); // Map asset catalog paths to their shell script arguments relative to the project's root String combinedAssetCatalogsToBeSplitIntoBundlesScriptArguments = Joiner.on(' ') .join(assetCatalogsToSplitIntoBundles); String combinedCommonAssetCatalogsScriptArguments = Joiner.on(' ').join(commonAssetCatalogs); PBXShellScriptBuildPhase phase = new PBXShellScriptBuildPhase(); StringBuilder scriptBuilder = new StringBuilder("set -e\n"); if (commonAssetCatalogs.size() != 0) { scriptBuilder.append("\"$SRCROOT/\"" + assetCatalogBuildPhaseScriptRelativeToProjectRoot.toString() + " " + combinedCommonAssetCatalogsScriptArguments + "\n"); } if (assetCatalogsToSplitIntoBundles.size() != 0) { scriptBuilder.append("\"$SRCROOT/\"" + assetCatalogBuildPhaseScriptRelativeToProjectRoot.toString() + " -b " + combinedAssetCatalogsToBeSplitIntoBundlesScriptArguments); } phase.setShellScript(scriptBuilder.toString()); target.getBuildPhases().add(phase); } private void addFrameworksBuildPhase(BuildTarget buildTarget, PBXNativeTarget target, PBXGroup sharedFrameworksGroup, Iterable<String> frameworks, Iterable<PBXFileReference> archives) { PBXFrameworksBuildPhase frameworksBuildPhase = new PBXFrameworksBuildPhase(); target.getBuildPhases().add(frameworksBuildPhase); for (String framework : frameworks) { Path path = Paths.get(framework); String firstElement = Preconditions.checkNotNull(Iterables.getFirst(path, Paths.get(""))).toString(); if (firstElement.startsWith("$")) { // NOPMD - length() > 0 && charAt(0) == '$' is ridiculous Optional<PBXReference.SourceTree> sourceTree = PBXReference.SourceTree .fromBuildSetting(firstElement); if (sourceTree.isPresent()) { Path sdkRootRelativePath = path.subpath(1, path.getNameCount()); PBXFileReference fileReference = sharedFrameworksGroup.getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(sourceTree.get(), sdkRootRelativePath)); frameworksBuildPhase.getFiles().add(new PBXBuildFile(fileReference)); } else { throw new HumanReadableException( String.format("Unknown SourceTree: %s in build target: %s. Should be one of: %s", firstElement, buildTarget, Joiner.on(',') .join(Iterables.transform( ImmutableList.copyOf(PBXReference.SourceTree.values()), new Function<PBXReference.SourceTree, String>() { @Override public String apply(PBXReference.SourceTree input) { return "$" + input.toString(); } })))); } } else { // regular path PBXFileReference fileReference = sharedFrameworksGroup .getOrCreateFileReferenceBySourceTreePath(new SourceTreePath(PBXReference.SourceTree.GROUP, relativizeBuckRelativePathToGeneratedProject(buildTarget, path.toString()))); frameworksBuildPhase.getFiles().add(new PBXBuildFile(fileReference)); } } for (PBXFileReference archive : archives) { frameworksBuildPhase.getFiles().add(new PBXBuildFile(archive)); } } private void addCopyFrameworksBuildPhase(BuildTarget buildTarget, PBXNativeTarget target, PBXGroup sharedFrameworksGroup, Iterable<String> frameworks) { PBXCopyFilesBuildPhase copyFrameworksBuildPhase = new PBXCopyFilesBuildPhase( PBXCopyFilesBuildPhase.Destination.FRAMEWORKS, ""); target.getBuildPhases().add(copyFrameworksBuildPhase); for (String framework : frameworks) { Path path = Paths.get(framework); String firstElement = Preconditions.checkNotNull(Iterables.getFirst(path, Paths.get(""))).toString(); if (firstElement.charAt(0) == '$') { Optional<PBXReference.SourceTree> sourceTree = PBXReference.SourceTree .fromBuildSetting(firstElement); if (sourceTree.isPresent() && (sourceTree.get() == PBXReference.SourceTree.BUILT_PRODUCTS_DIR || sourceTree.get() == PBXReference.SourceTree.ABSOLUTE)) { Path sdkRootRelativePath = path.subpath(1, path.getNameCount()); PBXFileReference fileReference = sharedFrameworksGroup.getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(sourceTree.get(), sdkRootRelativePath)); copyFrameworksBuildPhase.getFiles().add(new PBXBuildFile(fileReference)); } } else { // regular path PBXFileReference fileReference = sharedFrameworksGroup .getOrCreateFileReferenceBySourceTreePath(new SourceTreePath(PBXReference.SourceTree.GROUP, relativizeBuckRelativePathToGeneratedProject(buildTarget, path.toString()))); copyFrameworksBuildPhase.getFiles().add(new PBXBuildFile(fileReference)); } } } private void addGeneratedSignedSourceTarget(PBXProject project) { PBXAggregateTarget target = new PBXAggregateTarget("GeneratedSignedSourceTarget"); // If we don't do this, Xcode "helpfully" generates a new configuration list // with a new GID every time. XCConfigurationList buildConfigurationList = new XCConfigurationList(); for (String projectConfigurationName : project.getBuildConfigurationList().getBuildConfigurationsByName() .asMap().keySet()) { buildConfigurationList.getBuildConfigurationsByName().getUnchecked(projectConfigurationName); } target.setBuildConfigurationList(buildConfigurationList); setTargetGIDIfNameInMap(target, targetNameToGIDMap); PBXShellScriptBuildPhase generatedSignedSourceScriptPhase = new PBXShellScriptBuildPhase(); generatedSignedSourceScriptPhase .setShellScript("# Do not change or remove this. This is a generated script phase\n" + "# used solely to include a signature in the generated Xcode project.\n" + "# " + SourceSigner.SIGNED_SOURCE_PLACEHOLDER); target.getBuildPhases().add(generatedSignedSourceScriptPhase); project.getTargets().add(target); } /** * Create the project bundle structure and write {@code project.pbxproj}. */ private Path writeProjectFile(PBXProject project) throws IOException { Preconditions.checkNotNull(targetNameToGIDMap); XcodeprojSerializer serializer = new XcodeprojSerializer( new GidGenerator(ImmutableSet.copyOf(targetNameToGIDMap.values())), project); NSDictionary rootObject = serializer.toPlist(); Path xcodeprojDir = outputDirectory.resolve(projectName + ".xcodeproj"); projectFilesystem.mkdirs(xcodeprojDir); Path serializedProject = xcodeprojDir.resolve("project.pbxproj"); String unsignedXmlProject = rootObject.toXMLPropertyList(); Optional<String> signedXmlProject = SourceSigner.sign(unsignedXmlProject); String contentsToWrite; if (signedXmlProject.isPresent()) { contentsToWrite = signedXmlProject.get(); } else { contentsToWrite = unsignedXmlProject; } projectFilesystem.writeContentsToPath(contentsToWrite, serializedProject); return xcodeprojDir; } /** * Create the workspace bundle structure and write the workspace file. * * Updates {@link #workspace} with the written document for examination. */ private void writeWorkspace(Path xcodeprojDir) throws IOException { DocumentBuilder docBuilder; Transformer transformer; try { docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); transformer = TransformerFactory.newInstance().newTransformer(); } catch (ParserConfigurationException | TransformerConfigurationException e) { throw new RuntimeException(e); } DOMImplementation domImplementation = docBuilder.getDOMImplementation(); Document doc = domImplementation.createDocument(null, "Workspace", null); doc.setXmlVersion("1.0"); Element rootElem = doc.getDocumentElement(); rootElem.setAttribute("version", "1.0"); Element fileRef = doc.createElement("FileRef"); fileRef.setAttribute("location", "container:" + xcodeprojDir.getFileName().toString()); rootElem.appendChild(fileRef); workspace = doc; Path projectWorkspaceDir = xcodeprojDir.getParent().resolve(projectName + ".xcworkspace"); projectFilesystem.mkdirs(projectWorkspaceDir); Path serializedWorkspace = projectWorkspaceDir.resolve("contents.xcworkspacedata"); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { DOMSource source = new DOMSource(doc); StreamResult result = new StreamResult(outputStream); transformer.transform(source, result); projectFilesystem.writeContentsToPath(outputStream.toString(), serializedWorkspace); } catch (TransformerException e) { throw new RuntimeException(e); } } private String serializeBuildConfiguration(XcodeRuleConfiguration configuration, ImmutableList<Path> searchPaths, ImmutableMap<String, String> extra) { XcconfigStack.Builder builder = XcconfigStack.builder(); for (XcodeRuleConfiguration.Layer layer : configuration.getLayers()) { switch (layer.getLayerType()) { case FILE: builder.addSettingsFromFile(projectFilesystem, searchPaths, layer.getPath().get()); break; case INLINE_SETTINGS: ImmutableMap<String, String> entries = layer.getInlineSettings().get(); for (ImmutableMap.Entry<String, String> entry : entries.entrySet()) { builder.addSetting(entry.getKey(), entry.getValue()); } break; } builder.pushLayer(); } for (ImmutableMap.Entry<String, String> entry : extra.entrySet()) { builder.addSetting(entry.getKey(), entry.getValue()); } builder.pushLayer(); XcconfigStack stack = builder.build(); ImmutableList<String> resolvedConfigs = stack.resolveConfigStack(); ImmutableSortedSet<String> sortedConfigs = ImmutableSortedSet.copyOf(resolvedConfigs); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("// This configuration is autogenerated.\n" + "// Re-run buck to update this file after modifying the hand-written configs.\n"); for (String line : sortedConfigs) { stringBuilder.append(line); stringBuilder.append('\n'); } return stringBuilder.toString(); } /** * Mangle the full build target string such that it's safe for use in a path. */ private static String mangledBuildTargetName(BuildTarget buildTarget) { return buildTarget.getFullyQualifiedName().replace('/', '-'); } private static String getProductName(BuildTarget buildTarget) { return buildTarget.getShortName(); } /** * Given a path relative to a BUCK file, return a path relative to the generated project. * * @param buildTarget * @param path path relative to build target * @return the given path relative to the generated project */ private Path relativizeBuckRelativePathToGeneratedProject(BuildTarget buildTarget, String path) { Path originalProjectPath = projectFilesystem .getPathForRelativePath(Paths.get(buildTarget.getBasePathWithSlash())); return this.repoRootRelativeToOutputDirectory.resolve(originalProjectPath).resolve(path); } private void collectRecursiveFrameworkDependencies(BuildRule rule, ImmutableSet.Builder<String> frameworksBuilder) { for (BuildRule ruleDependency : getRecursiveRuleDependenciesOfType(rule, IosLibraryDescription.TYPE)) { IosLibrary iosLibrary = (IosLibrary) Preconditions.checkNotNull(ruleDependency.getBuildable()); frameworksBuilder.addAll(iosLibrary.getFrameworks()); } } private ImmutableSet<PBXFileReference> collectRecursiveLibraryDependencies(BuildRule rule) { return FluentIterable.from( getRecursiveRuleDependenciesOfType(rule, IosLibraryDescription.TYPE, XcodeNativeDescription.TYPE)) .transform(new Function<BuildRule, PBXFileReference>() { @Override public PBXFileReference apply(BuildRule input) { return getLibraryFileReferenceForRule(input); } }).toSet(); } private PBXFileReference getLibraryFileReferenceForRule(BuildRule rule) { if (rule.getType().equals(IosLibraryDescription.TYPE)) { if (isBuiltByCurrentProject(rule)) { PBXNativeTarget target = (PBXNativeTarget) buildRuleToXcodeTarget.getUnchecked(rule).get(); return target.getProductReference(); } else { return project.getMainGroup().getOrCreateChildGroupByName("Frameworks") .getOrCreateFileReferenceBySourceTreePath(new SourceTreePath( PBXReference.SourceTree.BUILT_PRODUCTS_DIR, Paths.get(getLibraryNameFromTargetName(rule.getBuildTarget().getShortName())))); } } else if (rule.getType().equals(XcodeNativeDescription.TYPE)) { XcodeNative xcodeNative = (XcodeNative) Preconditions.checkNotNull(rule.getBuildable()); return project.getMainGroup().getOrCreateChildGroupByName("Frameworks") .getOrCreateFileReferenceBySourceTreePath(new SourceTreePath( PBXReference.SourceTree.BUILT_PRODUCTS_DIR, Paths.get(xcodeNative.getProduct()))); } else { throw new RuntimeException("Unexpected type: " + rule.getType()); } } /** * Whether a given build rule is built by the project being generated, or being build elsewhere. */ private boolean isBuiltByCurrentProject(BuildRule rule) { return options.contains(Option.GENERATE_TARGETS_FOR_DEPENDENCIES) || initialTargets.contains(rule.getBuildTarget()); } private static String getLibraryNameFromTargetName(String string) { return "lib" + string + ".a"; } private String getXcodeTargetName(BuildRule rule) { return options.contains(Option.USE_SHORT_NAMES_FOR_TARGETS) ? rule.getBuildTarget().getShortName() : rule.getBuildTarget().getFullyQualifiedName(); } /** * Collect resources from recursive dependencies. * * @param rule Build rule at the tip of the traversal. * @return Paths to resource files and folders, children of folder are not included. */ private Iterable<Path> collectRecursiveResources(BuildRule rule, BuildRuleType resourceRuleType) { Iterable<BuildRule> resourceRules = getRecursiveRuleDependenciesOfType(rule, resourceRuleType); ImmutableSet.Builder<Path> paths = ImmutableSet.builder(); for (BuildRule resourceRule : resourceRules) { AppleResource resource = (AppleResource) Preconditions.checkNotNull(resourceRule.getBuildable()); paths.addAll(resource.getDirs()); paths.addAll(SourcePaths.toPaths(resource.getFiles())); } return paths.build(); } /** * Collect asset catalogs from recursive dependencies. */ private Iterable<AppleAssetCatalog> collectRecursiveAssetCatalogs(BuildRule rule) { Iterable<BuildRule> assetCatalogRules = getRecursiveRuleDependenciesOfType(rule, AppleAssetCatalogDescription.TYPE); ImmutableSet.Builder<AppleAssetCatalog> assetCatalogs = ImmutableSet.builder(); for (BuildRule assetCatalogRule : assetCatalogRules) { AppleAssetCatalog assetCatalog = (AppleAssetCatalog) Preconditions .checkNotNull(assetCatalogRule.getBuildable()); assetCatalogs.add(assetCatalog); } return assetCatalogs.build(); } private Iterable<BuildRule> getRecursiveRuleDependenciesOfType(final BuildRule rule, BuildRuleType... types) { final ImmutableSet<BuildRuleType> requestedTypes = ImmutableSet.copyOf(types); final ImmutableList.Builder<BuildRule> filteredRules = ImmutableList.builder(); AbstractAcyclicDepthFirstPostOrderTraversal<BuildRule> traversal = new AbstractAcyclicDepthFirstPostOrderTraversal<BuildRule>() { @Override protected Iterator<BuildRule> findChildren(BuildRule node) throws IOException { return node.getDeps().iterator(); } @Override protected void onNodeExplored(BuildRule node) { if (node != rule && requestedTypes.contains(node.getType())) { filteredRules.add(node); } } @Override protected void onTraversalComplete(Iterable<BuildRule> nodesInExplorationOrder) { } }; try { traversal.traverse(ImmutableList.of(rule)); } catch (AbstractAcyclicDepthFirstPostOrderTraversal.CycleException | IOException e) { // actual load failures and cycle exceptions should have been caught at an earlier stage throw new RuntimeException(e); } return filteredRules.build(); } /** * Once we've generated the target, check if there's already a GID for a * target with the same name in an existing on-disk Xcode project. * * If there is, then re-use that target's GID instead of generating * a new one based on the target's name. */ private static void setTargetGIDIfNameInMap(PBXTarget target, ImmutableMap<String, String> targetNameToGIDMap) { @Nullable String existingTargetGID = targetNameToGIDMap.get(target.getName()); if (existingTargetGID == null) { return; } if (target.getGlobalID() == null) { target.setGlobalID(existingTargetGID); } else { // We better not have already generated some other GID for this // target. Preconditions.checkState(target.getGlobalID().equals(existingTargetGID)); } } /** * Reads in an existing Xcode project at * "projectPath/project.pbxproj" and returns a map of {target-name: * GID} pairs. * * If no such project exists, returns an empty map. */ @SuppressWarnings("PMD.EmptyCatchBlock") private ImmutableMap<String, String> buildTargetNameToGIDMap() throws IOException { ImmutableMap.Builder<String, String> targetNameToGIDMapBuilder = ImmutableMap.builder(); try { InputStream projectInputStream = projectFilesystem .newFileInputStream(projectPath.resolve(Paths.get("project.pbxproj"))); NSDictionary projectObjects = ProjectParser.extractObjectsFromXcodeProject(projectInputStream); ProjectParser.extractTargetNameToGIDMap(projectObjects, targetNameToGIDMapBuilder); } catch (NoSuchFileException e) { // We'll leave the builder empty in this case and return an empty map. } return targetNameToGIDMapBuilder.build(); } private static PBXTarget.ProductType testTypeToTargetProductType(IosTestType testType) { switch (testType) { case OCTEST: return PBXTarget.ProductType.IOS_TEST_OCTEST; case XCTEST: return PBXTarget.ProductType.IOS_TEST_XCTEST; default: throw new IllegalStateException("Invalid test type value: " + testType.toString()); } } /** * For all inputs by name, verify every entry has identical project level config, and pick one * such config to return. * * @param configInXcodeLayoutMultimap input mapping of { Config Name -> Config List } * @throws com.facebook.buck.util.HumanReadableException * if project-level configs are not identical for a named configuration */ private static ImmutableMap<String, ConfigInXcodeLayout> collectProjectLevelConfigsIfIdenticalOrFail( ImmutableMultimap<String, ConfigInXcodeLayout> configInXcodeLayoutMultimap) { ImmutableMap.Builder<String, ConfigInXcodeLayout> builder = ImmutableMap.builder(); for (String configName : configInXcodeLayoutMultimap.keySet()) { ConfigInXcodeLayout firstConfig = null; for (ConfigInXcodeLayout config : configInXcodeLayoutMultimap.get(configName)) { if (firstConfig == null) { firstConfig = config; } else if (!firstConfig.projectLevelConfigFile.equals(config.projectLevelConfigFile) || !firstConfig.projectLevelInlineSettings.equals(config.projectLevelInlineSettings)) { throw new HumanReadableException(String.format( "Project level configurations should be identical:\n" + " Config named: `%s` in `%s` and `%s` ", configName, firstConfig.buildTarget, config.buildTarget)); } } Preconditions.checkNotNull(firstConfig); builder.put(configName, firstConfig); } return builder.build(); } private static void setProjectLevelConfigs(PBXProject project, Path repoRootRelativeToOutputDirectory, ImmutableMap<String, ConfigInXcodeLayout> configs) { for (Map.Entry<String, ConfigInXcodeLayout> configEntry : configs.entrySet()) { XCBuildConfiguration outputConfig = project.getBuildConfigurationList().getBuildConfigurationsByName() .getUnchecked(configEntry.getKey()); ConfigInXcodeLayout config = configEntry.getValue(); PBXGroup configurationsGroup = project.getMainGroup().getOrCreateChildGroupByName("Configurations"); PBXFileReference fileReference = configurationsGroup.getOrCreateFileReferenceBySourceTreePath( new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT, repoRootRelativeToOutputDirectory .resolve(config.projectLevelConfigFile.get()).normalize())); outputConfig.setBaseConfigurationReference(fileReference); NSDictionary inlineSettings = new NSDictionary(); for (Map.Entry<String, String> entry : config.projectLevelInlineSettings.entrySet()) { inlineSettings.put(entry.getKey(), entry.getValue()); } outputConfig.setBuildSettings(inlineSettings); } } /** * Take a List of configuration layers and try to fit it into the xcode configuration layers * layout. * * @throws com.facebook.buck.util.HumanReadableException if the configuration layers are not in * the right layout to be coerced into standard xcode layout. */ private static ConfigInXcodeLayout extractXcodeConfigurationLayers(BuildTarget buildTarget, XcodeRuleConfiguration configuration) { ConfigInXcodeLayout extractedLayers = null; ImmutableList<XcodeRuleConfiguration.Layer> layers = configuration.getLayers(); switch (layers.size()) { case 2: if (layers.get(0).getLayerType() == XcodeRuleConfiguration.LayerType.FILE && layers.get(1).getLayerType() == XcodeRuleConfiguration.LayerType.FILE) { extractedLayers = new ConfigInXcodeLayout(buildTarget, layers.get(0).getPath(), ImmutableMap.<String, String>of(), layers.get(1).getPath(), ImmutableMap.<String, String>of()); } break; case 4: if (layers.get(0).getLayerType() == XcodeRuleConfiguration.LayerType.FILE && layers.get(1).getLayerType() == XcodeRuleConfiguration.LayerType.INLINE_SETTINGS && layers.get(2).getLayerType() == XcodeRuleConfiguration.LayerType.FILE && layers.get(3).getLayerType() == XcodeRuleConfiguration.LayerType.INLINE_SETTINGS) { extractedLayers = new ConfigInXcodeLayout(buildTarget, layers.get(0).getPath(), layers.get(1).getInlineSettings().or(ImmutableMap.<String, String>of()), layers.get(2).getPath(), layers.get(3).getInlineSettings().or(ImmutableMap.<String, String>of())); } break; default: // handled later on by the fact that extractLayers is null break; } if (extractedLayers == null) { throw new HumanReadableException("Configuration layers cannot be expressed in xcode for target: " + buildTarget + "\n" + " expected: [File, Inline settings, File, Inline settings]"); } return extractedLayers; } private static class ConfigInXcodeLayout { /** Tracks the originating build target for error reporting. */ public final BuildTarget buildTarget; public final Optional<Path> projectLevelConfigFile; public final ImmutableMap<String, String> projectLevelInlineSettings; public final Optional<Path> targetLevelConfigFile; public final ImmutableMap<String, String> targetLevelInlineSettings; private ConfigInXcodeLayout(BuildTarget buildTarget, Optional<Path> projectLevelConfigFile, ImmutableMap<String, String> projectLevelInlineSettings, Optional<Path> targetLevelConfigFile, ImmutableMap<String, String> targetLevelInlineSettings) { this.buildTarget = Preconditions.checkNotNull(buildTarget); this.projectLevelConfigFile = Preconditions.checkNotNull(projectLevelConfigFile); this.projectLevelInlineSettings = Preconditions.checkNotNull(projectLevelInlineSettings); this.targetLevelConfigFile = Preconditions.checkNotNull(targetLevelConfigFile); this.targetLevelInlineSettings = Preconditions.checkNotNull(targetLevelInlineSettings); } } }