/*
* 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.python;
import com.facebook.buck.cxx.CxxBuckConfig;
import com.facebook.buck.cxx.CxxPlatform;
import com.facebook.buck.cxx.WindowsLinker;
import com.facebook.buck.file.WriteFile;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.FlavorDomain;
import com.facebook.buck.model.InternalFlavor;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.CellPathResolver;
import com.facebook.buck.rules.CommonDescriptionArg;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.HasDeclaredDeps;
import com.facebook.buck.rules.HasTests;
import com.facebook.buck.rules.ImplicitDepsInferringDescription;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.SymlinkTree;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.args.MacroArg;
import com.facebook.buck.rules.coercer.PatternMatchedCollection;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.OptionalCompat;
import com.facebook.buck.util.immutables.BuckStyleImmutable;
import com.facebook.buck.versions.VersionRoot;
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.Maps;
import com.google.common.collect.Sets;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.immutables.value.Value;
public class PythonBinaryDescription
implements Description<PythonBinaryDescriptionArg>,
ImplicitDepsInferringDescription<
PythonBinaryDescription.AbstractPythonBinaryDescriptionArg>,
VersionRoot<PythonBinaryDescriptionArg> {
private static final Logger LOG = Logger.get(PythonBinaryDescription.class);
private final PythonBuckConfig pythonBuckConfig;
private final FlavorDomain<PythonPlatform> pythonPlatforms;
private final CxxBuckConfig cxxBuckConfig;
private final CxxPlatform defaultCxxPlatform;
private final FlavorDomain<CxxPlatform> cxxPlatforms;
public PythonBinaryDescription(
PythonBuckConfig pythonBuckConfig,
FlavorDomain<PythonPlatform> pythonPlatforms,
CxxBuckConfig cxxBuckConfig,
CxxPlatform defaultCxxPlatform,
FlavorDomain<CxxPlatform> cxxPlatforms) {
this.pythonBuckConfig = pythonBuckConfig;
this.pythonPlatforms = pythonPlatforms;
this.cxxBuckConfig = cxxBuckConfig;
this.defaultCxxPlatform = defaultCxxPlatform;
this.cxxPlatforms = cxxPlatforms;
}
@Override
public Class<PythonBinaryDescriptionArg> getConstructorArgType() {
return PythonBinaryDescriptionArg.class;
}
public static BuildTarget getEmptyInitTarget(BuildTarget baseTarget) {
return baseTarget.withAppendedFlavors(InternalFlavor.of("__init__"));
}
public static SourcePath createEmptyInitModule(
BuildRuleParams params, BuildRuleResolver resolver) {
BuildTarget emptyInitTarget = getEmptyInitTarget(params.getBuildTarget());
Path emptyInitPath =
BuildTargets.getGenPath(
params.getProjectFilesystem(), params.getBuildTarget(), "%s/__init__.py");
WriteFile rule =
resolver.addToIndex(
new WriteFile(
params
.withBuildTarget(emptyInitTarget)
.copyReplacingDeclaredAndExtraDeps(
Suppliers.ofInstance(ImmutableSortedSet.of()),
Suppliers.ofInstance(ImmutableSortedSet.of())),
"",
emptyInitPath,
/* executable */ false));
return rule.getSourcePathToOutput();
}
public static ImmutableMap<Path, SourcePath> addMissingInitModules(
ImmutableMap<Path, SourcePath> modules, SourcePath emptyInit) {
Map<Path, SourcePath> initModules = Maps.newLinkedHashMap();
// Insert missing `__init__.py` modules.
Set<Path> packages = Sets.newHashSet();
for (Path module : modules.keySet()) {
Path pkg = module;
while ((pkg = pkg.getParent()) != null && !packages.contains(pkg)) {
Path init = pkg.resolve("__init__.py");
if (!modules.containsKey(init)) {
initModules.put(init, emptyInit);
}
packages.add(pkg);
}
}
return ImmutableMap.<Path, SourcePath>builder().putAll(modules).putAll(initModules).build();
}
private PythonInPlaceBinary createInPlaceBinaryRule(
BuildRuleParams params,
BuildRuleResolver resolver,
SourcePathRuleFinder ruleFinder,
PythonPlatform pythonPlatform,
CxxPlatform cxxPlatform,
String mainModule,
Optional<String> extension,
PythonPackageComponents components,
ImmutableSet<String> preloadLibraries) {
// We don't currently support targeting Windows.
if (cxxPlatform.getLd().resolve(resolver) instanceof WindowsLinker) {
throw new HumanReadableException(
"%s: cannot build in-place python binaries for Windows (%s)",
params.getBuildTarget(), cxxPlatform.getFlavor());
}
// Add in any missing init modules into the python components.
SourcePath emptyInit = createEmptyInitModule(params, resolver);
components = components.withModules(addMissingInitModules(components.getModules(), emptyInit));
BuildTarget linkTreeTarget =
params.getBuildTarget().withAppendedFlavors(InternalFlavor.of("link-tree"));
Path linkTreeRoot =
BuildTargets.getGenPath(params.getProjectFilesystem(), linkTreeTarget, "%s");
SymlinkTree linkTree =
resolver.addToIndex(
new SymlinkTree(
linkTreeTarget,
params.getProjectFilesystem(),
linkTreeRoot,
ImmutableMap.<Path, SourcePath>builder()
.putAll(components.getModules())
.putAll(components.getResources())
.putAll(components.getNativeLibraries())
.build(),
ruleFinder));
return PythonInPlaceBinary.from(
params,
resolver,
cxxPlatform,
pythonPlatform,
mainModule,
components,
extension.orElse(pythonBuckConfig.getPexExtension()),
preloadLibraries,
pythonBuckConfig.legacyOutputPath(),
ruleFinder,
linkTree,
pythonPlatform.getEnvironment());
}
PythonBinary createPackageRule(
BuildRuleParams params,
BuildRuleResolver resolver,
SourcePathRuleFinder ruleFinder,
PythonPlatform pythonPlatform,
CxxPlatform cxxPlatform,
String mainModule,
Optional<String> extension,
PythonPackageComponents components,
ImmutableList<String> buildArgs,
PythonBuckConfig.PackageStyle packageStyle,
ImmutableSet<String> preloadLibraries) {
switch (packageStyle) {
case INPLACE:
return createInPlaceBinaryRule(
params,
resolver,
ruleFinder,
pythonPlatform,
cxxPlatform,
mainModule,
extension,
components,
preloadLibraries);
case STANDALONE:
return PythonPackagedBinary.from(
params,
ruleFinder,
pythonPlatform,
pythonBuckConfig.getPexTool(resolver),
buildArgs,
pythonBuckConfig.getPexExecutor(resolver).orElse(pythonPlatform.getEnvironment()),
extension.orElse(pythonBuckConfig.getPexExtension()),
pythonPlatform.getEnvironment(),
mainModule,
components,
preloadLibraries,
pythonBuckConfig.shouldCacheBinaries(),
pythonBuckConfig.legacyOutputPath());
default:
throw new IllegalStateException();
}
}
private CxxPlatform getCxxPlatform(BuildTarget target, AbstractPythonBinaryDescriptionArg args) {
return cxxPlatforms
.getValue(target)
.orElse(args.getCxxPlatform().map(cxxPlatforms::getValue).orElse(defaultCxxPlatform));
}
@Override
public PythonBinary createBuildRule(
TargetGraph targetGraph,
BuildRuleParams params,
BuildRuleResolver resolver,
CellPathResolver cellRoots,
PythonBinaryDescriptionArg args)
throws NoSuchBuildTargetException {
if (!(args.getMain().isPresent() ^ args.getMainModule().isPresent())) {
throw new HumanReadableException(
"%s: must set exactly one of `main_module` and `main`", params.getBuildTarget());
}
Path baseModule = PythonUtil.getBasePath(params.getBuildTarget(), args.getBaseModule());
String mainModule;
ImmutableMap.Builder<Path, SourcePath> modules = ImmutableMap.builder();
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder);
// If `main` is set, add it to the map of modules for this binary and also set it as the
// `mainModule`, otherwise, use the explicitly set main module.
if (args.getMain().isPresent()) {
LOG.warn(
"%s: parameter `main` is deprecated, please use `main_module` instead.",
params.getBuildTarget());
String mainName =
pathResolver.getSourcePathName(params.getBuildTarget(), args.getMain().get());
Path main = baseModule.resolve(mainName);
modules.put(baseModule.resolve(mainName), args.getMain().get());
mainModule = PythonUtil.toModuleName(params.getBuildTarget(), main.toString());
} else {
mainModule = args.getMainModule().get();
}
// Build up the list of all components going into the python binary.
PythonPackageComponents binaryPackageComponents =
PythonPackageComponents.of(
modules.build(),
/* resources */ ImmutableMap.of(),
/* nativeLibraries */ ImmutableMap.of(),
/* prebuiltLibraries */ ImmutableSet.of(),
/* zipSafe */ args.getZipSafe());
// Extract the platforms from the flavor, falling back to the default platforms if none are
// found.
PythonPlatform pythonPlatform =
pythonPlatforms
.getValue(params.getBuildTarget())
.orElse(
pythonPlatforms.getValue(
args.getPlatform()
.<Flavor>map(InternalFlavor::of)
.orElse(pythonPlatforms.getFlavors().iterator().next())));
CxxPlatform cxxPlatform = getCxxPlatform(params.getBuildTarget(), args);
PythonPackageComponents allPackageComponents =
PythonUtil.getAllComponents(
params,
resolver,
ruleFinder,
PythonUtil.getDeps(pythonPlatform, cxxPlatform, args.getDeps(), args.getPlatformDeps())
.stream()
.map(resolver::getRule)
.collect(MoreCollectors.toImmutableList()),
binaryPackageComponents,
pythonPlatform,
cxxBuckConfig,
cxxPlatform,
args.getLinkerFlags()
.stream()
.map(
MacroArg.toMacroArgFunction(
PythonUtil.MACRO_HANDLER, params.getBuildTarget(), cellRoots, resolver)
::apply)
.collect(MoreCollectors.toImmutableList()),
pythonBuckConfig.getNativeLinkStrategy(),
args.getPreloadDeps());
return createPackageRule(
params,
resolver,
ruleFinder,
pythonPlatform,
cxxPlatform,
mainModule,
args.getExtension(),
allPackageComponents,
args.getBuildArgs(),
args.getPackageStyle().orElse(pythonBuckConfig.getPackageStyle()),
PythonUtil.getPreloadNames(resolver, cxxPlatform, args.getPreloadDeps()));
}
@Override
public void findDepsForTargetFromConstructorArgs(
BuildTarget buildTarget,
CellPathResolver cellRoots,
AbstractPythonBinaryDescriptionArg constructorArg,
ImmutableCollection.Builder<BuildTarget> extraDepsBuilder,
ImmutableCollection.Builder<BuildTarget> targetGraphOnlyDepsBuilder) {
// We need to use the C/C++ linker for native libs handling, so add in the C/C++ linker to
// parse time deps.
extraDepsBuilder.addAll(getCxxPlatform(buildTarget, constructorArg).getLd().getParseTimeDeps());
if (constructorArg.getPackageStyle().orElse(pythonBuckConfig.getPackageStyle())
== PythonBuckConfig.PackageStyle.STANDALONE) {
extraDepsBuilder.addAll(OptionalCompat.asSet(pythonBuckConfig.getPexTarget()));
extraDepsBuilder.addAll(OptionalCompat.asSet(pythonBuckConfig.getPexExecutorTarget()));
}
}
@Override
public boolean isVersionRoot(ImmutableSet<Flavor> flavors) {
return true;
}
@BuckStyleImmutable
@Value.Immutable
interface AbstractPythonBinaryDescriptionArg
extends CommonDescriptionArg, HasDeclaredDeps, HasTests {
Optional<SourcePath> getMain();
Optional<String> getMainModule();
@Value.Default
default PatternMatchedCollection<ImmutableSortedSet<BuildTarget>> getPlatformDeps() {
return PatternMatchedCollection.of();
}
Optional<String> getBaseModule();
Optional<Boolean> getZipSafe();
ImmutableList<String> getBuildArgs();
Optional<String> getPlatform();
Optional<Flavor> getCxxPlatform();
Optional<PythonBuckConfig.PackageStyle> getPackageStyle();
ImmutableSet<BuildTarget> getPreloadDeps();
ImmutableList<String> getLinkerFlags();
Optional<String> getExtension();
Optional<String> getVersionUniverse();
}
}