/*
* 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.facebook.buck.cxx.CxxCompilationDatabase;
import com.facebook.buck.cxx.CxxDescriptionEnhancer;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.cxx.CxxStrip;
import com.facebook.buck.cxx.Linker;
import com.facebook.buck.cxx.LinkerMapMode;
import com.facebook.buck.cxx.NativeLinkable;
import com.facebook.buck.cxx.NativeLinkables;
import com.facebook.buck.cxx.StripStyle;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Either;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.FlavorDomain;
import com.facebook.buck.model.FlavorDomainException;
import com.facebook.buck.model.Flavored;
import com.facebook.buck.model.InternalFlavor;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.AbstractBuildRule;
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.CellPathResolver;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.ImplicitDepsInferringDescription;
import com.facebook.buck.rules.MetadataProvidingDescription;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.swift.SwiftLibraryDescription;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.OptionalCompat;
import com.facebook.buck.util.immutables.BuckStyleImmutable;
import com.facebook.buck.util.immutables.BuckStyleTuple;
import com.facebook.buck.versions.Version;
import com.facebook.buck.zip.UnzipStep;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableCollection;
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.Sets;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import org.immutables.value.Value;
public class AppleTestDescription
implements Description<AppleTestDescriptionArg>,
Flavored,
ImplicitDepsInferringDescription<AppleTestDescription.AbstractAppleTestDescriptionArg>,
MetadataProvidingDescription<AppleTestDescriptionArg> {
/** Flavors for the additional generated build rules. */
static final Flavor LIBRARY_FLAVOR = InternalFlavor.of("apple-test-library");
static final Flavor BUNDLE_FLAVOR = InternalFlavor.of("apple-test-bundle");
private static final Flavor UNZIP_XCTOOL_FLAVOR = InternalFlavor.of("unzip-xctool");
private static final ImmutableSet<Flavor> SUPPORTED_FLAVORS =
ImmutableSet.of(LIBRARY_FLAVOR, BUNDLE_FLAVOR);
/**
* Auxiliary build modes which makes this description emit just the results of the underlying
* library delegate.
*/
private static final Set<Flavor> AUXILIARY_LIBRARY_FLAVORS =
ImmutableSet.of(
CxxCompilationDatabase.COMPILATION_DATABASE,
CxxDescriptionEnhancer.HEADER_SYMLINK_TREE_FLAVOR,
CxxDescriptionEnhancer.EXPORTED_HEADER_SYMLINK_TREE_FLAVOR,
CxxDescriptionEnhancer.SANDBOX_TREE_FLAVOR);
private final AppleConfig appleConfig;
private final AppleLibraryDescription appleLibraryDescription;
private final FlavorDomain<CxxPlatform> cxxPlatformFlavorDomain;
private final FlavorDomain<AppleCxxPlatform> appleCxxPlatformFlavorDomain;
private final CxxPlatform defaultCxxPlatform;
private final CodeSignIdentityStore codeSignIdentityStore;
private final ProvisioningProfileStore provisioningProfileStore;
private final Supplier<Optional<Path>> xcodeDeveloperDirectorySupplier;
private final Optional<Long> defaultTestRuleTimeoutMs;
public AppleTestDescription(
AppleConfig appleConfig,
AppleLibraryDescription appleLibraryDescription,
FlavorDomain<CxxPlatform> cxxPlatformFlavorDomain,
FlavorDomain<AppleCxxPlatform> appleCxxPlatformFlavorDomain,
CxxPlatform defaultCxxPlatform,
CodeSignIdentityStore codeSignIdentityStore,
ProvisioningProfileStore provisioningProfileStore,
Supplier<Optional<Path>> xcodeDeveloperDirectorySupplier,
Optional<Long> defaultTestRuleTimeoutMs) {
this.appleConfig = appleConfig;
this.appleLibraryDescription = appleLibraryDescription;
this.cxxPlatformFlavorDomain = cxxPlatformFlavorDomain;
this.appleCxxPlatformFlavorDomain = appleCxxPlatformFlavorDomain;
this.defaultCxxPlatform = defaultCxxPlatform;
this.codeSignIdentityStore = codeSignIdentityStore;
this.provisioningProfileStore = provisioningProfileStore;
this.xcodeDeveloperDirectorySupplier = xcodeDeveloperDirectorySupplier;
this.defaultTestRuleTimeoutMs = defaultTestRuleTimeoutMs;
}
@Override
public Class<AppleTestDescriptionArg> getConstructorArgType() {
return AppleTestDescriptionArg.class;
}
@Override
public Optional<ImmutableSet<FlavorDomain<?>>> flavorDomains() {
return appleLibraryDescription.flavorDomains();
}
@Override
public boolean hasFlavors(ImmutableSet<Flavor> flavors) {
return Sets.difference(flavors, SUPPORTED_FLAVORS).isEmpty()
|| appleLibraryDescription.hasFlavors(flavors);
}
@Override
public BuildRule createBuildRule(
TargetGraph targetGraph,
BuildRuleParams params,
BuildRuleResolver resolver,
CellPathResolver cellRoots,
AppleTestDescriptionArg args)
throws NoSuchBuildTargetException {
AppleDebugFormat debugFormat =
AppleDebugFormat.FLAVOR_DOMAIN
.getValue(params.getBuildTarget())
.orElse(appleConfig.getDefaultDebugInfoFormatForTests());
if (params.getBuildTarget().getFlavors().contains(debugFormat.getFlavor())) {
params = params.withoutFlavor(debugFormat.getFlavor());
}
boolean createBundle =
Sets.intersection(params.getBuildTarget().getFlavors(), AUXILIARY_LIBRARY_FLAVORS)
.isEmpty();
// Flavors pertaining to the library targets that are generated.
Sets.SetView<Flavor> libraryFlavors =
Sets.difference(params.getBuildTarget().getFlavors(), AUXILIARY_LIBRARY_FLAVORS);
boolean addDefaultPlatform = libraryFlavors.isEmpty();
ImmutableSet.Builder<Flavor> extraFlavorsBuilder = ImmutableSet.builder();
if (createBundle) {
extraFlavorsBuilder.add(LIBRARY_FLAVOR, CxxDescriptionEnhancer.MACH_O_BUNDLE_FLAVOR);
}
extraFlavorsBuilder.add(debugFormat.getFlavor());
if (addDefaultPlatform) {
extraFlavorsBuilder.add(defaultCxxPlatform.getFlavor());
}
Optional<MultiarchFileInfo> multiarchFileInfo =
MultiarchFileInfos.create(appleCxxPlatformFlavorDomain, params.getBuildTarget());
AppleCxxPlatform appleCxxPlatform;
ImmutableList<CxxPlatform> cxxPlatforms;
if (multiarchFileInfo.isPresent()) {
ImmutableList.Builder<CxxPlatform> cxxPlatformBuilder = ImmutableList.builder();
for (BuildTarget thinTarget : multiarchFileInfo.get().getThinTargets()) {
cxxPlatformBuilder.add(cxxPlatformFlavorDomain.getValue(thinTarget).get());
}
cxxPlatforms = cxxPlatformBuilder.build();
appleCxxPlatform = multiarchFileInfo.get().getRepresentativePlatform();
} else {
CxxPlatform cxxPlatform =
cxxPlatformFlavorDomain.getValue(params.getBuildTarget()).orElse(defaultCxxPlatform);
cxxPlatforms = ImmutableList.of(cxxPlatform);
try {
appleCxxPlatform = appleCxxPlatformFlavorDomain.getValue(cxxPlatform.getFlavor());
} catch (FlavorDomainException e) {
throw new HumanReadableException(
e,
"%s: Apple test requires an Apple platform, found '%s'",
params.getBuildTarget(),
cxxPlatform.getFlavor().getName());
}
}
Optional<TestHostInfo> testHostInfo;
if (args.getTestHostApp().isPresent()) {
testHostInfo =
Optional.of(
createTestHostInfo(
params,
resolver,
args.getTestHostApp().get(),
debugFormat,
libraryFlavors,
cxxPlatforms));
} else {
testHostInfo = Optional.empty();
}
BuildTarget libraryTarget =
params
.getBuildTarget()
.withAppendedFlavors(extraFlavorsBuilder.build())
.withAppendedFlavors(debugFormat.getFlavor())
.withAppendedFlavors(LinkerMapMode.NO_LINKER_MAP.getFlavor());
BuildRule library =
createTestLibraryRule(
targetGraph,
params,
resolver,
cellRoots,
args,
testHostInfo.map(TestHostInfo::getTestHostAppBinarySourcePath),
testHostInfo.map(TestHostInfo::getBlacklist).orElse(ImmutableSet.of()),
libraryTarget,
ImmutableSortedSet.copyOf(OptionalCompat.asSet(args.getTestHostApp())));
if (!createBundle || SwiftLibraryDescription.isSwiftTarget(libraryTarget)) {
return library;
}
String platformName = appleCxxPlatform.getAppleSdk().getApplePlatform().getName();
AppleBundle bundle =
AppleDescriptions.createAppleBundle(
cxxPlatformFlavorDomain,
defaultCxxPlatform,
appleCxxPlatformFlavorDomain,
targetGraph,
params
.withBuildTarget(
params
.getBuildTarget()
.withAppendedFlavors(
BUNDLE_FLAVOR,
debugFormat.getFlavor(),
LinkerMapMode.NO_LINKER_MAP.getFlavor(),
AppleDescriptions.NO_INCLUDE_FRAMEWORKS_FLAVOR))
.copyReplacingDeclaredAndExtraDeps(
Suppliers.ofInstance(
ImmutableSortedSet.<BuildRule>naturalOrder()
.add(library)
.addAll(params.getDeclaredDeps().get())
.build()),
params.getExtraDeps()),
resolver,
codeSignIdentityStore,
provisioningProfileStore,
library.getBuildTarget(),
args.getExtension(),
Optional.empty(),
args.getInfoPlist(),
args.getInfoPlistSubstitutions(),
args.getDeps(),
args.getTests(),
debugFormat,
appleConfig.useDryRunCodeSigning(),
appleConfig.cacheBundlesAndPackages());
resolver.addToIndex(bundle);
Optional<SourcePath> xctool = getXctool(params, resolver);
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
return new AppleTest(
xctool,
appleConfig.getXctoolStutterTimeoutMs(),
appleCxxPlatform.getXctest(),
appleConfig.getXctestPlatformNames().contains(platformName),
platformName,
appleConfig.getXctoolDefaultDestinationSpecifier(),
Optional.of(args.getDestinationSpecifier()),
params.copyReplacingDeclaredAndExtraDeps(
Suppliers.ofInstance(ImmutableSortedSet.of(bundle)),
Suppliers.ofInstance(ImmutableSortedSet.of())),
bundle,
testHostInfo.map(TestHostInfo::getTestHostApp),
args.getContacts(),
args.getLabels(),
args.getRunTestSeparately(),
xcodeDeveloperDirectorySupplier,
appleConfig.getTestLogDirectoryEnvironmentVariable(),
appleConfig.getTestLogLevelEnvironmentVariable(),
appleConfig.getTestLogLevel(),
args.getTestRuleTimeoutMs().map(Optional::of).orElse(defaultTestRuleTimeoutMs),
args.getIsUiTest(),
args.getSnapshotReferenceImagesPath(),
ruleFinder);
}
private Optional<SourcePath> getXctool(BuildRuleParams params, BuildRuleResolver resolver) {
// If xctool is specified as a build target in the buck config, it's wrapping ZIP file which
// we need to unpack to get at the actual binary. Otherwise, if it's specified as a path, we
// can use that directly.
if (appleConfig.getXctoolZipTarget().isPresent()) {
final BuildRule xctoolZipBuildRule = resolver.getRule(appleConfig.getXctoolZipTarget().get());
BuildTarget unzipXctoolTarget =
BuildTarget.builder(xctoolZipBuildRule.getBuildTarget())
.addFlavors(UNZIP_XCTOOL_FLAVOR)
.build();
final Path outputDirectory =
BuildTargets.getGenPath(params.getProjectFilesystem(), unzipXctoolTarget, "%s/unzipped");
if (!resolver.getRuleOptional(unzipXctoolTarget).isPresent()) {
BuildRuleParams unzipXctoolParams =
params
.withBuildTarget(unzipXctoolTarget)
.copyReplacingDeclaredAndExtraDeps(
Suppliers.ofInstance(ImmutableSortedSet.of(xctoolZipBuildRule)),
Suppliers.ofInstance(ImmutableSortedSet.of()));
resolver.addToIndex(
new AbstractBuildRule(unzipXctoolParams) {
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
buildableContext.recordArtifact(outputDirectory);
return new ImmutableList.Builder<Step>()
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), outputDirectory))
.add(
new UnzipStep(
getProjectFilesystem(),
context
.getSourcePathResolver()
.getAbsolutePath(
Preconditions.checkNotNull(
xctoolZipBuildRule.getSourcePathToOutput())),
outputDirectory))
.build();
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), outputDirectory);
}
});
}
return Optional.of(
new ExplicitBuildTargetSourcePath(
unzipXctoolTarget, outputDirectory.resolve("bin/xctool")));
} else if (appleConfig.getXctoolPath().isPresent()) {
return Optional.of(
new PathSourcePath(params.getProjectFilesystem(), appleConfig.getXctoolPath().get()));
} else {
return Optional.empty();
}
}
private BuildRule createTestLibraryRule(
TargetGraph targetGraph,
BuildRuleParams params,
BuildRuleResolver resolver,
CellPathResolver cellRoots,
AppleTestDescriptionArg args,
Optional<SourcePath> testHostAppBinarySourcePath,
ImmutableSet<BuildTarget> blacklist,
BuildTarget libraryTarget,
ImmutableSortedSet<BuildTarget> extraCxxDeps)
throws NoSuchBuildTargetException {
BuildTarget existingLibraryTarget =
libraryTarget
.withAppendedFlavors(AppleDebuggableBinary.RULE_FLAVOR, CxxStrip.RULE_FLAVOR)
.withAppendedFlavors(StripStyle.NON_GLOBAL_SYMBOLS.getFlavor());
Optional<BuildRule> existingLibrary = resolver.getRuleOptional(existingLibraryTarget);
BuildRule library;
if (existingLibrary.isPresent()) {
library = existingLibrary.get();
} else {
library =
appleLibraryDescription.createLibraryBuildRule(
targetGraph,
params.withBuildTarget(libraryTarget),
resolver,
cellRoots,
args,
args::withExportedDeps,
// For now, instead of building all deps as dylibs and fixing up their install_names,
// we'll just link them statically.
Optional.of(Linker.LinkableDepType.STATIC),
testHostAppBinarySourcePath,
blacklist,
extraCxxDeps);
resolver.addToIndex(library);
}
return library;
}
@Override
public void findDepsForTargetFromConstructorArgs(
BuildTarget buildTarget,
CellPathResolver cellRoots,
AbstractAppleTestDescriptionArg constructorArg,
ImmutableCollection.Builder<BuildTarget> extraDepsBuilder,
ImmutableCollection.Builder<BuildTarget> targetGraphOnlyDepsBuilder) {
// TODO(beng, coneko): This should technically only be a runtime dependency;
// doing this adds it to the extra deps in BuildRuleParams passed to
// the bundle and test rule.
Optional<BuildTarget> xctoolZipTarget = appleConfig.getXctoolZipTarget();
if (xctoolZipTarget.isPresent()) {
extraDepsBuilder.add(xctoolZipTarget.get());
}
appleLibraryDescription.findDepsForTargetFromConstructorArgs(
buildTarget, cellRoots, constructorArg, extraDepsBuilder, targetGraphOnlyDepsBuilder);
}
private TestHostInfo createTestHostInfo(
BuildRuleParams params,
BuildRuleResolver resolver,
BuildTarget testHostAppBuildTarget,
AppleDebugFormat debugFormat,
Iterable<Flavor> additionalFlavors,
ImmutableList<CxxPlatform> cxxPlatforms)
throws NoSuchBuildTargetException {
BuildRule rule =
resolver.requireRule(
BuildTarget.builder(testHostAppBuildTarget)
.addAllFlavors(additionalFlavors)
.addFlavors(debugFormat.getFlavor())
.addFlavors(StripStyle.NON_GLOBAL_SYMBOLS.getFlavor())
.build());
if (!(rule instanceof AppleBundle)) {
throw new HumanReadableException(
"Apple test rule '%s' has test_host_app '%s' not of type '%s'.",
params.getBuildTarget(),
testHostAppBuildTarget,
Description.getBuildRuleType(AppleBundleDescription.class));
}
AppleBundle testHostApp = (AppleBundle) rule;
SourcePath testHostAppBinarySourcePath =
testHostApp.getBinaryBuildRule().getSourcePathToOutput();
ImmutableMap<BuildTarget, NativeLinkable> roots =
NativeLinkables.getNativeLinkableRoots(
testHostApp.getBinary().get().getBuildDeps(), x -> true);
// Union the blacklist of all the platforms. This should give a superset for each particular
// platform, which should be acceptable as items in the blacklist thare are unmatched are simply
// ignored.
ImmutableSet.Builder<BuildTarget> blacklistBuilder = ImmutableSet.builder();
for (CxxPlatform platform : cxxPlatforms) {
blacklistBuilder.addAll(
NativeLinkables.getTransitiveNativeLinkables(platform, roots.values()).keySet());
}
return TestHostInfo.of(testHostApp, testHostAppBinarySourcePath, blacklistBuilder.build());
}
@Override
public <U> Optional<U> createMetadata(
BuildTarget buildTarget,
BuildRuleResolver resolver,
AppleTestDescriptionArg args,
Optional<ImmutableMap<BuildTarget, Version>> selectedVersions,
Class<U> metadataClass)
throws NoSuchBuildTargetException {
return appleLibraryDescription.createMetadataForLibrary(
buildTarget, resolver, selectedVersions, args, metadataClass);
}
@Value.Immutable
@BuckStyleTuple
interface AbstractTestHostInfo {
AppleBundle getTestHostApp();
/**
* Location of the test host binary that can be passed as the "bundle loader" option when
* linking the test library.
*/
SourcePath getTestHostAppBinarySourcePath();
/** Libraries included in test host that should not be linked into the test library. */
ImmutableSet<BuildTarget> getBlacklist();
}
@BuckStyleImmutable
@Value.Immutable
interface AbstractAppleTestDescriptionArg
extends AppleNativeTargetDescriptionArg, HasAppleBundleFields {
@Value.NaturalOrder
ImmutableSortedSet<String> getContacts();
@Value.Default
default boolean getRunTestSeparately() {
return false;
}
@Value.Default
default boolean getIsUiTest() {
return false;
}
Optional<BuildTarget> getTestHostApp();
// for use with FBSnapshotTestcase, injects the path as FB_REFERENCE_IMAGE_DIR
Optional<Either<SourcePath, String>> getSnapshotReferenceImagesPath();
// Bundle related fields.
ImmutableMap<String, String> getDestinationSpecifier();
Optional<Long> getTestRuleTimeoutMs();
@Override
default Either<AppleBundleExtension, String> getExtension() {
return Either.ofLeft(AppleBundleExtension.XCTEST);
}
@Override
default Optional<String> getProductName() {
return Optional.empty();
}
}
}