Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for conda envs on windows #52

Merged
merged 9 commits into from
Jan 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ matrix:
fast_finish: true

install:
- cinst miniconda3
- ps: |
# For the gnu target we need gcc, provided by mingw. mingw which is already preinstalled,
# but we need the right version (32-bit or 64-bit) to the PATH.
Expand Down
2 changes: 1 addition & 1 deletion src/build_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub struct BuildContext {
impl BuildContext {
/// Checks which kind of bindings we have (pyo3/rust-cypthon or cffi or bin) and calls the
/// correct builder. Returns a Vec that contains location, python tag (e.g. py2.py3 or cp35)
/// and for bindings the python intepreter they bind against.
/// and for bindings the python interpreter they bind against.
pub fn build_wheels(&self) -> Result<Vec<(PathBuf, String, Option<PythonInterpreter>)>, Error> {
fs::create_dir_all(&self.out)
.context("Failed to create the target directory for the wheels")?;
Expand Down
231 changes: 170 additions & 61 deletions src/python_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::Target;
use failure::{Error, Fail, ResultExt};
use regex::Regex;
use serde_json;
use std::collections::HashSet;
use std::fmt;
use std::io;
use std::path::Path;
Expand Down Expand Up @@ -29,79 +30,187 @@ print(json.dumps({
}))
"##;

/// Uses `py -0` to get a list of all installed python versions and then
/// `sys.executable` to determine the path.
/// Identifies conditions where we do not want to build wheels
fn windows_interpreter_no_build(
major: usize,
minor: usize,
target_width: usize,
pointer_width: usize,
) -> bool {
// Don't use python 2.6
if major == 2 && minor != 7 {
return true;
}

// Ignore python 3.0 - 3.4
if major == 3 && minor < 5 {
return true;
}

// There can be 32-bit installations on a 64-bit machine, but we can't link
// those for 64-bit targets
if pointer_width != target_width {
println!(
"{}.{} is installed as {}-bit, while the target is {}-bit. Skipping.",
major, minor, pointer_width, target_width
);
return true;
}
false
}

/// On windows regular Python installs are supported along with environments
/// being managed by `conda`.
///
/// We can't use the linux trick with trying different binary names since on
/// windows the binary is always called "python.exe". However, whether dealing
/// with regular Python installs or `conda` environments there are tools we can
/// use to query the information regarding installed interpreters.
///
/// Regular Python installs downloaded from Python.org will include the python
/// launcher by default. We can use the launcher to find the information we need
/// for each installed interpreter using `py -0` which produces something like
/// the following output (the path can by determined using `sys.executable`):
///
/// ```bash
/// Installed Pythons found by py Launcher for Windows
/// -3.7-64 *
/// -3.6-32
/// -2.7-64
/// ```
///
/// When using `conda` we can use the `conda info -e` command to retrieve information
/// regarding the installed interpreters being managed by `conda`. This is an example
/// of the output expected:
///
/// ```bash
/// # conda environments:
/// #
/// base C:\Users\<user-name>\Anaconda3
/// foo1 * C:\Users\<user-name>\Anaconda3\envs\foo1
/// foo2 * C:\Users\<user-name>\Anaconda3\envs\foo2
/// ```
///
/// We can't use the the linux trick with trying different binary names since
/// on windows the binary is always called "python.exe". We also have to make
/// sure that the pointer width (32-bit or 64-bit) matches across platforms
/// The information required can either by obtained by parsing this output directly or
/// by invoking the interpreters to obtain the information.
///
/// As well as the version numbers, etc. of the interpreters we also have to find the
/// pointer width to make sure that the pointer width (32-bit or 64-bit) matches across
/// platforms.
fn find_all_windows(target: &Target) -> Result<Vec<String>, Error> {
let execution = Command::new("py").arg("-0").output();
let output = execution
.context("Couldn't run 'py' command. Do you have python installed and in PATH?")?;
let expr = Regex::new(r" -(\d).(\d)-(\d+)(?: .*)?").unwrap();
let lines = str::from_utf8(&output.stdout).unwrap().lines();
let code = "import sys; print(sys.executable or '')";
let mut interpreter = vec![];
for line in lines {
if let Some(capture) = expr.captures(line) {
let code = "import sys; print(sys.executable or '')";
let context = "Expected a digit";

let major = capture
.get(1)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
let minor = capture
.get(2)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
let pointer_width = capture
.get(3)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;

// Don't use python 2.6
if major == 2 && minor != 7 {
continue;
}
let mut versions_found = HashSet::new();

// Ignore python 3.0 - 3.4
if major == 3 && minor < 5 {
continue;
// If Python is installed from Python.org it should include the "python launcher"
// which is used to find the installed interpreters
let execution = Command::new("py").arg("-0").output();
if let Ok(output) = execution {
let expr = Regex::new(r" -(\d).(\d)-(\d+)(?: .*)?").unwrap();
let lines = str::from_utf8(&output.stdout).unwrap().lines();
for line in lines {
if let Some(capture) = expr.captures(line) {
let context = "Expected a digit";

let major = capture
.get(1)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
let minor = capture
.get(2)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;
if !versions_found.contains(&(major, minor)) {
let pointer_width = capture
.get(3)
.unwrap()
.as_str()
.parse::<usize>()
.context(context)?;

if windows_interpreter_no_build(
major,
minor,
target.pointer_width(),
pointer_width,
) {
continue;
}

let version = format!("-{}.{}-{}", major, minor, pointer_width);

let output = Command::new("py")
.args(&[&version, "-c", code])
.output()
.unwrap();
let path = str::from_utf8(&output.stdout).unwrap().trim();
if !output.status.success() || path.trim().is_empty() {
bail!("Couldn't determine the path to python for `py {}`", version);
}
interpreter.push(path.to_string());
versions_found.insert((major, minor));
}
}
}
}

// There can be 32-bit installations on a 64-bit machine, but we can't link
// those for 64-bit targets
if pointer_width != target.pointer_width() {
println!(
"{}.{} is installed as {}-bit, while the target is {}-bit. Skipping.",
major,
minor,
pointer_width,
target.pointer_width()
);
continue;
// Conda environments are also supported on windows
let conda_info = Command::new("conda").arg("info").arg("-e").output();
if let Ok(output) = conda_info {
let lines = str::from_utf8(&output.stdout).unwrap().lines();
let re = Regex::new(r"(\w|\\|:|-)+$").unwrap();
let mut paths = vec![];
for i in lines {
if !i.starts_with('#') {
if let Some(capture) = re.captures(&i) {
paths.push(String::from(&capture[0]));
}
}
}

let version = format!("-{}.{}-{}", major, minor, pointer_width);

let output = Command::new("py")
.args(&[&version, "-c", code])
for path in paths {
let executable = Path::new(&path).join("python");
let python_info = Command::new(&executable)
.arg("-c")
.arg("import sys; print(sys.version)")
.output()
.unwrap();
let path = str::from_utf8(&output.stdout).unwrap().trim();
if !output.status.success() || path.trim().is_empty() {
bail!("Couldn't determine the path to python for `py {}`", version);
.expect("Error getting Python version info from conda env...");
let version_info = str::from_utf8(&python_info.stdout).unwrap();
let expr = Regex::new(r"(\d).(\d).(\d+)").unwrap();
if let Some(capture) = expr.captures(version_info) {
let major = capture.get(1).unwrap().as_str().parse::<usize>().unwrap();
let minor = capture.get(2).unwrap().as_str().parse::<usize>().unwrap();
if !versions_found.contains(&(major, minor)) {
let pointer_width = if version_info.contains("64 bit (AMD64)") {
64_usize
} else {
32_usize
};

if windows_interpreter_no_build(
major,
minor,
target.pointer_width(),
pointer_width,
) {
continue;
}

interpreter.push(String::from(executable.to_str().unwrap()));
versions_found.insert((major, minor));
}
}
interpreter.push(path.to_string());
}
}
if interpreter.is_empty() {
bail!(
"Could not find any interpreters, are you sure you have python installed on your PATH?"
);
};
Ok(interpreter)
}

Expand Down
86 changes: 85 additions & 1 deletion tests/test_integration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::common::check_installed;
use pyo3_pack::{BuildOptions, Target};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
use structopt::StructOpt;
Expand Down Expand Up @@ -31,6 +31,12 @@ fn test_integration_get_fourtytwo() {
test_integration(Path::new("get-fourtytwo"), None);
}

#[cfg(target_os = "windows")]
#[test]
fn test_integration_get_fourtytwo_conda() {
test_integration_conda(Path::new("get-fourtytwo"), None);
}

#[test]
fn test_integration_points() {
test_integration(Path::new("points"), Some("cffi".to_string()));
Expand Down Expand Up @@ -134,3 +140,81 @@ fn test_integration(package: &Path, bindings: Option<String>) {
check_installed(&package, &python).unwrap();
}
}

/// Creates conda environments
fn create_conda_env(name: &str, major: usize, minor: usize) {
Command::new("conda")
.arg("create")
.arg("-n")
.arg(name)
.arg(format!("python={}.{}", major, minor))
.arg("-q")
.arg("-y")
.output()
.expect("Conda not available.");
}

fn test_integration_conda(package: &Path, bindings: Option<String>) {
let package_string = package.join("Cargo.toml").display().to_string();

// Create environments to build against, prepended with "A" to ensure that integration
// tests are executed with these environments
create_conda_env("A-pyo3-build-env-27", 2, 7);
create_conda_env("A-pyo3-build-env-35", 3, 5);
create_conda_env("A-pyo3-build-env-36", 3, 6);
create_conda_env("A-pyo3-build-env-37", 3, 7);

// The first string is ignored by clap
let cli = if let Some(ref bindings) = bindings {
vec![
"build",
"--manifest-path",
&package_string,
"--bindings",
bindings,
]
} else {
vec!["build", "--manifest-path", &package_string]
};

let options = BuildOptions::from_iter_safe(cli).unwrap();

let wheels = options
.into_build_context(false, false)
.unwrap()
.build_wheels()
.unwrap();

let mut conda_wheels: Vec<(PathBuf, PathBuf)> = vec![];
for (filename, _, python_interpreter) in wheels {
if let Some(pi) = python_interpreter {
let executable = pi.executable;
if executable.to_str().unwrap().contains("pyo3-build-env-") {
conda_wheels.push((filename, executable))
}
}
}

assert_eq!(
4,
conda_wheels.len(),
"Error creating or detecting conda environments."
);
for (wheel_file, executable) in conda_wheels {
let output = Command::new(&executable)
.args(&[
"-m",
"pip",
"install",
"--force-reinstall",
&adjust_canonicalization(wheel_file),
])
.stderr(Stdio::inherit())
.output()
.unwrap();
if !output.status.success() {
panic!();
}
check_installed(&package, &executable).unwrap();
}
}