diff --git a/Cargo.lock b/Cargo.lock index 9157052..b852120 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.16.1" +version = "0.17.0" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index d9c2e28..b32fbc6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.16.1" +version = "0.17.0" edition = "2024" [lints.rust] diff --git a/meson.build b/meson.build index 58e1e8f..9447238 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.16.1', + version: '0.17.0', meson_version: '>=0.60.0', default_options: ['buildtype=release'], ) diff --git a/src/builder/mod.rs b/src/builder/mod.rs index ea26fe0..5a1d72f 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -7,7 +7,7 @@ mod custom; mod makefile; mod meson; mod perl; -mod python; +pub(crate) mod python; mod rust; pub mod state; diff --git a/src/builder/python.rs b/src/builder/python.rs index dd18a0c..71b53f1 100644 --- a/src/builder/python.rs +++ b/src/builder/python.rs @@ -62,19 +62,7 @@ pub fn build( fs::create_dir_all(&dist_dir) .with_context(|| format!("Failed to create dist dir: {}", dist_dir.display()))?; - match detect_frontend(&actual_src)? { - BuildFrontend::Pep517(cfg) => { - build_wheel_pep517(&actual_src, &dist_dir, &env_vars, &cfg, &config_settings)? - } - BuildFrontend::LegacySetupPy => { - if !config_settings.is_empty() { - bail!( - "build.flags.config_setting is only supported for PEP 517 builds (pyproject.toml)" - ); - } - build_wheel_setup_py(&actual_src, &dist_dir, &env_vars)?; - } - } + build_wheels(&actual_src, &dist_dir, &env_vars, &config_settings)?; hooks::run_post_compile_commands(spec, &actual_src, destdir)?; state.mark_done(BuildStep::PostCompileDone)?; @@ -84,11 +72,7 @@ pub fn build( if !state.is_done(BuildStep::PostInstallDone) { let wheels = collect_wheels(&dist_dir)?; - let py_version = detect_python_major_minor(&env_vars)?; - let prefix_rel = normalized_prefix(&flags.prefix)?; - for wheel in wheels { - install_wheel(&wheel, destdir, &prefix_rel, &py_version)?; - } + install_wheels(&wheels, destdir, &flags.prefix, &env_vars)?; hooks::run_post_install_commands(spec, &actual_src, destdir)?; state.mark_done(BuildStep::PostInstallDone)?; @@ -356,7 +340,28 @@ fn build_wheel_pep517( Ok(()) } -fn collect_wheels(dist_dir: &Path) -> Result> { +pub(crate) fn build_wheels( + src_dir: &Path, + dist_dir: &Path, + env_vars: &[(String, String)], + config_settings: &[String], +) -> Result<()> { + match detect_frontend(src_dir)? { + BuildFrontend::Pep517(cfg) => { + build_wheel_pep517(src_dir, dist_dir, env_vars, &cfg, config_settings) + } + BuildFrontend::LegacySetupPy => { + if !config_settings.is_empty() { + bail!( + "build.flags.config_setting is only supported for PEP 517 builds (pyproject.toml)" + ); + } + build_wheel_setup_py(src_dir, dist_dir, env_vars) + } + } +} + +pub(crate) fn collect_wheels(dist_dir: &Path) -> Result> { let mut wheels = Vec::new(); for entry in fs::read_dir(dist_dir).with_context(|| { format!( @@ -513,6 +518,20 @@ fn install_wheel( Ok(()) } +pub(crate) fn install_wheels( + wheel_paths: &[PathBuf], + destdir: &Path, + prefix: &str, + env_vars: &[(String, String)], +) -> Result<()> { + let py_version = detect_python_major_minor(env_vars)?; + let prefix_rel = normalized_prefix(prefix)?; + for wheel in wheel_paths { + install_wheel(wheel, destdir, &prefix_rel, &py_version)?; + } + Ok(()) +} + fn is_top_level_dist_info_entry_points(rel: &Path) -> bool { let parts = match path_parts(rel, "wheel entry") { Ok(p) => p, diff --git a/src/cli.rs b/src/cli.rs index cd439ad..52f7bad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -152,6 +152,33 @@ pub enum Commands { #[arg(short, long)] output: Option, }, + #[command(hide = true)] + Internal { + #[command(subcommand)] + command: InternalCommands, + }, +} + +#[derive(Subcommand)] +pub enum InternalCommands { + #[command(hide = true)] + PythonBuild { + #[arg(long, default_value = ".")] + src_dir: PathBuf, + #[arg(long, default_value = "dist")] + dist_dir: PathBuf, + #[arg(long = "config-setting")] + config_settings: Vec, + }, + #[command(hide = true)] + PythonInstall { + #[arg(long, default_value = "dist")] + dist_dir: PathBuf, + #[arg(long = "wheel", value_name = "FILE")] + wheels: Vec, + #[arg(long, default_value = "/usr")] + prefix: String, + }, } #[derive(Subcommand)] diff --git a/src/commands.rs b/src/commands.rs index 5d09a8b..95f9e0e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,4 +1,4 @@ -use crate::cli::{Cli, Commands, RepoCommands, RepoKindArg}; +use crate::cli::{Cli, Commands, InternalCommands, RepoCommands, RepoKindArg}; use crate::{ builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner, signing, source, staging, ui, @@ -260,6 +260,43 @@ fn parse_keep_list(metadata: &toml::Value) -> Vec { Vec::new() } +fn current_process_env_vars() -> Vec<(String, String)> { + std::env::vars().collect() +} + +fn run_internal_command(command: InternalCommands) -> Result<()> { + match command { + InternalCommands::PythonBuild { + src_dir, + dist_dir, + config_settings, + } => { + let env_vars = current_process_env_vars(); + crate::builder::python::build_wheels(&src_dir, &dist_dir, &env_vars, &config_settings) + } + InternalCommands::PythonInstall { + dist_dir, + wheels, + prefix, + } => { + let env_vars = current_process_env_vars(); + let wheel_paths = if wheels.is_empty() { + crate::builder::python::collect_wheels(&dist_dir)? + } else { + wheels + }; + let destdir = std::env::var("DESTDIR") + .context("DESTDIR must be set for internal python-install")?; + crate::builder::python::install_wheels( + &wheel_paths, + Path::new(&destdir), + &prefix, + &env_vars, + ) + } + } +} + fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::PackageSpec { let mut spec = package::PackageSpec { package: package::PackageInfo { @@ -4465,6 +4502,9 @@ pub fn run(cli: Cli) -> Result<()> { output_path.display() )); } + Commands::Internal { command } => { + run_internal_command(command)?; + } } Ok(()) diff --git a/src/fakeroot.rs b/src/fakeroot.rs index 5799450..6d452b6 100755 --- a/src/fakeroot.rs +++ b/src/fakeroot.rs @@ -15,12 +15,12 @@ pub fn is_root() -> bool { pub fn wrap_install_command(program: &str, destdir: &Path) -> Command { let script_path = shell_script_path(program); if is_root() { - build_command(program, script_path.as_deref()) + build_command(program, script_path) } else { // Use system fakeroot command which handles LD_PRELOAD internally let mut cmd = Command::new("fakeroot"); cmd.arg("--"); - if let Some(script_path) = script_path.as_deref() { + if let Some(script_path) = script_path { cmd.arg("sh"); cmd.arg(script_path); } else { diff --git a/src/shell_helpers.rs b/src/shell_helpers.rs index 36d5b2d..8337ea0 100644 --- a/src/shell_helpers.rs +++ b/src/shell_helpers.rs @@ -12,14 +12,20 @@ pub const INTERNAL_DEPOT_DIR: &str = ".depot"; pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs"; const DEPOT_HAUL_HELPER_ENV: &str = "DEPOT_HAUL_HELPER"; const DEPOT_SUBDESTDIR_HELPER_ENV: &str = "DEPOT_SUBDESTDIR_HELPER"; +const DEPOT_PYTHON_BUILD_HELPER_ENV: &str = "DEPOT_PYTHON_BUILD_HELPER"; +const DEPOT_PYTHON_INSTALL_HELPER_ENV: &str = "DEPOT_PYTHON_INSTALL_HELPER"; +const DEPOT_EXECUTABLE_ENV: &str = "DEPOT_EXECUTABLE"; /// Ephemeral helper command directory to prepend to PATH while running scripts. pub struct ShellHelpers { _tempdir: TempDir, path_value: String, outputs_dir: PathBuf, + depot_executable: PathBuf, haul_path: PathBuf, subdestdir_path: PathBuf, + python_build_path: PathBuf, + python_install_path: PathBuf, } impl ShellHelpers { @@ -37,6 +43,8 @@ impl ShellHelpers { let bin_dir = tempdir.path().join("bin"); fs::create_dir_all(&bin_dir) .with_context(|| format!("Failed to create helper bin dir: {}", bin_dir.display()))?; + let depot_executable = std::env::current_exe() + .context("Failed to locate depot executable for shell helpers")?; let haul_path = bin_dir.join("haul"); fs::write(&haul_path, HAUL_SCRIPT).with_context(|| { @@ -75,14 +83,57 @@ impl ShellHelpers { })?; } + let python_build_path = bin_dir.join("python_build"); + fs::write(&python_build_path, PYTHON_BUILD_SCRIPT).with_context(|| { + format!( + "Failed to write shell helper command: {}", + python_build_path.display() + ) + })?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&python_build_path) + .with_context(|| format!("Failed to stat helper: {}", python_build_path.display()))? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&python_build_path, perms).with_context(|| { + format!("Failed to chmod helper: {}", python_build_path.display()) + })?; + } + + let python_install_path = bin_dir.join("python_install"); + fs::write(&python_install_path, PYTHON_INSTALL_SCRIPT).with_context(|| { + format!( + "Failed to write shell helper command: {}", + python_install_path.display() + ) + })?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&python_install_path) + .with_context(|| { + format!("Failed to stat helper: {}", python_install_path.display()) + })? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&python_install_path, perms).with_context(|| { + format!("Failed to chmod helper: {}", python_install_path.display()) + })?; + } + let path_value = crate::runtime_env::prepend_helper_to_safe_path(&bin_dir); Ok(Self { _tempdir: tempdir, path_value, outputs_dir: destdir.join(INTERNAL_OUTPUTS_DIR), + depot_executable, haul_path, subdestdir_path, + python_build_path, + python_install_path, }) } @@ -105,6 +156,21 @@ impl ShellHelpers { DEPOT_SUBDESTDIR_HELPER_ENV, self.subdestdir_path.to_string_lossy().into_owned(), ); + set_env_var( + env_vars, + DEPOT_PYTHON_BUILD_HELPER_ENV, + self.python_build_path.to_string_lossy().into_owned(), + ); + set_env_var( + env_vars, + DEPOT_PYTHON_INSTALL_HELPER_ENV, + self.python_install_path.to_string_lossy().into_owned(), + ); + set_env_var( + env_vars, + DEPOT_EXECUTABLE_ENV, + self.depot_executable.to_string_lossy().into_owned(), + ); } /// Apply helper-related variables directly to a `std::process::Command`. @@ -113,7 +179,10 @@ impl ShellHelpers { .env("DEPOT_OUTPUTS_DIR", &self.outputs_dir) .env("DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR) .env(DEPOT_HAUL_HELPER_ENV, &self.haul_path) - .env(DEPOT_SUBDESTDIR_HELPER_ENV, &self.subdestdir_path); + .env(DEPOT_SUBDESTDIR_HELPER_ENV, &self.subdestdir_path) + .env(DEPOT_PYTHON_BUILD_HELPER_ENV, &self.python_build_path) + .env(DEPOT_PYTHON_INSTALL_HELPER_ENV, &self.python_install_path) + .env(DEPOT_EXECUTABLE_ENV, &self.depot_executable); } } @@ -121,7 +190,7 @@ impl ShellHelpers { /// through `/bin/sh`, avoiding direct execution from mounts that may be `noexec`. pub fn wrap_shell_command(command: &str) -> String { format!( - "haul() {{ /bin/sh \"${{{DEPOT_HAUL_HELPER_ENV}:?}}\" \"$@\"; }}\nsubdestdir() {{ /bin/sh \"${{{DEPOT_SUBDESTDIR_HELPER_ENV}:?}}\" \"$@\"; }}\n{command}" + "haul() {{ /bin/sh \"${{{DEPOT_HAUL_HELPER_ENV}:?}}\" \"$@\"; }}\nsubdestdir() {{ /bin/sh \"${{{DEPOT_SUBDESTDIR_HELPER_ENV}:?}}\" \"$@\"; }}\npython_build() {{ /bin/sh \"${{{DEPOT_PYTHON_BUILD_HELPER_ENV}:?}}\" \"$@\"; }}\npython_install() {{ /bin/sh \"${{{DEPOT_PYTHON_INSTALL_HELPER_ENV}:?}}\" \"$@\"; }}\n{command}" ) } @@ -273,9 +342,36 @@ mkdir -p "$path" printf '%s\n' "$path" "#; +const PYTHON_BUILD_SCRIPT: &str = r#"#!/bin/sh +set -eu + +fail() { + echo "python_build: $*" >&2 + exit 1 +} + +[ "${DEPOT_EXECUTABLE:-}" != "" ] || fail "DEPOT_EXECUTABLE is not set" + +exec "$DEPOT_EXECUTABLE" internal python-build "$@" +"#; + +const PYTHON_INSTALL_SCRIPT: &str = r#"#!/bin/sh +set -eu + +fail() { + echo "python_install: $*" >&2 + exit 1 +} + +[ "${DEPOT_EXECUTABLE:-}" != "" ] || fail "DEPOT_EXECUTABLE is not set" +[ "${DESTDIR:-}" != "" ] || fail "DESTDIR is not set" + +exec "$DEPOT_EXECUTABLE" internal python-install "$@" +"#; + #[cfg(test)] mod tests { - use super::{INTERNAL_DEPOT_DIR, ShellHelpers, shell_ident_suffix}; + use super::{INTERNAL_DEPOT_DIR, ShellHelpers, shell_ident_suffix, wrap_shell_command}; use tempfile::tempdir; #[test] @@ -307,4 +403,29 @@ mod tests { .into_owned(); assert!(path.starts_with(&helper_prefix)); } + + #[test] + fn shell_helpers_export_python_helper_paths() { + let destdir = tempdir().unwrap(); + let helpers = ShellHelpers::new(destdir.path()).unwrap(); + let mut envs = Vec::new(); + helpers.apply_to_env_vars(&mut envs); + + assert!( + envs.iter() + .any(|(key, _)| key == "DEPOT_PYTHON_BUILD_HELPER") + ); + assert!( + envs.iter() + .any(|(key, _)| key == "DEPOT_PYTHON_INSTALL_HELPER") + ); + assert!(envs.iter().any(|(key, _)| key == "DEPOT_EXECUTABLE")); + } + + #[test] + fn wrap_shell_command_exposes_python_helpers() { + let wrapped = wrap_shell_command("python_build\npython_install"); + assert!(wrapped.contains("python_build()")); + assert!(wrapped.contains("python_install()")); + } }