Skip to content

Commit

Permalink
Merge pull request #292 from zvezdan/wheel-builder
Browse files Browse the repository at this point in the history
Add Wheel Builder action class and tests.
  • Loading branch information
zvezdan authored Apr 13, 2019
2 parents d06823c + 14b7b8a commit 97fa6bb
Show file tree
Hide file tree
Showing 7 changed files with 831 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public File findExecutable(String name) {
return findExecutable(prefixBuilder(), name);
}

static File findExecutable(Path path, String name) {
public static File findExecutable(Path path, String name) {
return path.resolve(OperatingSystem.current().getExecutableName(name)).toFile();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;


public class PipInstallAction extends AbstractPipAction {

private static Logger logger = Logging.getLogger(PipInstallAction.class);

private final Path sitePackagesPath;
private final WheelBuilder wheelBuilder;

public PipInstallAction(PackageSettings<PackageInfo> packageSettings,
Project project,
Expand All @@ -54,8 +55,14 @@ public PipInstallAction(PackageSettings<PackageInfo> packageSettings,
WheelCache wheelCache, EnvironmentMerger environmentMerger,
Spec<PackageInfo> packageExcludeFilter) {
super(packageSettings, project, externalExec, baseEnvironment, pythonDetails, wheelCache,
environmentMerger, packageExcludeFilter);
environmentMerger, packageExcludeFilter);
this.sitePackagesPath = findSitePackages(pythonDetails);
this.wheelBuilder = new WheelBuilder(packageSettings, project, externalExec, baseEnvironment, pythonDetails,
wheelCache, environmentMerger, packageExcludeFilter);
}

public Path getSitePackagesPath() {
return sitePackagesPath;
}

private static Path findSitePackages(PythonDetails pythonDetails) {
Expand Down Expand Up @@ -121,51 +128,13 @@ void doPipOperation(PackageInfo packageInfo, List<String> extraArgs) {
}
}

private List<String> prepareCommandLine(PackageInfo packageInfo, List<String> extraArgs) {
private List<String> makeCommandLine(PackageInfo packageInfo, List<String> extraArgs) {
List<String> commandLine = new ArrayList<>();
commandLine.addAll(baseInstallArguments());
commandLine.addAll(extraArgs);
commandLine.addAll(getGlobalOptions(packageInfo));
commandLine.addAll(getInstallOptions(packageInfo));

return commandLine;
}

private boolean appendCachedWheel(PackageInfo packageInfo, Optional<File> cachedWheel, List<String> commandLine) {
if (!packageSettings.requiresSourceBuild(packageInfo)) {
// TODO: Check whether project layer cache exists.

if (!cachedWheel.isPresent() && !packageSettings.isCustomized(packageInfo)) {
cachedWheel = wheelCache.findWheel(packageInfo.getName(), packageInfo.getVersion(), pythonDetails);
}
}

if (cachedWheel.isPresent()) {
if (PythonHelpers.isPlainOrVerbose(project)) {
logger.lifecycle("{} from wheel: {}", packageInfo.toShortHand(), cachedWheel.get().getAbsolutePath());
}
commandLine.add(cachedWheel.get().getAbsolutePath());
return true;
}

return false;
}

private List<String> makeCommandLine(PackageInfo packageInfo, List<String> extraArgs) {
List<String> commandLine = prepareCommandLine(packageInfo, extraArgs);
Optional<File> cachedWheel = Optional.empty();
boolean allowBuildingFromSdist = false;

while (!appendCachedWheel(packageInfo, cachedWheel, commandLine)) {
if (allowBuildingFromSdist) {
commandLine.add(packageInfo.getPackageFile().getAbsolutePath());
break;
} else {
// TODO: Make wheel from sdist, store to local cache and global cache if needed.
}

allowBuildingFromSdist = true;
}
commandLine.add(wheelBuilder.getPackage(packageInfo, extraArgs).toString());

return commandLine;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
/*
* Copyright 2016 LinkedIn Corp.
*
* 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.linkedin.gradle.python.tasks.action.pip;

import com.linkedin.gradle.python.exception.PipExecutionException;
import com.linkedin.gradle.python.extension.PythonDetails;
import com.linkedin.gradle.python.plugin.PythonHelpers;
import com.linkedin.gradle.python.tasks.exec.ExternalExec;
import com.linkedin.gradle.python.util.EnvironmentMerger;
import com.linkedin.gradle.python.util.PackageInfo;
import com.linkedin.gradle.python.util.PackageSettings;
import com.linkedin.gradle.python.wheel.WheelCache;
import com.linkedin.gradle.python.wheel.WheelCacheLayer;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.specs.Spec;
import org.gradle.process.ExecResult;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;


/**
* Action class that handles wheel building and fetching from cache.
*
* This class is used from PipInstallAction and does not perform the
* checks already done in that class. It does nicely separate the concern
* of wheel building and finding from PipInstallAction and ensures
* it returns back the package file in some form while leaving
* the wheel in at least one cache layer.
*/
public class WheelBuilder extends AbstractPipAction {
// Options for "pip install" that do not work with "pip wheel" command.
private static final List<String> NOT_WHEEL_OPTIONS = Arrays.asList("--upgrade", "--ignore-installed");

// Environment variables used for a specific package only and customizing its build.
private static final Map<String, List<String>> CUSTOM_ENVIRONMENT = Collections.unmodifiableMap(Stream.of(
new AbstractMap.SimpleEntry<>("numpy", Arrays.asList("BLAS", "OPENBLAS", "ATLAS")),
new AbstractMap.SimpleEntry<>("pycurl", Collections.singletonList("PYCURL_SSL_LIBRARY"))
).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)));

private static final Logger logger = Logging.getLogger(WheelBuilder.class);

private boolean customBuild = true;

// package-private
WheelBuilder(PackageSettings<PackageInfo> packageSettings,
Project project,
ExternalExec externalExec,
Map<String, String> baseEnvironment,
PythonDetails pythonDetails,
WheelCache wheelCache,
EnvironmentMerger environmentMerger,
Spec<PackageInfo> packageExcludeFilter) {
super(packageSettings, project, externalExec, baseEnvironment,
pythonDetails, wheelCache, environmentMerger, packageExcludeFilter);
}

@Override
Logger getLogger() {
return logger;
}

/*
* Since WheelBuilder class is always called from PipInstallAction,
* and that class already performed all the filtering checks,
* we're overriding the original execute method from the abstract class
* to avoid duplicate checks. It becomes just a call to doPipOperation,
* but we have to implement that abstract method.
*/
@Override
public void execute(PackageInfo packageInfo, List<String> extraArgs) {
doPipOperation(packageInfo, extraArgs);
}

/*
* Similar to execute method above, we're skipping some checks already
* done in PipInstallAction where WheelBuilder is called from.
* For example, the check for supported versions.
* Some other checks, such as source build requirement, are moved to
* the main method of the wheel builder -- getPackage.
*/
@Override
void doPipOperation(PackageInfo packageInfo, List<String> extraArgs) {
List<String> commandLine = makeCommandLine(packageInfo, extraArgs);
if (commandLine.isEmpty()) {
return;
}

if (PythonHelpers.isPlainOrVerbose(project)) {
logger.lifecycle("Building wheel for {}", packageInfo.toShortHand());
}

Map<String, String> mergedEnv;
// The flag is set in getPackage method.
if (customBuild) {
mergedEnv = environmentMerger.mergeEnvironments(
Arrays.asList(baseEnvironment, packageSettings.getEnvironment(packageInfo)));
} else {
// Have to use this for customized environments that are explicitly marked non-customized
mergedEnv = packageSettings.getEnvironment(packageInfo);
}

OutputStream stream = new ByteArrayOutputStream();

ExecResult installResult = execCommand(mergedEnv, commandLine, stream);

if (installResult.getExitValue() == 0) {
logger.info(stream.toString().trim());
} else {
logger.error("Error building package wheel using `{}`", commandLine);
logger.error(stream.toString().trim());
throw PipExecutionException.failedWheel(packageInfo, stream.toString().trim());
}

}

// package-private
File getPackage(PackageInfo packageInfo, List<String> extraArgs) {
File packageFile = packageInfo.getPackageFile();

// Cut it short if there's no target directory in the cache.
if (!wheelCache.getTargetDirectory().isPresent()) {
return packageFile;
}

String name = packageInfo.getName();
String version = packageInfo.getVersion();
boolean isProject = isProjectDirectory(packageInfo);
Optional<File> wheel;

/*
* Current project is a directory, not a package, so version may be null.
* Compensate for that.
*/
if (isProject) {
name = project.getName();
version = project.getVersion().toString();
}

// set the flag for doPipOperation
customBuild = packageSettings.requiresSourceBuild(packageInfo)
|| packageSettings.isCustomized(packageInfo)
|| isCustomEnvironment(name);

// Look in cache layers first when applicable.
if (!packageSettings.requiresSourceBuild(packageInfo)) {
wheel = wheelCache.findWheel(name, version, pythonDetails, WheelCacheLayer.PROJECT_LAYER);
if (wheel.isPresent()) {
packageFile = wheel.get();
if (PythonHelpers.isPlainOrVerbose(project)) {
logger.lifecycle("{} from wheel: {}",
packageInfo.toShortHand(), packageFile.getAbsolutePath());
}
return packageFile;
} else if (!customBuild) {
wheel = wheelCache.findWheel(name, version, pythonDetails, WheelCacheLayer.HOST_LAYER);
if (wheel.isPresent()) {
packageFile = wheel.get();
wheelCache.storeWheel(packageFile, WheelCacheLayer.PROJECT_LAYER);
if (PythonHelpers.isPlainOrVerbose(project)) {
logger.lifecycle("{} from wheel: {}",
packageInfo.toShortHand(), packageFile.getAbsolutePath());
}
return packageFile;
}
}
}

// Build the wheel into the project layer by default.
try {
execute(packageInfo, extraArgs);
} catch (PipExecutionException e) {
if (!customBuild) {
/*
* The users may need PythonEnvironment for their wheel build.
* We must treat this as a custom build and set a flag for
* doPipOperation to merge PythonEnvironment in accordingly.
* Then retry.
*/
customBuild = true;
execute(packageInfo, extraArgs);
} else {
throw e;
}
}

wheel = wheelCache.findWheel(name, version, pythonDetails, WheelCacheLayer.PROJECT_LAYER);
if (wheel.isPresent()) {
packageFile = wheel.get();
if (!customBuild) {
wheelCache.storeWheel(packageFile, WheelCacheLayer.HOST_LAYER);
}
}

/*
* After ensuring we have a wheel built for our project,
* we still return the project directory back to PipInstallAction
* so that it can install it in editable (development) mode
* into virtualenv.
*/
if (isProject) {
return packageInfo.getPackageFile();
}

return packageFile;
}

private List<String> makeCommandLine(PackageInfo packageInfo, List<String> extraArgs) {
List<String> commandLine = new ArrayList<>();
Optional<File> targetDir = wheelCache.getTargetDirectory();

if (targetDir.isPresent()) {
String wheelDirPath = targetDir.get().toString();

commandLine.addAll(Arrays.asList(
pythonDetails.getVirtualEnvInterpreter().toString(),
pythonDetails.getVirtualEnvironment().getPip().toString(),
"wheel",
"--disable-pip-version-check",
"--wheel-dir", wheelDirPath,
"--no-deps"
));
commandLine.addAll(cleanupArgs(extraArgs));
commandLine.addAll(getGlobalOptions(packageInfo));
commandLine.addAll(getBuildOptions(packageInfo));

commandLine.add(packageInfo.getPackageFile().toString());
}

return commandLine;
}

private List<String> cleanupArgs(List<String> args) {
List<String> cleanArgs = new ArrayList<>(args);
cleanArgs.removeAll(NOT_WHEEL_OPTIONS);
return cleanArgs;
}

private boolean isProjectDirectory(PackageInfo packageInfo) {
File packageDir = packageInfo.getPackageFile();
String version = packageInfo.getVersion();
return version == null && Files.isDirectory(packageDir.toPath()) && project.getProjectDir().equals(packageDir);
}

// Use of pythonEnvironment may hide really customized packages. Catch them!
private boolean isCustomEnvironment(String name) {
if (CUSTOM_ENVIRONMENT.containsKey(name)) {
for (String entry : CUSTOM_ENVIRONMENT.get(name)) {
if (baseEnvironment.containsKey(entry)) {
return true;
}
}
}
return false;
}
}
Loading

0 comments on commit 97fa6bb

Please sign in to comment.