/*
* 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.cli.BuckConfig;
import com.facebook.buck.cxx.NativeLinkStrategy;
import com.facebook.buck.io.ExecutableFinder;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuckVersion;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.InternalFlavor;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.CommandTool;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.Tool;
import com.facebook.buck.rules.VersionedTool;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.PackagedResource;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.ProcessExecutorParams;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.Optional;
import javax.annotation.Nonnull;
public class PythonBuckConfig {
public static final Flavor DEFAULT_PYTHON_PLATFORM = InternalFlavor.of("py-default");
private static final String SECTION = "python";
private static final String PYTHON_PLATFORM_SECTION_PREFIX = "python#";
// Prefer "python2" where available (Linux), but fall back to "python" (Mac).
private static final ImmutableList<String> PYTHON_INTERPRETER_NAMES =
ImmutableList.of("python2", "python");
private static final Path DEFAULT_PATH_TO_PEX =
Paths.get(System.getProperty("buck.path_to_pex", "src/com/facebook/buck/python/make_pex.py"))
.toAbsolutePath();
private static final LoadingCache<ProjectFilesystem, PathSourcePath> PATH_TO_TEST_MAIN =
CacheBuilder.newBuilder()
.build(
new CacheLoader<ProjectFilesystem, PathSourcePath>() {
@Override
public PathSourcePath load(@Nonnull ProjectFilesystem filesystem) {
return new PathSourcePath(
filesystem,
PythonBuckConfig.class + "/__test_main__.py",
new PackagedResource(filesystem, PythonBuckConfig.class, "__test_main__.py"));
}
});
private final BuckConfig delegate;
private final ExecutableFinder exeFinder;
public PythonBuckConfig(BuckConfig config, ExecutableFinder exeFinder) {
this.delegate = config;
this.exeFinder = exeFinder;
}
@VisibleForTesting
protected PythonPlatform getDefaultPythonPlatform(ProcessExecutor executor)
throws InterruptedException {
return getPythonPlatform(
executor,
DEFAULT_PYTHON_PLATFORM,
delegate.getValue(SECTION, "interpreter"),
delegate.getBuildTarget(SECTION, "library"));
}
/**
* Constructs set of Python platform flavors given in a .buckconfig file, as is specified by
* section names of the form python#{flavor name}.
*/
public ImmutableList<PythonPlatform> getPythonPlatforms(ProcessExecutor processExecutor)
throws InterruptedException {
ImmutableList.Builder<PythonPlatform> builder = ImmutableList.builder();
// Add the python platform described in the top-level section first.
builder.add(getDefaultPythonPlatform(processExecutor));
// Then add all additional python platform described in the extended sections.
for (String section : delegate.getSections()) {
if (section.startsWith(PYTHON_PLATFORM_SECTION_PREFIX)) {
builder.add(
getPythonPlatform(
processExecutor,
InternalFlavor.of(section.substring(PYTHON_PLATFORM_SECTION_PREFIX.length())),
delegate.getValue(section, "interpreter"),
delegate.getBuildTarget(section, "library")));
}
}
return builder.build();
}
private PythonPlatform getPythonPlatform(
ProcessExecutor processExecutor,
Flavor flavor,
Optional<String> interpreter,
Optional<BuildTarget> library)
throws InterruptedException {
return PythonPlatform.of(flavor, getPythonEnvironment(processExecutor, interpreter), library);
}
/** @return true if file is executable and not a directory. */
private boolean isExecutableFile(File file) {
return file.canExecute() && !file.isDirectory();
}
/**
* Returns the path to python interpreter. If python is specified in 'interpreter' key of the
* 'python' section that is used and an error reported if invalid.
*
* @return The found python interpreter.
*/
public String getPythonInterpreter(Optional<String> configPath) {
ImmutableList<String> pythonInterpreterNames = PYTHON_INTERPRETER_NAMES;
if (configPath.isPresent()) {
// Python path in config. Use it or report error if invalid.
File python = new File(configPath.get());
if (isExecutableFile(python)) {
return python.getAbsolutePath();
}
if (python.isAbsolute()) {
throw new HumanReadableException("Not a python executable: " + configPath.get());
}
pythonInterpreterNames = ImmutableList.of(configPath.get());
}
for (String interpreterName : pythonInterpreterNames) {
Optional<Path> python =
exeFinder.getOptionalExecutable(Paths.get(interpreterName), delegate.getEnvironment());
if (python.isPresent()) {
return python.get().toAbsolutePath().toString();
}
}
if (configPath.isPresent()) {
throw new HumanReadableException("Not a python executable: " + configPath.get());
} else {
throw new HumanReadableException("No python2 or python found.");
}
}
public String getPythonInterpreter() {
Optional<String> configPath = delegate.getValue(SECTION, "interpreter");
return getPythonInterpreter(configPath);
}
public PythonEnvironment getPythonEnvironment(
ProcessExecutor processExecutor, Optional<String> configPath) throws InterruptedException {
Path pythonPath = Paths.get(getPythonInterpreter(configPath));
PythonVersion pythonVersion = getPythonVersion(processExecutor, pythonPath);
return new PythonEnvironment(pythonPath, pythonVersion);
}
public PythonEnvironment getPythonEnvironment(ProcessExecutor processExecutor)
throws InterruptedException {
Optional<String> configPath = delegate.getValue(SECTION, "interpreter");
return getPythonEnvironment(processExecutor, configPath);
}
public SourcePath getPathToTestMain(ProjectFilesystem filesystem) {
return PATH_TO_TEST_MAIN.getUnchecked(filesystem);
}
public Optional<BuildTarget> getPexTarget() {
return delegate.getMaybeBuildTarget(SECTION, "path_to_pex");
}
public Tool getPexTool(BuildRuleResolver resolver) {
CommandTool.Builder builder = new CommandTool.Builder(getRawPexTool(resolver));
for (String flag :
Splitter.on(' ')
.omitEmptyStrings()
.split(delegate.getValue(SECTION, "pex_flags").orElse(""))) {
builder.addArg(flag);
}
return builder.build();
}
private Tool getRawPexTool(BuildRuleResolver resolver) {
Optional<Tool> executable = delegate.getTool(SECTION, "path_to_pex", resolver);
if (executable.isPresent()) {
return executable.get();
}
return VersionedTool.builder()
.setName("pex")
.setVersion(BuckVersion.getVersion())
.setPath(Paths.get(getPythonInterpreter()))
.addExtraArgs(DEFAULT_PATH_TO_PEX.toString())
.build();
}
public Optional<BuildTarget> getPexExecutorTarget() {
return delegate.getMaybeBuildTarget(SECTION, "path_to_pex_executer");
}
public Optional<Tool> getPexExecutor(BuildRuleResolver resolver) {
return delegate.getTool(SECTION, "path_to_pex_executer", resolver);
}
public NativeLinkStrategy getNativeLinkStrategy() {
return delegate
.getEnum(SECTION, "native_link_strategy", NativeLinkStrategy.class)
.orElse(NativeLinkStrategy.SEPARATE);
}
public String getPexExtension() {
return delegate.getValue(SECTION, "pex_extension").orElse(".pex");
}
private static PythonVersion getPythonVersion(ProcessExecutor processExecutor, Path pythonPath)
throws InterruptedException {
try {
// Taken from pex's interpreter.py.
String versionId =
"import sys\n"
+ "\n"
+ "if hasattr(sys, 'pypy_version_info'):\n"
+ " subversion = 'PyPy'\n"
+ "elif sys.platform.startswith('java'):\n"
+ " subversion = 'Jython'\n"
+ "else:\n"
+ " subversion = 'CPython'\n"
+ "\n"
+ "print('%s %s %s' % (subversion, sys.version_info[0], "
+ "sys.version_info[1]))\n";
ProcessExecutor.Result versionResult =
processExecutor.launchAndExecute(
ProcessExecutorParams.builder().addCommand(pythonPath.toString(), "-").build(),
EnumSet.of(
ProcessExecutor.Option.EXPECTING_STD_OUT,
ProcessExecutor.Option.EXPECTING_STD_ERR),
Optional.of(versionId),
/* timeOutMs */ Optional.empty(),
/* timeoutHandler */ Optional.empty());
return extractPythonVersion(pythonPath, versionResult);
} catch (IOException e) {
throw new HumanReadableException(
e, "Could not run \"%s - < [code]\": %s", pythonPath, e.getMessage());
}
}
@VisibleForTesting
static PythonVersion extractPythonVersion(Path pythonPath, ProcessExecutor.Result versionResult) {
if (versionResult.getExitCode() == 0) {
String versionString =
CharMatcher.whitespace()
.trimFrom(
CharMatcher.whitespace().trimFrom(versionResult.getStderr().get())
+ CharMatcher.whitespace()
.trimFrom(versionResult.getStdout().get())
.replaceAll("\u001B\\[[;\\d]*m", ""));
String[] versionLines = versionString.split("\\r?\\n");
String[] compatibilityVersion = versionLines[0].split(" ");
if (compatibilityVersion.length != 3) {
throw new HumanReadableException(
"`%s - < [code]` returned an invalid version string %s", pythonPath, versionString);
}
return PythonVersion.of(
compatibilityVersion[0], compatibilityVersion[1] + "." + compatibilityVersion[2]);
} else {
throw new HumanReadableException(versionResult.getStderr().get());
}
}
public boolean shouldCacheBinaries() {
return delegate.getBooleanValue(SECTION, "cache_binaries", true);
}
public boolean legacyOutputPath() {
return delegate.getBooleanValue(SECTION, "legacy_output_path", false);
}
public PackageStyle getPackageStyle() {
return delegate
.getEnum(SECTION, "package_style", PackageStyle.class)
.orElse(PackageStyle.STANDALONE);
}
public enum PackageStyle {
STANDALONE,
INPLACE,
}
}