/*
* Copyright 2014-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;
import com.dd.plist.NSArray;
import com.dd.plist.NSNumber;
import com.dd.plist.NSObject;
import com.dd.plist.NSString;
import com.facebook.buck.cxx.BuildRuleWithBinary;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.cxx.CxxPreprocessorInput;
import com.facebook.buck.cxx.HeaderVisibility;
import com.facebook.buck.cxx.NativeTestable;
import com.facebook.buck.cxx.ProvidesLinkedBinaryDeps;
import com.facebook.buck.file.WriteFile;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Either;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BinaryBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.CommandTool;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.HasRuntimeDeps;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.Tool;
import com.facebook.buck.rules.args.SourcePathArg;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.CopyStep;
import com.facebook.buck.step.fs.FindAndReplaceStep;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.MoveStep;
import com.facebook.buck.step.fs.RmStep;
import com.facebook.buck.step.fs.WriteFileStep;
import com.facebook.buck.swift.SwiftPlatform;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashCode;
import com.google.common.io.Files;
import com.google.common.util.concurrent.Futures;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
/**
* Creates a bundle: a directory containing files and subdirectories, described by an Info.plist.
*/
public class AppleBundle extends AbstractBuildRule
implements NativeTestable, BuildRuleWithBinary, HasRuntimeDeps, BinaryBuildRule {
private static final Logger LOG = Logger.get(AppleBundle.class);
public static final String CODE_SIGN_ENTITLEMENTS = "CODE_SIGN_ENTITLEMENTS";
private static final String FRAMEWORK_EXTENSION =
AppleBundleExtension.FRAMEWORK.toFileExtension();
private static final String PP_DRY_RUN_RESULT_FILE = "BUCK_pp_dry_run.plist";
private static final String CODE_SIGN_DRY_RUN_ARGS_FILE = "BUCK_code_sign_args.plist";
private static final String CODE_SIGN_DRY_RUN_ENTITLEMENTS_FILE =
"BUCK_code_sign_entitlements.plist";
@AddToRuleKey private final String extension;
@AddToRuleKey private final Optional<String> productName;
@AddToRuleKey private final SourcePath infoPlist;
@AddToRuleKey private final ImmutableMap<String, String> infoPlistSubstitutions;
@AddToRuleKey private final Optional<BuildRule> binary;
@AddToRuleKey private final Optional<AppleDsym> appleDsym;
@AddToRuleKey private final AppleBundleDestinations destinations;
@AddToRuleKey private final AppleBundleResources resources;
@AddToRuleKey private final Set<SourcePath> frameworks;
@AddToRuleKey private final Tool ibtool;
@AddToRuleKey private final ImmutableSortedSet<BuildTarget> tests;
@AddToRuleKey private final ApplePlatform platform;
@AddToRuleKey private final String sdkName;
@AddToRuleKey private final String sdkVersion;
@AddToRuleKey private final ProvisioningProfileStore provisioningProfileStore;
@AddToRuleKey private final CodeSignIdentityStore codeSignIdentityStore;
@AddToRuleKey private final Optional<Tool> codesignAllocatePath;
@AddToRuleKey private final Tool codesign;
@AddToRuleKey private final Optional<Tool> swiftStdlibTool;
@AddToRuleKey private final boolean dryRunCodeSigning;
// Need to use String here as RuleKeyBuilder requires that paths exist to compute hashes.
@AddToRuleKey private final ImmutableMap<SourcePath, String> extensionBundlePaths;
private final Optional<AppleAssetCatalog> assetCatalog;
private final Optional<CoreDataModel> coreDataModel;
private final Optional<SceneKitAssets> sceneKitAssets;
private final Optional<String> platformBuildVersion;
private final Optional<String> xcodeVersion;
private final Optional<String> xcodeBuildVersion;
private final Path sdkPath;
private final String minOSVersion;
private final String binaryName;
private final Path bundleRoot;
private final Path binaryPath;
private final Path bundleBinaryPath;
private final boolean hasBinary;
private final boolean cacheable;
AppleBundle(
BuildRuleParams params,
BuildRuleResolver buildRuleResolver,
Either<AppleBundleExtension, String> extension,
Optional<String> productName,
SourcePath infoPlist,
Map<String, String> infoPlistSubstitutions,
Optional<BuildRule> binary,
Optional<AppleDsym> appleDsym,
AppleBundleDestinations destinations,
AppleBundleResources resources,
ImmutableMap<SourcePath, String> extensionBundlePaths,
Set<SourcePath> frameworks,
AppleCxxPlatform appleCxxPlatform,
Optional<AppleAssetCatalog> assetCatalog,
Optional<CoreDataModel> coreDataModel,
Optional<SceneKitAssets> sceneKitAssets,
Set<BuildTarget> tests,
CodeSignIdentityStore codeSignIdentityStore,
ProvisioningProfileStore provisioningProfileStore,
boolean dryRunCodeSigning,
boolean cacheable) {
super(params);
this.extension =
extension.isLeft() ? extension.getLeft().toFileExtension() : extension.getRight();
this.productName = productName;
this.infoPlist = infoPlist;
this.infoPlistSubstitutions = ImmutableMap.copyOf(infoPlistSubstitutions);
this.binary = binary;
this.appleDsym = appleDsym;
this.destinations = destinations;
this.resources = resources;
this.extensionBundlePaths = extensionBundlePaths;
this.frameworks = frameworks;
this.ibtool = appleCxxPlatform.getIbtool();
this.assetCatalog = assetCatalog;
this.coreDataModel = coreDataModel;
this.sceneKitAssets = sceneKitAssets;
this.binaryName = getBinaryName(getBuildTarget(), this.productName);
this.bundleRoot =
getBundleRoot(getProjectFilesystem(), getBuildTarget(), this.binaryName, this.extension);
this.binaryPath = this.destinations.getExecutablesPath().resolve(this.binaryName);
this.tests = ImmutableSortedSet.copyOf(tests);
AppleSdk sdk = appleCxxPlatform.getAppleSdk();
this.platform = sdk.getApplePlatform();
this.sdkName = sdk.getName();
this.sdkPath = appleCxxPlatform.getAppleSdkPaths().getSdkPath();
this.sdkVersion = sdk.getVersion();
this.minOSVersion = appleCxxPlatform.getMinVersion();
this.platformBuildVersion = appleCxxPlatform.getBuildVersion();
this.xcodeBuildVersion = appleCxxPlatform.getXcodeBuildVersion();
this.xcodeVersion = appleCxxPlatform.getXcodeVersion();
this.dryRunCodeSigning = dryRunCodeSigning;
this.cacheable = cacheable;
bundleBinaryPath = bundleRoot.resolve(binaryPath);
hasBinary = binary.isPresent() && binary.get().getSourcePathToOutput() != null;
if (needCodeSign() && !adHocCodeSignIsSufficient()) {
this.provisioningProfileStore = provisioningProfileStore;
this.codeSignIdentityStore = codeSignIdentityStore;
} else {
this.provisioningProfileStore =
ProvisioningProfileStore.fromProvisioningProfiles(ImmutableList.of());
this.codeSignIdentityStore = CodeSignIdentityStore.fromIdentities(ImmutableList.of());
}
this.codesignAllocatePath = appleCxxPlatform.getCodesignAllocate();
this.codesign = appleCxxPlatform.getCodesignProvider().resolve(buildRuleResolver);
this.swiftStdlibTool =
appleCxxPlatform.getSwiftPlatform().map(SwiftPlatform::getSwiftStdlibTool);
}
public static String getBinaryName(BuildTarget buildTarget, Optional<String> productName) {
if (productName.isPresent()) {
return productName.get();
} else {
return buildTarget.getShortName();
}
}
public static Path getBundleRoot(
ProjectFilesystem filesystem, BuildTarget buildTarget, String binaryName, String extension) {
return BuildTargets.getGenPath(filesystem, buildTarget, "%s")
.resolve(binaryName + "." + extension);
}
public String getExtension() {
return extension;
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), bundleRoot);
}
public Path getInfoPlistPath() {
return getMetadataPath().resolve("Info.plist");
}
public Path getUnzippedOutputFilePathToBinary() {
return this.binaryPath;
}
private Path getMetadataPath() {
return bundleRoot.resolve(destinations.getMetadataPath());
}
public String getPlatformName() {
return platform.getName();
}
public Optional<BuildRule> getBinary() {
return binary;
}
public boolean isLegacyWatchApp() {
return extension.equals(AppleBundleExtension.APP.toFileExtension())
&& binary.isPresent()
&& binary
.get()
.getBuildTarget()
.getFlavors()
.contains(AppleBinaryDescription.LEGACY_WATCH_FLAVOR);
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
ImmutableList.Builder<Step> stepsBuilder = ImmutableList.builder();
stepsBuilder.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), bundleRoot));
Path resourcesDestinationPath = bundleRoot.resolve(this.destinations.getResourcesPath());
if (assetCatalog.isPresent()) {
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), resourcesDestinationPath));
Path bundleDir = assetCatalog.get().getOutputDir();
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
bundleDir,
resourcesDestinationPath,
CopyStep.DirectoryMode.CONTENTS_ONLY));
}
if (coreDataModel.isPresent()) {
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), resourcesDestinationPath));
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
context
.getSourcePathResolver()
.getRelativePath(coreDataModel.get().getSourcePathToOutput()),
resourcesDestinationPath,
CopyStep.DirectoryMode.CONTENTS_ONLY));
}
if (sceneKitAssets.isPresent()) {
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), resourcesDestinationPath));
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
context
.getSourcePathResolver()
.getRelativePath(sceneKitAssets.get().getSourcePathToOutput()),
resourcesDestinationPath,
CopyStep.DirectoryMode.CONTENTS_ONLY));
}
Path metadataPath = getMetadataPath();
Path infoPlistInputPath = context.getSourcePathResolver().getAbsolutePath(infoPlist);
Path infoPlistSubstitutionTempPath =
BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), "%s.plist");
Path infoPlistOutputPath = metadataPath.resolve("Info.plist");
stepsBuilder.add(
MkdirStep.of(getProjectFilesystem(), metadataPath),
// TODO(bhamiltoncx): This is only appropriate for .app bundles.
new WriteFileStep(
getProjectFilesystem(),
"APPLWRUN",
metadataPath.resolve("PkgInfo"),
/* executable */ false),
MkdirStep.of(getProjectFilesystem(), infoPlistSubstitutionTempPath.getParent()),
new FindAndReplaceStep(
getProjectFilesystem(),
infoPlistInputPath,
infoPlistSubstitutionTempPath,
InfoPlistSubstitution.createVariableExpansionFunction(
withDefaults(
infoPlistSubstitutions,
ImmutableMap.of(
"EXECUTABLE_NAME", binaryName,
"PRODUCT_NAME", binaryName)))),
new PlistProcessStep(
getProjectFilesystem(),
infoPlistSubstitutionTempPath,
assetCatalog.isPresent()
? Optional.of(assetCatalog.get().getOutputPlist())
: Optional.empty(),
infoPlistOutputPath,
getInfoPlistAdditionalKeys(),
getInfoPlistOverrideKeys(),
PlistProcessStep.OutputFormat.BINARY));
if (hasBinary) {
appendCopyBinarySteps(stepsBuilder, context.getSourcePathResolver());
appendCopyDsymStep(stepsBuilder, buildableContext, context.getSourcePathResolver());
}
if (!Iterables.isEmpty(
Iterables.concat(
resources.getResourceDirs(),
resources.getDirsContainingResourceDirs(),
resources.getResourceFiles()))) {
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), resourcesDestinationPath));
for (SourcePath dir : resources.getResourceDirs()) {
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
context.getSourcePathResolver().getAbsolutePath(dir),
resourcesDestinationPath,
CopyStep.DirectoryMode.DIRECTORY_AND_CONTENTS));
}
for (SourcePath dir : resources.getDirsContainingResourceDirs()) {
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
context.getSourcePathResolver().getAbsolutePath(dir),
resourcesDestinationPath,
CopyStep.DirectoryMode.CONTENTS_ONLY));
}
for (SourcePath file : resources.getResourceFiles()) {
Path resolvedFilePath = context.getSourcePathResolver().getAbsolutePath(file);
Path destinationPath = resourcesDestinationPath.resolve(resolvedFilePath.getFileName());
addResourceProcessingSteps(
context.getSourcePathResolver(), resolvedFilePath, destinationPath, stepsBuilder);
}
}
ImmutableList.Builder<Path> codeSignOnCopyPathsBuilder = ImmutableList.builder();
addStepsToCopyExtensionBundlesDependencies(
context.getSourcePathResolver(), stepsBuilder, codeSignOnCopyPathsBuilder);
for (SourcePath variantSourcePath : resources.getResourceVariantFiles()) {
Path variantFilePath = context.getSourcePathResolver().getAbsolutePath(variantSourcePath);
Path variantDirectory = variantFilePath.getParent();
if (variantDirectory == null || !variantDirectory.toString().endsWith(".lproj")) {
throw new HumanReadableException(
"Variant files have to be in a directory with name ending in '.lproj', "
+ "but '%s' is not.",
variantFilePath);
}
Path bundleVariantDestinationPath =
resourcesDestinationPath.resolve(variantDirectory.getFileName());
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), bundleVariantDestinationPath));
Path destinationPath = bundleVariantDestinationPath.resolve(variantFilePath.getFileName());
addResourceProcessingSteps(
context.getSourcePathResolver(), variantFilePath, destinationPath, stepsBuilder);
}
if (!frameworks.isEmpty()) {
Path frameworksDestinationPath = bundleRoot.resolve(this.destinations.getFrameworksPath());
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), frameworksDestinationPath));
for (SourcePath framework : frameworks) {
Path srcPath = context.getSourcePathResolver().getAbsolutePath(framework);
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
srcPath,
frameworksDestinationPath,
CopyStep.DirectoryMode.DIRECTORY_AND_CONTENTS));
codeSignOnCopyPathsBuilder.add(frameworksDestinationPath.resolve(srcPath.getFileName()));
}
}
if (needCodeSign()) {
Optional<Path> signingEntitlementsTempPath;
Supplier<CodeSignIdentity> codeSignIdentitySupplier;
if (adHocCodeSignIsSufficient()) {
signingEntitlementsTempPath = Optional.empty();
codeSignIdentitySupplier = () -> CodeSignIdentity.AD_HOC;
} else {
// Copy the .mobileprovision file if the platform requires it, and sign the executable.
Optional<Path> entitlementsPlist = Optional.empty();
final Path srcRoot =
getProjectFilesystem().getRootPath().resolve(getBuildTarget().getBasePath());
Optional<String> entitlementsPlistString =
InfoPlistSubstitution.getVariableExpansionForPlatform(
CODE_SIGN_ENTITLEMENTS,
platform.getName(),
withDefaults(
infoPlistSubstitutions,
ImmutableMap.of(
"SOURCE_ROOT", srcRoot.toString(),
"SRCROOT", srcRoot.toString())));
entitlementsPlist =
entitlementsPlistString.map(
entitlementsPlistName -> {
ProjectFilesystem filesystem = getProjectFilesystem();
Path originalEntitlementsPlist =
srcRoot.resolve(Paths.get(entitlementsPlistName));
Path entitlementsPlistWithSubstitutions =
BuildTargets.getScratchPath(
filesystem, getBuildTarget(), "%s-Entitlements.plist");
stepsBuilder.add(
new FindAndReplaceStep(
filesystem,
originalEntitlementsPlist,
entitlementsPlistWithSubstitutions,
InfoPlistSubstitution.createVariableExpansionFunction(
infoPlistSubstitutions)));
return filesystem.resolve(entitlementsPlistWithSubstitutions);
});
signingEntitlementsTempPath =
Optional.of(
BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), "%s.xcent"));
final Path dryRunResultPath = bundleRoot.resolve(PP_DRY_RUN_RESULT_FILE);
final ProvisioningProfileCopyStep provisioningProfileCopyStep =
new ProvisioningProfileCopyStep(
getProjectFilesystem(),
infoPlistOutputPath,
platform,
Optional.empty(), // Provisioning profile UUID -- find automatically.
entitlementsPlist,
provisioningProfileStore,
resourcesDestinationPath.resolve("embedded.mobileprovision"),
dryRunCodeSigning
? bundleRoot.resolve(CODE_SIGN_DRY_RUN_ENTITLEMENTS_FILE)
: signingEntitlementsTempPath.get(),
codeSignIdentityStore,
dryRunCodeSigning ? Optional.of(dryRunResultPath) : Optional.empty());
stepsBuilder.add(provisioningProfileCopyStep);
codeSignIdentitySupplier =
() -> {
// Using getUnchecked here because the previous step should already throw if exception
// occurred, and this supplier would never be evaluated.
Optional<ProvisioningProfileMetadata> selectedProfile =
Futures.getUnchecked(
provisioningProfileCopyStep.getSelectedProvisioningProfileFuture());
if (!selectedProfile.isPresent()) {
// This should only happen in dry-run codesign mode (since otherwise an exception
// would have been thrown already.) Still, we need to return *something*.
Preconditions.checkState(dryRunCodeSigning);
return CodeSignIdentity.AD_HOC;
}
ImmutableSet<HashCode> fingerprints =
selectedProfile.get().getDeveloperCertificateFingerprints();
if (fingerprints.isEmpty()) {
// No constraints, pick an arbitrary identity.
// If no identities are available, use an ad-hoc identity.
return Iterables.getFirst(
codeSignIdentityStore.getIdentities(), CodeSignIdentity.AD_HOC);
}
for (CodeSignIdentity identity : codeSignIdentityStore.getIdentities()) {
if (identity.getFingerprint().isPresent()
&& fingerprints.contains(identity.getFingerprint().get())) {
return identity;
}
}
throw new HumanReadableException(
"No code sign identity available for provisioning profile: %s\n"
+ "Profile requires an identity with one of the following SHA1 fingerprints "
+ "available in your keychain: \n %s",
selectedProfile.get().getProfilePath(), Joiner.on("\n ").join(fingerprints));
};
}
addSwiftStdlibStepIfNeeded(
context.getSourcePathResolver(),
bundleRoot.resolve(Paths.get("Frameworks")),
dryRunCodeSigning
? Optional.<Supplier<CodeSignIdentity>>empty()
: Optional.of(codeSignIdentitySupplier),
stepsBuilder,
false /* is for packaging? */);
for (Path codeSignOnCopyPath : codeSignOnCopyPathsBuilder.build()) {
stepsBuilder.add(
new CodeSignStep(
getProjectFilesystem(),
context.getSourcePathResolver(),
codeSignOnCopyPath,
Optional.empty(),
codeSignIdentitySupplier,
codesign,
codesignAllocatePath,
dryRunCodeSigning
? Optional.of(codeSignOnCopyPath.resolve(CODE_SIGN_DRY_RUN_ARGS_FILE))
: Optional.empty()));
}
stepsBuilder.add(
new CodeSignStep(
getProjectFilesystem(),
context.getSourcePathResolver(),
bundleRoot,
signingEntitlementsTempPath,
codeSignIdentitySupplier,
codesign,
codesignAllocatePath,
dryRunCodeSigning
? Optional.of(bundleRoot.resolve(CODE_SIGN_DRY_RUN_ARGS_FILE))
: Optional.empty()));
} else {
addSwiftStdlibStepIfNeeded(
context.getSourcePathResolver(),
bundleRoot.resolve(Paths.get("Frameworks")),
Optional.<Supplier<CodeSignIdentity>>empty(),
stepsBuilder,
false /* is for packaging? */);
}
// Ensure the bundle directory is archived so we can fetch it later.
buildableContext.recordArtifact(
context.getSourcePathResolver().getRelativePath(getSourcePathToOutput()));
return stepsBuilder.build();
}
private void appendCopyBinarySteps(
ImmutableList.Builder<Step> stepsBuilder, SourcePathResolver pathResolver) {
Preconditions.checkArgument(hasBinary);
final Path binaryOutputPath =
pathResolver.getRelativePath(
Preconditions.checkNotNull(binary.get().getSourcePathToOutput()));
copyBinaryIntoBundle(stepsBuilder, binaryOutputPath);
copyAnotherCopyOfWatchKitStub(stepsBuilder, binaryOutputPath);
}
private void copyBinaryIntoBundle(
ImmutableList.Builder<Step> stepsBuilder, Path binaryOutputPath) {
stepsBuilder.add(
MkdirStep.of(
getProjectFilesystem(), bundleRoot.resolve(this.destinations.getExecutablesPath())));
stepsBuilder.add(CopyStep.forFile(getProjectFilesystem(), binaryOutputPath, bundleBinaryPath));
}
private void copyAnotherCopyOfWatchKitStub(
ImmutableList.Builder<Step> stepsBuilder, Path binaryOutputPath) {
if ((isLegacyWatchApp() || (platform.getName().contains("watch") && minOSVersion.equals("2.0")))
&& binary.get() instanceof WriteFile) {
final Path watchKitStubDir = bundleRoot.resolve("_WatchKitStub");
stepsBuilder.add(
MkdirStep.of(getProjectFilesystem(), watchKitStubDir),
CopyStep.forFile(
getProjectFilesystem(), binaryOutputPath, watchKitStubDir.resolve("WK")));
}
}
private void appendCopyDsymStep(
ImmutableList.Builder<Step> stepsBuilder,
BuildableContext buildableContext,
SourcePathResolver pathResolver) {
if (appleDsym.isPresent()) {
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
pathResolver.getRelativePath(appleDsym.get().getSourcePathToOutput()),
bundleRoot.getParent(),
CopyStep.DirectoryMode.DIRECTORY_AND_CONTENTS));
appendDsymRenameStepToMatchBundleName(stepsBuilder, buildableContext, pathResolver);
}
}
private void appendDsymRenameStepToMatchBundleName(
ImmutableList.Builder<Step> stepsBuilder,
BuildableContext buildableContext,
SourcePathResolver pathResolver) {
Preconditions.checkArgument(hasBinary && appleDsym.isPresent());
// rename dSYM bundle to match bundle name
Path dsymPath = pathResolver.getRelativePath(appleDsym.get().getSourcePathToOutput());
Path dsymSourcePath = bundleRoot.getParent().resolve(dsymPath.getFileName());
Path dsymDestinationPath =
bundleRoot
.getParent()
.resolve(bundleRoot.getFileName() + "." + AppleBundleExtension.DSYM.toFileExtension());
stepsBuilder.add(RmStep.of(getProjectFilesystem(), dsymDestinationPath).withRecursive(true));
stepsBuilder.add(new MoveStep(getProjectFilesystem(), dsymSourcePath, dsymDestinationPath));
String dwarfFilename =
AppleDsym.getDwarfFilenameForDsymTarget(appleDsym.get().getBuildTarget());
// rename DWARF file inside dSYM bundle to match bundle name
Path dwarfFolder = dsymDestinationPath.resolve(AppleDsym.DSYM_DWARF_FILE_FOLDER);
Path dwarfSourcePath = dwarfFolder.resolve(dwarfFilename);
Path dwarfDestinationPath = dwarfFolder.resolve(MorePaths.getNameWithoutExtension(bundleRoot));
stepsBuilder.add(new MoveStep(getProjectFilesystem(), dwarfSourcePath, dwarfDestinationPath));
// record dSYM so we can fetch it from cache
buildableContext.recordArtifact(dsymDestinationPath);
}
private void addStepsToCopyExtensionBundlesDependencies(
SourcePathResolver resolver,
ImmutableList.Builder<Step> stepsBuilder,
ImmutableList.Builder<Path> codeSignOnCopyPathsBuilder) {
for (Map.Entry<SourcePath, String> entry : extensionBundlePaths.entrySet()) {
Path srcPath = resolver.getAbsolutePath(entry.getKey());
Path destPath = bundleRoot.resolve(entry.getValue());
stepsBuilder.add(MkdirStep.of(getProjectFilesystem(), destPath));
stepsBuilder.add(
CopyStep.forDirectory(
getProjectFilesystem(),
srcPath,
destPath,
CopyStep.DirectoryMode.DIRECTORY_AND_CONTENTS));
if (srcPath.toString().endsWith("." + FRAMEWORK_EXTENSION)) {
codeSignOnCopyPathsBuilder.add(destPath.resolve(srcPath.getFileName()));
}
}
}
public static ImmutableMap<String, String> withDefaults(
ImmutableMap<String, String> map, ImmutableMap<String, String> defaults) {
ImmutableMap.Builder<String, String> builder =
ImmutableMap.<String, String>builder().putAll(map);
for (ImmutableMap.Entry<String, String> entry : defaults.entrySet()) {
if (!map.containsKey(entry.getKey())) {
builder = builder.put(entry.getKey(), entry.getValue());
}
}
return builder.build();
}
private ImmutableMap<String, NSObject> getInfoPlistOverrideKeys() {
ImmutableMap.Builder<String, NSObject> keys = ImmutableMap.builder();
if (platform.getName().contains("osx")) {
keys.put("LSRequiresIPhoneOS", new NSNumber(false));
} else if (!platform.getName().contains("watch") && !isLegacyWatchApp()) {
keys.put("LSRequiresIPhoneOS", new NSNumber(true));
}
return keys.build();
}
private ImmutableMap<String, NSObject> getInfoPlistAdditionalKeys() {
ImmutableMap.Builder<String, NSObject> keys = ImmutableMap.builder();
final String platformName = platform.getName();
if (platformName.contains("osx")) {
keys.put("NSHighResolutionCapable", new NSNumber(true));
keys.put("NSSupportsAutomaticGraphicsSwitching", new NSNumber(true));
keys.put("CFBundleSupportedPlatforms", new NSArray(new NSString("MacOSX")));
} else if (platformName.contains("iphoneos")) {
keys.put("CFBundleSupportedPlatforms", new NSArray(new NSString("iPhoneOS")));
} else if (platformName.contains("iphonesimulator")) {
keys.put("CFBundleSupportedPlatforms", new NSArray(new NSString("iPhoneSimulator")));
} else if (platformName.contains("watchos") && !isLegacyWatchApp()) {
keys.put("CFBundleSupportedPlatforms", new NSArray(new NSString("WatchOS")));
} else if (platformName.contains("watchsimulator") && !isLegacyWatchApp()) {
keys.put("CFBundleSupportedPlatforms", new NSArray(new NSString("WatchSimulator")));
}
keys.put("DTPlatformName", new NSString(platformName));
keys.put("DTPlatformVersion", new NSString(sdkVersion));
keys.put("DTSDKName", new NSString(sdkName + sdkVersion));
keys.put("MinimumOSVersion", new NSString(minOSVersion));
if (platformBuildVersion.isPresent()) {
keys.put("DTPlatformBuild", new NSString(platformBuildVersion.get()));
keys.put("DTSDKBuild", new NSString(platformBuildVersion.get()));
}
if (xcodeBuildVersion.isPresent()) {
keys.put("DTXcodeBuild", new NSString(xcodeBuildVersion.get()));
}
if (xcodeVersion.isPresent()) {
keys.put("DTXcode", new NSString(xcodeVersion.get()));
}
return keys.build();
}
public void addSwiftStdlibStepIfNeeded(
SourcePathResolver resolver,
Path destinationPath,
Optional<Supplier<CodeSignIdentity>> codeSignIdentitySupplier,
ImmutableList.Builder<Step> stepsBuilder,
boolean isForPackaging) {
// It's apparently safe to run this even on a non-swift bundle (in that case, no libs
// are copied over).
if (swiftStdlibTool.isPresent()) {
ImmutableList.Builder<String> swiftStdlibCommand = ImmutableList.builder();
swiftStdlibCommand.addAll(swiftStdlibTool.get().getCommandPrefix(resolver));
swiftStdlibCommand.add(
"--scan-executable",
bundleBinaryPath.toString(),
"--scan-folder",
bundleRoot.resolve(this.destinations.getFrameworksPath()).toString(),
"--scan-folder",
bundleRoot.resolve(destinations.getPlugInsPath()).toString());
String tempDirPattern = isForPackaging ? "__swift_packaging_temp__%s" : "__swift_temp__%s";
stepsBuilder.add(
new SwiftStdlibStep(
getProjectFilesystem().getRootPath(),
BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), tempDirPattern),
this.sdkPath,
destinationPath,
swiftStdlibCommand.build(),
codeSignIdentitySupplier));
}
}
private void addStoryboardProcessingSteps(
SourcePathResolver resolver,
Path sourcePath,
Path destinationPath,
ImmutableList.Builder<Step> stepsBuilder) {
if (platform.getName().contains("watch") || isLegacyWatchApp()) {
LOG.debug(
"Compiling storyboard %s to storyboardc %s and linking", sourcePath, destinationPath);
Path compiledStoryboardPath =
BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), "%s.storyboardc");
stepsBuilder.add(
new IbtoolStep(
getProjectFilesystem(),
ibtool.getEnvironment(resolver),
ibtool.getCommandPrefix(resolver),
ImmutableList.of("--target-device", "watch", "--compile"),
sourcePath,
compiledStoryboardPath));
stepsBuilder.add(
new IbtoolStep(
getProjectFilesystem(),
ibtool.getEnvironment(resolver),
ibtool.getCommandPrefix(resolver),
ImmutableList.of("--target-device", "watch", "--link"),
compiledStoryboardPath,
destinationPath.getParent()));
} else {
LOG.debug("Compiling storyboard %s to storyboardc %s", sourcePath, destinationPath);
String compiledStoryboardFilename =
Files.getNameWithoutExtension(destinationPath.toString()) + ".storyboardc";
Path compiledStoryboardPath = destinationPath.getParent().resolve(compiledStoryboardFilename);
stepsBuilder.add(
new IbtoolStep(
getProjectFilesystem(),
ibtool.getEnvironment(resolver),
ibtool.getCommandPrefix(resolver),
ImmutableList.of("--compile"),
sourcePath,
compiledStoryboardPath));
}
}
private void addResourceProcessingSteps(
SourcePathResolver resolver,
Path sourcePath,
Path destinationPath,
ImmutableList.Builder<Step> stepsBuilder) {
String sourcePathExtension =
Files.getFileExtension(sourcePath.toString()).toLowerCase(Locale.US);
switch (sourcePathExtension) {
case "plist":
case "stringsdict":
LOG.debug("Converting plist %s to binary plist %s", sourcePath, destinationPath);
stepsBuilder.add(
new PlistProcessStep(
getProjectFilesystem(),
sourcePath,
Optional.empty(),
destinationPath,
ImmutableMap.of(),
ImmutableMap.of(),
PlistProcessStep.OutputFormat.BINARY));
break;
case "storyboard":
addStoryboardProcessingSteps(resolver, sourcePath, destinationPath, stepsBuilder);
break;
case "xib":
String compiledNibFilename =
Files.getNameWithoutExtension(destinationPath.toString()) + ".nib";
Path compiledNibPath = destinationPath.getParent().resolve(compiledNibFilename);
LOG.debug("Compiling XIB %s to NIB %s", sourcePath, destinationPath);
stepsBuilder.add(
new IbtoolStep(
getProjectFilesystem(),
ibtool.getEnvironment(resolver),
ibtool.getCommandPrefix(resolver),
ImmutableList.of("--compile"),
sourcePath,
compiledNibPath));
break;
default:
stepsBuilder.add(CopyStep.forFile(getProjectFilesystem(), sourcePath, destinationPath));
break;
}
}
@Override
public boolean isTestedBy(BuildTarget testRule) {
if (tests.contains(testRule)) {
return true;
}
if (binary.isPresent()) {
BuildRule binaryRule = binary.get();
if (binaryRule instanceof NativeTestable) {
return ((NativeTestable) binaryRule).isTestedBy(testRule);
}
}
return false;
}
@Override
public CxxPreprocessorInput getCxxPreprocessorInput(
CxxPlatform cxxPlatform, HeaderVisibility headerVisibility)
throws NoSuchBuildTargetException {
if (binary.isPresent()) {
BuildRule binaryRule = binary.get();
if (binaryRule instanceof NativeTestable) {
return ((NativeTestable) binaryRule).getCxxPreprocessorInput(cxxPlatform, headerVisibility);
}
}
return CxxPreprocessorInput.EMPTY;
}
private boolean adHocCodeSignIsSufficient() {
return ApplePlatform.adHocCodeSignIsSufficient(platform.getName());
}
// .framework bundles will be code-signed when they're copied into the containing bundle.
private boolean needCodeSign() {
return binary.isPresent()
&& ApplePlatform.needsCodeSign(platform.getName())
&& !extension.equals(FRAMEWORK_EXTENSION);
}
@Override
public BuildRule getBinaryBuildRule() {
return binary.get();
}
@Override
public Stream<BuildTarget> getRuntimeDeps() {
if (binary.get() instanceof ProvidesLinkedBinaryDeps) {
List<BuildRule> linkDeps = new ArrayList<>();
linkDeps.addAll(((ProvidesLinkedBinaryDeps) binary.get()).getCompileDeps());
linkDeps.addAll(((ProvidesLinkedBinaryDeps) binary.get()).getStaticLibraryDeps());
if (linkDeps.size() > 0) {
return Stream.concat(Stream.of(binary.get()), linkDeps.stream())
.map(BuildRule::getBuildTarget);
}
}
return Stream.empty();
}
@Override
public boolean isCacheable() {
return cacheable;
}
@Override
public Tool getExecutableCommand() {
return new CommandTool.Builder()
.addArg(SourcePathArg.of(new PathSourcePath(getProjectFilesystem(), bundleBinaryPath)))
.build();
}
}