/*
* 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.project_generator;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSObject;
import com.dd.plist.NSString;
import com.dd.plist.PropertyListParser;
import com.facebook.buck.apple.AppleAssetCatalogDescriptionArg;
import com.facebook.buck.apple.AppleBinaryDescription;
import com.facebook.buck.apple.AppleBinaryDescriptionArg;
import com.facebook.buck.apple.AppleBuildRules;
import com.facebook.buck.apple.AppleBundle;
import com.facebook.buck.apple.AppleBundleDescription;
import com.facebook.buck.apple.AppleBundleDescriptionArg;
import com.facebook.buck.apple.AppleBundleExtension;
import com.facebook.buck.apple.AppleDependenciesCache;
import com.facebook.buck.apple.AppleDescriptions;
import com.facebook.buck.apple.AppleHeaderVisibilities;
import com.facebook.buck.apple.AppleLibraryDescription;
import com.facebook.buck.apple.AppleLibraryDescriptionArg;
import com.facebook.buck.apple.AppleNativeTargetDescriptionArg;
import com.facebook.buck.apple.AppleResourceDescription;
import com.facebook.buck.apple.AppleResourceDescriptionArg;
import com.facebook.buck.apple.AppleResources;
import com.facebook.buck.apple.AppleTestDescription;
import com.facebook.buck.apple.AppleTestDescriptionArg;
import com.facebook.buck.apple.AppleWrapperResourceArg;
import com.facebook.buck.apple.CoreDataModelDescription;
import com.facebook.buck.apple.HasAppleBundleFields;
import com.facebook.buck.apple.InfoPlistSubstitution;
import com.facebook.buck.apple.PrebuiltAppleFrameworkDescription;
import com.facebook.buck.apple.PrebuiltAppleFrameworkDescriptionArg;
import com.facebook.buck.apple.SceneKitAssetsDescription;
import com.facebook.buck.apple.XcodePostbuildScriptDescription;
import com.facebook.buck.apple.XcodePrebuildScriptDescription;
import com.facebook.buck.apple.clang.HeaderMap;
import com.facebook.buck.apple.xcode.GidGenerator;
import com.facebook.buck.apple.xcode.XcodeprojSerializer;
import com.facebook.buck.apple.xcode.xcodeproj.CopyFilePhaseDestinationSpec;
import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildFile;
import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXCopyFilesBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXFileReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXGroup;
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.PBXShellScriptBuildPhase;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget;
import com.facebook.buck.apple.xcode.xcodeproj.ProductType;
import com.facebook.buck.apple.xcode.xcodeproj.SourceTreePath;
import com.facebook.buck.apple.xcode.xcodeproj.XCBuildConfiguration;
import com.facebook.buck.apple.xcode.xcodeproj.XCVersionGroup;
import com.facebook.buck.cxx.CxxBuckConfig;
import com.facebook.buck.cxx.CxxDescriptionEnhancer;
import com.facebook.buck.cxx.CxxLibraryDescription;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.cxx.CxxSource;
import com.facebook.buck.cxx.HasSystemFrameworkAndLibraries;
import com.facebook.buck.cxx.HeaderVisibility;
import com.facebook.buck.cxx.NativeLinkable;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.PerfEventId;
import com.facebook.buck.event.ProjectGenerationEvent;
import com.facebook.buck.event.SimplePerfEvent;
import com.facebook.buck.graph.AcyclicDepthFirstPostOrderTraversal;
import com.facebook.buck.graph.GraphTraversable;
import com.facebook.buck.halide.HalideBuckConfig;
import com.facebook.buck.halide.HalideCompile;
import com.facebook.buck.halide.HalideLibraryDescription;
import com.facebook.buck.halide.HalideLibraryDescriptionArg;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.MoreProjectFilesystems;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.js.IosReactNativeLibraryDescription;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuckVersion;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Either;
import com.facebook.buck.model.MacroException;
import com.facebook.buck.model.Pair;
import com.facebook.buck.model.UnflavoredBuildTarget;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.BuildTargetSourcePath;
import com.facebook.buck.rules.Cell;
import com.facebook.buck.rules.CellPathResolver;
import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.HasTests;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.rules.args.StringWithMacrosArg;
import com.facebook.buck.rules.coercer.FrameworkPath;
import com.facebook.buck.rules.coercer.SourceList;
import com.facebook.buck.rules.macros.AbstractMacroExpander;
import com.facebook.buck.rules.macros.LocationMacro;
import com.facebook.buck.rules.macros.Macro;
import com.facebook.buck.rules.macros.StringWithMacros;
import com.facebook.buck.shell.AbstractGenruleDescription;
import com.facebook.buck.shell.ExportFileDescriptionArg;
import com.facebook.buck.swift.SwiftBuckConfig;
import com.facebook.buck.util.Escaper;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreMaps;
import com.facebook.buck.util.RichStream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
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.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.UncheckedExecutionException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/** Generator for xcode project and associated files from a set of xcode/ios rules. */
public class ProjectGenerator {
private static final Logger LOG = Logger.get(ProjectGenerator.class);
private static final ImmutableList<String> DEFAULT_CFLAGS = ImmutableList.of();
private static final ImmutableList<String> DEFAULT_CXXFLAGS = ImmutableList.of();
private static final String PRODUCT_NAME = "PRODUCT_NAME";
private static final ImmutableSet<Class<? extends Description<?>>>
APPLE_NATIVE_DESCRIPTION_CLASSES =
ImmutableSet.of(
AppleBinaryDescription.class,
AppleLibraryDescription.class,
CxxLibraryDescription.class);
private static final ImmutableSet<AppleBundleExtension> APPLE_NATIVE_BUNDLE_EXTENSIONS =
ImmutableSet.of(AppleBundleExtension.APP, AppleBundleExtension.FRAMEWORK);
public enum Option {
/** Use short BuildTarget name instead of full name for targets */
USE_SHORT_NAMES_FOR_TARGETS,
/** Put targets into groups reflecting directory structure of their BUCK files */
CREATE_DIRECTORY_STRUCTURE,
/** Generate read-only project files */
GENERATE_READ_ONLY_FILES,
/** Include tests that test root targets in the scheme */
INCLUDE_TESTS,
/** Include dependencies tests in the scheme */
INCLUDE_DEPENDENCIES_TESTS,
/** Don't use header maps as header search paths */
DISABLE_HEADER_MAPS,
/**
* Generate one header map containing all the headers it's using and reference only this header
* map in the header search paths.
*/
MERGE_HEADER_MAPS,
/** Generates only headers symlink trees. */
GENERATE_HEADERS_SYMLINK_TREES_ONLY,
}
/** Standard options for generating a separated project */
public static final ImmutableSet<Option> SEPARATED_PROJECT_OPTIONS =
ImmutableSet.of(Option.USE_SHORT_NAMES_FOR_TARGETS);
/** Standard options for generating a combined project */
public static final ImmutableSet<Option> COMBINED_PROJECT_OPTIONS =
ImmutableSet.of(Option.CREATE_DIRECTORY_STRUCTURE, Option.USE_SHORT_NAMES_FOR_TARGETS);
private static final FileAttribute<?> READ_ONLY_FILE_ATTRIBUTE =
PosixFilePermissions.asFileAttribute(
ImmutableSet.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.GROUP_READ,
PosixFilePermission.OTHERS_READ));
private final TargetGraph targetGraph;
private final AppleDependenciesCache dependenciesCache;
private final Cell projectCell;
private final ProjectFilesystem projectFilesystem;
private final Path outputDirectory;
private final String projectName;
private final ImmutableSet<BuildTarget> initialTargets;
private final Path projectPath;
private final PathRelativizer pathRelativizer;
private final String buildFileName;
private final ImmutableSet<Option> options;
private final CxxPlatform defaultCxxPlatform;
// These fields are created/filled when creating the projects.
private final PBXProject project;
private final LoadingCache<TargetNode<?, ?>, Optional<PBXTarget>> targetNodeToProjectTarget;
private final ImmutableMultimap.Builder<TargetNode<?, ?>, PBXTarget>
targetNodeToGeneratedProjectTargetBuilder;
private boolean projectGenerated;
private final List<Path> headerSymlinkTrees;
private final ImmutableSet.Builder<BuildTarget> requiredBuildTargetsBuilder =
ImmutableSet.builder();
private final Function<? super TargetNode<?, ?>, BuildRuleResolver> buildRuleResolverForNode;
private final BuildRuleResolver defaultBuildRuleResolver;
private final SourcePathResolver defaultPathResolver;
private final BuckEventBus buckEventBus;
/**
* Populated while generating project configurations, in order to collect the possible
* project-level configurations to set.
*/
private final ImmutableSet.Builder<String> targetConfigNamesBuilder;
private final Map<String, String> gidsToTargetNames;
private final HalideBuckConfig halideBuckConfig;
private final CxxBuckConfig cxxBuckConfig;
private final SwiftBuckConfig swiftBuckConfig;
private final FocusedModuleTargetMatcher focusModules;
private final boolean isMainProject;
private final Optional<BuildTarget> workspaceTarget;
private final ImmutableSet<BuildTarget> targetsInRequiredProjects;
public ProjectGenerator(
TargetGraph targetGraph,
AppleDependenciesCache dependenciesCache,
Set<BuildTarget> initialTargets,
Cell cell,
Path outputDirectory,
String projectName,
String buildFileName,
Set<Option> options,
boolean isMainProject,
Optional<BuildTarget> workspaceTarget,
ImmutableSet<BuildTarget> targetsInRequiredProjects,
FocusedModuleTargetMatcher focusModules,
CxxPlatform defaultCxxPlatform,
Function<? super TargetNode<?, ?>, BuildRuleResolver> buildRuleResolverForNode,
BuckEventBus buckEventBus,
HalideBuckConfig halideBuckConfig,
CxxBuckConfig cxxBuckConfig,
SwiftBuckConfig swiftBuckConfig) {
this.targetGraph = targetGraph;
this.dependenciesCache = dependenciesCache;
this.initialTargets = ImmutableSet.copyOf(initialTargets);
this.projectCell = cell;
this.projectFilesystem = cell.getFilesystem();
this.outputDirectory = outputDirectory;
this.projectName = projectName;
this.buildFileName = buildFileName;
this.options = ImmutableSet.copyOf(options);
this.isMainProject = isMainProject;
this.workspaceTarget = workspaceTarget;
this.targetsInRequiredProjects = targetsInRequiredProjects;
this.defaultCxxPlatform = defaultCxxPlatform;
this.buildRuleResolverForNode = buildRuleResolverForNode;
this.defaultBuildRuleResolver =
new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer());
this.defaultPathResolver =
new SourcePathResolver(new SourcePathRuleFinder(this.defaultBuildRuleResolver));
this.buckEventBus = buckEventBus;
this.projectPath = outputDirectory.resolve(projectName + ".xcodeproj");
this.pathRelativizer = new PathRelativizer(outputDirectory, this::resolveSourcePath);
LOG.debug(
"Output directory %s, profile fs root path %s, repo root relative to output dir %s",
this.outputDirectory,
projectFilesystem.getRootPath(),
this.pathRelativizer.outputDirToRootRelative(Paths.get(".")));
this.project = new PBXProject(projectName);
this.headerSymlinkTrees = new ArrayList<>();
this.targetNodeToGeneratedProjectTargetBuilder = ImmutableMultimap.builder();
this.targetNodeToProjectTarget =
CacheBuilder.newBuilder()
.build(
new CacheLoader<TargetNode<?, ?>, Optional<PBXTarget>>() {
@Override
public Optional<PBXTarget> load(TargetNode<?, ?> key) throws Exception {
return generateProjectTarget(key);
}
});
targetConfigNamesBuilder = ImmutableSet.builder();
gidsToTargetNames = new HashMap<>();
this.halideBuckConfig = halideBuckConfig;
this.cxxBuckConfig = cxxBuckConfig;
this.swiftBuckConfig = swiftBuckConfig;
this.focusModules = focusModules;
}
@VisibleForTesting
PBXProject getGeneratedProject() {
return project;
}
@VisibleForTesting
List<Path> getGeneratedHeaderSymlinkTrees() {
return headerSymlinkTrees;
}
public Path getProjectPath() {
return projectPath;
}
private boolean isHeaderMapDisabled() {
return options.contains(Option.DISABLE_HEADER_MAPS);
}
private boolean shouldMergeHeaderMaps() {
return options.contains(Option.MERGE_HEADER_MAPS)
&& workspaceTarget.isPresent()
&& !isHeaderMapDisabled();
}
private boolean shouldGenerateHeaderSymlinkTreesOnly() {
return options.contains(Option.GENERATE_HEADERS_SYMLINK_TREES_ONLY);
}
public ImmutableMap<BuildTarget, PBXTarget> getBuildTargetToGeneratedTargetMap() {
Preconditions.checkState(projectGenerated, "Must have called createXcodeProjects");
ImmutableMap.Builder<BuildTarget, PBXTarget> buildTargetToPbxTargetMap = ImmutableMap.builder();
for (Map.Entry<TargetNode<?, ?>, PBXTarget> entry :
targetNodeToGeneratedProjectTargetBuilder.build().entries()) {
buildTargetToPbxTargetMap.put(entry.getKey().getBuildTarget(), entry.getValue());
}
return buildTargetToPbxTargetMap.build();
}
public ImmutableSet<BuildTarget> getRequiredBuildTargets() {
Preconditions.checkState(projectGenerated, "Must have called createXcodeProjects");
return requiredBuildTargetsBuilder.build();
}
// Returns true if we ran the project generation and we decided to eventually generate
// the project.
public boolean isProjectGenerated() {
return projectGenerated;
}
public void createXcodeProjects() throws IOException {
LOG.debug("Creating projects for targets %s", initialTargets);
boolean hasAtLeastOneTarget = false;
try (SimplePerfEvent.Scope scope =
SimplePerfEvent.scope(
buckEventBus,
PerfEventId.of("xcode_project_generation"),
ImmutableMap.of("Path", getProjectPath()))) {
for (TargetNode<?, ?> targetNode : targetGraph.getNodes()) {
if (isBuiltByCurrentProject(targetNode.getBuildTarget())) {
LOG.debug("Including rule %s in project", targetNode);
// Trigger the loading cache to call the generateProjectTarget function.
Optional<PBXTarget> target = targetNodeToProjectTarget.getUnchecked(targetNode);
target.ifPresent(
pbxTarget -> targetNodeToGeneratedProjectTargetBuilder.put(targetNode, pbxTarget));
if (focusModules.isFocusedOn(targetNode.getBuildTarget())) {
// If the target is not included, we still need to do other operations to generate
// the required header maps.
hasAtLeastOneTarget = true;
}
} else {
LOG.verbose("Excluding rule %s (not built by current project)", targetNode);
}
}
if (!hasAtLeastOneTarget && focusModules.hasFocus()) {
return;
}
for (String configName : targetConfigNamesBuilder.build()) {
XCBuildConfiguration outputConfig =
project
.getBuildConfigurationList()
.getBuildConfigurationsByName()
.getUnchecked(configName);
outputConfig.setBuildSettings(new NSDictionary());
}
if (!shouldGenerateHeaderSymlinkTreesOnly()) {
writeProjectFile(project);
}
projectGenerated = true;
} 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.
UncheckedExecutionException originalException = e;
while (e.getCause() instanceof UncheckedExecutionException) {
e = (UncheckedExecutionException) e.getCause();
}
if (e.getCause() instanceof HumanReadableException) {
throw (HumanReadableException) e.getCause();
} else {
throw originalException;
}
}
}
private static Optional<String> getProductNameForTargetNode(TargetNode<?, ?> targetNode) {
return targetNode
.castArg(AppleBundleDescriptionArg.class)
.flatMap(node -> node.getConstructorArg().getProductName());
}
@SuppressWarnings("unchecked")
private Optional<PBXTarget> generateProjectTarget(TargetNode<?, ?> targetNode)
throws IOException {
Preconditions.checkState(
isBuiltByCurrentProject(targetNode.getBuildTarget()),
"should not generate rule if it shouldn't be built by current project");
Optional<PBXTarget> result = Optional.empty();
if (targetNode.getDescription() instanceof AppleLibraryDescription) {
result =
Optional.of(
generateAppleLibraryTarget(
project,
(TargetNode<AppleNativeTargetDescriptionArg, ?>) targetNode,
Optional.empty()));
} else if (targetNode.getDescription() instanceof CxxLibraryDescription) {
result =
Optional.of(
generateCxxLibraryTarget(
project,
(TargetNode<CxxLibraryDescription.CommonArg, ?>) targetNode,
ImmutableSet.of(),
ImmutableSet.of(),
Optional.empty()));
} else if (targetNode.getDescription() instanceof AppleBinaryDescription) {
result =
Optional.of(
generateAppleBinaryTarget(
project, (TargetNode<AppleNativeTargetDescriptionArg, ?>) targetNode));
} else if (targetNode.getDescription() instanceof AppleBundleDescription) {
TargetNode<AppleBundleDescriptionArg, ?> bundleTargetNode =
(TargetNode<AppleBundleDescriptionArg, ?>) targetNode;
result =
Optional.of(
generateAppleBundleTarget(
project,
bundleTargetNode,
(TargetNode<AppleNativeTargetDescriptionArg, ?>)
targetGraph.get(bundleTargetNode.getConstructorArg().getBinary()),
Optional.empty()));
} else if (targetNode.getDescription() instanceof AppleTestDescription) {
result =
Optional.of(generateAppleTestTarget((TargetNode<AppleTestDescriptionArg, ?>) targetNode));
} else if (targetNode.getDescription() instanceof AppleResourceDescription) {
checkAppleResourceTargetNodeReferencingValidContents(
(TargetNode<AppleResourceDescriptionArg, ?>) targetNode);
} else if (targetNode.getDescription() instanceof HalideLibraryDescription) {
TargetNode<HalideLibraryDescriptionArg, ?> halideTargetNode =
(TargetNode<HalideLibraryDescriptionArg, ?>) targetNode;
BuildTarget buildTarget = targetNode.getBuildTarget();
// The generated target just runs a shell script that invokes the "compiler" with the
// correct target architecture.
result = generateHalideLibraryTarget(project, halideTargetNode);
// Make sure the compiler gets built at project time, since we'll need
// it to generate the shader code during the Xcode build.
requiredBuildTargetsBuilder.add(
HalideLibraryDescription.createHalideCompilerBuildTarget(buildTarget));
// HACK: Don't generate the Halide headers unless the compiler is expected
// to generate output for the default platform -- a Halide library that
// uses a platform regex may not be able to use the default platform.
// This assumes that there's a 'default' variant of the rule to generate
// headers from.
if (HalideLibraryDescription.isPlatformSupported(
halideTargetNode.getConstructorArg(), defaultCxxPlatform)) {
// Run the compiler once at project time to generate the header
// file needed for compilation if the Halide target is for the default
// platform.
requiredBuildTargetsBuilder.add(
buildTarget.withFlavors(
HalideLibraryDescription.HALIDE_COMPILE_FLAVOR, defaultCxxPlatform.getFlavor()));
}
} else if (targetNode.getDescription() instanceof AbstractGenruleDescription) {
addGenruleFiles(project, (TargetNode<AbstractGenruleDescription.CommonArg, ?>) targetNode);
}
buckEventBus.post(ProjectGenerationEvent.processed());
return result;
}
private static Path getHalideOutputPath(ProjectFilesystem filesystem, BuildTarget target) {
return filesystem
.getBuckPaths()
.getBuckOut()
.resolve("halide")
.resolve(target.getBasePath())
.resolve(target.getShortName());
}
private Optional<PBXTarget> generateHalideLibraryTarget(
PBXProject project, TargetNode<HalideLibraryDescriptionArg, ?> targetNode)
throws IOException {
final BuildTarget buildTarget = targetNode.getBuildTarget();
boolean isFocusedOnTarget = focusModules.isFocusedOn(buildTarget);
String productName = getProductNameForBuildTarget(buildTarget);
Path outputPath = getHalideOutputPath(targetNode.getFilesystem(), buildTarget);
Path scriptPath = halideBuckConfig.getXcodeCompileScriptPath();
Optional<String> script = projectFilesystem.readFileIfItExists(scriptPath);
PBXShellScriptBuildPhase scriptPhase = new PBXShellScriptBuildPhase();
scriptPhase.setShellScript(script.orElse(""));
NewNativeTargetProjectMutator mutator =
new NewNativeTargetProjectMutator(pathRelativizer, this::resolveSourcePath);
mutator
.setTargetName(getXcodeTargetName(buildTarget))
.setProduct(ProductType.STATIC_LIBRARY, productName, outputPath)
.setPreBuildRunScriptPhases(ImmutableList.of(scriptPhase));
NewNativeTargetProjectMutator.Result targetBuilderResult;
targetBuilderResult = mutator.buildTargetAndAddToProject(project, isFocusedOnTarget);
BuildTarget compilerTarget =
HalideLibraryDescription.createHalideCompilerBuildTarget(buildTarget);
Path compilerPath = BuildTargets.getGenPath(projectFilesystem, compilerTarget, "%s");
ImmutableMap<String, String> appendedConfig = ImmutableMap.of();
ImmutableMap<String, String> extraSettings = ImmutableMap.of();
ImmutableMap.Builder<String, String> defaultSettingsBuilder = ImmutableMap.builder();
defaultSettingsBuilder.put(
"REPO_ROOT", projectFilesystem.getRootPath().toAbsolutePath().normalize().toString());
defaultSettingsBuilder.put("HALIDE_COMPILER_PATH", compilerPath.toString());
// pass the source list to the xcode script
String halideCompilerSrcs;
Iterable<Path> compilerSrcFiles =
Iterables.transform(
targetNode.getConstructorArg().getSrcs(),
input -> resolveSourcePath(input.getSourcePath()));
halideCompilerSrcs = Joiner.on(" ").join(compilerSrcFiles);
defaultSettingsBuilder.put("HALIDE_COMPILER_SRCS", halideCompilerSrcs);
String halideCompilerFlags;
halideCompilerFlags = Joiner.on(" ").join(targetNode.getConstructorArg().getCompilerFlags());
defaultSettingsBuilder.put("HALIDE_COMPILER_FLAGS", halideCompilerFlags);
defaultSettingsBuilder.put("HALIDE_OUTPUT_PATH", outputPath.toString());
defaultSettingsBuilder.put("HALIDE_FUNC_NAME", buildTarget.getShortName());
defaultSettingsBuilder.put(PRODUCT_NAME, productName);
Optional<ImmutableSortedMap<String, ImmutableMap<String, String>>> configs =
getXcodeBuildConfigurationsForTargetNode(targetNode, appendedConfig);
PBXNativeTarget target = targetBuilderResult.target;
setTargetBuildConfigurations(
buildTarget,
target,
project.getMainGroup(),
configs.get(),
extraSettings,
defaultSettingsBuilder.build(),
appendedConfig);
return Optional.of(target);
}
private PBXTarget generateAppleTestTarget(TargetNode<AppleTestDescriptionArg, ?> testTargetNode)
throws IOException {
Optional<TargetNode<AppleBundleDescriptionArg, ?>> testHostBundle =
testTargetNode
.getConstructorArg()
.getTestHostApp()
.map(
testHostBundleTarget -> {
TargetNode<?, ?> testHostBundleNode = targetGraph.get(testHostBundleTarget);
return testHostBundleNode
.castArg(AppleBundleDescriptionArg.class)
.orElseGet(
() -> {
throw new HumanReadableException(
"The test host target '%s' has the wrong type (%s), must be apple_bundle",
testHostBundleTarget,
testHostBundleNode.getDescription().getClass());
});
});
return generateAppleBundleTarget(project, testTargetNode, testTargetNode, testHostBundle);
}
private void checkAppleResourceTargetNodeReferencingValidContents(
TargetNode<AppleResourceDescriptionArg, ?> resource) {
// Check that the resource target node is referencing valid files or directories.
// If a SourcePath is a BuildTargetSourcePath (or some hypothetical future implementation of
// SourcePath), just assume it's the right type; we have no way of checking now as it
// may not exist yet.
AppleResourceDescriptionArg arg = resource.getConstructorArg();
for (SourcePath dir : arg.getDirs()) {
if (dir instanceof PathSourcePath && !projectFilesystem.isDirectory(resolveSourcePath(dir))) {
throw new HumanReadableException(
"%s specified in the dirs parameter of %s is not a directory",
dir.toString(), resource.toString());
}
}
for (SourcePath file : arg.getFiles()) {
if (file instanceof PathSourcePath && !projectFilesystem.isFile(resolveSourcePath(file))) {
throw new HumanReadableException(
"%s specified in the files parameter of %s is not a regular file",
file.toString(), resource.toString());
}
}
}
private PBXNativeTarget generateAppleBundleTarget(
PBXProject project,
TargetNode<? extends HasAppleBundleFields, ?> targetNode,
TargetNode<? extends AppleNativeTargetDescriptionArg, ?> binaryNode,
Optional<TargetNode<AppleBundleDescriptionArg, ?>> bundleLoaderNode)
throws IOException {
Path infoPlistPath =
Preconditions.checkNotNull(
resolveSourcePath(targetNode.getConstructorArg().getInfoPlist()));
// -- copy any binary and bundle targets into this bundle
Iterable<TargetNode<?, ?>> copiedRules =
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.COPYING,
targetNode,
Optional.of(AppleBuildRules.XCODE_TARGET_DESCRIPTION_CLASSES));
if (bundleRequiresRemovalOfAllTransitiveFrameworks(targetNode)) {
copiedRules = rulesWithoutFrameworkBundles(copiedRules);
} else if (bundleRequiresAllTransitiveFrameworks(binaryNode)) {
copiedRules =
ImmutableSet.<TargetNode<?, ?>>builder()
.addAll(copiedRules)
.addAll(getTransitiveFrameworkNodes(targetNode))
.build();
}
if (bundleLoaderNode.isPresent()) {
copiedRules = rulesWithoutBundleLoader(copiedRules, bundleLoaderNode.get());
}
ImmutableList<PBXBuildPhase> copyFilesBuildPhases = getCopyFilesBuildPhases(copiedRules);
PBXNativeTarget target =
generateBinaryTarget(
project,
Optional.of(targetNode),
binaryNode,
bundleToTargetProductType(targetNode, binaryNode),
"%s." + getExtensionString(targetNode.getConstructorArg().getExtension()),
Optional.of(infoPlistPath),
/* includeFrameworks */ true,
AppleResources.collectRecursiveResources(
targetGraph, Optional.of(dependenciesCache), ImmutableList.of(targetNode)),
AppleResources.collectDirectResources(targetGraph, targetNode),
AppleBuildRules.collectRecursiveAssetCatalogs(
targetGraph, Optional.of(dependenciesCache), ImmutableList.of(targetNode)),
AppleBuildRules.collectDirectAssetCatalogs(targetGraph, targetNode),
AppleBuildRules.collectRecursiveWrapperResources(
targetGraph, Optional.of(dependenciesCache), ImmutableList.of(targetNode)),
Optional.of(copyFilesBuildPhases),
bundleLoaderNode);
LOG.debug("Generated iOS bundle target %s", target);
return target;
}
/**
* Traverses the graph to find all (non-system) frameworks that should be embedded into the
* target's bundle.
*/
private ImmutableSet<TargetNode<?, ?>> getTransitiveFrameworkNodes(
TargetNode<? extends HasAppleBundleFields, ?> targetNode) {
GraphTraversable<TargetNode<?, ?>> graphTraversable =
node -> {
if (!(node.getDescription() instanceof AppleResourceDescription)) {
return targetGraph.getAll(node.getBuildDeps()).iterator();
} else {
return Collections.emptyIterator();
}
};
final ImmutableSet.Builder<TargetNode<?, ?>> filteredRules = ImmutableSet.builder();
AcyclicDepthFirstPostOrderTraversal<TargetNode<?, ?>> traversal =
new AcyclicDepthFirstPostOrderTraversal<>(graphTraversable);
try {
for (TargetNode<?, ?> node : traversal.traverse(ImmutableList.of(targetNode))) {
if (node != targetNode) {
node.castArg(AppleBundleDescriptionArg.class)
.ifPresent(
appleBundleNode -> {
if (isFrameworkBundle(appleBundleNode.getConstructorArg())) {
filteredRules.add(node);
}
});
node.castArg(PrebuiltAppleFrameworkDescriptionArg.class)
.ifPresent(
prebuiltFramework -> {
// Technically (see Apple Tech Notes 2435), static frameworks are lies. In case a static
// framework is used, they can escape the incorrect project generation by marking its
// preferred linkage static (what does preferred linkage even mean for a prebuilt thing?
// none of this makes sense anyways).
if (prebuiltFramework.getConstructorArg().getPreferredLinkage()
!= NativeLinkable.Linkage.STATIC) {
filteredRules.add(node);
}
});
}
}
} catch (AcyclicDepthFirstPostOrderTraversal.CycleException e) {
throw new RuntimeException(e);
}
return filteredRules.build();
}
/** Returns a new list of rules which does not contain framework bundles. */
private ImmutableList<TargetNode<?, ?>> rulesWithoutFrameworkBundles(
Iterable<TargetNode<?, ?>> copiedRules) {
return RichStream.from(copiedRules)
.filter(
input ->
input
.castArg(AppleBundleDescriptionArg.class)
.map(argTargetNode -> !isFrameworkBundle(argTargetNode.getConstructorArg()))
.orElse(true))
.toImmutableList();
}
private ImmutableList<TargetNode<?, ?>> rulesWithoutBundleLoader(
Iterable<TargetNode<?, ?>> copiedRules, TargetNode<?, ?> bundleLoader) {
return RichStream.from(copiedRules).filter(x -> !bundleLoader.equals(x)).toImmutableList();
}
private PBXNativeTarget generateAppleBinaryTarget(
PBXProject project, TargetNode<AppleNativeTargetDescriptionArg, ?> targetNode)
throws IOException {
PBXNativeTarget target =
generateBinaryTarget(
project,
Optional.empty(),
targetNode,
ProductType.TOOL,
"%s",
Optional.empty(),
/* includeFrameworks */ true,
ImmutableSet.of(),
AppleResources.collectDirectResources(targetGraph, targetNode),
ImmutableSet.of(),
AppleBuildRules.collectDirectAssetCatalogs(targetGraph, targetNode),
ImmutableSet.of(),
Optional.empty(),
Optional.empty());
LOG.debug("Generated Apple binary target %s", target);
return target;
}
private PBXNativeTarget generateAppleLibraryTarget(
PBXProject project,
TargetNode<? extends AppleNativeTargetDescriptionArg, ?> targetNode,
Optional<TargetNode<AppleBundleDescriptionArg, ?>> bundleLoaderNode)
throws IOException {
PBXNativeTarget target =
generateCxxLibraryTarget(
project,
targetNode,
AppleResources.collectDirectResources(targetGraph, targetNode),
AppleBuildRules.collectDirectAssetCatalogs(targetGraph, targetNode),
bundleLoaderNode);
LOG.debug("Generated iOS library target %s", target);
return target;
}
private PBXNativeTarget generateCxxLibraryTarget(
PBXProject project,
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
ImmutableSet<AppleResourceDescriptionArg> directResources,
ImmutableSet<AppleAssetCatalogDescriptionArg> directAssetCatalogs,
Optional<TargetNode<AppleBundleDescriptionArg, ?>> bundleLoaderNode)
throws IOException {
boolean isShared =
targetNode.getBuildTarget().getFlavors().contains(CxxDescriptionEnhancer.SHARED_FLAVOR);
ProductType productType = isShared ? ProductType.DYNAMIC_LIBRARY : ProductType.STATIC_LIBRARY;
PBXNativeTarget target =
generateBinaryTarget(
project,
Optional.empty(),
targetNode,
productType,
AppleBuildRules.getOutputFileNameFormatForLibrary(isShared),
Optional.empty(),
/* includeFrameworks */ isShared,
ImmutableSet.of(),
directResources,
ImmutableSet.of(),
directAssetCatalogs,
ImmutableSet.of(),
Optional.empty(),
bundleLoaderNode);
LOG.debug("Generated Cxx library target %s", target);
return target;
}
private ImmutableList<String> convertStringWithMacros(
TargetNode<?, ?> node, Iterable<StringWithMacros> flags) {
ImmutableList.Builder<String> result = new ImmutableList.Builder<>();
ImmutableList<? extends AbstractMacroExpander<? extends Macro>> expanders =
ImmutableList.of(new AsIsLocationMacroExpander());
for (StringWithMacros flag : flags) {
StringWithMacrosArg.of(
flag, expanders, node.getBuildTarget(), node.getCellNames(), defaultBuildRuleResolver)
.appendToCommandLine(result, defaultPathResolver);
}
return result.build();
}
private PBXNativeTarget generateBinaryTarget(
PBXProject project,
Optional<? extends TargetNode<? extends HasAppleBundleFields, ?>> bundle,
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
ProductType productType,
String productOutputFormat,
Optional<Path> infoPlistOptional,
boolean includeFrameworks,
ImmutableSet<AppleResourceDescriptionArg> recursiveResources,
ImmutableSet<AppleResourceDescriptionArg> directResources,
ImmutableSet<AppleAssetCatalogDescriptionArg> recursiveAssetCatalogs,
ImmutableSet<AppleAssetCatalogDescriptionArg> directAssetCatalogs,
ImmutableSet<AppleWrapperResourceArg> wrapperResources,
Optional<Iterable<PBXBuildPhase>> copyFilesPhases,
Optional<TargetNode<AppleBundleDescriptionArg, ?>> bundleLoaderNode)
throws IOException {
LOG.debug("Generating binary target for node %s", targetNode);
TargetNode<?, ?> buildTargetNode = bundle.isPresent() ? bundle.get() : targetNode;
final BuildTarget buildTarget = buildTargetNode.getBuildTarget();
String buildTargetName = getProductNameForBuildTarget(buildTarget);
CxxLibraryDescription.CommonArg arg = targetNode.getConstructorArg();
NewNativeTargetProjectMutator mutator =
new NewNativeTargetProjectMutator(pathRelativizer, this::resolveSourcePath);
ImmutableSet<SourcePath> exportedHeaders =
ImmutableSet.copyOf(getHeaderSourcePaths(arg.getExportedHeaders()));
ImmutableSet<SourcePath> headers = ImmutableSet.copyOf(getHeaderSourcePaths(arg.getHeaders()));
ImmutableMap<CxxSource.Type, ImmutableList<String>> langPreprocessorFlags =
targetNode.getConstructorArg().getLangPreprocessorFlags();
boolean isFocusedOnTarget = focusModules.isFocusedOn(buildTarget);
mutator
.setTargetName(getXcodeTargetName(buildTarget))
.setProduct(
productType,
buildTargetName,
Paths.get(String.format(productOutputFormat, buildTargetName)));
boolean isModularAppleFramework = isModularAppleFramework(targetNode) && isFocusedOnTarget;
mutator.setFrameworkHeadersEnabled(isModularAppleFramework);
if (!shouldGenerateHeaderSymlinkTreesOnly()) {
if (isFocusedOnTarget) {
mutator
.setLangPreprocessorFlags(langPreprocessorFlags)
.setPublicHeaders(exportedHeaders)
.setPrefixHeader(arg.getPrefixHeader())
.setSourcesWithFlags(ImmutableSet.copyOf(arg.getSrcs()))
.setPrivateHeaders(headers)
.setRecursiveResources(recursiveResources)
.setDirectResources(directResources)
.setWrapperResources(wrapperResources)
.setExtraXcodeSources(ImmutableSet.copyOf(arg.getExtraXcodeSources()))
.setExtraXcodeFiles(ImmutableSet.copyOf(arg.getExtraXcodeFiles()));
}
if (bundle.isPresent() && isFocusedOnTarget) {
HasAppleBundleFields bundleArg = bundle.get().getConstructorArg();
mutator.setInfoPlist(Optional.of(bundleArg.getInfoPlist()));
}
mutator.setBridgingHeader(arg.getBridgingHeader());
if (options.contains(Option.CREATE_DIRECTORY_STRUCTURE) && isFocusedOnTarget) {
mutator.setTargetGroupPath(
RichStream.from(buildTarget.getBasePath()).map(Object::toString).toImmutableList());
}
if (!recursiveAssetCatalogs.isEmpty() && isFocusedOnTarget) {
mutator.setRecursiveAssetCatalogs(recursiveAssetCatalogs);
}
if (!directAssetCatalogs.isEmpty() && isFocusedOnTarget) {
mutator.setDirectAssetCatalogs(directAssetCatalogs);
}
if (includeFrameworks && isFocusedOnTarget) {
ImmutableSet.Builder<FrameworkPath> frameworksBuilder = ImmutableSet.builder();
frameworksBuilder.addAll(targetNode.getConstructorArg().getFrameworks());
frameworksBuilder.addAll(targetNode.getConstructorArg().getLibraries());
frameworksBuilder.addAll(collectRecursiveFrameworkDependencies(targetNode));
mutator.setFrameworks(frameworksBuilder.build());
mutator.setArchives(collectRecursiveLibraryDependencies(targetNode));
}
// TODO(Task #3772930): Go through all dependencies of the rule
// and add any shell script rules here
ImmutableList.Builder<TargetNode<?, ?>> preScriptPhases = ImmutableList.builder();
ImmutableList.Builder<TargetNode<?, ?>> postScriptPhases = ImmutableList.builder();
if (bundle.isPresent() && targetNode != bundle.get() && isFocusedOnTarget) {
collectBuildScriptDependencies(
targetGraph.getAll(bundle.get().getDeclaredDeps()), preScriptPhases, postScriptPhases);
}
collectBuildScriptDependencies(
targetGraph.getAll(targetNode.getDeclaredDeps()), preScriptPhases, postScriptPhases);
if (isFocusedOnTarget) {
mutator.setPreBuildRunScriptPhasesFromTargetNodes(preScriptPhases.build());
if (copyFilesPhases.isPresent()) {
mutator.setCopyFilesPhases(copyFilesPhases.get());
}
mutator.setPostBuildRunScriptPhasesFromTargetNodes(postScriptPhases.build());
}
}
NewNativeTargetProjectMutator.Result targetBuilderResult =
mutator.buildTargetAndAddToProject(project, isFocusedOnTarget);
PBXNativeTarget target = targetBuilderResult.target;
Optional<PBXGroup> targetGroup = targetBuilderResult.targetGroup;
ImmutableMap.Builder<String, String> extraSettingsBuilder = ImmutableMap.builder();
ImmutableMap.Builder<String, String> defaultSettingsBuilder = ImmutableMap.builder();
if (!shouldGenerateHeaderSymlinkTreesOnly()) {
if (isFocusedOnTarget) {
SourceTreePath buckFilePath =
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputPathToBuildTargetPath(buildTarget).resolve(buildFileName),
Optional.empty());
PBXFileReference buckReference =
targetGroup.get().getOrCreateFileReferenceBySourceTreePath(buckFilePath);
buckReference.setExplicitFileType(Optional.of("text.script.python"));
}
// -- configurations
extraSettingsBuilder
.put("TARGET_NAME", buildTargetName)
.put("SRCROOT", pathRelativizer.outputPathToBuildTargetPath(buildTarget).toString());
if (productType == ProductType.UI_TEST && isFocusedOnTarget) {
if (bundleLoaderNode.isPresent()) {
BuildTarget testTarget = bundleLoaderNode.get().getBuildTarget();
extraSettingsBuilder.put("TEST_TARGET_NAME", getXcodeTargetName(testTarget));
} else {
throw new HumanReadableException(
"The test rule '%s' is configured with 'is_ui_test' but has no test_host_app",
buildTargetName);
}
} else if (bundleLoaderNode.isPresent() && isFocusedOnTarget) {
TargetNode<AppleBundleDescriptionArg, ?> bundleLoader = bundleLoaderNode.get();
String bundleLoaderProductName =
getProductNameForBuildTarget(bundleLoader.getBuildTarget());
String bundleLoaderBundleName =
bundleLoaderProductName
+ "."
+ getExtensionString(bundleLoader.getConstructorArg().getExtension());
// NOTE(grp): This is a hack. We need to support both deep (OS X) and flat (iOS)
// style bundles for the bundle loader, but at this point we don't know what platform
// the bundle loader (or current target) is going to be built for. However, we can be
// sure that it's the same as the target (presumably a test) we're building right now.
//
// Using that knowledge, we can do build setting tricks to defer choosing the bundle
// loader path until Xcode build time, when the platform is known. There's no build
// setting that conclusively says whether the current platform uses deep bundles:
// that would be too easy. But in the cases we care about (unit test bundles), the
// current bundle will have a style matching the style of the bundle loader app, so
// we can take advantage of that to do the determination.
//
// Unfortunately, the build setting for the bundle structure (CONTENTS_FOLDER_PATH)
// includes the WRAPPER_NAME, so we can't just interpolate that in. Instead, we have
// to use another trick with build setting operations and evaluation. By using the
// $(:file) operation, we can extract the last component of the contents path: either
// "Contents" or the current bundle name. Then, we can interpolate with that expected
// result in the build setting name to conditionally choose a different loader path.
// The conditional that decides which path is used. This is a complex Xcode build setting
// expression that expands to one of two values, depending on the last path component of
// the CONTENTS_FOLDER_PATH variable. As described above, this will be either "Contents"
// for deep bundles or the bundle file name itself for flat bundles. Finally, to santiize
// the potentially invalid build setting names from the bundle file name, it converts that
// to an identifier. We rely on BUNDLE_LOADER_BUNDLE_STYLE_CONDITIONAL_<bundle file name>
// being undefined (and thus expanding to nothing) for the path resolution to work.
//
// The operations on the CONTENTS_FOLDER_PATH are documented here:
// http://codeworkshop.net/posts/xcode-build-setting-transformations
String bundleLoaderOutputPathConditional =
"$(BUNDLE_LOADER_BUNDLE_STYLE_CONDITIONAL_$(CONTENTS_FOLDER_PATH:file:identifier))";
// If the $(CONTENTS_FOLDER_PATH:file:identifier) expands to this, we add the deep bundle
// path into the bundle loader. See above for the case when it will expand to this value.
String bundleLoaderOutputPathDeepSetting =
"BUNDLE_LOADER_BUNDLE_STYLE_CONDITIONAL_Contents";
String bundleLoaderOutputPathDeepValue = "Contents/MacOS/";
String bundleLoaderOutputPathValue =
Joiner.on('/')
.join(
getTargetOutputPath(bundleLoader),
bundleLoaderBundleName,
bundleLoaderOutputPathConditional,
bundleLoaderProductName);
extraSettingsBuilder
.put(bundleLoaderOutputPathDeepSetting, bundleLoaderOutputPathDeepValue)
.put("BUNDLE_LOADER", bundleLoaderOutputPathValue)
.put("TEST_HOST", "$(BUNDLE_LOADER)");
}
if (infoPlistOptional.isPresent()) {
Path infoPlistPath = pathRelativizer.outputDirToRootRelative(infoPlistOptional.get());
extraSettingsBuilder.put("INFOPLIST_FILE", infoPlistPath.toString());
}
if (arg.getBridgingHeader().isPresent()) {
Path bridgingHeaderPath =
pathRelativizer.outputDirToRootRelative(
resolveSourcePath(arg.getBridgingHeader().get()));
extraSettingsBuilder.put(
"SWIFT_OBJC_BRIDGING_HEADER",
Joiner.on('/').join("$(SRCROOT)", bridgingHeaderPath.toString()));
}
Optional<String> swiftVersion = swiftBuckConfig.getVersion();
swiftVersion.ifPresent(s -> extraSettingsBuilder.put("SWIFT_VERSION", s));
Optional<SourcePath> prefixHeaderOptional = targetNode.getConstructorArg().getPrefixHeader();
if (prefixHeaderOptional.isPresent()) {
Path prefixHeaderRelative = resolveSourcePath(prefixHeaderOptional.get());
Path prefixHeaderPath = pathRelativizer.outputDirToRootRelative(prefixHeaderRelative);
extraSettingsBuilder.put("GCC_PREFIX_HEADER", prefixHeaderPath.toString());
extraSettingsBuilder.put("GCC_PRECOMPILE_PREFIX_HEADER", "YES");
}
if (!isModularAppleFramework) {
extraSettingsBuilder.put("USE_HEADERMAP", "NO");
} else {
// Modular libraries need to have both USE_HEADERMAP & CLANG_ENABLE_MODULES enabled
// so that Xcode generates .framework VFS overlays
extraSettingsBuilder.put("USE_HEADERMAP", "YES");
extraSettingsBuilder.put("CLANG_ENABLE_MODULES", "YES");
extraSettingsBuilder.put("DEFINES_MODULE", "YES");
}
defaultSettingsBuilder.put(
"REPO_ROOT", projectFilesystem.getRootPath().toAbsolutePath().normalize().toString());
defaultSettingsBuilder.put(PRODUCT_NAME, getProductName(buildTargetNode, buildTarget));
bundle.ifPresent(
bundleNode ->
defaultSettingsBuilder.put(
"WRAPPER_EXTENSION",
getExtensionString(bundleNode.getConstructorArg().getExtension())));
// We use BUILT_PRODUCTS_DIR as the root for the everything being built. Target-
// specific output is placed within CONFIGURATION_BUILD_DIR, inside BUILT_PRODUCTS_DIR.
// That allows Copy Files build phases to reference files in the CONFIGURATION_BUILD_DIR
// of other targets by using paths relative to the target-independent BUILT_PRODUCTS_DIR.
defaultSettingsBuilder.put(
"BUILT_PRODUCTS_DIR",
// $EFFECTIVE_PLATFORM_NAME starts with a dash, so this expands to something like:
// $SYMROOT/Debug-iphonesimulator
Joiner.on('/').join("$SYMROOT", "$CONFIGURATION$EFFECTIVE_PLATFORM_NAME"));
defaultSettingsBuilder.put("CONFIGURATION_BUILD_DIR", "$BUILT_PRODUCTS_DIR");
boolean nodeIsAppleLibrary = targetNode.getDescription() instanceof AppleLibraryDescription;
boolean nodeIsCxxLibrary = targetNode.getDescription() instanceof CxxLibraryDescription;
if (!bundle.isPresent() && (nodeIsAppleLibrary || nodeIsCxxLibrary)) {
defaultSettingsBuilder.put("EXECUTABLE_PREFIX", "lib");
}
if (isFocusedOnTarget) {
ImmutableSet<Path> recursiveHeaderSearchPaths =
collectRecursiveHeaderSearchPaths(targetNode);
ImmutableSet<Path> headerMapBases =
recursiveHeaderSearchPaths.isEmpty()
? ImmutableSet.of()
: ImmutableSet.of(
pathRelativizer.outputDirToRootRelative(
buildTargetNode.getFilesystem().getBuckPaths().getBuckOut()));
ImmutableMap.Builder<String, String> appendConfigsBuilder = ImmutableMap.builder();
appendConfigsBuilder.putAll(getFrameworkAndLibrarySearchPathConfigs(targetNode));
appendConfigsBuilder.put(
"HEADER_SEARCH_PATHS",
Joiner.on(' ').join(Iterables.concat(recursiveHeaderSearchPaths, headerMapBases)));
Iterable<String> otherCFlags =
Iterables.concat(
cxxBuckConfig.getFlags("cflags").orElse(DEFAULT_CFLAGS),
collectRecursiveExportedPreprocessorFlags(targetNode),
targetNode.getConstructorArg().getCompilerFlags(),
targetNode.getConstructorArg().getPreprocessorFlags());
Iterable<String> otherCxxFlags =
Iterables.concat(
cxxBuckConfig.getFlags("cxxflags").orElse(DEFAULT_CXXFLAGS),
collectRecursiveExportedPreprocessorFlags(targetNode),
targetNode.getConstructorArg().getCompilerFlags(),
targetNode.getConstructorArg().getPreprocessorFlags());
ImmutableList<String> otherLdFlags =
convertStringWithMacros(
targetNode,
Iterables.concat(
targetNode.getConstructorArg().getLinkerFlags(),
collectRecursiveExportedLinkerFlags(targetNode)));
appendConfigsBuilder
.put(
"OTHER_CFLAGS",
Joiner.on(' ').join(Iterables.transform(otherCFlags, Escaper.BASH_ESCAPER)))
.put(
"OTHER_CPLUSPLUSFLAGS",
Joiner.on(' ').join(Iterables.transform(otherCxxFlags, Escaper.BASH_ESCAPER)))
.put(
"OTHER_LDFLAGS",
Joiner.on(' ').join(Iterables.transform(otherLdFlags, Escaper.BASH_ESCAPER)));
ImmutableMultimap.Builder<String, ImmutableList<String>> platformFlagsBuilder =
ImmutableMultimap.builder();
for (Pair<Pattern, ImmutableList<String>> flags :
Iterables.concat(
targetNode.getConstructorArg().getPlatformCompilerFlags().getPatternsAndValues(),
targetNode
.getConstructorArg()
.getPlatformPreprocessorFlags()
.getPatternsAndValues(),
collectRecursiveExportedPlatformPreprocessorFlags(targetNode))) {
String sdk = flags.getFirst().pattern().replaceAll("[*.]", "");
platformFlagsBuilder.put(sdk, flags.getSecond());
}
ImmutableMultimap<String, ImmutableList<String>> platformFlags =
platformFlagsBuilder.build();
for (String sdk : platformFlags.keySet()) {
appendConfigsBuilder
.put(
String.format("OTHER_CFLAGS[sdk=*%s*]", sdk),
Joiner.on(' ')
.join(
Iterables.transform(
Iterables.concat(
otherCFlags, Iterables.concat(platformFlags.get(sdk))),
Escaper.BASH_ESCAPER)))
.put(
String.format("OTHER_CPLUSPLUSFLAGS[sdk=*%s*]", sdk),
Joiner.on(' ')
.join(
Iterables.transform(
Iterables.concat(
otherCxxFlags, Iterables.concat(platformFlags.get(sdk))),
Escaper.BASH_ESCAPER)));
}
ImmutableMultimap.Builder<String, ImmutableList<String>> platformLinkerFlagsBuilder =
ImmutableMultimap.builder();
for (Pair<Pattern, ImmutableList<StringWithMacros>> flags :
Iterables.concat(
targetNode.getConstructorArg().getPlatformLinkerFlags().getPatternsAndValues(),
collectRecursiveExportedPlatformLinkerFlags(targetNode))) {
String sdk = flags.getFirst().pattern().replaceAll("[*.]", "");
platformLinkerFlagsBuilder.put(
sdk, convertStringWithMacros(targetNode, flags.getSecond()));
}
ImmutableMultimap<String, ImmutableList<String>> platformLinkerFlags =
platformLinkerFlagsBuilder.build();
for (String sdk : platformLinkerFlags.keySet()) {
appendConfigsBuilder.put(
String.format("OTHER_LDFLAGS[sdk=*%s*]", sdk),
Joiner.on(' ')
.join(
Iterables.transform(
Iterables.concat(
otherLdFlags, Iterables.concat(platformLinkerFlags.get(sdk))),
Escaper.BASH_ESCAPER)));
}
ImmutableMap<String, String> appendedConfig = appendConfigsBuilder.build();
Optional<ImmutableSortedMap<String, ImmutableMap<String, String>>> configs =
getXcodeBuildConfigurationsForTargetNode(targetNode, appendedConfig);
setTargetBuildConfigurations(
buildTarget,
target,
project.getMainGroup(),
configs.get(),
extraSettingsBuilder.build(),
defaultSettingsBuilder.build(),
appendedConfig);
}
}
// -- phases
createHeaderSymlinkTree(
getPublicCxxHeaders(targetNode),
getPathToHeaderSymlinkTree(targetNode, HeaderVisibility.PUBLIC),
arg.getXcodePublicHeadersSymlinks().orElse(cxxBuckConfig.getPublicHeadersSymlinksEnabled())
|| isHeaderMapDisabled(),
!shouldMergeHeaderMaps());
if (isFocusedOnTarget) {
createHeaderSymlinkTree(
getPrivateCxxHeaders(targetNode),
getPathToHeaderSymlinkTree(targetNode, HeaderVisibility.PRIVATE),
arg.getXcodePrivateHeadersSymlinks()
.orElse(cxxBuckConfig.getPrivateHeadersSymlinksEnabled())
|| isHeaderMapDisabled(),
!isHeaderMapDisabled());
}
if (shouldMergeHeaderMaps() && isMainProject) {
createMergedHeaderMap();
}
Optional<TargetNode<AppleNativeTargetDescriptionArg, ?>> appleTargetNode =
targetNode.castArg(AppleNativeTargetDescriptionArg.class);
if (appleTargetNode.isPresent()
&& isFocusedOnTarget
&& !shouldGenerateHeaderSymlinkTreesOnly()) {
// Use Core Data models from immediate dependencies only.
addCoreDataModelsIntoTarget(appleTargetNode.get(), targetGroup.get());
addSceneKitAssetsIntoTarget(appleTargetNode.get(), targetGroup.get());
}
if (bundle.isPresent() && isFocusedOnTarget && !shouldGenerateHeaderSymlinkTreesOnly()) {
addEntitlementsPlistIntoTarget(bundle.get(), targetGroup.get());
}
return target;
}
private ImmutableMap<String, String> getFrameworkAndLibrarySearchPathConfigs(
TargetNode<?, ?> node) {
HashSet<String> frameworkSearchPaths = new HashSet<>();
frameworkSearchPaths.add("$BUILT_PRODUCTS_DIR");
HashSet<String> librarySearchPaths = new HashSet<>();
librarySearchPaths.add("$BUILT_PRODUCTS_DIR");
HashSet<String> ldRunpathSearchPaths = new HashSet<>();
Stream.concat(
// Collect all the nodes that contribute to linking
// ... Which the node includes itself
Stream.of(node),
// ... And recursive dependencies that gets linked in
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.LINKING,
node,
ImmutableSet.of(
AppleLibraryDescription.class,
CxxLibraryDescription.class,
PrebuiltAppleFrameworkDescription.class))
.stream())
// Keep only the ones that may have frameworks and libraries fields.
.flatMap(input -> RichStream.from(input.castArg(HasSystemFrameworkAndLibraries.class)))
// Then for each of them
.forEach(
castedNode -> {
// ... Add the framework path strings.
castedNode
.getConstructorArg()
.getFrameworks()
.stream()
.map(
frameworkPath ->
FrameworkPath.getUnexpandedSearchPath(
this::resolveSourcePath,
pathRelativizer::outputDirToRootRelative,
frameworkPath)
.toString())
.forEach(frameworkSearchPaths::add);
// ... And do the same for libraries.
castedNode
.getConstructorArg()
.getLibraries()
.stream()
.map(
libraryPath ->
FrameworkPath.getUnexpandedSearchPath(
this::resolveSourcePath,
pathRelativizer::outputDirToRootRelative,
libraryPath)
.toString())
.forEach(librarySearchPaths::add);
// If the item itself is a prebuilt framework, add it to framework_search_paths.
// This is needed for prebuilt framework's headers to be reference-able.
castedNode
.castArg(PrebuiltAppleFrameworkDescriptionArg.class)
.ifPresent(
prebuilt -> {
frameworkSearchPaths.add(
"$REPO_ROOT/"
+ resolveSourcePath(prebuilt.getConstructorArg().getFramework())
.getParent());
if (prebuilt.getConstructorArg().getPreferredLinkage()
!= NativeLinkable.Linkage.STATIC) {
// Frameworks that are copied into the binary.
ldRunpathSearchPaths.add("@executable_path/Frameworks");
}
});
});
ImmutableMap.Builder<String, String> results =
ImmutableMap.<String, String>builder()
.put("FRAMEWORK_SEARCH_PATHS", Joiner.on(' ').join(frameworkSearchPaths))
.put("LIBRARY_SEARCH_PATHS", Joiner.on(' ').join(librarySearchPaths));
if (!ldRunpathSearchPaths.isEmpty()) {
results.put("LD_RUNPATH_SEARCH_PATHS", Joiner.on(' ').join(ldRunpathSearchPaths));
}
return results.build();
}
private void addGenruleFiles(
PBXProject project, TargetNode<AbstractGenruleDescription.CommonArg, ?> targetNode) {
PBXGroup group = project.getMainGroup();
for (SourcePath sourcePath : targetNode.getConstructorArg().getSrcs()) {
Path path = pathRelativizer.outputPathToSourcePath(sourcePath);
ImmutableList<String> targetGroupPath = null;
PBXGroup sourceGroup = group.getOrCreateChildGroupByName("Other");
if (path.getParent() != null) {
targetGroupPath = RichStream.from(path.getParent()).map(Object::toString).toImmutableList();
sourceGroup = sourceGroup.getOrCreateDescendantGroupByPath(targetGroupPath);
}
sourceGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(PBXReference.SourceTree.SOURCE_ROOT, path, Optional.empty()));
}
}
private static String getProductName(TargetNode<?, ?> buildTargetNode, BuildTarget buildTarget) {
return getProductNameForTargetNode(buildTargetNode)
.orElse(getProductNameForBuildTarget(buildTarget));
}
private ImmutableSortedMap<Path, SourcePath> getPublicCxxHeaders(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode) {
CxxLibraryDescription.CommonArg arg = targetNode.getConstructorArg();
if (arg instanceof AppleNativeTargetDescriptionArg) {
if (isModularAppleFramework(targetNode)) {
// Modular targets should not include Buck-generated hmaps as they break the VFS overlay
// that's generated by Xcode and consequently, all headers part of a framework's umbrella
// header fail the modularity test, as they're expected to be mapped by the VFS layer under
// $BUILT_PRODUCTS_DIR/Module.framework/Versions/A/Headers.
return ImmutableSortedMap.of();
}
Path headerPathPrefix =
AppleDescriptions.getHeaderPathPrefix(
(AppleNativeTargetDescriptionArg) arg, targetNode.getBuildTarget());
ImmutableSortedMap<String, SourcePath> cxxHeaders =
AppleDescriptions.convertAppleHeadersToPublicCxxHeaders(
this::resolveSourcePath, headerPathPrefix, arg);
return convertMapKeysToPaths(cxxHeaders);
} else {
BuildRuleResolver resolver = buildRuleResolverForNode.apply(targetNode);
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
try {
return ImmutableSortedMap.copyOf(
CxxDescriptionEnhancer.parseExportedHeaders(
targetNode.getBuildTarget(),
resolver,
ruleFinder,
pathResolver,
Optional.empty(),
arg));
} catch (NoSuchBuildTargetException e) {
throw new RuntimeException(e);
}
}
}
private ImmutableSortedMap<Path, SourcePath> getPrivateCxxHeaders(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode) {
CxxLibraryDescription.CommonArg arg = targetNode.getConstructorArg();
if (arg instanceof AppleNativeTargetDescriptionArg) {
Path headerPathPrefix =
AppleDescriptions.getHeaderPathPrefix(
(AppleNativeTargetDescriptionArg) arg, targetNode.getBuildTarget());
ImmutableSortedMap<String, SourcePath> cxxHeaders =
AppleDescriptions.convertAppleHeadersToPrivateCxxHeaders(
this::resolveSourcePath, headerPathPrefix, arg);
return convertMapKeysToPaths(cxxHeaders);
} else {
BuildRuleResolver resolver = buildRuleResolverForNode.apply(targetNode);
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
try {
return ImmutableSortedMap.copyOf(
CxxDescriptionEnhancer.parseHeaders(
targetNode.getBuildTarget(),
resolver,
ruleFinder,
pathResolver,
Optional.empty(),
arg));
} catch (NoSuchBuildTargetException e) {
throw new RuntimeException(e);
}
}
}
private ImmutableSortedMap<Path, SourcePath> convertMapKeysToPaths(
ImmutableSortedMap<String, SourcePath> input) {
ImmutableSortedMap.Builder<Path, SourcePath> output = ImmutableSortedMap.naturalOrder();
for (Map.Entry<String, SourcePath> entry : input.entrySet()) {
output.put(Paths.get(entry.getKey()), entry.getValue());
}
return output.build();
}
private Optional<ImmutableSortedMap<String, ImmutableMap<String, String>>>
getXcodeBuildConfigurationsForTargetNode(
TargetNode<?, ?> targetNode, ImmutableMap<String, String> appendedConfig) {
Optional<ImmutableSortedMap<String, ImmutableMap<String, String>>> configs = Optional.empty();
Optional<TargetNode<AppleNativeTargetDescriptionArg, ?>> appleTargetNode =
targetNode.castArg(AppleNativeTargetDescriptionArg.class);
Optional<TargetNode<HalideLibraryDescriptionArg, ?>> halideTargetNode =
targetNode.castArg(HalideLibraryDescriptionArg.class);
if (appleTargetNode.isPresent()) {
configs = Optional.of(appleTargetNode.get().getConstructorArg().getConfigs());
} else if (halideTargetNode.isPresent()) {
configs = Optional.of(halideTargetNode.get().getConstructorArg().getConfigs());
}
if (!configs.isPresent()
|| (configs.isPresent() && configs.get().isEmpty())
|| targetNode.getDescription() instanceof CxxLibraryDescription) {
ImmutableMap<String, ImmutableMap<String, String>> defaultConfig =
CxxPlatformXcodeConfigGenerator.getDefaultXcodeBuildConfigurationsFromCxxPlatform(
defaultCxxPlatform, appendedConfig);
configs = Optional.of(ImmutableSortedMap.copyOf(defaultConfig));
}
return configs;
}
private void addEntitlementsPlistIntoTarget(
TargetNode<? extends HasAppleBundleFields, ?> targetNode, PBXGroup targetGroup) {
ImmutableMap<String, String> infoPlistSubstitutions =
targetNode.getConstructorArg().getInfoPlistSubstitutions();
if (infoPlistSubstitutions.containsKey(AppleBundle.CODE_SIGN_ENTITLEMENTS)) {
String entitlementsPlistPath =
InfoPlistSubstitution.replaceVariablesInString(
"$(" + AppleBundle.CODE_SIGN_ENTITLEMENTS + ")",
AppleBundle.withDefaults(
infoPlistSubstitutions,
ImmutableMap.of(
"SOURCE_ROOT", ".",
"SRCROOT", ".")));
targetGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
Paths.get(entitlementsPlistPath),
Optional.empty()));
}
}
private void addCoreDataModelsIntoTarget(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode, PBXGroup targetGroup)
throws IOException {
addCoreDataModelBuildPhase(
targetGroup,
AppleBuildRules.collectTransitiveBuildRules(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.CORE_DATA_MODEL_DESCRIPTION_CLASSES,
ImmutableList.of(targetNode)));
}
private void addSceneKitAssetsIntoTarget(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode, PBXGroup targetGroup) {
ImmutableSet<AppleWrapperResourceArg> allSceneKitAssets =
AppleBuildRules.collectTransitiveBuildRules(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.SCENEKIT_ASSETS_DESCRIPTION_CLASSES,
ImmutableList.of(targetNode));
for (final AppleWrapperResourceArg sceneKitAssets : allSceneKitAssets) {
PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources");
resourcesGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(sceneKitAssets.getPath()),
Optional.empty()));
}
}
private Path getConfigurationXcconfigPath(BuildTarget buildTarget, String input) {
return BuildTargets.getGenPath(projectFilesystem, buildTarget, "%s-" + input + ".xcconfig");
}
private Iterable<SourcePath> getHeaderSourcePaths(SourceList headers) {
if (headers.getUnnamedSources().isPresent()) {
return headers.getUnnamedSources().get();
} else {
return headers.getNamedSources().get().values();
}
}
/**
* Create target level configuration entries.
*
* @param target Xcode target for which the configurations will be set.
* @param targetGroup Xcode group in which the configuration file references will be placed.
* @param configurations Configurations as extracted from the BUCK file.
* @param overrideBuildSettings Build settings that will override ones defined elsewhere.
* @param defaultBuildSettings Target-inline level build settings that will be set if not already
* defined.
* @param appendBuildSettings Target-inline level build settings that will incorporate the
* existing value or values at a higher level.
*/
private void setTargetBuildConfigurations(
BuildTarget buildTarget,
PBXTarget target,
PBXGroup targetGroup,
ImmutableMap<String, ImmutableMap<String, String>> configurations,
ImmutableMap<String, String> overrideBuildSettings,
ImmutableMap<String, String> defaultBuildSettings,
ImmutableMap<String, String> appendBuildSettings)
throws IOException {
if (shouldGenerateHeaderSymlinkTreesOnly()) {
return;
}
for (Map.Entry<String, ImmutableMap<String, String>> configurationEntry :
configurations.entrySet()) {
targetConfigNamesBuilder.add(configurationEntry.getKey());
ImmutableMap<String, String> targetLevelInlineSettings = configurationEntry.getValue();
XCBuildConfiguration outputConfiguration =
target
.getBuildConfigurationList()
.getBuildConfigurationsByName()
.getUnchecked(configurationEntry.getKey());
HashMap<String, String> combinedOverrideConfigs = Maps.newHashMap(overrideBuildSettings);
for (Map.Entry<String, String> entry : defaultBuildSettings.entrySet()) {
String existingSetting = targetLevelInlineSettings.get(entry.getKey());
if (existingSetting == null) {
combinedOverrideConfigs.put(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<String, String> entry : appendBuildSettings.entrySet()) {
String existingSetting = targetLevelInlineSettings.get(entry.getKey());
String settingPrefix = existingSetting != null ? existingSetting : "$(inherited)";
combinedOverrideConfigs.put(entry.getKey(), settingPrefix + " " + entry.getValue());
}
ImmutableSortedMap<String, String> mergedSettings =
MoreMaps.mergeSorted(targetLevelInlineSettings, combinedOverrideConfigs);
Path xcconfigPath = getConfigurationXcconfigPath(buildTarget, configurationEntry.getKey());
projectFilesystem.mkdirs(Preconditions.checkNotNull(xcconfigPath).getParent());
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : mergedSettings.entrySet()) {
stringBuilder.append(entry.getKey());
stringBuilder.append(" = ");
stringBuilder.append(entry.getValue());
stringBuilder.append('\n');
}
String xcconfigContents = stringBuilder.toString();
if (MoreProjectFilesystems.fileContentsDiffer(
new ByteArrayInputStream(xcconfigContents.getBytes(Charsets.UTF_8)),
xcconfigPath,
projectFilesystem)) {
if (shouldGenerateReadOnlyFiles()) {
projectFilesystem.writeContentsToPath(
xcconfigContents, xcconfigPath, READ_ONLY_FILE_ATTRIBUTE);
} else {
projectFilesystem.writeContentsToPath(xcconfigContents, xcconfigPath);
}
}
PBXFileReference fileReference = getConfigurationFileReference(targetGroup, xcconfigPath);
outputConfiguration.setBaseConfigurationReference(fileReference);
}
}
private PBXFileReference getConfigurationFileReference(PBXGroup targetGroup, Path xcconfigPath) {
return targetGroup
.getOrCreateChildGroupByName("Configurations")
.getOrCreateChildGroupByName("Buck (Do Not Modify)")
.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(xcconfigPath),
Optional.empty()));
}
private void collectBuildScriptDependencies(
Iterable<TargetNode<?, ?>> targetNodes,
ImmutableList.Builder<TargetNode<?, ?>> preRules,
ImmutableList.Builder<TargetNode<?, ?>> postRules) {
for (TargetNode<?, ?> targetNode : targetNodes) {
if (targetNode.getDescription() instanceof IosReactNativeLibraryDescription) {
postRules.add(targetNode);
requiredBuildTargetsBuilder.add(targetNode.getBuildTarget());
} else if (targetNode.getDescription() instanceof XcodePostbuildScriptDescription) {
postRules.add(targetNode);
} else if (targetNode.getDescription() instanceof XcodePrebuildScriptDescription) {
preRules.add(targetNode);
}
}
}
/** Adds the set of headers defined by headerVisibility to the merged header maps. */
private void addToMergedHeaderMap(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
HeaderMap.Builder headerMapBuilder) {
CxxLibraryDescription.CommonArg arg = targetNode.getConstructorArg();
boolean shouldCreateHeadersSymlinks =
arg.getXcodePublicHeadersSymlinks().orElse(cxxBuckConfig.getPublicHeadersSymlinksEnabled());
Path headerSymlinkTreeRoot = getPathToHeaderSymlinkTree(targetNode, HeaderVisibility.PUBLIC);
Path basePath;
if (shouldCreateHeadersSymlinks) {
basePath =
projectFilesystem
.getRootPath()
.resolve(targetNode.getBuildTarget().getCellPath())
.resolve(headerSymlinkTreeRoot);
} else {
basePath = projectFilesystem.getRootPath().resolve(targetNode.getBuildTarget().getCellPath());
}
for (Map.Entry<Path, SourcePath> entry : getPublicCxxHeaders(targetNode).entrySet()) {
Path path;
if (shouldCreateHeadersSymlinks) {
path = basePath.resolve(entry.getKey());
} else {
path = basePath.resolve(resolveSourcePath(entry.getValue()));
}
headerMapBuilder.add(entry.getKey().toString(), path);
}
}
/** Generates the merged header maps and write it to the public header symlink tree location. */
private void createMergedHeaderMap() throws IOException {
HeaderMap.Builder headerMapBuilder = new HeaderMap.Builder();
Set<TargetNode<? extends CxxLibraryDescription.CommonArg, ?>> processedNodes = new HashSet<>();
for (TargetNode<?, ?> targetNode : targetGraph.getAll(targetsInRequiredProjects)) {
// Includes the public headers of the dependencies in the merged header map.
getAppleNativeNode(targetGraph, targetNode)
.ifPresent(
argTargetNode ->
visitRecursiveHeaderSymlinkTrees(
argTargetNode,
(depNativeNode, headerVisibility) -> {
if (processedNodes.contains(depNativeNode)) {
return;
}
if (headerVisibility == HeaderVisibility.PUBLIC) {
addToMergedHeaderMap(depNativeNode, headerMapBuilder);
processedNodes.add(depNativeNode);
}
}));
}
// Writes the resulting header map.
Path mergedHeaderMapRoot = getPathToMergedHeaderMap();
Path headerMapLocation = getHeaderMapLocationFromSymlinkTreeRoot(mergedHeaderMapRoot);
projectFilesystem.mkdirs(mergedHeaderMapRoot);
projectFilesystem.writeBytesToPath(headerMapBuilder.build().getBytes(), headerMapLocation);
}
private void createHeaderSymlinkTree(
Map<Path, SourcePath> contents,
Path headerSymlinkTreeRoot,
boolean shouldCreateHeadersSymlinks,
boolean shouldCreateHeaderMap)
throws IOException {
if (!shouldCreateHeaderMap && !shouldCreateHeadersSymlinks) {
return;
}
LOG.verbose(
"Building header symlink tree at %s with contents %s", headerSymlinkTreeRoot, contents);
ImmutableSortedMap.Builder<Path, Path> resolvedContentsBuilder =
ImmutableSortedMap.naturalOrder();
for (Map.Entry<Path, SourcePath> entry : contents.entrySet()) {
Path link = headerSymlinkTreeRoot.resolve(entry.getKey());
Path existing = projectFilesystem.resolve(resolveSourcePath(entry.getValue()));
resolvedContentsBuilder.put(link, existing);
}
ImmutableSortedMap<Path, Path> resolvedContents = resolvedContentsBuilder.build();
Path headerMapLocation = getHeaderMapLocationFromSymlinkTreeRoot(headerSymlinkTreeRoot);
Path hashCodeFilePath = headerSymlinkTreeRoot.resolve(".contents-hash");
Optional<String> currentHashCode = projectFilesystem.readFileIfItExists(hashCodeFilePath);
String newHashCode =
getHeaderSymlinkTreeHashCode(
resolvedContents, shouldCreateHeadersSymlinks, shouldCreateHeaderMap)
.toString();
if (Optional.of(newHashCode).equals(currentHashCode)) {
LOG.debug(
"Symlink tree at %s is up to date, not regenerating (key %s).",
headerSymlinkTreeRoot, newHashCode);
} else {
LOG.debug(
"Updating symlink tree at %s (old key %s, new key %s).",
headerSymlinkTreeRoot, currentHashCode, newHashCode);
projectFilesystem.deleteRecursivelyIfExists(headerSymlinkTreeRoot);
projectFilesystem.mkdirs(headerSymlinkTreeRoot);
if (shouldCreateHeadersSymlinks) {
for (Map.Entry<Path, Path> entry : resolvedContents.entrySet()) {
Path link = entry.getKey();
Path existing = entry.getValue();
projectFilesystem.createParentDirs(link);
projectFilesystem.createSymLink(link, existing, /* force */ false);
}
}
projectFilesystem.writeContentsToPath(newHashCode, hashCodeFilePath);
if (shouldCreateHeaderMap) {
HeaderMap.Builder headerMapBuilder = new HeaderMap.Builder();
for (Map.Entry<Path, SourcePath> entry : contents.entrySet()) {
if (shouldCreateHeadersSymlinks) {
headerMapBuilder.add(
entry.getKey().toString(),
Paths.get("../../")
.resolve(projectCell.getRoot().getFileName())
.resolve(headerSymlinkTreeRoot)
.resolve(entry.getKey()));
} else {
headerMapBuilder.add(
entry.getKey().toString(),
projectFilesystem.resolve(resolveSourcePath(entry.getValue())));
}
}
projectFilesystem.writeBytesToPath(headerMapBuilder.build().getBytes(), headerMapLocation);
}
}
headerSymlinkTrees.add(headerSymlinkTreeRoot);
}
private HashCode getHeaderSymlinkTreeHashCode(
ImmutableSortedMap<Path, Path> contents,
boolean shouldCreateHeadersSymlinks,
boolean shouldCreateHeaderMap) {
Hasher hasher = Hashing.sha1().newHasher();
hasher.putBytes(BuckVersion.getVersion().getBytes(Charsets.UTF_8));
String symlinkState = shouldCreateHeadersSymlinks ? "symlinks-enabled" : "symlinks-disabled";
byte[] symlinkStateValue = symlinkState.getBytes(Charsets.UTF_8);
hasher.putInt(symlinkStateValue.length);
hasher.putBytes(symlinkStateValue);
String hmapState = shouldCreateHeaderMap ? "hmap-enabled" : "hmap-disabled";
byte[] hmapStateValue = hmapState.getBytes(Charsets.UTF_8);
hasher.putInt(hmapStateValue.length);
hasher.putBytes(hmapStateValue);
hasher.putInt(0);
for (Map.Entry<Path, Path> entry : contents.entrySet()) {
byte[] key = entry.getKey().toString().getBytes(Charsets.UTF_8);
byte[] value = entry.getValue().toString().getBytes(Charsets.UTF_8);
hasher.putInt(key.length);
hasher.putBytes(key);
hasher.putInt(value.length);
hasher.putBytes(value);
}
return hasher.hash();
}
private void addCoreDataModelBuildPhase(
PBXGroup targetGroup, Iterable<AppleWrapperResourceArg> dataModels) throws IOException {
// TODO(coneko): actually add a build phase
for (final AppleWrapperResourceArg dataModel : dataModels) {
// Core data models go in the resources group also.
PBXGroup resourcesGroup = targetGroup.getOrCreateChildGroupByName("Resources");
if (CoreDataModelDescription.isVersionedDataModel(dataModel)) {
// It's safe to do I/O here to figure out the current version because we're returning all
// the versions and the file pointing to the current version from
// getInputsToCompareToOutput(), so the rule will be correctly detected as stale if any of
// them change.
final String currentVersionFileName = ".xccurrentversion";
final String currentVersionKey = "_XCCurrentVersionName";
final XCVersionGroup versionGroup =
resourcesGroup.getOrCreateChildVersionGroupsBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(dataModel.getPath()),
Optional.empty()));
projectFilesystem.walkRelativeFileTree(
dataModel.getPath(),
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.equals(dataModel.getPath())) {
return FileVisitResult.CONTINUE;
}
versionGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(dir),
Optional.empty()));
return FileVisitResult.SKIP_SUBTREE;
}
});
Path currentVersionPath = dataModel.getPath().resolve(currentVersionFileName);
try (InputStream in = projectFilesystem.newFileInputStream(currentVersionPath)) {
NSObject rootObject;
try {
rootObject = PropertyListParser.parse(in);
} catch (IOException e) {
throw e;
} catch (Exception e) {
rootObject = null;
}
if (!(rootObject instanceof NSDictionary)) {
throw new HumanReadableException("Malformed %s file.", currentVersionFileName);
}
NSDictionary rootDictionary = (NSDictionary) rootObject;
NSObject currentVersionName = rootDictionary.objectForKey(currentVersionKey);
if (!(currentVersionName instanceof NSString)) {
throw new HumanReadableException("Malformed %s file.", currentVersionFileName);
}
PBXFileReference ref =
versionGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(
dataModel.getPath().resolve(currentVersionName.toString())),
Optional.empty()));
versionGroup.setCurrentVersion(Optional.of(ref));
} catch (NoSuchFileException e) {
if (versionGroup.getChildren().size() == 1) {
versionGroup.setCurrentVersion(
Optional.of(Iterables.get(versionGroup.getChildren(), 0)));
}
}
} else {
resourcesGroup.getOrCreateFileReferenceBySourceTreePath(
new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputDirToRootRelative(dataModel.getPath()),
Optional.empty()));
}
}
}
private Optional<CopyFilePhaseDestinationSpec> getDestinationSpec(TargetNode<?, ?> targetNode) {
if (targetNode.getDescription() instanceof AppleBundleDescription) {
AppleBundleDescriptionArg arg = (AppleBundleDescriptionArg) targetNode.getConstructorArg();
AppleBundleExtension extension =
arg.getExtension().isLeft() ? arg.getExtension().getLeft() : AppleBundleExtension.BUNDLE;
switch (extension) {
case FRAMEWORK:
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.FRAMEWORKS));
case APPEX:
case PLUGIN:
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.PLUGINS));
case APP:
if (isWatchApplicationNode(targetNode)) {
return Optional.of(
CopyFilePhaseDestinationSpec.builder()
.setDestination(PBXCopyFilesBuildPhase.Destination.PRODUCTS)
.setPath("$(CONTENTS_FOLDER_PATH)/Watch")
.build());
} else {
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.EXECUTABLES));
}
case BUNDLE:
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.PLUGINS));
//$CASES-OMITTED$
default:
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.PRODUCTS));
}
} else if (targetNode.getDescription() instanceof AppleLibraryDescription
|| targetNode.getDescription() instanceof CxxLibraryDescription) {
if (targetNode.getBuildTarget().getFlavors().contains(CxxDescriptionEnhancer.SHARED_FLAVOR)) {
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.FRAMEWORKS));
} else {
return Optional.empty();
}
} else if (targetNode.getDescription() instanceof AppleBinaryDescription) {
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.EXECUTABLES));
} else if (targetNode.getDescription() instanceof HalideLibraryDescription) {
return Optional.empty();
} else if (targetNode.getDescription() instanceof CoreDataModelDescription
|| targetNode.getDescription() instanceof SceneKitAssetsDescription) {
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.RESOURCES));
} else if (targetNode.getDescription() instanceof PrebuiltAppleFrameworkDescription) {
return Optional.of(
CopyFilePhaseDestinationSpec.of(PBXCopyFilesBuildPhase.Destination.FRAMEWORKS));
} else {
throw new RuntimeException("Unexpected type: " + targetNode.getDescription().getClass());
}
}
/**
* Convert a list of rules that should be somehow included into the bundle, into build phases
* which copies them into the bundle. The parameters of these copy phases are divined by
* scrutinizing the type of node we want to include.
*/
private ImmutableList<PBXBuildPhase> getCopyFilesBuildPhases(
Iterable<TargetNode<?, ?>> copiedNodes) {
// Bucket build rules into bins by their destinations
ImmutableSetMultimap.Builder<CopyFilePhaseDestinationSpec, TargetNode<?, ?>>
ruleByDestinationSpecBuilder = ImmutableSetMultimap.builder();
for (TargetNode<?, ?> copiedNode : copiedNodes) {
getDestinationSpec(copiedNode)
.ifPresent(
copyFilePhaseDestinationSpec ->
ruleByDestinationSpecBuilder.put(copyFilePhaseDestinationSpec, copiedNode));
}
ImmutableList.Builder<PBXBuildPhase> phases = ImmutableList.builder();
ImmutableSetMultimap<CopyFilePhaseDestinationSpec, TargetNode<?, ?>> ruleByDestinationSpec =
ruleByDestinationSpecBuilder.build();
// Emit a copy files phase for each destination.
for (CopyFilePhaseDestinationSpec destinationSpec : ruleByDestinationSpec.keySet()) {
Iterable<TargetNode<?, ?>> targetNodes = ruleByDestinationSpec.get(destinationSpec);
phases.add(getSingleCopyFilesBuildPhase(destinationSpec, targetNodes));
}
return phases.build();
}
private PBXCopyFilesBuildPhase getSingleCopyFilesBuildPhase(
CopyFilePhaseDestinationSpec destinationSpec, Iterable<TargetNode<?, ?>> targetNodes) {
PBXCopyFilesBuildPhase copyFilesBuildPhase = new PBXCopyFilesBuildPhase(destinationSpec);
HashSet<UnflavoredBuildTarget> frameworkTargets = new HashSet<UnflavoredBuildTarget>();
for (TargetNode<?, ?> targetNode : targetNodes) {
PBXFileReference fileReference = getLibraryFileReference(targetNode);
PBXBuildFile buildFile = new PBXBuildFile(fileReference);
if (fileReference.getExplicitFileType().equals(Optional.of("wrapper.framework"))) {
UnflavoredBuildTarget buildTarget = targetNode.getBuildTarget().getUnflavoredBuildTarget();
if (frameworkTargets.contains(buildTarget)) {
continue;
}
frameworkTargets.add(buildTarget);
NSDictionary settings = new NSDictionary();
settings.put("ATTRIBUTES", new String[] {"CodeSignOnCopy", "RemoveHeadersOnCopy"});
buildFile.setSettings(Optional.of(settings));
}
copyFilesBuildPhase.getFiles().add(buildFile);
}
return copyFilesBuildPhase;
}
/** Create the project bundle structure and write {@code project.pbxproj}. */
private void writeProjectFile(PBXProject project) throws IOException {
XcodeprojSerializer serializer =
new XcodeprojSerializer(
new GidGenerator(ImmutableSet.copyOf(gidsToTargetNames.keySet())), project);
NSDictionary rootObject = serializer.toPlist();
Path xcodeprojDir = outputDirectory.resolve(projectName + ".xcodeproj");
projectFilesystem.mkdirs(xcodeprojDir);
Path serializedProject = xcodeprojDir.resolve("project.pbxproj");
String contentsToWrite = rootObject.toXMLPropertyList();
// Before we write any files, check if the file contents have changed.
if (MoreProjectFilesystems.fileContentsDiffer(
new ByteArrayInputStream(contentsToWrite.getBytes(Charsets.UTF_8)),
serializedProject,
projectFilesystem)) {
LOG.debug("Regenerating project at %s", serializedProject);
if (shouldGenerateReadOnlyFiles()) {
projectFilesystem.writeContentsToPath(
contentsToWrite, serializedProject, READ_ONLY_FILE_ATTRIBUTE);
} else {
projectFilesystem.writeContentsToPath(contentsToWrite, serializedProject);
}
} else {
LOG.debug("Not regenerating project at %s (contents have not changed)", serializedProject);
}
}
private static String getProductNameForBuildTarget(BuildTarget buildTarget) {
return buildTarget.getShortName();
}
/** @param targetNode Must have a header symlink tree or an exception will be thrown. */
private Path getHeaderSymlinkTreeRelativePath(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
HeaderVisibility headerVisibility) {
Path treeRoot = getPathToHeaderSymlinkTree(targetNode, headerVisibility);
Path cellRoot =
MorePaths.relativize(
projectFilesystem.getRootPath(), targetNode.getBuildTarget().getCellPath());
return pathRelativizer.outputDirToRootRelative(cellRoot.resolve(treeRoot));
}
private Path getHeaderMapLocationFromSymlinkTreeRoot(Path headerSymlinkTreeRoot) {
return headerSymlinkTreeRoot.resolve(".hmap");
}
private Path getHeaderSearchPathFromSymlinkTreeRoot(Path headerSymlinkTreeRoot) {
if (isHeaderMapDisabled()) {
return headerSymlinkTreeRoot;
} else {
return getHeaderMapLocationFromSymlinkTreeRoot(headerSymlinkTreeRoot);
}
}
private Path getRelativePathToMergedHeaderMap() {
Path treeRoot = getPathToMergedHeaderMap();
Path cellRoot =
MorePaths.relativize(projectFilesystem.getRootPath(), workspaceTarget.get().getCellPath());
return pathRelativizer.outputDirToRootRelative(cellRoot.resolve(treeRoot));
}
private String getBuiltProductsRelativeTargetOutputPath(TargetNode<?, ?> targetNode) {
if (targetNode.getDescription() instanceof AppleBinaryDescription
|| targetNode.getDescription() instanceof AppleTestDescription
|| (targetNode.getDescription() instanceof AppleBundleDescription
&& !isFrameworkBundle((AppleBundleDescriptionArg) targetNode.getConstructorArg()))) {
// TODO(grp): These should be inside the path below. Right now, that causes issues with
// bundle loader paths hardcoded in .xcconfig files that don't expect the full target path.
// It also causes issues where Xcode doesn't know where to look for a final .app to run it.
return ".";
} else {
return BaseEncoding.base32()
.omitPadding()
.encode(targetNode.getBuildTarget().getFullyQualifiedName().getBytes());
}
}
private String getTargetOutputPath(TargetNode<?, ?> targetNode) {
return Joiner.on('/')
.join("$BUILT_PRODUCTS_DIR", getBuiltProductsRelativeTargetOutputPath(targetNode));
}
@SuppressWarnings("unchecked")
private static Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> getAppleNativeNodeOfType(
TargetGraph targetGraph,
TargetNode<?, ?> targetNode,
Set<Class<? extends Description<?>>> nodeTypes,
Set<AppleBundleExtension> bundleExtensions) {
Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> nativeNode = Optional.empty();
if (nodeTypes.contains(targetNode.getDescription().getClass())) {
nativeNode = Optional.of((TargetNode<CxxLibraryDescription.CommonArg, ?>) targetNode);
} else if (targetNode.getDescription() instanceof AppleBundleDescription) {
TargetNode<AppleBundleDescriptionArg, ?> bundle =
(TargetNode<AppleBundleDescriptionArg, ?>) targetNode;
Either<AppleBundleExtension, String> extension = bundle.getConstructorArg().getExtension();
if (extension.isLeft() && bundleExtensions.contains(extension.getLeft())) {
nativeNode =
Optional.of(
(TargetNode<CxxLibraryDescription.CommonArg, ?>)
targetGraph.get(bundle.getConstructorArg().getBinary()));
}
}
return nativeNode;
}
private static Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> getAppleNativeNode(
TargetGraph targetGraph, TargetNode<?, ?> targetNode) {
return getAppleNativeNodeOfType(
targetGraph, targetNode, APPLE_NATIVE_DESCRIPTION_CLASSES, APPLE_NATIVE_BUNDLE_EXTENSIONS);
}
private static Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> getLibraryNode(
TargetGraph targetGraph, TargetNode<?, ?> targetNode) {
return getAppleNativeNodeOfType(
targetGraph,
targetNode,
ImmutableSet.of(AppleLibraryDescription.class, CxxLibraryDescription.class),
ImmutableSet.of(AppleBundleExtension.FRAMEWORK));
}
private ImmutableSet<Path> collectRecursiveHeaderSearchPaths(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode) {
ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
if (shouldMergeHeaderMaps()) {
builder.add(
getHeaderSearchPathFromSymlinkTreeRoot(
getHeaderSymlinkTreeRelativePath(targetNode, HeaderVisibility.PRIVATE)));
builder.add(getHeaderSearchPathFromSymlinkTreeRoot(getRelativePathToMergedHeaderMap()));
visitRecursivePrivateHeaderSymlinkTreesForTests(
targetNode,
(nativeNode, headerVisibility) -> {
builder.add(
getHeaderSearchPathFromSymlinkTreeRoot(
getHeaderSymlinkTreeRelativePath(nativeNode, headerVisibility)));
});
} else {
for (Path headerSymlinkTreePath : collectRecursiveHeaderSymlinkTrees(targetNode)) {
builder.add(getHeaderSearchPathFromSymlinkTreeRoot(headerSymlinkTreePath));
}
}
for (Path halideHeaderPath : collectRecursiveHalideLibraryHeaderPaths(targetNode)) {
builder.add(halideHeaderPath);
}
return builder.build();
}
@SuppressWarnings("unchecked")
private ImmutableSet<Path> collectRecursiveHalideLibraryHeaderPaths(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode) {
ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
for (TargetNode<?, ?> input :
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.BUILDING,
targetNode,
Optional.of(ImmutableSet.of(HalideLibraryDescription.class)))) {
TargetNode<HalideLibraryDescriptionArg, ?> halideNode =
(TargetNode<HalideLibraryDescriptionArg, ?>) input;
BuildTarget buildTarget = halideNode.getBuildTarget();
builder.add(
pathRelativizer.outputDirToRootRelative(
HalideCompile.headerOutputPath(
buildTarget.withFlavors(
HalideLibraryDescription.HALIDE_COMPILE_FLAVOR,
defaultCxxPlatform.getFlavor()),
projectFilesystem,
halideNode.getConstructorArg().getFunctionName())
.getParent()));
}
return builder.build();
}
private void visitRecursiveHeaderSymlinkTrees(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
BiConsumer<TargetNode<? extends CxxLibraryDescription.CommonArg, ?>, HeaderVisibility>
visitor) {
// Visits public and private headers from current target.
visitor.accept(targetNode, HeaderVisibility.PRIVATE);
visitor.accept(targetNode, HeaderVisibility.PUBLIC);
// Visits public headers from dependencies.
for (TargetNode<?, ?> input :
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.BUILDING,
targetNode,
Optional.of(AppleBuildRules.XCODE_TARGET_DESCRIPTION_CLASSES))) {
getAppleNativeNode(targetGraph, input)
.ifPresent(argTargetNode -> visitor.accept(argTargetNode, HeaderVisibility.PUBLIC));
}
visitRecursivePrivateHeaderSymlinkTreesForTests(targetNode, visitor);
}
private void visitRecursivePrivateHeaderSymlinkTreesForTests(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
BiConsumer<TargetNode<? extends CxxLibraryDescription.CommonArg, ?>, HeaderVisibility>
visitor) {
// Visits headers of source under tests.
ImmutableSet<TargetNode<?, ?>> directDependencies =
ImmutableSet.copyOf(targetGraph.getAll(targetNode.getBuildDeps()));
for (TargetNode<?, ?> dependency : directDependencies) {
Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> nativeNode =
getAppleNativeNode(targetGraph, dependency);
if (nativeNode.isPresent() && isSourceUnderTest(dependency, nativeNode.get(), targetNode)) {
visitor.accept(nativeNode.get(), HeaderVisibility.PRIVATE);
}
}
}
private ImmutableSet<Path> collectRecursiveHeaderSymlinkTrees(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode) {
ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
visitRecursiveHeaderSymlinkTrees(
targetNode,
(nativeNode, headerVisibility) -> {
builder.add(getHeaderSymlinkTreeRelativePath(nativeNode, headerVisibility));
});
return builder.build();
}
private boolean isSourceUnderTest(
TargetNode<?, ?> dependencyNode,
TargetNode<CxxLibraryDescription.CommonArg, ?> nativeNode,
TargetNode<?, ?> testNode) {
boolean isSourceUnderTest =
nativeNode.getConstructorArg().getTests().contains(testNode.getBuildTarget());
if (dependencyNode != nativeNode && dependencyNode.getConstructorArg() instanceof HasTests) {
ImmutableSortedSet<BuildTarget> tests =
((HasTests) dependencyNode.getConstructorArg()).getTests();
if (tests.contains(testNode.getBuildTarget())) {
isSourceUnderTest = true;
}
}
return isSourceUnderTest;
}
/** List of frameworks and libraries that goes into the "Link Binary With Libraries" phase. */
private Iterable<FrameworkPath> collectRecursiveFrameworkDependencies(
TargetNode<?, ?> targetNode) {
return FluentIterable.from(
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.LINKING,
targetNode,
ImmutableSet.<Class<? extends Description<?>>>builder()
.addAll(AppleBuildRules.XCODE_TARGET_DESCRIPTION_CLASSES)
.add(PrebuiltAppleFrameworkDescription.class)
.build()))
.transformAndConcat(
input -> {
// Libraries and bundles which has system frameworks and libraries.
Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> library =
getLibraryNode(targetGraph, input);
if (library.isPresent()
&& !AppleLibraryDescription.isNotStaticallyLinkedLibraryNode(library.get())) {
return Iterables.concat(
library.get().getConstructorArg().getFrameworks(),
library.get().getConstructorArg().getLibraries());
}
Optional<TargetNode<PrebuiltAppleFrameworkDescriptionArg, ?>> prebuilt =
input.castArg(PrebuiltAppleFrameworkDescriptionArg.class);
if (prebuilt.isPresent()) {
return Iterables.concat(
prebuilt.get().getConstructorArg().getFrameworks(),
prebuilt.get().getConstructorArg().getLibraries(),
ImmutableList.of(
FrameworkPath.ofSourcePath(
prebuilt.get().getConstructorArg().getFramework())));
}
return ImmutableList.of();
});
}
private Iterable<String> collectRecursiveExportedPreprocessorFlags(TargetNode<?, ?> targetNode) {
return FluentIterable.from(
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.BUILDING,
targetNode,
ImmutableSet.of(AppleLibraryDescription.class, CxxLibraryDescription.class)))
.append(targetNode)
.transformAndConcat(
input ->
input
.castArg(CxxLibraryDescription.CommonArg.class)
.map(input1 -> input1.getConstructorArg().getExportedPreprocessorFlags())
.orElse(ImmutableList.of()));
}
private Iterable<Pair<Pattern, ImmutableList<String>>>
collectRecursiveExportedPlatformPreprocessorFlags(TargetNode<?, ?> targetNode) {
return FluentIterable.from(
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.BUILDING,
targetNode,
ImmutableSet.of(AppleLibraryDescription.class, CxxLibraryDescription.class)))
.append(targetNode)
.transformAndConcat(
input ->
input
.castArg(CxxLibraryDescription.CommonArg.class)
.map(
input1 ->
input1
.getConstructorArg()
.getExportedPlatformPreprocessorFlags()
.getPatternsAndValues())
.orElse(ImmutableList.of()));
}
private ImmutableList<StringWithMacros> collectRecursiveExportedLinkerFlags(
TargetNode<?, ?> targetNode) {
return FluentIterable.from(
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.LINKING,
targetNode,
ImmutableSet.of(
AppleLibraryDescription.class,
CxxLibraryDescription.class,
HalideLibraryDescription.class)))
.append(targetNode)
.transformAndConcat(
input ->
input
.castArg(CxxLibraryDescription.CommonArg.class)
.map(input1 -> input1.getConstructorArg().getExportedLinkerFlags())
.orElse(ImmutableList.of()))
.toList();
}
private Iterable<Pair<Pattern, ImmutableList<StringWithMacros>>>
collectRecursiveExportedPlatformLinkerFlags(TargetNode<?, ?> targetNode) {
return FluentIterable.from(
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.LINKING,
targetNode,
ImmutableSet.of(
AppleLibraryDescription.class,
CxxLibraryDescription.class,
HalideLibraryDescription.class)))
.append(targetNode)
.transformAndConcat(
input ->
input
.castArg(CxxLibraryDescription.CommonArg.class)
.map(
input1 ->
input1
.getConstructorArg()
.getExportedPlatformLinkerFlags()
.getPatternsAndValues())
.orElse(ImmutableList.of()));
}
private ImmutableSet<PBXFileReference> collectRecursiveLibraryDependencies(
TargetNode<?, ?> targetNode) {
return FluentIterable.from(
AppleBuildRules.getRecursiveTargetNodeDependenciesOfTypes(
targetGraph,
Optional.of(dependenciesCache),
AppleBuildRules.RecursiveDependenciesMode.LINKING,
targetNode,
AppleBuildRules.XCODE_TARGET_DESCRIPTION_CLASSES))
.filter(this::isLibraryWithSourcesToCompile)
.transform(this::getLibraryFileReference)
.toSet();
}
private SourceTreePath getProductsSourceTreePath(TargetNode<?, ?> targetNode) {
String productName = getProductNameForBuildTarget(targetNode.getBuildTarget());
String productOutputName;
if (targetNode.getDescription() instanceof AppleLibraryDescription
|| targetNode.getDescription() instanceof CxxLibraryDescription
|| targetNode.getDescription() instanceof HalideLibraryDescription) {
String productOutputFormat =
AppleBuildRules.getOutputFileNameFormatForLibrary(
targetNode
.getBuildTarget()
.getFlavors()
.contains(CxxDescriptionEnhancer.SHARED_FLAVOR));
productOutputName = String.format(productOutputFormat, productName);
} else if (targetNode.getDescription() instanceof AppleBundleDescription
|| targetNode.getDescription() instanceof AppleTestDescription) {
HasAppleBundleFields arg = (HasAppleBundleFields) targetNode.getConstructorArg();
productName = arg.getProductName().orElse(productName);
productOutputName = productName + "." + getExtensionString(arg.getExtension());
} else if (targetNode.getDescription() instanceof AppleBinaryDescription) {
productOutputName = productName;
} else if (targetNode.getDescription() instanceof PrebuiltAppleFrameworkDescription) {
PrebuiltAppleFrameworkDescriptionArg arg =
(PrebuiltAppleFrameworkDescriptionArg) targetNode.getConstructorArg();
// Prebuilt frameworks reside in the source repo, not outputs dir.
return new SourceTreePath(
PBXReference.SourceTree.SOURCE_ROOT,
pathRelativizer.outputPathToSourcePath(arg.getFramework()),
Optional.empty());
} else {
throw new RuntimeException("Unexpected type: " + targetNode.getDescription().getClass());
}
return new SourceTreePath(
PBXReference.SourceTree.BUILT_PRODUCTS_DIR, Paths.get(productOutputName), Optional.empty());
}
private PBXFileReference getLibraryFileReference(TargetNode<?, ?> targetNode) {
// Don't re-use the productReference from other targets in this project.
// File references set as a productReference don't work with custom paths.
SourceTreePath productsPath = getProductsSourceTreePath(targetNode);
if (isWatchApplicationNode(targetNode)) {
return project
.getMainGroup()
.getOrCreateChildGroupByName("Products")
.getOrCreateFileReferenceBySourceTreePath(productsPath);
} else if (targetNode.getDescription() instanceof AppleLibraryDescription
|| targetNode.getDescription() instanceof AppleBundleDescription
|| targetNode.getDescription() instanceof CxxLibraryDescription
|| targetNode.getDescription() instanceof HalideLibraryDescription
|| targetNode.getDescription() instanceof PrebuiltAppleFrameworkDescription) {
return project
.getMainGroup()
.getOrCreateChildGroupByName("Frameworks")
.getOrCreateFileReferenceBySourceTreePath(productsPath);
} else if (targetNode.getDescription() instanceof AppleBinaryDescription) {
return project
.getMainGroup()
.getOrCreateChildGroupByName("Dependencies")
.getOrCreateFileReferenceBySourceTreePath(productsPath);
} else {
throw new RuntimeException("Unexpected type: " + targetNode.getDescription().getClass());
}
}
/**
* Whether a given build target is built by the project being generated, or being build elsewhere.
*/
private boolean isBuiltByCurrentProject(BuildTarget buildTarget) {
return initialTargets.contains(buildTarget);
}
private String getXcodeTargetName(BuildTarget target) {
return options.contains(Option.USE_SHORT_NAMES_FOR_TARGETS)
? target.getShortName()
: target.getFullyQualifiedName();
}
private ProductType bundleToTargetProductType(
TargetNode<? extends HasAppleBundleFields, ?> targetNode,
TargetNode<? extends AppleNativeTargetDescriptionArg, ?> binaryNode) {
if (targetNode.getConstructorArg().getXcodeProductType().isPresent()) {
return ProductType.of(targetNode.getConstructorArg().getXcodeProductType().get());
} else if (targetNode.getConstructorArg().getExtension().isLeft()) {
AppleBundleExtension extension = targetNode.getConstructorArg().getExtension().getLeft();
boolean nodeIsAppleLibrary =
((Description<?>) binaryNode.getDescription()) instanceof AppleLibraryDescription;
boolean nodeIsCxxLibrary =
((Description<?>) binaryNode.getDescription()) instanceof CxxLibraryDescription;
if (nodeIsAppleLibrary || nodeIsCxxLibrary) {
if (binaryNode
.getBuildTarget()
.getFlavors()
.contains(CxxDescriptionEnhancer.SHARED_FLAVOR)) {
Optional<ProductType> productType = dylibProductTypeByBundleExtension(extension);
if (productType.isPresent()) {
return productType.get();
}
} else if (extension == AppleBundleExtension.FRAMEWORK) {
return ProductType.STATIC_FRAMEWORK;
}
} else if (binaryNode.getDescription() instanceof AppleBinaryDescription) {
if (extension == AppleBundleExtension.APP) {
return ProductType.APPLICATION;
}
} else if (binaryNode.getDescription() instanceof AppleTestDescription) {
TargetNode<AppleTestDescriptionArg, ?> testNode =
binaryNode.castArg(AppleTestDescriptionArg.class).get();
if (testNode.getConstructorArg().getIsUiTest()) {
return ProductType.UI_TEST;
} else {
return ProductType.UNIT_TEST;
}
}
}
return ProductType.BUNDLE;
}
private boolean shouldGenerateReadOnlyFiles() {
return options.contains(Option.GENERATE_READ_ONLY_FILES);
}
private static String getExtensionString(Either<AppleBundleExtension, String> extension) {
return extension.isLeft() ? extension.getLeft().toFileExtension() : extension.getRight();
}
private static boolean isFrameworkBundle(HasAppleBundleFields arg) {
return arg.getExtension().isLeft()
&& arg.getExtension().getLeft().equals(AppleBundleExtension.FRAMEWORK);
}
private static boolean isModularAppleFramework(TargetNode<?, ?> libraryNode) {
Optional<TargetNode<AppleLibraryDescriptionArg, ?>> appleLibNode =
libraryNode.castArg(AppleLibraryDescriptionArg.class);
if (appleLibNode.isPresent()) {
AppleLibraryDescriptionArg constructorArg = appleLibNode.get().getConstructorArg();
return constructorArg.isModular();
}
return false;
}
private static boolean bundleRequiresRemovalOfAllTransitiveFrameworks(
TargetNode<? extends HasAppleBundleFields, ?> targetNode) {
return isFrameworkBundle(targetNode.getConstructorArg());
}
private static boolean bundleRequiresAllTransitiveFrameworks(
TargetNode<? extends AppleNativeTargetDescriptionArg, ?> binaryNode) {
return binaryNode.castArg(AppleBinaryDescriptionArg.class).isPresent();
}
private Path resolveSourcePath(SourcePath sourcePath) {
if (sourcePath instanceof PathSourcePath) {
return projectFilesystem.relativize(defaultPathResolver.getAbsolutePath(sourcePath));
}
Preconditions.checkArgument(sourcePath instanceof BuildTargetSourcePath);
BuildTargetSourcePath buildTargetSourcePath = (BuildTargetSourcePath) sourcePath;
BuildTarget buildTarget = buildTargetSourcePath.getTarget();
TargetNode<?, ?> node = targetGraph.get(buildTarget);
Optional<TargetNode<ExportFileDescriptionArg, ?>> exportFileNode =
node.castArg(ExportFileDescriptionArg.class);
if (!exportFileNode.isPresent()) {
BuildRuleResolver resolver = buildRuleResolverForNode.apply(node);
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
Path output = pathResolver.getAbsolutePath(sourcePath);
if (output == null) {
throw new HumanReadableException(
"The target '%s' does not have an output.", node.getBuildTarget());
}
requiredBuildTargetsBuilder.add(buildTarget);
return projectFilesystem.relativize(output);
}
Optional<SourcePath> src = exportFileNode.get().getConstructorArg().getSrc();
if (!src.isPresent()) {
Path output =
buildTarget
.getCellPath()
.resolve(buildTarget.getBasePath())
.resolve(buildTarget.getShortNameAndFlavorPostfix());
return projectFilesystem.relativize(output);
}
return resolveSourcePath(src.get());
}
private boolean isLibraryWithSourcesToCompile(TargetNode<?, ?> input) {
if (input.getDescription() instanceof HalideLibraryDescription) {
return true;
}
Optional<TargetNode<CxxLibraryDescription.CommonArg, ?>> library =
getLibraryNode(targetGraph, input);
if (!library.isPresent()) {
return false;
}
return (library.get().getConstructorArg().getSrcs().size() != 0);
}
/** @return product type of a bundle containing a dylib. */
private static Optional<ProductType> dylibProductTypeByBundleExtension(
AppleBundleExtension extension) {
switch (extension) {
case FRAMEWORK:
return Optional.of(ProductType.FRAMEWORK);
case APPEX:
return Optional.of(ProductType.APP_EXTENSION);
case BUNDLE:
return Optional.of(ProductType.BUNDLE);
case XCTEST:
return Optional.of(ProductType.UNIT_TEST);
// $CASES-OMITTED$
default:
return Optional.empty();
}
}
/**
* Determines if a target node is for watchOS2 application
*
* @param targetNode A target node
* @return If the given target node is for an watchOS2 application
*/
private static boolean isWatchApplicationNode(TargetNode<?, ?> targetNode) {
if (targetNode.getDescription() instanceof AppleBundleDescription) {
AppleBundleDescriptionArg arg = (AppleBundleDescriptionArg) targetNode.getConstructorArg();
return arg.getXcodeProductType()
.equals(Optional.of(ProductType.WATCH_APPLICATION.getIdentifier()));
}
return false;
}
private Path getPathToHeaderMapsRoot() {
return projectFilesystem.getBuckPaths().getGenDir().resolve("_p");
}
private Path getPathToHeadersPath(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode, String suffix) {
String hashedPath =
BaseEncoding.base64Url()
.omitPadding()
.encode(
Hashing.sha1()
.hashString(
targetNode
.getBuildTarget()
.getUnflavoredBuildTarget()
.getFullyQualifiedName(),
Charsets.UTF_8)
.asBytes())
.substring(0, 10);
return getPathToHeaderMapsRoot().resolve(hashedPath + suffix);
}
private Path getPathToHeaderSymlinkTree(
TargetNode<? extends CxxLibraryDescription.CommonArg, ?> targetNode,
HeaderVisibility headerVisibility) {
return getPathToHeadersPath(
targetNode, AppleHeaderVisibilities.getHeaderSymlinkTreeSuffix(headerVisibility));
}
private Path getPathToMergedHeaderMap() {
return getPathToHeaderMapsRoot().resolve("pub-hmap");
}
/** An expander for the location macro which leaves it as-is. */
private static class AsIsLocationMacroExpander extends AbstractMacroExpander<LocationMacro> {
@Override
public Class<LocationMacro> getInputClass() {
return LocationMacro.class;
}
@Override
protected LocationMacro parse(
BuildTarget target, CellPathResolver cellNames, ImmutableList<String> input)
throws MacroException {
throw new UnsupportedOperationException();
}
@Override
public String expandFrom(
BuildTarget target,
CellPathResolver cellNames,
BuildRuleResolver resolver,
LocationMacro input)
throws MacroException {
return String.format("$(location %s)", input.getTarget());
}
}
}