From 23a2a14f50c0fefb99d42ef3e6e74edea99074c3 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 20 Jun 2023 13:18:48 -0400 Subject: [PATCH] Add support for PyPy distributions (#41) --- Cargo.toml | 3 + build.rs | 182 +++++++++++++++++++++++++++--- docs/changelog.md | 9 +- docs/config.md | 26 ++++- docs/runtime.md | 2 +- src/app.rs | 20 ++++ src/commands/self_cmd/metadata.rs | 25 +--- src/commands/self_cmd/update.rs | 9 +- src/distribution.rs | 135 ++++++++++++++++++---- 9 files changed, 343 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f0826b7..2cbed45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,9 @@ passthrough = [ "PYAPP_DISTRIBUTION_EMBED", "PYAPP_DISTRIBUTION_FORMAT", "PYAPP_DISTRIBUTION_PATH", + "PYAPP_DISTRIBUTION_PIP_AVAILABLE", "PYAPP_DISTRIBUTION_PYTHON_PATH", + "PYAPP_DISTRIBUTION_SITE_PACKAGES_PATH", "PYAPP_DISTRIBUTION_SOURCE", "PYAPP_DISTRIBUTION_VARIANT", "PYAPP_EXEC_CODE", @@ -64,4 +66,5 @@ passthrough = [ "PYAPP_PYTHON_VERSION", "PYAPP_SELF_COMMAND", "PYAPP_SKIP_INSTALL", + "PYAPP_UPGRADE_VIRTUALENV", ] diff --git a/build.rs b/build.rs index ae74075..785fec5 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,9 @@ use regex::Regex; const DEFAULT_PYTHON_VERSION: &str = "3.11"; const KNOWN_DISTRIBUTION_FORMATS: &[&str] = &["tar|bzip2", "tar|gzip", "tar|zstd", "zip"]; +const DEFAULT_CPYTHON_SOURCE: &str = + "https://github.com/indygreg/python-build-standalone/releases/download/"; +const DEFAULT_PYPY_SOURCE: &str = "https://downloads.python.org/pypy/"; // Python version in the form MAJOR.MINOR // Target OS https://doc.rust-lang.org/reference/conditional-compilation.html#target_os @@ -154,6 +157,41 @@ const DEFAULT_CPYTHON_DISTRIBUTIONS: &[(&str, &str, &str, &str, &str, &str)] = & ("3.7", "macos", "x86_64", "", "", "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.7.9-x86_64-apple-darwin-pgo-20200823T2228.tar.zst"), ]; +// See https://downloads.python.org/pypy/ +#[rustfmt::skip] +const DEFAULT_PYPY_DISTRIBUTIONS: &[(&str, &str, &str, &str, &str)] = &[ + ("pypy3.10", "linux", "aarch64", "gnu", + "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2"), + ("pypy3.10", "linux", "x86_64", "gnu", + "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2"), + ("pypy3.10", "windows", "x86_64", "msvc", + "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip"), + ("pypy3.10", "macos", "aarch64", "", + "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2"), + ("pypy3.10", "macos", "x86_64", "", + "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2"), + ("pypy3.9", "linux", "aarch64", "gnu", + "https://downloads.python.org/pypy/pypy3.9-v7.3.12-aarch64.tar.bz2"), + ("pypy3.9", "linux", "x86_64", "gnu", + "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux64.tar.bz2"), + ("pypy3.9", "windows", "x86_64", "msvc", + "https://downloads.python.org/pypy/pypy3.9-v7.3.12-win64.zip"), + ("pypy3.9", "macos", "aarch64", "", + "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_arm64.tar.bz2"), + ("pypy3.9", "macos", "x86_64", "", + "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_x86_64.tar.bz2"), + ("pypy2.7", "linux", "aarch64", "gnu", + "https://downloads.python.org/pypy/pypy2.7-v7.3.12-aarch64.tar.bz2"), + ("pypy2.7", "linux", "x86_64", "gnu", + "https://downloads.python.org/pypy/pypy2.7-v7.3.12-linux64.tar.bz2"), + ("pypy2.7", "windows", "x86_64", "msvc", + "https://downloads.python.org/pypy/pypy2.7-v7.3.12-win64.zip"), + ("pypy2.7", "macos", "aarch64", "", + "https://downloads.python.org/pypy/pypy2.7-v7.3.12-macos_arm64.tar.bz2"), + ("pypy2.7", "macos", "x86_64", "", + "https://downloads.python.org/pypy/pypy2.7-v7.3.12-macos_x86_64.tar.bz2"), +]; + fn set_runtime_variable(name: &str, value: impl Display) { println!("cargo:rustc-env={}={}", name, value) } @@ -250,8 +288,7 @@ fn get_distribution_source() -> String { abi }; - for (python_version, platform, arch, abi, variant, url) in DEFAULT_CPYTHON_DISTRIBUTIONS.iter() - { + for (python_version, platform, arch, abi, variant, url) in DEFAULT_CPYTHON_DISTRIBUTIONS { if python_version == &selected_python_version && platform == &selected_platform && arch == &selected_arch @@ -262,6 +299,16 @@ fn get_distribution_source() -> String { } } + for (python_version, platform, arch, abi, url) in DEFAULT_PYPY_DISTRIBUTIONS { + if python_version == &selected_python_version + && platform == &selected_platform + && arch == &selected_arch + && abi == &selected_abi + { + return url.to_string(); + } + } + panic!( "\n\nNo default distribution source found\nPython version: {}\nPlatform: {}\nArchitecture: {}\nABI: {}\nVariant: {}\n\n", selected_python_version, selected_platform, selected_arch, selected_abi, selected_variant @@ -431,6 +478,18 @@ fn set_distribution() { set_distribution_format(&distribution_source); set_python_path(&distribution_source); + set_site_packages_path(&distribution_source); + set_distribution_pip_available(&distribution_source); + + let python_isolation_flag = if get_python_version() == "pypy2.7" { + // https://docs.python.org/2/using/cmdline.html#cmdoption-e + // https://docs.python.org/2/using/cmdline.html#cmdoption-s + "-sE" + } else { + // https://docs.python.org/3/using/cmdline.html#cmdoption-I + "-I" + }; + set_runtime_variable("PYAPP__PYTHON_ISOLATION_FLAG", python_isolation_flag); } fn set_distribution_format(distribution_source: &String) { @@ -462,31 +521,41 @@ fn set_python_path(distribution_source: &str) { let on_windows = env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows"; let python_path = env::var(distribution_variable).unwrap_or_default(); let relative_path = if !python_path.is_empty() { - &python_path - } else if distribution_source - .starts_with("https://github.com/indygreg/python-build-standalone/releases/download/") - { + python_path + } else if distribution_source.starts_with(DEFAULT_CPYTHON_SOURCE) { if get_python_version() == "3.7" { if on_windows { - r"python\install\python.exe" + r"python\install\python.exe".to_string() } else { - "python/install/bin/python3" + "python/install/bin/python3".to_string() } } else if on_windows { - r"python\python.exe" + r"python\python.exe".to_string() + } else { + "python/bin/python3".to_string() + } + } else if distribution_source.starts_with(DEFAULT_PYPY_SOURCE) { + let directory = distribution_source + .split('/') + .last() + .unwrap() + .trim_end_matches(".tar.bz2") + .trim_end_matches(".zip"); + if on_windows { + format!(r"{}\pypy.exe", directory) } else { - "python/bin/python3" + format!("{}/bin/pypy3", directory) } } else if on_windows { - "python.exe" + "python.exe".to_string() } else { - "bin/python3" + "bin/python3".to_string() }; - set_runtime_variable(distribution_variable, relative_path); + set_runtime_variable(distribution_variable, &relative_path); let installation_variable = "PYAPP__INSTALLATION_PYTHON_PATH"; if is_enabled("PYAPP_FULL_ISOLATION") { - set_runtime_variable(installation_variable, relative_path); + set_runtime_variable(installation_variable, &relative_path); } else if on_windows { set_runtime_variable(installation_variable, r"Scripts\python.exe"); } else { @@ -494,6 +563,81 @@ fn set_python_path(distribution_source: &str) { }; } +fn set_site_packages_path(distribution_source: &str) { + let distribution_variable = "PYAPP_DISTRIBUTION_SITE_PACKAGES_PATH"; + let on_windows = env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows"; + let python_version = get_python_version(); + let site_packages_path = env::var(distribution_variable).unwrap_or_default(); + let relative_path = if !site_packages_path.is_empty() { + site_packages_path + } else if distribution_source.starts_with(DEFAULT_CPYTHON_SOURCE) { + if python_version == "3.7" { + if on_windows { + r"python\install\Lib\site-packages".to_string() + } else { + format!("python/install/lib/python{}/site-packages", python_version) + } + } else if on_windows { + r"python\Lib\site-packages".to_string() + } else { + format!("python/lib/python{}/site-packages", python_version) + } + } else if distribution_source.starts_with(DEFAULT_PYPY_SOURCE) { + let directory = distribution_source + .split('/') + .last() + .unwrap() + .trim_end_matches(".tar.bz2") + .trim_end_matches(".zip"); + if python_version == "pypy2.7" { + if on_windows { + format!(r"{}\site-packages", directory) + } else { + format!("{}/site-packages", directory) + } + } else if on_windows { + format!(r"{}\Lib\site-packages", directory) + } else { + format!("{}/lib/{}/site-packages", directory, python_version) + } + } else if on_windows { + r"Lib\site-packages".to_string() + } else { + format!("lib/python{}/site-packages", python_version) + }; + set_runtime_variable(distribution_variable, &relative_path); + + let installation_variable = "PYAPP__INSTALLATION_SITE_PACKAGES_PATH"; + if is_enabled("PYAPP_FULL_ISOLATION") { + set_runtime_variable(installation_variable, &relative_path); + } else if get_python_version() == "pypy2.7" { + set_runtime_variable(installation_variable, "site-packages"); + } else if on_windows { + set_runtime_variable(installation_variable, r"Lib\site-packages"); + } else { + set_runtime_variable( + installation_variable, + format!("lib/python{}/site-packages", python_version), + ); + }; +} + +fn set_distribution_pip_available(distribution_source: &str) { + let variable = "PYAPP_DISTRIBUTION_PIP_AVAILABLE"; + if is_enabled(variable) + // Enable if a default source is used and known to have pip installed already + || (!distribution_source.is_empty() + && !distribution_source.starts_with(DEFAULT_PYPY_SOURCE) + && env::var("PYAPP_DISTRIBUTION_SOURCE") + .unwrap_or_default() + .is_empty()) + { + set_runtime_variable(variable, "1"); + } else { + set_runtime_variable(variable, "0"); + } +} + fn set_execution_mode() { let module_variable = "PYAPP_EXEC_MODULE"; let module = env::var(module_variable).unwrap_or_default(); @@ -544,6 +688,15 @@ fn set_isolation_mode() { } } +fn set_upgrade_virtualenv() { + let variable = "PYAPP_UPGRADE_VIRTUALENV"; + if is_enabled(variable) || get_python_version() == "pypy2.7" { + set_runtime_variable(variable, "1"); + } else { + set_runtime_variable(variable, "0"); + } +} + fn set_pip_external() { let variable = "PYAPP_PIP_EXTERNAL"; if is_enabled(variable) { @@ -655,6 +808,7 @@ fn main() { set_distribution(); set_execution_mode(); set_isolation_mode(); + set_upgrade_virtualenv(); set_pip_external(); set_pip_version(); set_pip_extra_args(); diff --git a/docs/changelog.md b/docs/changelog.md index 9ac2295..d46f30f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,14 +8,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +***Changed:*** + +- Custom distributions should now define the relative path to the `site-packages` directory + ***Added:*** -- Add support for distributions with `bzip2` compression +- Add support for PyPy distributions +- Add the `PYAPP_UPGRADE_VIRTUALENV` option to create virtual environments with `virtualenv` rather than the stdlib's `venv` +- Add support for custom distributions with `bzip2` compression ***Fixed:*** - Properly handle cases where temporary files are on different filesystems - Fix regression in the `metadata` management command on Windows +- Improve error messages when running binaries that were misconfigured ## 0.8.0 - 2023-06-09 diff --git a/docs/config.md b/docs/config.md index a8823ee..6d61869 100644 --- a/docs/config.md +++ b/docs/config.md @@ -68,6 +68,16 @@ Some distributions have [variants](https://gregoryszorc.com/docs/python-build-st | Linux | | | Windows | | +#### PyPy + +| ID | +| --- | +| `pypy2.7` | +| `pypy3.9` | +| `pypy3.10` | + +The source of distributions is the [PyPy](https://www.pypy.org) project. + ### Custom You may explicitly set the `PYAPP_DISTRIBUTION_SOURCE` option which overrides the [known](#known) distribution settings. The source must be a URL that points to an archived version of the desired Python distribution. @@ -89,6 +99,14 @@ The following formats are supported for the `PYAPP_DISTRIBUTION_FORMAT` option, You may set the relative path to the Python executable after unpacking the archive with the `PYAPP_DISTRIBUTION_PYTHON_PATH` option. The default is `python.exe` on Windows and `bin/python3` on all other platforms. +#### Site packages location + +You may set the relative path to the [`site-packages`](https://docs.python.org/3/library/site.html) directory after unpacking the archive with the `PYAPP_DISTRIBUTION_SITE_PACKAGES_PATH` option. The default is `Lib\site-packages` on Windows and `lib/python/site-packages` on all other platforms where `` is the [distribution ID](#known) is defined. + +#### pip availability + +You may indicate whether pip is already installed by setting the `PYAPP_DISTRIBUTION_PIP_AVAILABLE` option to `true` or `1`. This elides the check for installation when [upgraded virtual environments](#virtual-environments) are enabled. + ### Embedding ### {: #distribution-embedding } You may set the `PYAPP_DISTRIBUTION_EMBED` option to `true` or `1` to embed the distribution in the executable at build time to avoid fetching it at runtime. When distribution embedding is enabled, you can set the `PYAPP_DISTRIBUTION_PATH` option to use a local path rather than fetching the source. @@ -104,7 +122,7 @@ You may set the `PYAPP_PIP_EXTERNAL` option to `true` or `1` to use the [standal By default, the latest version is used. You may use a specific `X.Y.Z` version by setting the `PYAPP_PIP_VERSION` option. !!! tip - This provides a significant installation speed up when [full isolation](#isolation) is not enabled. + This provides a significant installation speed up when [full isolation](#full-isolation) is not enabled. ### Extra arguments @@ -114,10 +132,14 @@ You may set the `PYAPP_PIP_EXTRA_ARGS` option to provide extra arguments to the You may set the `PYAPP_PIP_ALLOW_CONFIG` option to `true` or `1` to allow the use of environment variables and other configuration at runtime. -## Isolation +## Full isolation You may set the `PYAPP_FULL_ISOLATION` option to `true` or `1` to provide each installation with a full copy of the distribution rather than a virtual environment. +## Virtual environments + +When [full isolation](#full-isolation) is not enabled, you may set the `PYAPP_UPGRADE_VIRTUALENV` option to `true` or `1` to create virtual environments with [virtualenv](https://github.com/pypa/virtualenv) rather than the standard library's `venv` module. + ## Skipping project installation You may set the `PYAPP_SKIP_INSTALL` option to `true` or `1` to skip installing the project in the distribution. This allows for entirely predefined distributions and thus no network calls at runtime if used in conjunction with [distribution embedding](#distribution-embedding). diff --git a/docs/runtime.md b/docs/runtime.md index 747f967..caa7962 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -42,7 +42,7 @@ flowchart TD MNGCMD -- No --> EXECUTE MNGCMD -- Yes --> MANAGE[[Run management command]] click DISTEMBEDDED href "../config/#distribution-embedding" - click FULLISOLATION href "../config/#isolation" + click FULLISOLATION href "../config/#full-isolation" click EXTERNALPIP href "../config/#externally-managed" click PROJEMBEDDED href "../config/#project-embedding" click DEPFILE href "../config/#dependency-file" diff --git a/src/app.rs b/src/app.rs index 0ea9096..0eef33e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -37,6 +37,10 @@ fn installation_python_path() -> String { env!("PYAPP__INSTALLATION_PYTHON_PATH").into() } +fn installation_site_packages_path() -> String { + env!("PYAPP__INSTALLATION_SITE_PACKAGES_PATH").into() +} + pub fn exposed_command() -> String { env!("PYAPP__EXPOSED_COMMAND").into() } @@ -45,6 +49,10 @@ pub fn distribution_id() -> String { env!("PYAPP__DISTRIBUTION_ID").into() } +pub fn python_isolation_flag() -> String { + env!("PYAPP__PYTHON_ISOLATION_FLAG").into() +} + pub fn distribution_source() -> String { env!("PYAPP_DISTRIBUTION_SOURCE").into() } @@ -57,6 +65,10 @@ pub fn distribution_python_path() -> String { env!("PYAPP_DISTRIBUTION_PYTHON_PATH").into() } +pub fn distribution_pip_available() -> bool { + env!("PYAPP_DISTRIBUTION_PIP_AVAILABLE") == "1" +} + pub fn project_name() -> String { env!("PYAPP_PROJECT_NAME").into() } @@ -105,6 +117,10 @@ pub fn full_isolation() -> bool { env!("PYAPP_FULL_ISOLATION") == "1" } +pub fn upgrade_virtualenv() -> bool { + env!("PYAPP_UPGRADE_VIRTUALENV") == "1" +} + pub fn skip_install() -> bool { env!("PYAPP_SKIP_INSTALL") == "1" } @@ -121,6 +137,10 @@ pub fn python_path(installation_directory: &Path) -> PathBuf { installation_directory.join(installation_python_path()) } +pub fn site_packages_path(installation_directory: &Path) -> PathBuf { + installation_directory.join(installation_site_packages_path()) +} + pub fn cache_directory() -> PathBuf { platform_dirs().cache_dir().to_path_buf() } diff --git a/src/commands/self_cmd/metadata.rs b/src/commands/self_cmd/metadata.rs index d73cae6..06cf4b4 100644 --- a/src/commands/self_cmd/metadata.rs +++ b/src/commands/self_cmd/metadata.rs @@ -17,30 +17,7 @@ impl Cli { return Ok(()); } - let python = app::python_path(&installation_directory); - let site_packages = if cfg!(windows) { - (|| Some(python.parent()?.parent()?.join("Lib").join("site-packages")))() - } else { - (|| { - let lib_dir = python.parent()?.parent()?.join("lib"); - let version_dir = - fs::read_dir(lib_dir) - .ok()? - .filter_map(Result::ok) - .find(|entry| { - let file_name = entry.file_name().to_string_lossy().to_string(); - file_name.starts_with("python") || file_name.starts_with("pypy") - })?; - - Some(version_dir.path().join("site-packages")) - })() - }; - - let site_packages = if let Some(site_packages) = site_packages.filter(|p| p.is_dir()) { - site_packages - } else { - return Ok(()); - }; + let site_packages = app::site_packages_path(&installation_directory); let expected_prefix = format!("{}-", app::project_name().replace('-', "_")); let metadata_file = fs::read_dir(site_packages).ok().and_then(|entries| { diff --git a/src/commands/self_cmd/update.rs b/src/commands/self_cmd/update.rs index 87e7956..e9cd9f4 100644 --- a/src/commands/self_cmd/update.rs +++ b/src/commands/self_cmd/update.rs @@ -48,9 +48,14 @@ impl Cli { let dependency_file = app::project_dependency_file(); let (status, output) = if dependency_file.is_empty() { command.arg(app::project_name().as_str()); - distribution::pip_install(command, wait_message)? + distribution::pip_install(command, wait_message, &installation_directory)? } else { - distribution::pip_install_dependency_file(&dependency_file, command, wait_message)? + distribution::pip_install_dependency_file( + &dependency_file, + command, + wait_message, + &installation_directory, + )? }; if !status.success() { diff --git a/src/distribution.rs b/src/distribution.rs index 199d155..d03acd1 100644 --- a/src/distribution.rs +++ b/src/distribution.rs @@ -11,9 +11,7 @@ use crate::{app, compression, fs_utils, network, process}; pub fn python_command(python: &PathBuf) -> Command { let mut command = Command::new(python); - - // https://docs.python.org/3/using/cmdline.html#cmdoption-I - command.arg("-I"); + command.arg(app::python_isolation_flag()); command } @@ -140,6 +138,10 @@ pub fn materialize(installation_directory: &PathBuf) -> Result<()> { err ); })?; + + if !app::skip_install() { + ensure_base_pip(installation_directory, installation_directory)?; + } } else { let unpacked_distribution = distributions_dir.join(format!("_{}", app::distribution_id())); if !unpacked_distribution.is_dir() { @@ -160,18 +162,53 @@ pub fn materialize(installation_directory: &PathBuf) -> Result<()> { let mut command = python_command(&unpacked_distribution.join(app::distribution_python_path())); - command.args(["-m", "venv"]); - if app::pip_external() { - command.arg("--without-pip"); + + if app::upgrade_virtualenv() { + ensure_base_pip(&unpacked_distribution, installation_directory)?; + + let mut upgrade_command = + python_command(&unpacked_distribution.join(app::distribution_python_path())); + upgrade_command.args([ + "-m", + "pip", + "install", + "--upgrade", + "--isolated", + "--disable-pip-version-check", + "--no-warn-script-location", + "virtualenv", + ]); + let (status, output) = run_setup_command( + upgrade_command, + "Upgrading virtualenv".to_string(), + installation_directory, + )?; + check_setup_status(status, output, installation_directory)?; + + command.args(["-m", "virtualenv"]); + if app::pip_external() { + command.arg("--no-pip"); + } + } else { + command.args(["-m", "venv"]); + if app::pip_external() { + command.arg("--without-pip"); + } } + command.arg(installation_directory.to_string_lossy().as_ref()); - process::wait_for(command, "Creating virtual environment".to_string())?; + let (status, output) = run_setup_command( + command, + "Creating virtual environment".to_string(), + installation_directory, + )?; + check_setup_status(status, output, installation_directory)?; } Ok(()) } -fn install_project(installation_directory: &PathBuf) -> Result<()> { +fn install_project(installation_directory: &Path) -> Result<()> { let install_target = format!("{} {}", app::project_name(), app::project_version()); let binary_only = app::pip_extra_args().contains("--only-binary :all:") || app::pip_extra_args().contains("--only-binary=:all:"); @@ -199,7 +236,7 @@ fn install_project(installation_directory: &PathBuf) -> Result<()> { } else { format!("Installing {}", install_target) }; - pip_install(command, wait_message)? + pip_install(command, wait_message, installation_directory) } else { let wait_message = if binary_only { format!("Unpacking {}", install_target) @@ -210,30 +247,35 @@ fn install_project(installation_directory: &PathBuf) -> Result<()> { let dependency_file = app::project_dependency_file(); if dependency_file.is_empty() { command.arg(format!("{}=={}", app::project_name(), app::project_version()).as_str()); - pip_install(command, wait_message)? + pip_install(command, wait_message, installation_directory) } else { - pip_install_dependency_file(&dependency_file, command, wait_message)? + pip_install_dependency_file( + &dependency_file, + command, + wait_message, + installation_directory, + ) } - }; - - if !status.success() { - fs::remove_dir_all(installation_directory).ok(); - println!("{}", output.trim_end()); - exit(status.code().unwrap_or(1)); - } + }?; + check_setup_status(status, output, installation_directory)?; Ok(()) } -pub fn pip_install(command: Command, wait_message: String) -> Result<(ExitStatus, String)> { - ensure_pip()?; - process::wait_for(command, wait_message) +pub fn pip_install( + command: Command, + wait_message: String, + installation_directory: &Path, +) -> Result<(ExitStatus, String)> { + ensure_external_pip()?; + run_setup_command(command, wait_message, installation_directory) } pub fn pip_install_dependency_file( dependency_file: &String, mut command: Command, wait_message: String, + installation_directory: &Path, ) -> Result<(ExitStatus, String)> { let dir = tempdir().with_context(|| "unable to create temporary directory")?; let file_name = app::project_dependency_file_name(); @@ -250,11 +292,27 @@ pub fn pip_install_dependency_file( command.args(["-r", temp_path.to_string_lossy().as_ref()]); - ensure_pip()?; - process::wait_for(command, wait_message) + ensure_external_pip()?; + run_setup_command(command, wait_message, installation_directory) +} + +fn ensure_base_pip(distribution_directory: &Path, installation_directory: &Path) -> Result<()> { + if app::distribution_pip_available() { + return Ok(()); + } + + let mut command = python_command(&distribution_directory.join(app::distribution_python_path())); + command.args(["-m", "ensurepip"]); + + run_setup_command( + command, + "Validating pip".to_string(), + installation_directory, + )?; + Ok(()) } -fn ensure_pip() -> Result<()> { +fn ensure_external_pip() -> Result<()> { if !app::pip_external() { return Ok(()); } @@ -296,3 +354,32 @@ fn ensure_pip() -> Result<()> { fs_utils::move_temp_file(&temp_path, &external_pip) } + +fn run_setup_command( + command: Command, + message: String, + installation_directory: &Path, +) -> Result<(ExitStatus, String)> { + let (status, output) = process::wait_for(command, message).with_context(|| { + format!( + "could not run Python, verify distribution build metadata options: {}", + app::python_path(installation_directory).display() + ) + })?; + + Ok((status, output)) +} + +fn check_setup_status( + status: ExitStatus, + output: String, + installation_directory: &Path, +) -> Result<()> { + if !status.success() { + fs::remove_dir_all(installation_directory).ok(); + println!("{}", output.trim_end()); + exit(status.code().unwrap_or(1)); + } + + Ok(()) +}