feat: improve depot UX and packaging workflow

- add shared terminal UI helpers for colored info/warn/success logs and yes/no prompts
- add Python build backend (PEP 517/setup.py wheel build + install) and expose python in interactive spec creation
- expand spec/build flags (post_configure, make phase targets/dirs, configure_file, no_strip, no_compress_man)
- support split-output staging with internal .depot output dirs, shell helpers (haul/subdestdir), and per-output deps/provides overrides
- improve staging with ELF auto-strip and zstd manpage compression (with opt-out flags)
- enforce runtime deps before install and support legacy lifecycle hook script names
- improve manual source handling with files/urls lists and stricter validation
This commit is contained in:
2026-02-22 14:52:45 -06:00
parent 0c676f6743
commit d63ad03e98
28 changed files with 4639 additions and 412 deletions
+536 -79
View File
@@ -6,7 +6,7 @@ use crate::package::PackageSpec;
use crate::source::hooks;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn build(
@@ -17,6 +17,7 @@ pub fn build(
export_compiler_flags: bool,
) -> Result<()> {
let flags = &spec.build.flags;
let make_exec = resolve_make_exec(&flags.make_exec);
let export_compiler_flags = export_compiler_flags && !flags.no_flags;
let actual_src = resolve_actual_src(spec, src_dir)?;
@@ -45,22 +46,18 @@ pub fn build(
let build_dir = if let Some(dir) = &flags.build_dir {
let bdir = actual_src.join(dir);
fs::create_dir_all(&bdir)?;
println!(" Build directory: {}", bdir.display());
crate::log_info!(" Build directory: {}", bdir.display());
bdir
} else {
actual_src.clone()
};
if !state.is_done(BuildStep::Configured) {
println!("Running configure...");
let configure_path = if flags.build_dir.is_some() {
"../configure"
} else {
"./configure"
};
println!(" Configure path: {}", configure_path);
crate::log_info!("Running configure...");
let configure_path = resolve_configure_path(spec, &actual_src);
crate::log_info!(" Configure path: {}", configure_path.display());
let mut configure_cmd = Command::new(configure_path);
let mut configure_cmd = Command::new(&configure_path);
configure_cmd.current_dir(&build_dir);
crate::builder::prepare_command(&mut configure_cmd, &env_vars);
@@ -69,15 +66,11 @@ pub fn build(
// Some projects use non-GNU configure scripts that reject --host/--build.
// Probe support first and only add these options when advertised.
let help_text = configure_help_text(configure_path, &build_dir, &env_vars);
let supports_host = help_text
.as_deref()
.map(|s| configure_help_supports_option(s, "--host"))
.unwrap_or(true);
let supports_build = help_text
.as_deref()
.map(|s| configure_help_supports_option(s, "--build"))
.unwrap_or(true);
let help_text = configure_help_text(&configure_path, &build_dir, &env_vars);
let supports_host =
configure_supports_option(help_text.as_deref(), "--host", &flags.configure_file);
let supports_build =
configure_supports_option(help_text.as_deref(), "--build", &flags.configure_file);
let requested_host = if let Some(cc_cfg) = cross {
Some(cc_cfg.host_triple().to_string())
@@ -99,7 +92,7 @@ pub fn build(
if supports_host {
configure_cmd.arg(format!("--host={}", host));
} else {
println!(" configure does not support --host; skipping {}", host);
crate::log_info!(" configure does not support --host; skipping {}", host);
}
}
@@ -107,12 +100,13 @@ pub fn build(
if supports_build {
configure_cmd.arg(format!("--build={}", build));
} else {
println!(" configure does not support --build; skipping {}", build);
crate::log_info!(" configure does not support --build; skipping {}", build);
}
}
for arg in &flags.configure {
configure_cmd.arg(arg);
let expanded = expand_configure_arg(spec, arg, &env_vars);
configure_cmd.arg(expanded);
}
let status = configure_cmd
@@ -122,64 +116,155 @@ pub fn build(
if !status.success() {
anyhow::bail!("configure failed with status: {}", status);
}
// Run post-configure hooks (after configure, before make)
hooks::run_post_configure_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::Configured)?;
} else {
println!("Skipping configure (already done)");
crate::log_info!("Skipping configure (already done)");
}
if !state.is_done(BuildStep::PostCompileDone) {
// Run make
println!("Running make...");
let mut make_cmd = Command::new("make");
make_cmd.current_dir(&build_dir);
make_cmd.arg("-j").arg(num_cpus().to_string());
add_make_variable_overrides(&mut make_cmd, &flags.make_vars, "build")?;
let build_targets = phase_targets(&flags.make_target, &flags.make_targets, None);
let make_dirs = resolve_make_dirs(&build_dir, &flags.make_dirs, "build.flags.make_dirs")?;
for make_dir in make_dirs {
crate::log_info!("Running {} in {}...", make_exec, make_dir.display());
let mut make_cmd = Command::new(make_exec);
make_cmd.current_dir(&make_dir);
make_cmd.arg("-j").arg(num_cpus().to_string());
add_make_variable_overrides_if_supported(
&mut make_cmd,
make_exec,
&flags.make_vars,
"build",
)?;
for target in &build_targets {
make_cmd.arg(target);
}
crate::builder::prepare_command(&mut make_cmd, &env_vars);
crate::builder::prepare_command(&mut make_cmd, &env_vars);
let status = make_cmd
.status()
.with_context(|| format!("Failed to run make in {}", build_dir.display()))?;
let status = make_cmd.status().with_context(|| {
format!("Failed to run {} in {}", make_exec, make_dir.display())
})?;
if !status.success() {
anyhow::bail!("make failed with status: {}", status);
if !status.success() {
anyhow::bail!(
"{} failed with status: {} (dir: {})",
make_exec,
status,
make_dir.display()
);
}
}
if let Some(test_target) = maybe_find_autotools_test_target(&build_dir, flags.skip_tests)? {
println!("Running make {}...", test_target);
let mut test_cmd = Command::new("make");
test_cmd.current_dir(&build_dir);
add_make_variable_overrides(&mut test_cmd, &flags.make_test_vars, "test")?;
test_cmd.arg(test_target);
crate::builder::prepare_command(&mut test_cmd, &env_vars);
let status = test_cmd.status().with_context(|| {
format!(
"Failed to run make {} in {}",
test_target,
build_dir.display()
)
})?;
if !status.success() {
anyhow::bail!("make {} failed with status: {}", test_target, status);
}
} else if flags.skip_tests {
println!("Skipping tests: disabled by build.flags.skip_tests");
if flags.skip_tests {
crate::log_info!("Skipping tests: disabled by build.flags.skip_tests");
} else {
println!("Skipping tests: no 'check' or 'test' target in Makefile");
let test_dirs = resolve_make_dirs(
&build_dir,
&flags.make_test_dirs,
"build.flags.make_test_dirs",
)?;
let mut ran_any_tests = false;
let configured_test_targets =
phase_targets(&flags.make_test_target, &flags.make_test_targets, None);
for test_dir in test_dirs {
let test_targets = if !configured_test_targets.is_empty() {
configured_test_targets.clone()
} else if make_exec_supports_make_assignments(make_exec) {
maybe_find_autotools_test_target(&test_dir, false)?
.map(|t| vec![t.to_string()])
.unwrap_or_default()
} else {
Vec::new()
};
if !test_targets.is_empty() {
crate::log_info!("Running {} in {}...", make_exec, test_dir.display());
let mut test_cmd = Command::new(make_exec);
test_cmd.current_dir(&test_dir);
add_make_variable_overrides_if_supported(
&mut test_cmd,
make_exec,
&flags.make_test_vars,
"test",
)?;
for test_target in &test_targets {
test_cmd.arg(test_target);
}
crate::builder::prepare_command(&mut test_cmd, &env_vars);
let test_targets_display = test_targets.join(" ");
let status = test_cmd.status().with_context(|| {
format!(
"Failed to run {} {} in {}",
make_exec,
test_targets_display,
test_dir.display()
)
})?;
if !status.success() {
anyhow::bail!(
"{} {} failed with status: {} (dir: {})",
make_exec,
test_targets_display,
status,
test_dir.display()
);
}
ran_any_tests = true;
}
}
if !ran_any_tests {
if flags.make_test_dirs.is_empty() {
if !configured_test_targets.is_empty() {
crate::log_info!("Skipping tests: no test directories to run");
} else if make_exec_supports_make_assignments(make_exec) {
crate::log_info!("Skipping tests: no 'check' or 'test' target in Makefile");
} else {
crate::log_info!(
"Skipping tests: set build.flags.make_test_target when using build.flags.make_exec='{}'",
make_exec
);
}
} else if !configured_test_targets.is_empty() {
crate::log_info!(
"Skipping tests: no test targets ran in build.flags.make_test_dirs"
);
} else if make_exec_supports_make_assignments(make_exec) {
crate::log_info!(
"Skipping tests: no 'check' or 'test' target in build.flags.make_test_dirs"
);
} else {
crate::log_info!(
"Skipping tests: set build.flags.make_test_target when using build.flags.make_exec='{}'",
make_exec
);
}
}
}
// Run post-compile hooks (after make, before make install)
hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostCompileDone)?;
} else {
println!("Skipping make and post-compile hooks (already done)");
crate::log_info!("Skipping make and post-compile hooks (already done)");
}
if !state.is_done(BuildStep::PostInstallDone) {
// Run make install with fakeroot if not root
println!(
"Running make install{}...",
crate::log_info!(
"Running {} {}{}...",
make_exec,
phase_targets(
&flags.make_install_target,
&flags.make_install_targets,
Some("install")
)
.join(" "),
if fakeroot::is_root() {
""
} else {
@@ -187,34 +272,67 @@ pub fn build(
}
);
let mut install_cmd = fakeroot::wrap_install_command("make", destdir);
install_cmd.current_dir(&build_dir);
if !has_make_variable_override(&flags.make_install_vars, "DESTDIR") {
install_cmd.arg(format!("DESTDIR={}", destdir.to_string_lossy()));
}
add_make_variable_overrides(&mut install_cmd, &flags.make_install_vars, "install")?;
install_cmd.arg("install");
let install_dirs = resolve_make_dirs(
&build_dir,
&flags.make_install_dirs,
"build.flags.make_install_dirs",
)?;
let install_targets = phase_targets(
&flags.make_install_target,
&flags.make_install_targets,
Some("install"),
);
for install_dir in install_dirs {
let mut install_cmd = fakeroot::wrap_install_command(make_exec, destdir);
install_cmd.current_dir(&install_dir);
if make_exec_supports_make_assignments(make_exec)
&& !has_make_variable_override(&flags.make_install_vars, "DESTDIR")
{
install_cmd.arg(format!("DESTDIR={}", destdir.to_string_lossy()));
}
add_make_variable_overrides_if_supported(
&mut install_cmd,
make_exec,
&flags.make_install_vars,
"install",
)?;
for install_target in &install_targets {
install_cmd.arg(install_target);
}
let mut install_env = env_vars.clone();
install_env.push((
"DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(),
));
crate::builder::prepare_command(&mut install_cmd, &install_env);
let mut install_env = env_vars.clone();
install_env.push((
"DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(),
));
crate::builder::prepare_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
.with_context(|| format!("Failed to run make install for {}", spec.package.name))?;
let status = install_cmd.status().with_context(|| {
format!(
"Failed to run {} {} for {} in {}",
make_exec,
install_targets.join(" "),
spec.package.name,
install_dir.display()
)
})?;
if !status.success() {
anyhow::bail!("make install failed with status: {}", status);
if !status.success() {
anyhow::bail!(
"{} {} failed with status: {} (dir: {})",
make_exec,
install_targets.join(" "),
status,
install_dir.display()
);
}
}
// Run post-install hooks (after make install)
hooks::run_post_install_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostInstallDone)?;
} else {
println!("Skipping make install and post-install hooks (already done)");
crate::log_info!("Skipping make install and post-install hooks (already done)");
}
Ok(())
@@ -248,8 +366,23 @@ fn resolve_actual_src(spec: &PackageSpec, src_dir: &Path) -> Result<std::path::P
Ok(actual_src)
}
fn resolve_configure_path(spec: &PackageSpec, actual_src: &Path) -> PathBuf {
let configured = spec.expand_vars(&spec.build.flags.configure_file);
let trimmed = configured.trim();
if trimmed.is_empty() {
return actual_src.join("configure");
}
let path = Path::new(trimmed);
if path.is_absolute() {
path.to_path_buf()
} else {
actual_src.join(path)
}
}
fn configure_help_text(
configure_path: &str,
configure_path: &Path,
build_dir: &Path,
env_vars: &crate::builder::EnvVars,
) -> Option<String> {
@@ -273,6 +406,12 @@ fn configure_help_supports_option(help_text: &str, option: &str) -> bool {
help_text.contains(&with_eq) || help_text.contains(&with_space) || help_text.contains(option)
}
fn configure_supports_option(help_text: Option<&str>, option: &str, configure_file: &str) -> bool {
help_text
.map(|text| configure_help_supports_option(text, option))
.unwrap_or(configure_file.trim().is_empty())
}
fn find_autotools_test_target(build_dir: &Path) -> Result<Option<&'static str>> {
for target in ["check", "test"] {
if makefile_has_target(build_dir, target)? {
@@ -292,6 +431,48 @@ fn maybe_find_autotools_test_target(
find_autotools_test_target(build_dir)
}
fn resolve_make_dirs(build_dir: &Path, dirs: &[String], field_name: &str) -> Result<Vec<PathBuf>> {
if dirs.is_empty() {
return Ok(vec![build_dir.to_path_buf()]);
}
let canonical_build = fs::canonicalize(build_dir)
.with_context(|| format!("Failed to resolve build directory {}", build_dir.display()))?;
let mut out = Vec::with_capacity(dirs.len());
for raw in dirs {
let trimmed = raw.trim();
if trimmed.is_empty() {
anyhow::bail!("{} contains an empty directory entry", field_name);
}
let rel = Path::new(trimmed);
if rel.is_absolute() {
anyhow::bail!("{} entry '{}' must be a relative path", field_name, trimmed);
}
let candidate = build_dir.join(rel);
let canonical_candidate = fs::canonicalize(&candidate).with_context(|| {
format!(
"{} entry '{}' does not exist in {}",
field_name,
trimmed,
build_dir.display()
)
})?;
if !canonical_candidate.starts_with(&canonical_build) {
anyhow::bail!(
"{} entry '{}' resolves outside build directory",
field_name,
trimmed
);
}
if !canonical_candidate.is_dir() {
anyhow::bail!("{} entry '{}' is not a directory", field_name, trimmed);
}
out.push(canonical_candidate);
}
Ok(out)
}
fn makefile_has_target(build_dir: &Path, target: &str) -> Result<bool> {
for name in ["GNUmakefile", "Makefile", "makefile"] {
let path = build_dir.join(name);
@@ -339,6 +520,75 @@ fn makefile_content_has_target(content: &str, target: &str) -> bool {
false
}
fn nonempty_trimmed(value: &str) -> Option<&str> {
let trimmed = value.trim();
(!trimmed.is_empty()).then_some(trimmed)
}
fn resolve_make_exec(configured: &str) -> &str {
nonempty_trimmed(configured).unwrap_or("make")
}
fn phase_targets(single: &str, many: &[String], default: Option<&str>) -> Vec<String> {
let mut targets = Vec::new();
if let Some(target) = nonempty_trimmed(single) {
targets.push(target.to_string());
}
for target in many {
if let Some(target) = nonempty_trimmed(target) {
targets.push(target.to_string());
}
}
if targets.is_empty()
&& let Some(default_target) = default
{
targets.push(default_target.to_string());
}
targets
}
fn make_exec_supports_make_assignments(make_exec: &str) -> bool {
let Some(tool) = Path::new(make_exec)
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_ascii_lowercase())
else {
return false;
};
tool == "make"
|| tool == "gmake"
|| tool == "bmake"
|| tool == "nmake"
|| tool.ends_with("-make")
|| tool.ends_with("make.exe")
}
fn add_make_variable_overrides_if_supported(
cmd: &mut Command,
make_exec: &str,
vars: &[String],
phase: &str,
) -> Result<()> {
if make_exec_supports_make_assignments(make_exec) {
return add_make_variable_overrides(cmd, vars, phase);
}
if !vars.is_empty() {
let field_name = match phase {
"build" => "build.flags.make_vars",
"test" => "build.flags.make_test_vars",
"install" => "build.flags.make_install_vars",
_ => "build.flags.make_*_vars",
};
anyhow::bail!(
"{} is only supported with make-like executables; build.flags.make_exec='{}'",
field_name,
make_exec
);
}
Ok(())
}
fn add_make_variable_overrides(cmd: &mut Command, vars: &[String], phase: &str) -> Result<()> {
for raw in vars {
let var = raw.trim();
@@ -396,7 +646,7 @@ fn expand_shell_commands(input: &str, cc: &str) -> Result<String> {
}
_ => {
// Silently skip failed commands (e.g., gcc doesn't support -print-resource-dir)
eprintln!("Warning: shell command '{}' failed, skipping", cmd);
crate::log_warn!("Shell command '{}' failed, skipping", cmd);
String::new()
}
};
@@ -410,6 +660,29 @@ fn expand_shell_commands(input: &str, cc: &str) -> Result<String> {
Ok(result)
}
fn expand_env_vars(input: &str) -> String {
let mut result = input.to_string();
for (key, value) in std::env::vars() {
result = result.replace(&format!("${}", key), &value);
result = result.replace(&format!("${{{}}}", key), &value);
}
result
}
fn expand_with_envs(input: &str, envs: &[(String, String)]) -> String {
let mut result = input.to_string();
for (k, v) in envs {
result = result.replace(&format!("${}", k), v);
result = result.replace(&format!("${{{}}}", k), v);
}
expand_env_vars(&result)
}
fn expand_configure_arg(spec: &PackageSpec, arg: &str, envs: &[(String, String)]) -> String {
let with_spec_vars = spec.expand_vars(arg);
expand_with_envs(&with_spec_vars, envs)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -433,6 +706,48 @@ mod tests {
Ok(())
}
#[test]
fn test_expand_with_envs_prefers_provided_envs() {
let envs = vec![
("CARCH".to_string(), "x86_64".to_string()),
("CHOST".to_string(), "x86_64-sfg-linux-gnu".to_string()),
];
let out = expand_with_envs("--with-gcc-arch=$CARCH --host=${CHOST}", &envs);
assert!(out.contains("--with-gcc-arch=x86_64"));
assert!(out.contains("--host=x86_64-sfg-linux-gnu"));
}
#[test]
fn test_expand_configure_arg_expands_spec_and_env_vars() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
version: "1.2.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let envs = vec![("CARCH".to_string(), "aarch64".to_string())];
let expanded =
expand_configure_arg(&spec, "--program-prefix=$name-$version-$CARCH-", &envs);
assert_eq!(expanded, "--program-prefix=foo-1.2.3-aarch64-");
}
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
@@ -447,6 +762,16 @@ mod tests {
assert!(!configure_help_supports_option(help, "--target"));
}
#[test]
fn test_configure_supports_option_defaults_by_configure_file_usage() {
assert!(configure_supports_option(None, "--host", ""));
assert!(!configure_supports_option(
None,
"--host",
"build-aux/Configure"
));
}
#[test]
fn test_makefile_content_has_target_detects_check_and_test() {
let content = r#"
@@ -486,6 +811,31 @@ foo: bar
Ok(())
}
#[test]
fn test_resolve_make_dirs_defaults_to_build_dir() -> Result<()> {
let tmp = tempdir().unwrap();
let dirs = resolve_make_dirs(tmp.path(), &[], "build.flags.make_dirs")?;
assert_eq!(dirs, vec![tmp.path().to_path_buf()]);
Ok(())
}
#[test]
fn test_resolve_make_dirs_resolves_multiple_relative_dirs() -> Result<()> {
let tmp = tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("lib"))?;
std::fs::create_dir_all(tmp.path().join("libelf"))?;
let dirs = resolve_make_dirs(
tmp.path(),
&["lib".to_string(), "libelf".to_string()],
"build.flags.make_dirs",
)?;
assert_eq!(
dirs,
vec![tmp.path().join("lib"), tmp.path().join("libelf")]
);
Ok(())
}
#[test]
fn test_add_make_variable_overrides_accepts_valid_assignments() -> Result<()> {
let mut cmd = Command::new("make");
@@ -530,6 +880,49 @@ foo: bar
));
}
#[test]
fn test_resolve_make_exec_defaults_and_trims() {
assert_eq!(resolve_make_exec(""), "make");
assert_eq!(resolve_make_exec(" "), "make");
assert_eq!(resolve_make_exec(" ninja "), "ninja");
}
#[test]
fn test_make_exec_supports_make_assignments_detects_make_variants() {
assert!(make_exec_supports_make_assignments("make"));
assert!(make_exec_supports_make_assignments("/usr/bin/gmake"));
assert!(!make_exec_supports_make_assignments("ninja"));
}
#[test]
fn test_phase_targets_merges_singular_plural_and_default() {
assert_eq!(
phase_targets("bootstrap", &["stage1".into(), "stage2".into()], None),
vec![
"bootstrap".to_string(),
"stage1".to_string(),
"stage2".to_string()
]
);
assert_eq!(
phase_targets("", &[], Some("install")),
vec!["install".to_string()]
);
}
#[test]
fn test_add_make_variable_overrides_if_supported_rejects_ninja_vars() {
let mut cmd = Command::new("ninja");
let err = add_make_variable_overrides_if_supported(
&mut cmd,
"ninja",
&["V=1".to_string()],
"build",
)
.expect_err("ninja should reject make variable override syntax");
assert!(err.to_string().contains("build.flags.make_vars"));
}
#[test]
fn test_resolve_actual_src_expands_source_subdir_vars() {
let tmp = tempdir().unwrap();
@@ -558,10 +951,74 @@ foo: bar
},
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let resolved = resolve_actual_src(&spec, &src_root).unwrap();
assert_eq!(resolved, expanded);
}
#[test]
fn test_resolve_configure_path_defaults_to_source_configure() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let actual_src = PathBuf::from("/tmp/src");
let configure = resolve_configure_path(&spec, &actual_src);
assert_eq!(configure, actual_src.join("configure"));
}
#[test]
fn test_resolve_configure_path_uses_configure_file_and_expands_vars() {
let mut flags = BuildFlags::default();
flags.configure_file = "build-aux/$name-configure".into();
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Autotools,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let actual_src = PathBuf::from("/tmp/src");
let configure = resolve_configure_path(&spec, &actual_src);
assert_eq!(configure, actual_src.join("build-aux/foo-configure"));
}
}
+3 -1
View File
@@ -18,7 +18,7 @@ pub fn build(
_cross: Option<&CrossConfig>,
_export_compiler_flags: bool,
) -> Result<()> {
println!(
crate::log_info!(
"Binary install: copying files from {} to {} (pkg type={})",
src_dir.display(),
destdir.display(),
@@ -87,6 +87,8 @@ mod tests {
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: std::path::PathBuf::from("."),
}
}
+174 -7
View File
@@ -16,6 +16,7 @@ pub fn build(
export_compiler_flags: bool,
) -> Result<()> {
let flags = &spec.build.flags;
let make_exec_override = flags.make_exec.trim();
// Determine actual source directory (support source_subdir)
let actual_src = resolve_actual_src(spec, src_dir)?;
@@ -49,7 +50,7 @@ pub fn build(
// Run cmake configure
if !state.is_done(BuildStep::Configured) {
println!("Running cmake configure...");
crate::log_info!("Running cmake configure...");
let mut cmake_cmd = Command::new("cmake");
cmake_cmd.current_dir(&build_dir);
cmake_cmd.arg("-S").arg(&actual_src);
@@ -62,6 +63,17 @@ pub fn build(
cmake_cmd.arg(format!("-DCMAKE_TOOLCHAIN_FILE={}", tf.display()));
}
if !make_exec_override.is_empty() {
if !cmake_configure_flags_specify_generator(&flags.configure)
&& let Some(generator) = cmake_generator_for_make_exec(make_exec_override)
{
cmake_cmd.arg("-G").arg(generator);
}
if !cmake_configure_flags_set_make_program(&flags.configure) {
cmake_cmd.arg(format!("-DCMAKE_MAKE_PROGRAM={make_exec_override}"));
}
}
// Add custom configure flags from spec (supports cross-compilation overrides).
// Expand using env-vars we will supply to the child process first so patterns
// like `$CXX` (coming from flags.cxx) are substituted.
@@ -76,17 +88,26 @@ pub fn build(
if !status.success() {
anyhow::bail!("cmake configure failed");
}
crate::source::hooks::run_post_configure_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::Configured)?;
} else {
println!("Skipping cmake configure (already done)");
crate::log_info!("Skipping cmake configure (already done)");
}
if !state.is_done(BuildStep::PostCompileDone) {
// Run cmake build
println!("Running cmake build...");
crate::log_info!("Running cmake build...");
let build_targets = phase_targets(&flags.make_target, &flags.make_targets);
let mut build_cmd = Command::new("cmake");
build_cmd.arg("--build").arg(&build_dir);
build_cmd.arg("-j").arg(num_cpus().to_string());
if !build_targets.is_empty() {
build_cmd.arg("--target");
for target in &build_targets {
build_cmd.arg(target);
}
}
crate::builder::prepare_command(&mut build_cmd, &env_vars);
@@ -97,15 +118,44 @@ pub fn build(
anyhow::bail!("cmake build failed");
}
if flags.skip_tests {
crate::log_info!("Skipping tests: disabled by build.flags.skip_tests");
} else {
let test_targets = phase_targets(&flags.make_test_target, &flags.make_test_targets);
if !test_targets.is_empty() {
let joined = test_targets.join(" ");
crate::log_info!("Running cmake test target(s): {}...", joined);
let mut test_cmd = Command::new("cmake");
test_cmd.arg("--build").arg(&build_dir);
test_cmd.arg("--target");
for target in &test_targets {
test_cmd.arg(target);
}
crate::builder::prepare_command(&mut test_cmd, &env_vars);
let status = test_cmd.status().with_context(|| {
format!(
"Failed to run cmake build target(s) '{}' for {}",
joined, spec.package.name
)
})?;
if !status.success() {
anyhow::bail!("cmake test target(s) '{}' failed", joined);
}
} else {
crate::log_info!("Skipping tests: no build.flags.make_test_target(s) for cmake");
}
}
crate::source::hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostCompileDone)?;
} else {
println!("Skipping cmake build and post-compile hooks (already done)");
crate::log_info!("Skipping cmake build and post-compile hooks (already done)");
}
if !state.is_done(BuildStep::PostInstallDone) {
// Run cmake install with fakeroot if not root
println!(
crate::log_info!(
"Running cmake install{}...",
if fakeroot::is_root() {
""
@@ -114,8 +164,18 @@ pub fn build(
}
);
let install_targets =
phase_targets(&flags.make_install_target, &flags.make_install_targets);
let mut install_cmd = fakeroot::wrap_install_command("cmake", destdir);
install_cmd.arg("--install").arg(&build_dir);
if !install_targets.is_empty() {
install_cmd.arg("--build").arg(&build_dir);
install_cmd.arg("--target");
for target in &install_targets {
install_cmd.arg(target);
}
} else {
install_cmd.arg("--install").arg(&build_dir);
}
let mut install_env = env_vars.clone();
install_env.push((
@@ -128,13 +188,19 @@ pub fn build(
.status()
.with_context(|| format!("Failed to run cmake install for {}", spec.package.name))?;
if !status.success() {
if !install_targets.is_empty() {
anyhow::bail!(
"cmake install target(s) '{}' failed",
install_targets.join(" ")
);
}
anyhow::bail!("cmake install failed");
}
crate::source::hooks::run_post_install_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostInstallDone)?;
} else {
println!("Skipping cmake install and post-install hooks (already done)");
crate::log_info!("Skipping cmake install and post-install hooks (already done)");
}
Ok(())
@@ -161,6 +227,61 @@ fn expand_with_envs(input: &str, envs: &[(String, String)]) -> String {
expand_env_vars(&result)
}
fn nonempty_trimmed(value: &str) -> Option<&str> {
let trimmed = value.trim();
(!trimmed.is_empty()).then_some(trimmed)
}
fn phase_targets(single: &str, many: &[String]) -> Vec<String> {
let mut targets = Vec::new();
if let Some(target) = nonempty_trimmed(single) {
targets.push(target.to_string());
}
for target in many {
if let Some(target) = nonempty_trimmed(target) {
targets.push(target.to_string());
}
}
targets
}
fn cmake_generator_for_make_exec(make_exec: &str) -> Option<&'static str> {
let tool = Path::new(make_exec)
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_ascii_lowercase())?;
if tool.contains("ninja") {
Some("Ninja")
} else if tool == "make"
|| tool == "gmake"
|| tool == "bmake"
|| tool == "nmake"
|| tool.ends_with("-make")
|| tool.ends_with("make.exe")
{
Some("Unix Makefiles")
} else {
None
}
}
fn cmake_configure_flags_specify_generator(flags: &[String]) -> bool {
flags.iter().any(|flag| {
let trimmed = flag.trim();
trimmed == "-G"
|| trimmed.starts_with("-G")
|| trimmed == "--generator"
|| trimmed.starts_with("--generator=")
})
}
fn cmake_configure_flags_set_make_program(flags: &[String]) -> bool {
flags.iter().any(|flag| {
let trimmed = flag.trim();
trimmed.starts_with("-DCMAKE_MAKE_PROGRAM=")
})
}
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
@@ -257,6 +378,50 @@ mod tests {
assert!(n >= 1);
}
#[test]
fn test_phase_targets_merges_singular_and_plural() {
assert_eq!(
phase_targets("bootstrap", &["stage1".into(), "stage2".into()]),
vec![
"bootstrap".to_string(),
"stage1".to_string(),
"stage2".to_string()
]
);
assert!(phase_targets("", &[]).is_empty());
}
#[test]
fn test_cmake_generator_for_make_exec_detects_ninja_and_make() {
assert_eq!(cmake_generator_for_make_exec("ninja"), Some("Ninja"));
assert_eq!(
cmake_generator_for_make_exec("/usr/bin/gmake"),
Some("Unix Makefiles")
);
assert_eq!(cmake_generator_for_make_exec("samurai"), None);
}
#[test]
fn test_cmake_configure_flag_detectors() {
assert!(cmake_configure_flags_specify_generator(&[
"-G".to_string(),
"Ninja".to_string()
]));
assert!(cmake_configure_flags_specify_generator(&[
"--generator=Unix Makefiles".to_string()
]));
assert!(!cmake_configure_flags_specify_generator(&[
"-DCMAKE_BUILD_TYPE=Release".to_string()
]));
assert!(cmake_configure_flags_set_make_program(&[
"-DCMAKE_MAKE_PROGRAM=/usr/bin/ninja".to_string()
]));
assert!(!cmake_configure_flags_set_make_program(&[
"-DCMAKE_C_COMPILER=clang".to_string()
]));
}
#[test]
fn resolve_actual_src_prefers_srcdir_then_specdir_and_handles_absolute() {
let tmp = tempdir().unwrap();
@@ -291,6 +456,8 @@ mod tests {
},
},
dependencies: Default::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.clone(),
};
+187 -12
View File
@@ -3,9 +3,11 @@
use crate::cross::CrossConfig;
use crate::fakeroot;
use crate::package::PackageSpec;
use crate::source::hooks;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(
spec: &PackageSpec,
@@ -20,6 +22,8 @@ pub fn build(
fs::create_dir_all(destdir)?;
let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
shell_helpers.apply_to_env_vars(&mut env_vars);
// For custom builds, look for a build.sh script in the source directory
let build_script = src_dir.join("build.sh");
@@ -44,7 +48,7 @@ pub fn build(
perms.set_mode(0o755);
fs::set_permissions(&build_script, perms)?;
}
println!("Using build.sh from spec dir: {}", spec_build.display());
crate::log_info!("Using build.sh from spec dir: {}", spec_build.display());
}
if !build_script.exists() {
@@ -57,8 +61,15 @@ pub fn build(
use crate::builder::state::{BuildStep, StateTracker};
let mut state = StateTracker::new(src_dir)?;
if !state.is_done(BuildStep::Configured) {
hooks::run_post_configure_commands(spec, src_dir, destdir)?;
state.mark_done(BuildStep::Configured)?;
} else {
crate::log_info!("Skipping post-configure hooks (already done)");
}
if !state.is_done(BuildStep::PostInstallDone) {
println!(
crate::log_info!(
"Running custom build script{}...",
if fakeroot::is_root() {
""
@@ -75,9 +86,17 @@ pub fn build(
src_dir.to_path_buf()
};
// Use POSIX `sh` (more likely to be available in minimal/chroot environments)
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
cmd.current_dir(&build_dir);
crate::builder::set_env_var(
&mut env_vars,
"DESTDIR",
destdir.to_string_lossy().into_owned(),
);
crate::builder::set_env_var(
&mut env_vars,
"DEPOT_PRIMARY_DESTDIR",
destdir.to_string_lossy().into_owned(),
);
add_output_destdir_envs(spec, destdir, &mut env_vars);
// Ensure build script path is absolute for when we are in a sub-build-dir
let abs_build_script = if build_script.is_absolute() {
@@ -85,13 +104,19 @@ pub fn build(
} else {
std::env::current_dir()?.join(&build_script)
};
cmd.arg(&abs_build_script);
crate::builder::set_env_var(
&mut env_vars,
"DESTDIR",
destdir.to_string_lossy().into_owned(),
);
// Use POSIX `sh` (more likely to be available in minimal/chroot environments)
let mut cmd = if custom_function_mode_enabled(&abs_build_script)? {
crate::log_info!(
"Using custom build.sh function mode (per-output install functions enabled)"
);
build_function_mode_command(spec, destdir, &abs_build_script)?
} else {
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
cmd.arg(&abs_build_script);
cmd
};
cmd.current_dir(&build_dir);
crate::builder::prepare_command(&mut cmd, &env_vars);
@@ -109,12 +134,108 @@ pub fn build(
}
state.mark_done(BuildStep::PostInstallDone)?;
} else {
println!("Skipping custom build script (already done)");
crate::log_info!("Skipping custom build script (already done)");
}
Ok(())
}
fn add_output_destdir_envs(
spec: &PackageSpec,
destdir: &Path,
env_vars: &mut crate::builder::EnvVars,
) {
for out in spec.outputs() {
let out_destdir = if out.name == spec.package.name {
destdir.to_path_buf()
} else {
crate::staging::output_staging_dir(destdir, &out.name)
};
let suffix = crate::shell_helpers::shell_ident_suffix(&out.name);
crate::builder::set_env_var(
env_vars,
&format!("DEPOT_SUBDESTDIR_{suffix}"),
out_destdir.to_string_lossy().into_owned(),
);
}
}
fn custom_function_mode_enabled(build_script: &Path) -> Result<bool> {
let contents = fs::read_to_string(build_script)
.with_context(|| format!("Failed to read build script: {}", build_script.display()))?;
Ok(contents.contains("depot_build()")
|| contents.contains("depot_install()")
|| contents.contains("depot_install_")
|| contents.contains("install_"))
}
fn build_function_mode_command(
spec: &PackageSpec,
destdir: &Path,
build_script: &Path,
) -> Result<Command> {
let mut wrapper = String::new();
wrapper.push_str("set -eu\n");
wrapper.push_str(". \"$1\"\n");
wrapper.push_str("if command -v depot_build >/dev/null 2>&1; then depot_build;\n");
wrapper.push_str("elif command -v build >/dev/null 2>&1; then build;\n");
wrapper.push_str("fi\n");
let primary = &spec.package.name;
for out in spec.outputs() {
let out_destdir = if out.name == *primary {
destdir.to_path_buf()
} else {
crate::staging::output_staging_dir(destdir, &out.name)
};
let fn_suffix = shell_fn_suffix(&out.name);
let q_name = sh_single_quote(&out.name);
let q_dest = sh_single_quote(&out_destdir.to_string_lossy());
wrapper.push_str(&format!(
"DEPOT_OUTPUT_NAME='{q_name}'; DEPOT_OUTPUT_DESTDIR='{q_dest}'; DESTDIR=\"$DEPOT_OUTPUT_DESTDIR\"; export DEPOT_OUTPUT_NAME DEPOT_OUTPUT_DESTDIR DESTDIR\n"
));
wrapper.push_str(&format!(
"if command -v depot_install_{fn_suffix} >/dev/null 2>&1; then depot_install_{fn_suffix};\n"
));
wrapper.push_str(&format!(
"elif command -v install_{fn_suffix} >/dev/null 2>&1; then install_{fn_suffix};\n"
));
if out.name == *primary {
wrapper
.push_str("elif command -v depot_install >/dev/null 2>&1; then depot_install;\n");
wrapper.push_str("elif command -v install >/dev/null 2>&1; then install;\n");
}
wrapper.push_str("fi\n");
}
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
cmd.arg("-c").arg(wrapper).arg("sh").arg(build_script);
Ok(cmd)
}
fn shell_fn_suffix(pkg_name: &str) -> String {
let mut out = String::with_capacity(pkg_name.len().max(1));
for ch in pkg_name.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
out.push('_');
}
if out.as_bytes().first().is_some_and(|b| b.is_ascii_digit()) {
out.insert(0, '_');
}
out
}
fn sh_single_quote(s: &str) -> String {
s.replace('\'', "'\"'\"'")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -146,6 +267,8 @@ mod tests {
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: std::path::PathBuf::from("."),
}
}
@@ -188,4 +311,56 @@ mod tests {
assert!(tmp_src.path().join("build.sh").exists());
Ok(())
}
#[test]
fn test_build_function_mode_uses_per_output_destdirs() -> Result<()> {
let tmp_src = tempdir()?;
let tmp_dest = tempdir()?;
let build_sh = tmp_src.path().join("build.sh");
std::fs::write(
&build_sh,
r#"#!/bin/sh
depot_build() {
:
}
depot_install() {
mkdir -p "$DESTDIR/usr/share"
echo primary > "$DESTDIR/usr/share/primary.txt"
}
depot_install_dev_pkg() {
mkdir -p "$DESTDIR/usr/include"
echo header > "$DESTDIR/usr/include/dev.h"
}
"#,
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&build_sh)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&build_sh, perms)?;
}
let mut spec = mk_spec("demo", "1.0");
spec.packages.push(crate::package::PackageInfo {
name: "dev-pkg".into(),
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
});
build(&spec, tmp_src.path(), tmp_dest.path(), None, true)?;
assert!(tmp_dest.path().join("usr/share/primary.txt").exists());
assert!(
tmp_dest
.path()
.join(".depot/outputs/dev-pkg/usr/include/dev.h")
.exists()
);
Ok(())
}
}
+15 -6
View File
@@ -26,12 +26,19 @@ pub fn build(
);
}
if !state.is_done(BuildStep::Configured) {
crate::source::hooks::run_post_configure_commands(spec, src_dir, destdir)?;
state.mark_done(BuildStep::Configured)?;
} else {
crate::log_info!("Skipping post-configure hooks (already done)");
}
if !state.is_done(BuildStep::PostCompileDone) {
println!("Running makefile build commands...");
crate::log_info!("Running makefile build commands...");
for cmd_str in &spec.build.flags.makefile_commands {
let cmd_str = spec.expand_vars(cmd_str);
println!(" Executing: {}", cmd_str);
crate::log_info!(" Executing: {}", cmd_str);
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(&cmd_str);
@@ -49,12 +56,12 @@ pub fn build(
crate::source::hooks::run_post_compile_commands(spec, src_dir, destdir)?;
state.mark_done(BuildStep::PostCompileDone)?;
} else {
println!("Skipping makefile build commands (already done)");
crate::log_info!("Skipping makefile build commands (already done)");
}
if !state.is_done(BuildStep::PostInstallDone) {
// Run install commands with fakeroot
println!(
crate::log_info!(
"Running makefile install commands{}...",
if crate::fakeroot::is_root() {
""
@@ -65,7 +72,7 @@ pub fn build(
for cmd_str in &spec.build.flags.makefile_install_commands {
let cmd_str = spec.expand_vars(cmd_str);
println!(" Executing: {}", cmd_str);
crate::log_info!(" Executing: {}", cmd_str);
// We need to run each command under fakeroot
let mut cmd = crate::fakeroot::wrap_install_command("sh", destdir);
@@ -90,7 +97,7 @@ pub fn build(
crate::source::hooks::run_post_install_commands(spec, src_dir, destdir)?;
state.mark_done(BuildStep::PostInstallDone)?;
} else {
println!("Skipping makefile install commands (already done)");
crate::log_info!("Skipping makefile install commands (already done)");
}
Ok(())
@@ -128,6 +135,8 @@ mod tests {
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: std::path::PathBuf::from("."),
}
}
+220 -26
View File
@@ -3,9 +3,9 @@
use crate::cross::CrossConfig;
use crate::fakeroot;
use crate::package::PackageSpec;
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn build(
@@ -16,7 +16,11 @@ pub fn build(
export_compiler_flags: bool,
) -> Result<()> {
let flags = &spec.build.flags;
let build_dir = src_dir.join("builddir");
// Determine actual source directory (support source_subdir)
let actual_src = resolve_actual_src(spec, src_dir)?;
let build_dir = resolve_build_dir(&actual_src, flags);
// Create directories
fs::create_dir_all(&build_dir)?;
@@ -25,14 +29,6 @@ pub fn build(
// Environment variables
let env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
// Extract prefix from configure flags
let prefix = flags
.configure
.iter()
.find(|s| s.starts_with("--prefix="))
.map(|s| s.trim_start_matches("--prefix="))
.unwrap_or(&flags.prefix);
// Generate cross file if cross-compiling
let cross_file = if let Some(cc_cfg) = cross {
Some(cc_cfg.generate_meson_cross_file(&build_dir)?)
@@ -41,21 +37,18 @@ pub fn build(
};
use crate::builder::state::{BuildStep, StateTracker};
let mut state = StateTracker::new(src_dir)?;
let mut state = StateTracker::new(&actual_src)?;
// Run meson setup
if !state.is_done(BuildStep::Configured) {
println!("Running meson setup...");
crate::log_info!("Running meson setup...");
let mut meson_cmd = Command::new("meson");
meson_cmd.current_dir(src_dir);
meson_cmd.current_dir(&actual_src);
meson_cmd.arg("setup");
meson_cmd.arg(&build_dir);
meson_cmd.arg(format!("--prefix={}", prefix));
meson_cmd.arg("--buildtype=release");
// Add cross file for cross-compilation
if let Some(ref cf) = cross_file {
meson_cmd.arg(format!("--cross-file={}", cf.display()));
for arg in meson_setup_args(flags, cross_file.as_deref(), &env_vars) {
meson_cmd.arg(arg);
}
crate::builder::prepare_command(&mut meson_cmd, &env_vars);
@@ -64,14 +57,16 @@ pub fn build(
if !status.success() {
anyhow::bail!("meson setup failed");
}
crate::source::hooks::run_post_configure_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::Configured)?;
} else {
println!("Skipping meson setup (already done)");
crate::log_info!("Skipping meson setup (already done)");
}
if !state.is_done(BuildStep::PostCompileDone) {
// Run ninja build
println!("Running ninja...");
crate::log_info!("Running ninja...");
let mut ninja_cmd = Command::new("ninja");
ninja_cmd.current_dir(&build_dir);
ninja_cmd.arg("-j").arg(num_cpus().to_string());
@@ -85,15 +80,15 @@ pub fn build(
anyhow::bail!("ninja build failed");
}
crate::source::hooks::run_post_compile_commands(spec, src_dir, destdir)?;
crate::source::hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostCompileDone)?;
} else {
println!("Skipping ninja build and post-compile hooks (already done)");
crate::log_info!("Skipping ninja build and post-compile hooks (already done)");
}
if !state.is_done(BuildStep::PostInstallDone) {
// Run meson install with fakeroot if not root
println!(
crate::log_info!(
"Running meson install{}...",
if fakeroot::is_root() {
""
@@ -120,10 +115,10 @@ pub fn build(
anyhow::bail!("meson install failed");
}
crate::source::hooks::run_post_install_commands(spec, src_dir, destdir)?;
crate::source::hooks::run_post_install_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostInstallDone)?;
} else {
println!("Skipping meson install and post-install hooks (already done)");
crate::log_info!("Skipping meson install and post-install hooks (already done)");
}
Ok(())
@@ -135,13 +130,212 @@ fn num_cpus() -> usize {
.unwrap_or(1)
}
fn resolve_build_dir(actual_src: &Path, flags: &crate::package::BuildFlags) -> PathBuf {
if let Some(dir) = flags
.build_dir
.as_ref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
{
actual_src.join(dir)
} else {
actual_src.join("builddir")
}
}
fn has_option(configure: &[String], long: &str) -> bool {
let prefix = format!("{long}=");
for arg in configure {
if arg == long || arg.starts_with(&prefix) {
return true;
}
}
false
}
fn meson_setup_args(
flags: &crate::package::BuildFlags,
cross_file: Option<&Path>,
env_vars: &[(String, String)],
) -> Vec<String> {
let mut args = Vec::new();
if !has_option(&flags.configure, "--prefix") {
args.push(format!("--prefix={}", flags.prefix));
}
if !has_option(&flags.configure, "--buildtype") {
args.push("--buildtype=release".to_string());
}
if let Some(cf) = cross_file {
args.push(format!("--cross-file={}", cf.display()));
}
// Append user flags last so they can override defaults when Meson allows it.
for arg in &flags.configure {
args.push(expand_with_envs(arg, env_vars));
}
args
}
/// Expand environment variables in a string (e.g., $DEPOT_SYSROOT)
fn expand_env_vars(input: &str) -> String {
let mut result = input.to_string();
// Simple expansion for $VAR and ${VAR} patterns using process environment only
for (key, value) in std::env::vars() {
result = result.replace(&format!("${key}"), &value);
result = result.replace(&format!("${{{key}}}"), &value);
}
result
}
/// Expand using a provided set of env vars (used to expand flags before spawning child).
fn expand_with_envs(input: &str, envs: &[(String, String)]) -> String {
let mut result = input.to_string();
for (k, v) in envs {
result = result.replace(&format!("${k}"), v);
result = result.replace(&format!("${{{k}}}"), v);
}
expand_env_vars(&result)
}
/// Resolve `source_subdir` with multiple fallbacks:
/// - empty -> use `src_dir`
/// - absolute path -> use if exists
/// - `src_dir/<sub>` -> use if exists
/// - `spec.spec_dir/<sub>` -> use if exists
/// - bare relative path (cwd)
fn resolve_actual_src(spec: &crate::package::PackageSpec, src_dir: &Path) -> Result<PathBuf> {
let source_subdir = spec.expand_vars(&spec.build.flags.source_subdir);
if source_subdir.is_empty() {
return Ok(src_dir.to_path_buf());
}
let candidate = Path::new(&source_subdir);
if candidate.is_absolute() {
if candidate.exists() {
return Ok(candidate.to_path_buf());
}
bail!(
"Source directory not found: {} (source_subdir: {} -> {})",
candidate.display(),
spec.build.flags.source_subdir,
source_subdir
);
}
let under_src = src_dir.join(&source_subdir);
if under_src.exists() {
return Ok(under_src);
}
let under_spec = spec.spec_dir.join(&source_subdir);
if under_spec.exists() {
return Ok(under_spec);
}
if candidate.exists() {
return Ok(candidate.to_path_buf());
}
bail!(
"Source directory not found: {} (expanded from '{}'; tried src_dir, spec_dir, and absolute path)",
source_subdir,
spec.build.flags.source_subdir
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source};
use tempfile::tempdir;
#[test]
fn test_num_cpus_at_least_one() {
let n = num_cpus();
assert!(n >= 1);
}
#[test]
fn test_meson_setup_args_include_configure_flags() {
let mut flags = BuildFlags {
prefix: "/usr".to_string(),
..BuildFlags::default()
};
flags.configure = vec!["-Dmanpages=false".to_string()];
let args = meson_setup_args(&flags, None, &[]);
assert!(args.iter().any(|a| a == "-Dmanpages=false"));
assert!(args.iter().any(|a| a == "--prefix=/usr"));
assert!(args.iter().any(|a| a == "--buildtype=release"));
}
#[test]
fn test_meson_setup_args_honor_explicit_prefix() {
let mut flags = BuildFlags::default();
flags.prefix = "/usr".to_string();
flags.configure = vec!["--prefix=/opt".to_string()];
let args = meson_setup_args(&flags, None, &[]);
assert_eq!(args.iter().filter(|a| a.starts_with("--prefix")).count(), 1);
assert!(args.iter().any(|a| a == "--prefix=/opt"));
}
#[test]
fn test_resolve_build_dir_uses_flag() {
let flags = BuildFlags {
build_dir: Some("build".to_string()),
..BuildFlags::default()
};
let src = Path::new("/tmp/src");
assert_eq!(
resolve_build_dir(src, &flags),
PathBuf::from("/tmp/src/build")
);
}
#[test]
fn test_resolve_actual_src_uses_source_subdir_under_source() -> Result<()> {
let src = tempdir()?;
let spec_dir = tempdir()?;
fs::create_dir_all(src.path().join("sub"))?;
let spec = PackageSpec {
package: PackageInfo {
name: "pkg".into(),
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "u".into(),
sha256: "s".into(),
extract_dir: "e".into(),
patches: Vec::new(),
post_extract: Vec::new(),
}],
build: Build {
build_type: BuildType::Meson,
flags: BuildFlags {
source_subdir: "sub".into(),
..BuildFlags::default()
},
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: spec_dir.path().to_path_buf(),
};
let resolved = resolve_actual_src(&spec, src.path())?;
assert_eq!(resolved, src.path().join("sub"));
Ok(())
}
}
+18 -4
View File
@@ -6,6 +6,7 @@ mod cmake;
mod custom;
mod makefile;
mod meson;
mod python;
mod rust;
pub mod state;
@@ -123,11 +124,15 @@ pub fn standard_build_env(
pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
cmd.env_clear();
// Preserve essential environment variables
for var in &["PATH", "LANG", "HOME", "SHELL", "DESTDIR", "DEPOT_ROOTFS"] {
for var in &["PATH", "LANG", "HOME", "DESTDIR", "DEPOT_ROOTFS"] {
if let Ok(val) = std::env::var(var) {
cmd.env(var, val);
}
}
// Use a deterministic POSIX shell for build tooling. Inheriting an
// interactive shell (e.g. zsh) can make Autotools-generated scripts
// produce non-reproducible or incompatible shell fragments.
cmd.env("SHELL", "/bin/sh");
// Set requested environment variables
for (key, val) in env_vars {
cmd.env(key, val);
@@ -143,12 +148,13 @@ pub fn build(
export_compiler_flags: bool,
) -> Result<()> {
if let Some(cc) = cross {
println!(
crate::log_info!(
"Cross-compiling for {} with {:?}...",
cc.prefix, spec.build.build_type
cc.prefix,
spec.build.build_type
);
} else {
println!("Building with {:?}...", spec.build.build_type);
crate::log_info!("Building with {:?}...", spec.build.build_type);
}
// Clean destdir to prevent stale files/directories (e.g., directories where symlinks should be)
@@ -163,6 +169,7 @@ pub fn build(
BuildType::CMake => cmake::build(spec, src_dir, destdir, cross, export_compiler_flags),
BuildType::Meson => meson::build(spec, src_dir, destdir, cross, export_compiler_flags),
BuildType::Custom => custom::build(spec, src_dir, destdir, cross, export_compiler_flags),
BuildType::Python => python::build(spec, src_dir, destdir, cross, export_compiler_flags),
BuildType::Rust => rust::build(spec, src_dir, destdir, cross, export_compiler_flags),
BuildType::Bin => bin::build(spec, src_dir, destdir, cross, export_compiler_flags),
BuildType::Makefile => {
@@ -206,6 +213,8 @@ mod tests {
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
@@ -219,6 +228,7 @@ mod tests {
unsafe {
std::env::set_var("PATH", "/usr/bin");
std::env::set_var("HOME", "/home/test");
std::env::set_var("SHELL", "/bin/zsh");
std::env::set_var("DEPOT_ROOTFS", "/my/rootfs");
}
@@ -228,6 +238,10 @@ mod tests {
assert!(envs.get(OsStr::new("PATH")).is_some());
assert!(envs.get(OsStr::new("HOME")).is_some());
assert!(envs.get(OsStr::new("FORBIDDEN")).is_none());
assert_eq!(
envs.get(OsStr::new("SHELL")),
Some(&Some(std::ffi::OsString::from("/bin/sh").as_os_str()))
);
assert_eq!(
envs.get(OsStr::new("MYVAR")),
Some(&Some(std::ffi::OsString::from("myval").as_os_str()))
+825
View File
@@ -0,0 +1,825 @@
//! Python build backend without external helper modules.
//!
//! This backend intentionally avoids `python -m build` and
//! `python -m installer`. It drives PEP 517 backends directly via
//! `python3` and installs wheel contents in Rust.
use crate::builder::state::{BuildStep, StateTracker};
use crate::cross::CrossConfig;
use crate::package::PackageSpec;
use crate::source::hooks;
use anyhow::{Context, Result, bail};
use std::fs;
use std::io::Read;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone)]
struct Pep517Config {
backend: String,
requires: Vec<String>,
backend_paths: Vec<PathBuf>,
}
enum BuildFrontend {
Pep517(Pep517Config),
LegacySetupPy,
}
pub fn build(
spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
export_compiler_flags: bool,
) -> Result<()> {
let flags = &spec.build.flags;
let actual_src = resolve_actual_src(spec, src_dir)?;
fs::create_dir_all(destdir)
.with_context(|| format!("Failed to create DESTDIR: {}", destdir.display()))?;
let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
crate::builder::set_env_var(&mut env_vars, "PYTHONNOUSERSITE", "1");
crate::builder::set_env_var(&mut env_vars, "PYTHONDONTWRITEBYTECODE", "1");
crate::builder::set_env_var(&mut env_vars, "SETUPTOOLS_USE_DISTUTILS", "local");
let mut state = StateTracker::new(&actual_src)?;
if !state.is_done(BuildStep::Configured) {
hooks::run_post_configure_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::Configured)?;
}
let dist_dir = actual_src.join("dist");
if !state.is_done(BuildStep::PostCompileDone) {
if dist_dir.exists() {
fs::remove_dir_all(&dist_dir)
.with_context(|| format!("Failed to clean dist dir: {}", dist_dir.display()))?;
}
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)?
}
BuildFrontend::LegacySetupPy => {
build_wheel_setup_py(&actual_src, &dist_dir, &env_vars)?;
}
}
hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostCompileDone)?;
} else {
crate::log_info!("Skipping python wheel build and post-compile hooks (already done)");
}
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)?;
}
hooks::run_post_install_commands(spec, &actual_src, destdir)?;
state.mark_done(BuildStep::PostInstallDone)?;
} else {
crate::log_info!("Skipping python wheel install and post-install hooks (already done)");
}
Ok(())
}
fn resolve_actual_src(spec: &PackageSpec, src_dir: &Path) -> Result<PathBuf> {
let source_subdir = spec.expand_vars(&spec.build.flags.source_subdir);
if source_subdir.is_empty() {
return Ok(src_dir.to_path_buf());
}
let candidate = Path::new(&source_subdir);
if candidate.is_absolute() {
if candidate.exists() {
return Ok(candidate.to_path_buf());
}
bail!(
"Source directory not found: {} (source_subdir: {} -> {})",
candidate.display(),
spec.build.flags.source_subdir,
source_subdir
);
}
let under_src = src_dir.join(&source_subdir);
if under_src.exists() {
return Ok(under_src);
}
let under_spec = spec.spec_dir.join(&source_subdir);
if under_spec.exists() {
return Ok(under_spec);
}
if candidate.exists() {
return Ok(candidate.to_path_buf());
}
bail!(
"Source directory not found: {} (expanded from '{}'; tried src_dir, spec_dir, and absolute path)",
source_subdir,
spec.build.flags.source_subdir
);
}
fn detect_frontend(src_dir: &Path) -> Result<BuildFrontend> {
let pyproject = src_dir.join("pyproject.toml");
if !pyproject.exists() {
if src_dir.join("setup.py").exists() {
return Ok(BuildFrontend::LegacySetupPy);
}
bail!(
"Python build requires pyproject.toml (PEP 517) or setup.py in {}",
src_dir.display()
);
}
let content = fs::read_to_string(&pyproject)
.with_context(|| format!("Failed to read {}", pyproject.display()))?;
let parsed: toml::Value = toml::from_str(&content)
.with_context(|| format!("Failed to parse {}", pyproject.display()))?;
let build_system = parsed.get("build-system").and_then(|v| v.as_table());
let backend = build_system
.and_then(|t| t.get("build-backend"))
.and_then(|v| v.as_str())
.unwrap_or("setuptools.build_meta:__legacy__")
.to_string();
let requires = match build_system.and_then(|t| t.get("requires")) {
Some(v) => {
let arr = v.as_array().with_context(|| {
format!(
"Invalid build-system.requires in {}: expected array",
pyproject.display()
)
})?;
arr.iter()
.map(|x| {
x.as_str().map(String::from).with_context(|| {
format!(
"Invalid build-system.requires entry in {}: expected string",
pyproject.display()
)
})
})
.collect::<Result<Vec<_>>>()?
}
None => Vec::new(),
};
let src_root = src_dir
.canonicalize()
.with_context(|| format!("Failed to canonicalize source dir {}", src_dir.display()))?;
let backend_paths = match build_system.and_then(|t| t.get("backend-path")) {
Some(v) => {
let arr = v.as_array().with_context(|| {
format!(
"Invalid build-system.backend-path in {}: expected array",
pyproject.display()
)
})?;
let mut out = Vec::with_capacity(arr.len());
for raw in arr {
let rel = raw.as_str().with_context(|| {
format!(
"Invalid build-system.backend-path entry in {}: expected string",
pyproject.display()
)
})?;
let rel_path = Path::new(rel);
if rel_path.is_absolute() {
bail!(
"Invalid build-system.backend-path entry in {}: absolute paths are not allowed ({})",
pyproject.display(),
rel
);
}
let joined = src_dir.join(rel_path);
let canon = joined.canonicalize().with_context(|| {
format!(
"build-system.backend-path entry not found in {}: {}",
pyproject.display(),
joined.display()
)
})?;
if !canon.starts_with(&src_root) {
bail!(
"Invalid build-system.backend-path entry in {}: path escapes source tree ({})",
pyproject.display(),
rel
);
}
out.push(canon);
}
out
}
None => Vec::new(),
};
Ok(BuildFrontend::Pep517(Pep517Config {
backend,
requires,
backend_paths,
}))
}
fn build_wheel_setup_py(
src_dir: &Path,
dist_dir: &Path,
env_vars: &[(String, String)],
) -> Result<()> {
crate::log_info!("Building wheel with setup.py...");
let mut cmd = Command::new("python3");
cmd.current_dir(src_dir)
.arg("setup.py")
.arg("bdist_wheel")
.arg("--dist-dir")
.arg(dist_dir);
crate::builder::prepare_command(&mut cmd, &env_vars.to_vec());
let status = cmd
.status()
.with_context(|| format!("Failed to run setup.py in {}", src_dir.display()))?;
if !status.success() {
bail!("setup.py bdist_wheel failed with status {}", status);
}
Ok(())
}
fn build_wheel_pep517(
src_dir: &Path,
dist_dir: &Path,
env_vars: &[(String, String)],
cfg: &Pep517Config,
) -> Result<()> {
crate::log_info!("Building wheel via PEP 517 backend {}...", cfg.backend);
let mut cmd = Command::new("python3");
cmd.current_dir(src_dir)
.arg("-c")
.arg(PEP517_BUILD_SNIPPET)
.arg(dist_dir);
let mut build_env = env_vars.to_vec();
crate::builder::set_env_var(&mut build_env, "DEPOT_PY_BACKEND", cfg.backend.clone());
crate::builder::set_env_var(
&mut build_env,
"DEPOT_PY_BACKEND_PATHS",
cfg.backend_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("\n"),
);
crate::builder::prepare_command(&mut cmd, &build_env);
let output = cmd.output().with_context(|| {
format!(
"Failed to run python3 for PEP 517 build in {}",
src_dir.display()
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let requires = if cfg.requires.is_empty() {
"(none declared)".to_string()
} else {
cfg.requires.join(", ")
};
bail!(
"PEP 517 wheel build failed with status {}. Backend: {}. Declared build requirements: {}. stderr: {}",
output.status,
cfg.backend,
requires,
stderr.trim()
);
}
Ok(())
}
fn collect_wheels(dist_dir: &Path) -> Result<Vec<PathBuf>> {
let mut wheels = Vec::new();
for entry in fs::read_dir(dist_dir).with_context(|| {
format!(
"Failed to read wheel output directory {}",
dist_dir.display()
)
})? {
let entry = entry?;
let path = entry.path();
if path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e == "whl")
{
wheels.push(path);
}
}
wheels.sort();
if wheels.is_empty() {
bail!("No wheel files produced in {}", dist_dir.display());
}
Ok(wheels)
}
fn detect_python_major_minor(env_vars: &[(String, String)]) -> Result<String> {
let mut cmd = Command::new("python3");
cmd.arg("-c")
.arg("import sys;print(f\"{sys.version_info[0]}.{sys.version_info[1]}\")");
crate::builder::prepare_command(&mut cmd, &env_vars.to_vec());
let output = cmd
.output()
.context("Failed to execute python3 for version detection")?;
if !output.status.success() {
bail!("python3 version probe failed with status {}", output.status);
}
let version = String::from_utf8(output.stdout)
.context("python3 version probe output was not valid UTF-8")?
.trim()
.to_string();
if version.split('.').count() != 2 || !version.chars().all(|c| c == '.' || c.is_ascii_digit()) {
bail!("Unexpected python3 version probe output: {}", version);
}
Ok(version)
}
fn normalized_prefix(prefix: &str) -> Result<PathBuf> {
let trimmed = prefix.trim();
let rel = trimmed.trim_start_matches('/');
let path = PathBuf::from(rel);
ensure_safe_relative_path(&path, "build.flags.prefix")?;
Ok(path)
}
fn site_packages_rel(prefix_rel: &Path, py_version: &str) -> PathBuf {
let mut out = PathBuf::new();
if !prefix_rel.as_os_str().is_empty() {
out.push(prefix_rel);
}
out.push("lib");
out.push(format!("python{}", py_version));
out.push("site-packages");
out
}
fn install_wheel(
wheel_path: &Path,
destdir: &Path,
prefix_rel: &Path,
py_version: &str,
) -> Result<()> {
crate::log_info!("Installing wheel: {}", wheel_path.display());
let site_packages = site_packages_rel(prefix_rel, py_version);
let file = fs::File::open(wheel_path)
.with_context(|| format!("Failed to open wheel {}", wheel_path.display()))?;
let mut archive = zip::ZipArchive::new(file)
.with_context(|| format!("Failed to read wheel zip {}", wheel_path.display()))?;
let mut entry_points_contents: Option<String> = None;
for i in 0..archive.len() {
let mut entry = archive.by_index(i).with_context(|| {
format!(
"Failed to read entry {} in wheel {}",
i,
wheel_path.display()
)
})?;
let rel = entry.enclosed_name().ok_or_else(|| {
anyhow::anyhow!(
"Unsafe wheel path in {}: {}",
wheel_path.display(),
entry.name()
)
})?;
let target_rel = map_wheel_path(&rel, prefix_rel, &site_packages)?;
let target_path = destdir.join(&target_rel);
if let Some(mode) = entry.unix_mode()
&& (mode & 0o170000) == 0o120000
{
bail!(
"Symlink entry is not allowed in wheel {}: {}",
wheel_path.display(),
rel.display()
);
}
if entry.is_dir() {
fs::create_dir_all(&target_path)
.with_context(|| format!("Failed to create directory {}", target_path.display()))?;
continue;
}
let mut data = Vec::new();
entry.read_to_end(&mut data).with_context(|| {
format!(
"Failed to read file content from wheel {}: {}",
wheel_path.display(),
rel.display()
)
})?;
if entry_points_contents.is_none()
&& is_top_level_dist_info_entry_points(&rel)
&& let Ok(s) = String::from_utf8(data.clone())
{
entry_points_contents = Some(s);
}
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
fs::write(&target_path, &data)
.with_context(|| format!("Failed to write {}", target_path.display()))?;
#[cfg(unix)]
if let Some(mode) = entry.unix_mode() {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&target_path)?.permissions();
perms.set_mode(mode & 0o777);
fs::set_permissions(&target_path, perms)?;
}
}
if let Some(contents) = entry_points_contents {
let scripts = parse_console_scripts(&contents)?;
write_entry_point_scripts(destdir, prefix_rel, &scripts)?;
}
Ok(())
}
fn is_top_level_dist_info_entry_points(rel: &Path) -> bool {
let parts = match path_parts(rel, "wheel entry") {
Ok(p) => p,
Err(_) => return false,
};
if parts.len() != 2 {
return false;
}
parts[0].ends_with(".dist-info") && parts[1] == "entry_points.txt"
}
fn map_wheel_path(rel: &Path, prefix_rel: &Path, site_packages: &Path) -> Result<PathBuf> {
let parts = path_parts(rel, "wheel path")?;
if parts.is_empty() {
bail!("Invalid empty wheel path");
}
if parts[0].ends_with(".data") {
if parts.len() < 3 {
bail!(
"Invalid wheel .data path (expected at least 3 components): {}",
rel.display()
);
}
let mut out = match parts[1] {
"purelib" | "platlib" => site_packages.to_path_buf(),
"scripts" => join_rel(prefix_rel, "bin"),
"headers" => join_rel(prefix_rel, "include"),
"data" => prefix_rel.to_path_buf(),
scheme => {
bail!(
"Unsupported wheel .data scheme '{}' in {}",
scheme,
rel.display()
)
}
};
for part in &parts[2..] {
out.push(part);
}
ensure_safe_relative_path(&out, "installed wheel path")?;
return Ok(out);
}
let mut out = site_packages.to_path_buf();
for part in parts {
out.push(part);
}
ensure_safe_relative_path(&out, "installed wheel path")?;
Ok(out)
}
fn join_rel(base: &Path, tail: &str) -> PathBuf {
let mut out = PathBuf::new();
if !base.as_os_str().is_empty() {
out.push(base);
}
out.push(tail);
out
}
fn path_parts<'a>(path: &'a Path, label: &str) -> Result<Vec<&'a str>> {
let mut parts = Vec::new();
for c in path.components() {
match c {
Component::Normal(seg) => {
let s = seg.to_str().with_context(|| {
format!("Non-UTF-8 {} component in {}", label, path.display())
})?;
parts.push(s);
}
Component::CurDir => {}
_ => bail!("Unsafe {} component in {}", label, path.display()),
}
}
Ok(parts)
}
fn ensure_safe_relative_path(path: &Path, label: &str) -> Result<()> {
if path.is_absolute() {
bail!("{} must be relative: {}", label, path.display());
}
for c in path.components() {
match c {
Component::Normal(_) => {}
Component::CurDir => {}
_ => bail!("Unsafe {} path: {}", label, path.display()),
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ConsoleScript {
name: String,
module: String,
attr_path: String,
}
fn parse_console_scripts(entry_points: &str) -> Result<Vec<ConsoleScript>> {
let mut in_console = false;
let mut scripts = Vec::new();
for (idx, raw) in entry_points.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
in_console = &line[1..line.len() - 1] == "console_scripts";
continue;
}
if !in_console {
continue;
}
let (name_raw, target_raw) = line
.split_once('=')
.with_context(|| format!("Invalid console_scripts entry on line {}", idx + 1))?;
let name = name_raw.trim();
if name.is_empty() || name.contains('/') || name.contains('\\') {
bail!(
"Invalid console_scripts script name '{}' on line {}",
name,
idx + 1
);
}
let mut target = target_raw.trim();
if let Some(i) = target.find('[') {
target = &target[..i];
target = target.trim();
}
let (module, attr_path) = target
.split_once(':')
.with_context(|| format!("Invalid console_scripts target on line {}", idx + 1))?;
let module = module.trim();
let attr_path = attr_path.trim();
if module.is_empty() || attr_path.is_empty() {
bail!(
"Invalid console_scripts target '{}' on line {}",
target,
idx + 1
);
}
scripts.push(ConsoleScript {
name: name.to_string(),
module: module.to_string(),
attr_path: attr_path.to_string(),
});
}
Ok(scripts)
}
fn write_entry_point_scripts(
destdir: &Path,
prefix_rel: &Path,
scripts: &[ConsoleScript],
) -> Result<()> {
if scripts.is_empty() {
return Ok(());
}
let scripts_dir = join_rel(prefix_rel, "bin");
let scripts_path = destdir.join(&scripts_dir);
fs::create_dir_all(&scripts_path)
.with_context(|| format!("Failed to create {}", scripts_path.display()))?;
for script in scripts {
let target = scripts_path.join(&script.name);
let module = py_single_quote_escape(&script.module);
let attrs = py_single_quote_escape(&script.attr_path);
let content = format!(
"#!/usr/bin/python3\nimport importlib\nimport sys\n\nobj = importlib.import_module('{module}')\nfor attr in '{attrs}'.split('.'):\n obj = getattr(obj, attr)\n\nif __name__ == '__main__':\n raise SystemExit(obj())\n",
);
fs::write(&target, content)
.with_context(|| format!("Failed to write {}", target.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&target)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&target, perms)?;
}
}
Ok(())
}
fn py_single_quote_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('\'', "\\'")
}
const PEP517_BUILD_SNIPPET: &str = r#"
import importlib
import os
import pathlib
import sys
dist_dir = pathlib.Path(sys.argv[1])
dist_dir.mkdir(parents=True, exist_ok=True)
backend_spec = os.environ.get("DEPOT_PY_BACKEND", "").strip()
if not backend_spec:
raise RuntimeError("DEPOT_PY_BACKEND is required")
paths = [p for p in os.environ.get("DEPOT_PY_BACKEND_PATHS", "").splitlines() if p]
for p in reversed(paths):
if p not in sys.path:
sys.path.insert(0, p)
module_name, _, object_path = backend_spec.partition(":")
backend = importlib.import_module(module_name)
if object_path:
for attr in object_path.split("."):
if attr:
backend = getattr(backend, attr)
config_settings = {}
if not hasattr(backend, "build_wheel"):
raise RuntimeError(f"Backend {backend_spec!r} does not provide build_wheel")
wheel_name = backend.build_wheel(str(dist_dir), config_settings, None)
if not wheel_name:
raise RuntimeError(f"Backend {backend_spec!r} returned an empty wheel name")
"#;
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source};
use std::io::Write;
use tempfile::tempdir;
use zip::write::SimpleFileOptions;
fn mk_spec() -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "py-test".into(),
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Default::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.test/src.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "src".into(),
patches: Vec::new(),
post_extract: Vec::new(),
}],
build: Build {
build_type: BuildType::Python,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
#[test]
fn parse_console_scripts_basic() -> Result<()> {
let text = r#"
[console_scripts]
hello = demo.cli:main
tool = pkg.mod:run [extra]
"#;
let scripts = parse_console_scripts(text)?;
assert_eq!(scripts.len(), 2);
assert_eq!(scripts[0].name, "hello");
assert_eq!(scripts[0].module, "demo.cli");
assert_eq!(scripts[0].attr_path, "main");
assert_eq!(scripts[1].name, "tool");
assert_eq!(scripts[1].attr_path, "run");
Ok(())
}
#[test]
fn map_wheel_data_paths() -> Result<()> {
let prefix = PathBuf::from("usr");
let site = PathBuf::from("usr/lib/python3.12/site-packages");
let mapped = map_wheel_path(Path::new("pkg-1.0.data/scripts/tool"), &prefix, &site)?;
assert_eq!(mapped, PathBuf::from("usr/bin/tool"));
Ok(())
}
#[test]
fn install_wheel_extracts_files_and_scripts() -> Result<()> {
let _spec = mk_spec();
let tmp = tempdir()?;
let wheel_path = tmp.path().join("demo-1.0-py3-none-any.whl");
let file = fs::File::create(&wheel_path)?;
let mut zip = zip::ZipWriter::new(file);
let file_opts = SimpleFileOptions::default().unix_permissions(0o644);
let exec_opts = SimpleFileOptions::default().unix_permissions(0o755);
zip.start_file("demo/__init__.py", file_opts)?;
zip.write_all(b"__all__ = []\n")?;
zip.start_file("demo-1.0.dist-info/WHEEL", file_opts)?;
zip.write_all(b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n")?;
zip.start_file("demo-1.0.dist-info/entry_points.txt", file_opts)?;
zip.write_all(b"[console_scripts]\ndemo = demo.__main__:main\n")?;
zip.start_file("demo-1.0.data/scripts/raw-script", exec_opts)?;
zip.write_all(b"#!/bin/sh\necho raw\n")?;
zip.finish()?;
let dest = tmp.path().join("dest");
fs::create_dir_all(&dest)?;
let prefix = PathBuf::from("usr");
install_wheel(&wheel_path, &dest, &prefix, "3.12")?;
assert!(
dest.join("usr/lib/python3.12/site-packages/demo/__init__.py")
.exists()
);
assert!(dest.join("usr/bin/raw-script").exists());
assert!(dest.join("usr/bin/demo").exists());
Ok(())
}
#[test]
fn install_wheel_ignores_nested_dist_info_entry_points() -> Result<()> {
let tmp = tempdir()?;
let wheel_path = tmp.path().join("demo-1.0-py3-none-any.whl");
let file = fs::File::create(&wheel_path)?;
let mut zip = zip::ZipWriter::new(file);
let file_opts = SimpleFileOptions::default().unix_permissions(0o644);
zip.start_file("demo/__init__.py", file_opts)?;
zip.write_all(b"__all__ = []\n")?;
zip.start_file("demo-1.0.dist-info/WHEEL", file_opts)?;
zip.write_all(b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n")?;
zip.start_file("demo-1.0.dist-info/entry_points.txt", file_opts)?;
zip.write_all(b"[console_scripts]\ndemo = demo.__main__:main\n")?;
zip.start_file(
"demo/vendor/wheel-0.1.dist-info/entry_points.txt",
file_opts,
)?;
zip.write_all(b"[console_scripts]\nwheel = wheel.cli:main\n")?;
zip.finish()?;
let dest = tmp.path().join("dest");
fs::create_dir_all(&dest)?;
let prefix = PathBuf::from("usr");
install_wheel(&wheel_path, &dest, &prefix, "3.12")?;
assert!(dest.join("usr/bin/demo").exists());
assert!(!dest.join("usr/bin/wheel").exists());
Ok(())
}
}
+5 -3
View File
@@ -73,8 +73,10 @@ pub fn build(
crate::builder::set_env_var(&mut env_vars, "RUSTUP_TOOLCHAIN", "stable");
}
hooks::run_post_configure_commands(spec, src_dir, destdir)?;
// Run cargo build
println!(
crate::log_info!(
"Running cargo build ({})...",
if is_release { "release" } else { "debug" }
);
@@ -110,7 +112,7 @@ pub fn build(
hooks::run_post_compile_commands(spec, src_dir, destdir)?;
// Install binaries to destdir
println!("Installing binaries to DESTDIR...");
crate::log_info!("Installing binaries to DESTDIR...");
// Determine target directory
let target_dir = if let Some(ref t) = target {
@@ -160,7 +162,7 @@ pub fn build(
.is_some()
{
let dest = bin_dir.join(&*file_name);
println!(" Installing: {}", file_name);
crate::log_info!(" Installing: {}", file_name);
fs::copy(&path, &dest).with_context(|| {
format!("Failed to copy {} to {}", path.display(), dest.display())
})?;