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:
+515
-58
@@ -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);
|
||||
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(&mut make_cmd, &flags.make_vars, "build")?;
|
||||
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);
|
||||
|
||||
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);
|
||||
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")?;
|
||||
if flags.skip_tests {
|
||||
crate::log_info!("Skipping tests: disabled by build.flags.skip_tests");
|
||||
} else {
|
||||
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 make {} in {}",
|
||||
test_target,
|
||||
build_dir.display()
|
||||
"Failed to run {} {} in {}",
|
||||
make_exec,
|
||||
test_targets_display,
|
||||
test_dir.display()
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("make {} failed with status: {}", test_target, status);
|
||||
anyhow::bail!(
|
||||
"{} {} failed with status: {} (dir: {})",
|
||||
make_exec,
|
||||
test_targets_display,
|
||||
status,
|
||||
test_dir.display()
|
||||
);
|
||||
}
|
||||
} else if flags.skip_tests {
|
||||
println!("Skipping tests: disabled by build.flags.skip_tests");
|
||||
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 {
|
||||
println!("Skipping tests: no 'check' or 'test' target in Makefile");
|
||||
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,13 +272,33 @@ 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") {
|
||||
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(&mut install_cmd, &flags.make_install_vars, "install")?;
|
||||
install_cmd.arg("install");
|
||||
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((
|
||||
@@ -202,19 +307,32 @@ pub fn build(
|
||||
));
|
||||
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);
|
||||
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
@@ -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("."),
|
||||
}
|
||||
}
|
||||
|
||||
+173
-6
@@ -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);
|
||||
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(),
|
||||
};
|
||||
|
||||
|
||||
+186
-11
@@ -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
@@ -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
@@ -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
@@ -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()))
|
||||
|
||||
@@ -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
@@ -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())
|
||||
})?;
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ impl Config {
|
||||
};
|
||||
|
||||
if let Err(e) = config.load_system(&abs_rootfs) {
|
||||
eprintln!("Warning: Failed to load system config: {}", e);
|
||||
crate::log_warn!("Failed to load system config: {}", e);
|
||||
}
|
||||
|
||||
config
|
||||
|
||||
+6
-6
@@ -57,12 +57,12 @@ impl CrossConfig {
|
||||
let readelf = find_tool(prefix, &["readelf", "llvm-readelf"], false)
|
||||
.unwrap_or_else(|_| format!("{}-readelf", prefix));
|
||||
|
||||
println!("Cross-compilation tools discovered:");
|
||||
println!(" CC: {}", cc);
|
||||
println!(" CXX: {}", cxx);
|
||||
println!(" AR: {}", ar);
|
||||
println!(" RANLIB: {}", ranlib);
|
||||
println!(" STRIP: {}", strip);
|
||||
crate::log_info!("Cross-compilation tools discovered:");
|
||||
crate::log_info!(" CC: {}", cc);
|
||||
crate::log_info!(" CXX: {}", cxx);
|
||||
crate::log_info!(" AR: {}", ar);
|
||||
crate::log_info!(" RANLIB: {}", ranlib);
|
||||
crate::log_info!(" STRIP: {}", strip);
|
||||
|
||||
Ok(Self {
|
||||
prefix: prefix.to_string(),
|
||||
|
||||
+25
-21
@@ -117,15 +117,17 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
||||
let disk_path = rootfs.join(f);
|
||||
if disk_path.exists() {
|
||||
let _ = std::fs::remove_file(&disk_path);
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Auto-removed conflicting path: {} (was owned by {})",
|
||||
f, owner
|
||||
f,
|
||||
owner
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Auto-cleared DB ownership for path: {} (previously owned by {})",
|
||||
f, owner
|
||||
f,
|
||||
owner
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -149,7 +151,7 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Registered {} files and {} directories in database",
|
||||
manifest.files.len(),
|
||||
manifest.directories.len()
|
||||
@@ -226,7 +228,7 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
||||
let path = rootfs.join(file);
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
println!(" Removed file: {}", file);
|
||||
crate::log_info!(" Removed file: {}", file);
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Already gone, keep going.
|
||||
@@ -255,14 +257,14 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
||||
)?;
|
||||
|
||||
if other_owners > 0 {
|
||||
println!(" Keeping directory (owned by other package): {}", dir);
|
||||
crate::log_info!(" Keeping directory (owned by other package): {}", dir);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to remove (will fail if not empty, which is fine)
|
||||
match fs::remove_dir(&path) {
|
||||
Ok(()) => {
|
||||
println!(" Removed directory: {}", dir);
|
||||
crate::log_info!(" Removed directory: {}", dir);
|
||||
dirs_removed += 1;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
@@ -270,7 +272,7 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
||||
}
|
||||
Err(e) if e.raw_os_error() == Some(39) || e.raw_os_error() == Some(66) => {
|
||||
// ENOTEMPTY (39 on Linux, 66 on macOS) - directory not empty
|
||||
println!(" Keeping directory (not empty): {}", dir);
|
||||
crate::log_info!(" Keeping directory (not empty): {}", dir);
|
||||
}
|
||||
Err(_) => {
|
||||
// Other errors (permission, etc.) - just skip silently
|
||||
@@ -290,16 +292,16 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
||||
)?;
|
||||
conn.execute("DELETE FROM packages WHERE id = ?1", params![pkg_id])?;
|
||||
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Removed {} files and {} directories",
|
||||
files.len(),
|
||||
dirs_removed
|
||||
);
|
||||
|
||||
if !removal_errors.is_empty() {
|
||||
eprintln!("Warning: failed to remove some paths:");
|
||||
crate::log_warn!("Failed to remove some paths:");
|
||||
for err in removal_errors {
|
||||
eprintln!(" {}", err);
|
||||
crate::log_warn!(" {}", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -321,10 +323,10 @@ pub fn show_package_info(db_path: &Path, name: &str) -> Result<()> {
|
||||
)
|
||||
.context(format!("Package '{}' not found", name))?;
|
||||
|
||||
println!("Package: {} v{}-{}", name, version, revision);
|
||||
println!("Description: {}", description);
|
||||
println!("Homepage: {}", homepage);
|
||||
println!("License: {}", license);
|
||||
crate::log_info!("Package: {} v{}-{}", name, version, revision);
|
||||
crate::log_info!("Description: {}", description);
|
||||
crate::log_info!("Homepage: {}", homepage);
|
||||
crate::log_info!("License: {}", license);
|
||||
|
||||
// Count files
|
||||
let file_count: i64 = conn.query_row(
|
||||
@@ -333,7 +335,7 @@ pub fn show_package_info(db_path: &Path, name: &str) -> Result<()> {
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
println!("Files: {}", file_count);
|
||||
crate::log_info!("Files: {}", file_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -341,7 +343,7 @@ pub fn show_package_info(db_path: &Path, name: &str) -> Result<()> {
|
||||
/// List all installed packages
|
||||
pub fn list_packages(db_path: &Path) -> Result<()> {
|
||||
if !db_path.exists() {
|
||||
println!("No packages installed.");
|
||||
crate::log_info!("No packages installed.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -352,12 +354,12 @@ pub fn list_packages(db_path: &Path) -> Result<()> {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?;
|
||||
|
||||
println!("{:<30} VERSION", "PACKAGE");
|
||||
println!("{}", "-".repeat(50));
|
||||
crate::log_info!("{:<30} VERSION", "PACKAGE");
|
||||
crate::log_info!("{}", "-".repeat(50));
|
||||
|
||||
for pkg in packages {
|
||||
let (name, version) = pkg?;
|
||||
println!("{:<30} {}", name, version);
|
||||
crate::log_info!("{:<30} {}", name, version);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -494,6 +496,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -94,7 +94,7 @@ impl RepoManager {
|
||||
}
|
||||
|
||||
fn index_package(&self, conn: &mut Connection, pkg_path: &Path) -> Result<()> {
|
||||
println!("Indexing package {}...", pkg_path.display());
|
||||
crate::log_info!("Indexing package {}...", pkg_path.display());
|
||||
|
||||
let filename = pkg_path.file_name().unwrap().to_string_lossy();
|
||||
let size = pkg_path.metadata()?.len();
|
||||
@@ -238,7 +238,7 @@ pub fn sync_mirrors(
|
||||
for (name, url) in mirrors {
|
||||
let target = base.join(name);
|
||||
if !target.exists() {
|
||||
println!("Cloning mirror '{}' -> {}", name, target.display());
|
||||
crate::log_info!("Cloning mirror '{}' -> {}", name, target.display());
|
||||
|
||||
// Use git2 RepoBuilder to clone
|
||||
let mut cb = RemoteCallbacks::new();
|
||||
@@ -256,7 +256,7 @@ pub fn sync_mirrors(
|
||||
.clone(url, &target)
|
||||
.with_context(|| format!("Failed to clone {}", url))?;
|
||||
} else {
|
||||
println!("Updating mirror '{}' in {}", name, target.display());
|
||||
crate::log_info!("Updating mirror '{}' in {}", name, target.display());
|
||||
// Open repository and fetch updates
|
||||
let repo = Repository::open(&target)
|
||||
.with_context(|| format!("Failed to open repository at {}", target.display()))?;
|
||||
@@ -310,15 +310,15 @@ pub fn mirrors_status(
|
||||
|
||||
let base = repo_dir.to_path_buf();
|
||||
if !base.exists() {
|
||||
println!("Repo base directory does not exist: {}", base.display());
|
||||
crate::log_info!("Repo base directory does not exist: {}", base.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (name, _url) in mirrors {
|
||||
let target = base.join(name);
|
||||
println!("--- {} ---", name);
|
||||
crate::log_info!("--- {} ---", name);
|
||||
if !target.exists() {
|
||||
println!("Not cloned: {}", target.display());
|
||||
crate::log_info!("Not cloned: {}", target.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ pub fn mirrors_status(
|
||||
let statuses = match repo.statuses(None) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
println!("Warning: failed to read status for {}", target.display());
|
||||
crate::log_warn!("Failed to read status for {}", target.display());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -360,16 +360,16 @@ pub fn mirrors_status(
|
||||
)
|
||||
});
|
||||
|
||||
println!("Path: {}", target.display());
|
||||
println!("Branch/HEAD: {}", branch);
|
||||
println!("HEAD OID: {}", short);
|
||||
crate::log_info!("Path: {}", target.display());
|
||||
crate::log_info!("Branch/HEAD: {}", branch);
|
||||
crate::log_info!("HEAD OID: {}", short);
|
||||
if !commit_time.is_empty() {
|
||||
println!("Latest commit time (epoch): {}", commit_time);
|
||||
crate::log_info!("Latest commit time (epoch): {}", commit_time);
|
||||
}
|
||||
println!("Dirty: {}", if dirty { "yes" } else { "no" });
|
||||
crate::log_info!("Dirty: {}", if dirty { "yes" } else { "no" });
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to open repo at {}: {}", target.display(), e);
|
||||
crate::log_info!("Failed to open repo at {}: {}", target.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+75
-9
@@ -10,6 +10,7 @@
|
||||
|
||||
use crate::db;
|
||||
use crate::package::PackageSpec;
|
||||
use crate::ui;
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -200,26 +201,35 @@ pub fn print_dep_status(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||
let missing_test = check_test_deps(spec, db_path)?;
|
||||
|
||||
if !spec.dependencies.build.is_empty() {
|
||||
println!("Build dependencies: {}", spec.dependencies.build.join(", "));
|
||||
ui::info(format!(
|
||||
"Build dependencies: {}",
|
||||
spec.dependencies.build.join(", ")
|
||||
));
|
||||
if !missing_build.is_empty() {
|
||||
println!(" Missing: {}", missing_build.join(", "));
|
||||
ui::warn(format!("Build deps missing: {}", missing_build.join(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
if !spec.dependencies.runtime.is_empty() {
|
||||
println!(
|
||||
ui::info(format!(
|
||||
"Runtime dependencies: {}",
|
||||
spec.dependencies.runtime.join(", ")
|
||||
);
|
||||
));
|
||||
if !missing_runtime.is_empty() {
|
||||
println!(" Missing: {}", missing_runtime.join(", "));
|
||||
ui::warn(format!(
|
||||
"Runtime deps missing: {}",
|
||||
missing_runtime.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !spec.dependencies.test.is_empty() {
|
||||
println!("Test dependencies: {}", spec.dependencies.test.join(", "));
|
||||
ui::info(format!(
|
||||
"Test dependencies: {}",
|
||||
spec.dependencies.test.join(", ")
|
||||
));
|
||||
if !missing_test.is_empty() {
|
||||
println!(" Missing: {}", missing_test.join(", "));
|
||||
ui::warn(format!("Test deps missing: {}", missing_test.join(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +242,7 @@ pub fn require_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||
|
||||
if !missing.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Missing build dependencies: {}\nInstall them first with: nyapm install <package>",
|
||||
"Missing build dependencies: {}\nInstall them first with: depot install <package>",
|
||||
missing.join(", ")
|
||||
);
|
||||
}
|
||||
@@ -246,7 +256,21 @@ pub fn require_test_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||
|
||||
if !missing.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Missing test dependencies: {}\nInstall them first with: nyapm install <package>",
|
||||
"Missing test dependencies: {}\nInstall them first with: depot install <package>",
|
||||
missing.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify all runtime dependencies are installed, error if not.
|
||||
pub fn require_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||
let missing = check_runtime_deps(spec, db_path)?;
|
||||
|
||||
if !missing.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Missing runtime dependencies: {}\nInstall them first with: depot install <package>",
|
||||
missing.join(", ")
|
||||
);
|
||||
}
|
||||
@@ -333,10 +357,52 @@ mod tests {
|
||||
runtime: Vec::new(),
|
||||
test: vec!["bats".into(), "python".into()],
|
||||
},
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: std::path::PathBuf::from("."),
|
||||
};
|
||||
|
||||
let missing = check_test_deps(&spec, Path::new("/definitely/not/a/real/db")).unwrap();
|
||||
assert_eq!(missing, vec!["bats".to_string(), "python".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_require_runtime_deps_errors_when_db_missing() {
|
||||
let spec = PackageSpec {
|
||||
package: crate::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![crate::package::Source {
|
||||
url: "https://example.test/foo.tar.gz".into(),
|
||||
sha256: "skip".into(),
|
||||
extract_dir: "foo".into(),
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
}],
|
||||
build: crate::package::Build {
|
||||
build_type: crate::package::BuildType::Custom,
|
||||
flags: crate::package::BuildFlags::default(),
|
||||
},
|
||||
dependencies: crate::package::Dependencies {
|
||||
build: Vec::new(),
|
||||
runtime: vec!["python".into()],
|
||||
test: Vec::new(),
|
||||
},
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: std::path::PathBuf::from("."),
|
||||
};
|
||||
|
||||
let err = require_runtime_deps(&spec, Path::new("/definitely/not/a/real/db"))
|
||||
.expect_err("runtime deps should be required");
|
||||
assert!(err.to_string().contains("Missing runtime dependencies"));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -83,7 +83,7 @@ impl PackageIndex {
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Indexed {} packages ({} provides)",
|
||||
index.by_name.len(),
|
||||
index.by_provides.len()
|
||||
@@ -102,8 +102,8 @@ impl PackageIndex {
|
||||
// Then try by provides
|
||||
if let Some(paths) = self.by_provides.get(name) {
|
||||
if paths.len() > 1 {
|
||||
eprintln!(
|
||||
"Warning: Multiple packages provide '{}': {:?}",
|
||||
crate::log_warn!(
|
||||
"Multiple packages provide '{}': {:?}",
|
||||
name,
|
||||
paths
|
||||
.iter()
|
||||
|
||||
+176
-7
@@ -9,6 +9,14 @@ use std::process::Command;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const STAGED_SCRIPTS_DIR: &str = "scripts";
|
||||
const ALL_HOOKS: [Hook; 6] = [
|
||||
Hook::PreInstall,
|
||||
Hook::PostInstall,
|
||||
Hook::PreUpdate,
|
||||
Hook::PostUpdate,
|
||||
Hook::PreRemove,
|
||||
Hook::PostRemove,
|
||||
];
|
||||
|
||||
/// Lifecycle hook names supported by Depot package scripts.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -54,16 +62,24 @@ impl Hook {
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_names(self) -> [String; 4] {
|
||||
fn candidate_names(self) -> [String; 6] {
|
||||
let canonical = self.canonical_name();
|
||||
let dashed = canonical.replace('_', "-");
|
||||
let compact = canonical.replace('_', "");
|
||||
[
|
||||
canonical.to_string(),
|
||||
format!("{}.sh", canonical),
|
||||
dashed.clone(),
|
||||
format!("{}.sh", dashed),
|
||||
compact.clone(),
|
||||
format!("{}.sh", compact),
|
||||
]
|
||||
}
|
||||
|
||||
fn legacy_root_candidate_names(self) -> [String; 2] {
|
||||
let compact = self.canonical_name().replace('_', "");
|
||||
[compact.clone(), format!("{}.sh", compact)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the staged scripts directory path inside a package staging tree.
|
||||
@@ -81,19 +97,26 @@ pub fn installed_scripts_dir(rootfs: &Path, pkg_name: &str) -> PathBuf {
|
||||
|
||||
/// Copy optional scripts from `<specdir>/scripts` into `<destdir>/scripts`.
|
||||
///
|
||||
/// Also stages legacy root hook files (for example `postinstall.sh`) into their
|
||||
/// canonical names under `<destdir>/scripts`.
|
||||
///
|
||||
/// Returns `true` if scripts were found and staged.
|
||||
pub fn stage_scripts_from_spec_dir(spec: &PackageSpec, destdir: &Path) -> Result<bool> {
|
||||
let source_dir = spec.spec_dir.join(STAGED_SCRIPTS_DIR);
|
||||
if !source_dir.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
if !source_dir.is_dir() {
|
||||
let has_scripts_dir = if source_dir.exists() { true } else { false };
|
||||
|
||||
if has_scripts_dir && !source_dir.is_dir() {
|
||||
bail!(
|
||||
"Scripts path exists but is not a directory: {}",
|
||||
source_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
let legacy_hooks = collect_legacy_root_hooks(&spec.spec_dir)?;
|
||||
if !has_scripts_dir && legacy_hooks.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let staged_dir = staged_scripts_dir(destdir);
|
||||
if staged_dir.exists() {
|
||||
remove_tree_or_file(&staged_dir).with_context(|| {
|
||||
@@ -104,8 +127,12 @@ pub fn stage_scripts_from_spec_dir(spec: &PackageSpec, destdir: &Path) -> Result
|
||||
})?;
|
||||
}
|
||||
|
||||
if has_scripts_dir {
|
||||
copy_tree(&source_dir, &staged_dir)
|
||||
.with_context(|| format!("Failed to stage scripts from {}", source_dir.display()))?;
|
||||
}
|
||||
|
||||
stage_legacy_root_hooks(&legacy_hooks, &staged_dir)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -180,7 +207,7 @@ pub fn run_hook_if_present(
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Running lifecycle hook {}: {}",
|
||||
hook.canonical_name(),
|
||||
script_path.display()
|
||||
@@ -216,7 +243,7 @@ fn run_script_with_rootfs_context(
|
||||
.arg(rootfs)
|
||||
.arg("/bin/sh")
|
||||
.arg("-c")
|
||||
.arg("cd / && exec \"$1\"")
|
||||
.arg("cd / && exec /bin/sh \"$1\"")
|
||||
.arg("sh")
|
||||
.arg(rel_script)
|
||||
.env("DEPOT_PACKAGE", pkg_name)
|
||||
@@ -305,6 +332,100 @@ fn resolve_hook_script(script_dir: &Path, hook: Hook) -> Result<Option<PathBuf>>
|
||||
Ok(found.into_iter().next())
|
||||
}
|
||||
|
||||
fn collect_legacy_root_hooks(spec_dir: &Path) -> Result<Vec<(Hook, PathBuf)>> {
|
||||
let mut found = Vec::new();
|
||||
|
||||
for hook in ALL_HOOKS {
|
||||
let mut hook_matches = Vec::new();
|
||||
for candidate in hook.legacy_root_candidate_names() {
|
||||
let path = spec_dir.join(candidate);
|
||||
let metadata = match path.symlink_metadata() {
|
||||
Ok(meta) => meta,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(e) => {
|
||||
return Err(e).with_context(|| {
|
||||
format!("Failed to inspect legacy hook script: {}", path.display())
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let file_type = metadata.file_type();
|
||||
if !file_type.is_file() && !file_type.is_symlink() {
|
||||
bail!(
|
||||
"Legacy lifecycle hook candidate exists but is not a file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
hook_matches.push(path);
|
||||
}
|
||||
|
||||
if hook_matches.len() > 1 {
|
||||
let names = hook_matches
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
bail!(
|
||||
"Ambiguous legacy lifecycle hook '{}': multiple script candidates found: {}",
|
||||
hook.canonical_name(),
|
||||
names
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(path) = hook_matches.into_iter().next() {
|
||||
found.push((hook, path));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn stage_legacy_root_hooks(legacy_hooks: &[(Hook, PathBuf)], staged_dir: &Path) -> Result<()> {
|
||||
if legacy_hooks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::create_dir_all(staged_dir)
|
||||
.with_context(|| format!("Failed to create directory: {}", staged_dir.display()))?;
|
||||
|
||||
for (hook, src_path) in legacy_hooks {
|
||||
let dst_path = staged_dir.join(hook.canonical_name());
|
||||
if dst_path.exists() {
|
||||
bail!(
|
||||
"Lifecycle hook '{}' is defined more than once (legacy root hook conflicts with scripts/ entry): {}",
|
||||
hook.canonical_name(),
|
||||
dst_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let metadata = src_path.symlink_metadata().with_context(|| {
|
||||
format!(
|
||||
"Failed to inspect legacy hook script: {}",
|
||||
src_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
if metadata.file_type().is_symlink() {
|
||||
let target = fs::read_link(src_path)
|
||||
.with_context(|| format!("Failed to read symlink: {}", src_path.display()))?;
|
||||
std::os::unix::fs::symlink(target, &dst_path)
|
||||
.with_context(|| format!("Failed to create symlink: {}", dst_path.display()))?;
|
||||
} else {
|
||||
fs::copy(src_path, &dst_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy legacy hook '{}' to '{}'",
|
||||
src_path.display(),
|
||||
dst_path.display()
|
||||
)
|
||||
})?;
|
||||
ensure_executable(&dst_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn safe_rel_path(rel: &Path) -> Result<PathBuf> {
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in rel.components() {
|
||||
@@ -477,6 +598,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: spec_dir.to_path_buf(),
|
||||
}
|
||||
}
|
||||
@@ -529,6 +652,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_hook_if_present_accepts_compact_script_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let scripts = tmp.path().join("scripts");
|
||||
let rootfs = tmp.path().join("root");
|
||||
std::fs::create_dir_all(&scripts).unwrap();
|
||||
std::fs::create_dir_all(&rootfs).unwrap();
|
||||
|
||||
std::fs::write(
|
||||
scripts.join("postinstall.sh"),
|
||||
"echo compact > \"$DEPOT_ROOTFS/hook.out\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs, "foo").unwrap();
|
||||
assert!(ran);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
|
||||
"compact\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_hook_if_present_rejects_ambiguous_names() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -590,4 +735,28 @@ mod tests {
|
||||
assert!(!has_scripts);
|
||||
assert!(!installed_scripts_dir(&rootfs, "foo").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_scripts_from_spec_dir_stages_legacy_root_hook() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let spec_dir = tmp.path().join("spec");
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(&spec_dir).unwrap();
|
||||
std::fs::create_dir_all(&destdir).unwrap();
|
||||
|
||||
std::fs::write(spec_dir.join("postinstall.sh"), "echo post").unwrap();
|
||||
|
||||
let spec = mk_spec(&spec_dir);
|
||||
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
|
||||
assert!(staged);
|
||||
assert!(destdir.join("scripts/post_install").exists());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mode = std::fs::metadata(destdir.join("scripts/post_install"))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode();
|
||||
assert_ne!(mode & 0o111, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+82
-81
@@ -10,8 +10,10 @@ mod fakeroot;
|
||||
mod index;
|
||||
mod install;
|
||||
mod package;
|
||||
mod shell_helpers;
|
||||
mod source;
|
||||
mod staging;
|
||||
mod ui;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -37,24 +39,30 @@ fn clean_build_workspace(config: &config::Config) -> Result<()> {
|
||||
fs::remove_dir_all(&config.build_dir).with_context(|| {
|
||||
format!("Failed to clean build dir: {}", config.build_dir.display())
|
||||
})?;
|
||||
println!("Cleaned build workspace: {}", config.build_dir.display());
|
||||
ui::success(format!(
|
||||
"Cleaned build workspace: {}",
|
||||
config.build_dir.display()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn warn_if_running_as_root_for_build(command: &str, rootfs: &Path) {
|
||||
if crate::fakeroot::is_root() {
|
||||
eprintln!(
|
||||
"\x1b[33mWARNING: Running '{}' as root is discouraged.\x1b[0m",
|
||||
command
|
||||
ui::warn(format!("Running '{}' as root is discouraged.", command));
|
||||
ui::warn(
|
||||
"A misconfigured build environment or malicious/buggy build file can overwrite or delete critical system files.",
|
||||
);
|
||||
eprintln!(
|
||||
"\x1b[33mA misconfigured build environment or malicious/buggy build file can overwrite or delete critical system files.\x1b[0m"
|
||||
);
|
||||
eprintln!(
|
||||
"\x1b[33mRecommendation: use a non-root build user and only install as root.\x1b[0m"
|
||||
);
|
||||
eprintln!("\x1b[33mCurrent rootfs target: {}\x1b[0m", rootfs.display());
|
||||
ui::warn("Recommendation: use a non-root build user and only install as root.");
|
||||
ui::warn(format!("Current rootfs target: {}", rootfs.display()));
|
||||
}
|
||||
}
|
||||
|
||||
fn output_destdir_for(base_destdir: &Path, primary_pkg: &str, output_pkg: &str) -> PathBuf {
|
||||
if output_pkg == primary_pkg {
|
||||
base_destdir.to_path_buf()
|
||||
} else {
|
||||
staging::output_staging_dir(base_destdir, output_pkg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,14 +122,10 @@ fn install_staged_to_rootfs(
|
||||
&pkg_spec.build.flags.keep,
|
||||
)?;
|
||||
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
spec_for_out.package = out;
|
||||
if let Err(e) = db::register_package(&db_path, &spec_for_out, destdir) {
|
||||
if let Err(e) = db::register_package(&db_path, pkg_spec, destdir) {
|
||||
let _ = tx.rollback();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
|
||||
install::scripts::sync_staged_scripts_to_rootfs(
|
||||
@@ -273,7 +277,7 @@ fn main() -> Result<()> {
|
||||
// try to locate a spec under configured repo dir or local packages/.
|
||||
if !spec_path.exists() {
|
||||
let name = spec_path.to_string_lossy().to_string();
|
||||
println!("Looking up package '{}' in local indexes...", name);
|
||||
ui::info(format!("Looking up package '{}' in local indexes...", name));
|
||||
let pkg_index =
|
||||
index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
|
||||
if let Some(found) = pkg_index.find(&name) {
|
||||
@@ -281,12 +285,12 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("Installing package from: {}", spec_path.display());
|
||||
ui::info(format!("Installing package from: {}", spec_path.display()));
|
||||
|
||||
let (pkg_spec, staging_dir): (package::PackageSpec, Option<tempfile::TempDir>) =
|
||||
if spec_path.to_string_lossy().ends_with(".tar.zst") {
|
||||
// Install from archive
|
||||
println!("Detected package archive: {}", spec_path.display());
|
||||
ui::info(format!("Detected package archive: {}", spec_path.display()));
|
||||
let tmp_dir = tempfile::TempDir::new()?;
|
||||
let extract_dir = tmp_dir.path().to_path_buf();
|
||||
|
||||
@@ -383,6 +387,8 @@ fn main() -> Result<()> {
|
||||
Vec::new()
|
||||
},
|
||||
},
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
@@ -402,10 +408,10 @@ fn main() -> Result<()> {
|
||||
(pkg_spec, None)
|
||||
};
|
||||
|
||||
println!(
|
||||
ui::info(format!(
|
||||
"Package: {} v{}-{}",
|
||||
pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision
|
||||
);
|
||||
));
|
||||
|
||||
// Ensure database directory exists
|
||||
std::fs::create_dir_all(&config.db_dir).with_context(|| {
|
||||
@@ -454,14 +460,8 @@ fn main() -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
println!("\nMissing dependencies: {}", missing.join(", "));
|
||||
println!("Do you want to attempt to install them? [Y/n] ");
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let input = input.trim().to_lowercase();
|
||||
|
||||
if input == "y" || input == "yes" || input.is_empty() {
|
||||
ui::warn(format!("Missing dependencies: {}", missing.join(", ")));
|
||||
if ui::prompt_yes_no("Attempt to install them now?", true)? {
|
||||
// Build package index for fast lookups
|
||||
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(
|
||||
config.repo_clone_dir.clone(),
|
||||
@@ -480,7 +480,7 @@ fn main() -> Result<()> {
|
||||
let candidate = pkg_index.find(&dep);
|
||||
|
||||
if let Some(dep_spec_path) = candidate {
|
||||
println!("Installing dependency: {}...", dep);
|
||||
ui::info(format!("Installing dependency: {}...", dep));
|
||||
|
||||
let mut cmd = std::process::Command::new(std::env::current_exe()?);
|
||||
cmd.arg("-r").arg(&cli.rootfs);
|
||||
@@ -516,22 +516,14 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce build dependencies (runtime deps are warnings only if not installed/prompt declined)
|
||||
// Enforce required dependencies before building/installing.
|
||||
deps::require_build_deps(&pkg_spec, &db_path)?;
|
||||
deps::require_runtime_deps(&pkg_spec, &db_path)?;
|
||||
if needs_test_deps {
|
||||
deps::require_test_deps(&pkg_spec, &db_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure database directory exists
|
||||
std::fs::create_dir_all(&config.db_dir).with_context(|| {
|
||||
format!(
|
||||
"Failed to create database directory: {}",
|
||||
config.db_dir.display()
|
||||
)
|
||||
})?;
|
||||
let db_path = config.db_dir.join("packages.db");
|
||||
|
||||
let destdir = if let Some(dir) = &staging_dir {
|
||||
dir.path().to_path_buf()
|
||||
} else {
|
||||
@@ -569,34 +561,28 @@ fn main() -> Result<()> {
|
||||
staging::process(&destdir, &pkg_spec)?;
|
||||
|
||||
// 5-6. Install/update to rootfs and register in DB
|
||||
install_staged_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
let output_name = out.name.clone();
|
||||
spec_for_out.package = out;
|
||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||
let out_destdir =
|
||||
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
|
||||
install_staged_to_rootfs(&spec_for_out, &out_destdir, &cli.rootfs, &config)?;
|
||||
|
||||
// 7. Check runtime dependencies (warn only)
|
||||
if !cli.no_deps {
|
||||
let missing_runtime = deps::check_runtime_deps(&pkg_spec, &db_path)?;
|
||||
if !missing_runtime.is_empty() {
|
||||
eprintln!(
|
||||
"\x1b[33mWarning: Missing runtime dependencies: {}\x1b[0m",
|
||||
missing_runtime.join(", ")
|
||||
);
|
||||
eprintln!(
|
||||
"\x1b[33mContinuing without runtime deps; binaries may not run correctly.\x1b[0m"
|
||||
);
|
||||
eprintln!("\x1b[33mUse --no-deps to suppress this warning.\x1b[0m");
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
ui::success(format!(
|
||||
"Successfully installed {} v{}",
|
||||
pkg_spec.package.name, pkg_spec.package.version
|
||||
);
|
||||
spec_for_out.package.name, spec_for_out.package.version
|
||||
));
|
||||
}
|
||||
|
||||
if cli.clean {
|
||||
clean_build_workspace(&config)?;
|
||||
}
|
||||
}
|
||||
Commands::Remove { package } => {
|
||||
println!("Removing package: {}", package);
|
||||
ui::info(format!("Removing package: {}", package));
|
||||
let config = config::Config::for_rootfs(&cli.rootfs);
|
||||
let db_path = config.db_dir.join("packages.db");
|
||||
let script_dir = install::scripts::installed_scripts_dir(&cli.rootfs, &package);
|
||||
@@ -616,7 +602,7 @@ fn main() -> Result<()> {
|
||||
let cleanup_scripts = install::scripts::remove_installed_scripts(&cli.rootfs, &package);
|
||||
post_remove?;
|
||||
cleanup_scripts?;
|
||||
println!("Successfully removed {}", package);
|
||||
ui::success(format!("Successfully removed {}", package));
|
||||
}
|
||||
Commands::Build {
|
||||
spec_pos,
|
||||
@@ -625,7 +611,7 @@ fn main() -> Result<()> {
|
||||
} => {
|
||||
warn_if_running_as_root_for_build("build", &cli.rootfs);
|
||||
let spec_path = spec.or(spec_pos).context("No spec file provided")?;
|
||||
println!("Building package from: {}", spec_path.display());
|
||||
ui::info(format!("Building package from: {}", spec_path.display()));
|
||||
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
|
||||
|
||||
let config = config::Config::for_rootfs(&cli.rootfs);
|
||||
@@ -687,23 +673,37 @@ fn main() -> Result<()> {
|
||||
let mut created_files = Vec::new();
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
let output_name = out.name.clone();
|
||||
spec_for_out.package = out;
|
||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||
let out_destdir =
|
||||
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
|
||||
let packager =
|
||||
package::Packager::new(spec_for_out.clone(), destdir.clone(), config.clone());
|
||||
package::Packager::new(spec_for_out.clone(), out_destdir, config.clone());
|
||||
let pkg_file = packager.create_package(Path::new("."), arch)?;
|
||||
created_files.push(pkg_file);
|
||||
}
|
||||
|
||||
for f in &created_files {
|
||||
println!("Build complete. Package created: {}", f.display());
|
||||
ui::success(format!("Build complete. Package created: {}", f.display()));
|
||||
}
|
||||
|
||||
if install {
|
||||
install_staged_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
|
||||
println!(
|
||||
for out in pkg_spec.outputs() {
|
||||
let mut spec_for_out = pkg_spec.clone();
|
||||
let output_name = out.name.clone();
|
||||
spec_for_out.package = out;
|
||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||
let out_destdir =
|
||||
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
|
||||
install_staged_to_rootfs(&spec_for_out, &out_destdir, &cli.rootfs, &config)?;
|
||||
ui::success(format!(
|
||||
"Successfully installed {} v{}",
|
||||
pkg_spec.package.name, pkg_spec.package.version
|
||||
);
|
||||
spec_for_out.package.name, spec_for_out.package.version
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if cli.clean {
|
||||
@@ -736,7 +736,10 @@ fn main() -> Result<()> {
|
||||
RepoCommands::Create { dir } => {
|
||||
let repo = db::repo::RepoManager::new(dir);
|
||||
let db_path = repo.create_repo_db()?;
|
||||
println!("Created repository database: {}", db_path.display());
|
||||
ui::success(format!(
|
||||
"Created repository database: {}",
|
||||
db_path.display()
|
||||
));
|
||||
}
|
||||
RepoCommands::Sync => {
|
||||
// Only root may run sync
|
||||
@@ -745,19 +748,19 @@ fn main() -> Result<()> {
|
||||
}
|
||||
let config = config::Config::for_rootfs(&cli.rootfs);
|
||||
if config.mirrors.is_empty() {
|
||||
println!("No mirrors configured in /etc/depot.d/mirrors.toml");
|
||||
ui::info("No mirrors configured in /etc/depot.d/mirrors.toml");
|
||||
} else {
|
||||
db::repo::sync_mirrors(&config.repo_clone_dir, &config.mirrors)?;
|
||||
println!(
|
||||
ui::success(format!(
|
||||
"Mirrors synchronized into {}",
|
||||
config.repo_clone_dir.display()
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
RepoCommands::Status => {
|
||||
let config = config::Config::for_rootfs(&cli.rootfs);
|
||||
if config.mirrors.is_empty() {
|
||||
println!("No mirrors configured in /etc/depot.d/mirrors.toml");
|
||||
ui::info("No mirrors configured in /etc/depot.d/mirrors.toml");
|
||||
} else {
|
||||
db::repo::mirrors_status(&config.repo_clone_dir, &config.mirrors)?;
|
||||
}
|
||||
@@ -786,19 +789,17 @@ fn main() -> Result<()> {
|
||||
output.unwrap_or_else(|| PathBuf::from(format!("{}.toml", spec.package.name)));
|
||||
|
||||
if output_path.exists() {
|
||||
println!(
|
||||
"Warning: File {} already exists. Overwrite? [y/N]",
|
||||
output_path.display()
|
||||
);
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if !input.trim().eq_ignore_ascii_case("y") {
|
||||
ui::warn(format!("File {} already exists.", output_path.display()));
|
||||
if !ui::prompt_yes_no("Overwrite it?", false)? {
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&output_path, toml_string)?;
|
||||
println!("Package specification saved to {}", output_path.display());
|
||||
ui::success(format!(
|
||||
"Package specification saved to {}",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+184
-3
@@ -17,6 +17,7 @@ impl fmt::Display for BuildType {
|
||||
BuildType::CMake => write!(f, "CMake"),
|
||||
BuildType::Meson => write!(f, "Meson"),
|
||||
BuildType::Custom => write!(f, "Custom"),
|
||||
BuildType::Python => write!(f, "Python"),
|
||||
BuildType::Rust => write!(f, "Rust"),
|
||||
BuildType::Makefile => write!(f, "Makefile"),
|
||||
BuildType::Bin => write!(f, "Binary installer"),
|
||||
@@ -177,8 +178,8 @@ fn expand_known_package_vars(input: &str, name: &str, version: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn create_interactive() -> Result<PackageSpec> {
|
||||
println!("Interactive Package Specification Creator");
|
||||
println!("-----------------------------------------");
|
||||
crate::log_info!("Interactive Package Specification Creator");
|
||||
crate::log_info!("-----------------------------------------");
|
||||
|
||||
// Ask early whether this is a GNU project so we can pre-fill homepage and
|
||||
// avoid repeating the same question later for autotools sources.
|
||||
@@ -237,6 +238,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
BuildType::CMake,
|
||||
BuildType::Meson,
|
||||
BuildType::Makefile,
|
||||
BuildType::Python,
|
||||
BuildType::Rust,
|
||||
BuildType::Custom,
|
||||
BuildType::Bin,
|
||||
@@ -409,6 +411,22 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
}
|
||||
|
||||
if show_advanced && matches!(build_type, BuildType::Autotools) {
|
||||
flags.configure_file = prompt_optional_text(
|
||||
"Configure script path (optional):",
|
||||
"Relative to source root, e.g. build-aux/configure",
|
||||
)?;
|
||||
flags.make_dirs = prompt_repeating_list(
|
||||
"Make dir (build phase)",
|
||||
"Relative to build dir, e.g. lib, libelf (empty = build root)",
|
||||
)?;
|
||||
flags.make_test_dirs = prompt_repeating_list(
|
||||
"Make dir (test phase)",
|
||||
"Relative to build dir, e.g. tests (empty = build root)",
|
||||
)?;
|
||||
flags.make_install_dirs = prompt_repeating_list(
|
||||
"Make dir (install phase)",
|
||||
"Relative to build dir, e.g. lib, apps (empty = build root)",
|
||||
)?;
|
||||
flags.skip_tests = Confirm::new("Skip automatic tests (make check/test)?")
|
||||
.with_help_message(
|
||||
"If enabled, Depot will not auto-run detected Autotools test targets",
|
||||
@@ -418,7 +436,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
}
|
||||
|
||||
if matches!(build_type, BuildType::Makefile) {
|
||||
println!("Define Makefile build and install commands.");
|
||||
crate::log_info!("Define Makefile build and install commands.");
|
||||
flags.makefile_commands = prompt_repeating_list(
|
||||
"makefile build command",
|
||||
"Examples: make -j$(nproc), make all",
|
||||
@@ -467,6 +485,10 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
}
|
||||
|
||||
if show_advanced {
|
||||
flags.post_configure = prompt_repeating_list(
|
||||
"post_configure command",
|
||||
"Runs after configure/setup, before build",
|
||||
)?;
|
||||
flags.post_compile =
|
||||
prompt_repeating_list("post_compile command", "Runs after build, before install")?;
|
||||
flags.post_install =
|
||||
@@ -501,6 +523,8 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
runtime: runtime_deps,
|
||||
test: test_deps,
|
||||
},
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
})
|
||||
}
|
||||
@@ -596,6 +620,7 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
BuildType::CMake => "cmake",
|
||||
BuildType::Meson => "meson",
|
||||
BuildType::Custom => "custom",
|
||||
BuildType::Python => "python",
|
||||
BuildType::Rust => "rust",
|
||||
BuildType::Makefile => "makefile",
|
||||
BuildType::Bin => "bin",
|
||||
@@ -655,6 +680,15 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
if spec.build.flags.no_flags != defaults.no_flags {
|
||||
flags_tbl.insert("no_flags".into(), Value::Boolean(spec.build.flags.no_flags));
|
||||
}
|
||||
if spec.build.flags.no_strip != defaults.no_strip {
|
||||
flags_tbl.insert("no_strip".into(), Value::Boolean(spec.build.flags.no_strip));
|
||||
}
|
||||
if spec.build.flags.no_compress_man != defaults.no_compress_man {
|
||||
flags_tbl.insert(
|
||||
"no_compress_man".into(),
|
||||
Value::Boolean(spec.build.flags.no_compress_man),
|
||||
);
|
||||
}
|
||||
if spec.build.flags.skip_tests != defaults.skip_tests {
|
||||
flags_tbl.insert(
|
||||
"skip_tests".into(),
|
||||
@@ -674,6 +708,12 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.configure_file.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"configure_file".into(),
|
||||
Value::String(spec.build.flags.configure_file.clone()),
|
||||
);
|
||||
}
|
||||
if spec.build.flags.prefix != defaults.prefix {
|
||||
flags_tbl.insert(
|
||||
"prefix".into(),
|
||||
@@ -723,6 +763,44 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_exec.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_exec".into(),
|
||||
Value::String(spec.build.flags.make_exec.clone()),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_target.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_target".into(),
|
||||
Value::String(spec.build.flags.make_target.clone()),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_targets.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_targets".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.make_targets
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_dirs.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_dirs".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.make_dirs
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_test_vars.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_test_vars".into(),
|
||||
@@ -736,6 +814,38 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_test_target.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_test_target".into(),
|
||||
Value::String(spec.build.flags.make_test_target.clone()),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_test_targets.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_test_targets".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.make_test_targets
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_test_dirs.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_test_dirs".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.make_test_dirs
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_install_vars.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_install_vars".into(),
|
||||
@@ -749,6 +859,38 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_install_target.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_install_target".into(),
|
||||
Value::String(spec.build.flags.make_install_target.clone()),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_install_targets.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_install_targets".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.make_install_targets
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.make_install_dirs.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"make_install_dirs".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.make_install_dirs
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.passthrough_env.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"passthrough_env".into(),
|
||||
@@ -775,6 +917,19 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.post_configure.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"post_configure".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.post_configure
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.post_install.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"post_install".into(),
|
||||
@@ -949,6 +1104,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
@@ -996,6 +1153,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
@@ -1030,6 +1189,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
@@ -1044,6 +1205,8 @@ mod tests {
|
||||
let mut flags = BuildFlags::default();
|
||||
flags.source_subdir = "project/subdir".into();
|
||||
flags.configure = vec!["--disable-static".into(), "--enable-foo".into()];
|
||||
flags.configure_file = "build-aux/configure".into();
|
||||
flags.post_configure = vec!["./configure-helper.sh".into()];
|
||||
flags.post_compile = vec!["make check".into()];
|
||||
flags.post_install = vec!["strip $DESTDIR/usr/bin/foo".into()];
|
||||
flags.makefile_commands = vec!["make".into()];
|
||||
@@ -1053,10 +1216,15 @@ mod tests {
|
||||
flags.target = "x86_64-unknown-linux-gnu".into();
|
||||
flags.keep = vec!["etc/locale.gen".into()];
|
||||
flags.no_flags = true;
|
||||
flags.no_strip = true;
|
||||
flags.no_compress_man = true;
|
||||
flags.skip_tests = true;
|
||||
flags.make_vars = vec!["V=1".into()];
|
||||
flags.make_dirs = vec!["lib".into(), "libelf".into()];
|
||||
flags.make_test_vars = vec!["TESTS=unit".into()];
|
||||
flags.make_test_dirs = vec!["tests".into()];
|
||||
flags.make_install_vars = vec!["STRIPPROG=true".into()];
|
||||
flags.make_install_dirs = vec!["lib".into(), "apps".into()];
|
||||
|
||||
let spec = PackageSpec {
|
||||
package: PackageInfo {
|
||||
@@ -1082,12 +1250,16 @@ mod tests {
|
||||
flags,
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
let toml = spec_to_minimal_toml(&spec).unwrap();
|
||||
assert!(toml.contains("source_subdir = \"project/subdir\""));
|
||||
assert!(toml.contains("configure = ["));
|
||||
assert!(toml.contains("configure_file = \"build-aux/configure\""));
|
||||
assert!(toml.contains("post_configure = ["));
|
||||
assert!(toml.contains("post_compile = ["));
|
||||
assert!(toml.contains("post_install = ["));
|
||||
assert!(toml.contains("makefile_commands = ["));
|
||||
@@ -1098,10 +1270,15 @@ mod tests {
|
||||
assert!(toml.contains("keep = ["));
|
||||
assert!(toml.contains("\"etc/locale.gen\""));
|
||||
assert!(toml.contains("no_flags = true"));
|
||||
assert!(toml.contains("no_strip = true"));
|
||||
assert!(toml.contains("no_compress_man = true"));
|
||||
assert!(toml.contains("skip_tests = true"));
|
||||
assert!(toml.contains("make_vars = ["));
|
||||
assert!(toml.contains("make_dirs = ["));
|
||||
assert!(toml.contains("make_test_vars = ["));
|
||||
assert!(toml.contains("make_test_dirs = ["));
|
||||
assert!(toml.contains("make_install_vars = ["));
|
||||
assert!(toml.contains("make_install_dirs = ["));
|
||||
assert!(toml.contains("patches = ["));
|
||||
assert!(toml.contains("post_extract = ["));
|
||||
}
|
||||
@@ -1132,6 +1309,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
@@ -1169,6 +1348,8 @@ mod tests {
|
||||
runtime: vec![],
|
||||
test: vec!["python".into(), "bats".into()],
|
||||
},
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
|
||||
+35
-2
@@ -8,6 +8,14 @@ use std::path::{Path, PathBuf};
|
||||
use tar::Builder;
|
||||
use zstd::stream::write::Encoder;
|
||||
|
||||
fn is_internal_staging_rel_path(rel_path: &Path) -> bool {
|
||||
let s = rel_path.to_string_lossy();
|
||||
let p = s.trim_start_matches('/');
|
||||
p == crate::staging::INTERNAL_DEPOT_DIR
|
||||
|| p.strip_prefix(crate::staging::INTERNAL_DEPOT_DIR)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
}
|
||||
|
||||
fn license_value(licenses: &[String]) -> toml::Value {
|
||||
if licenses.len() == 1 {
|
||||
toml::Value::String(licenses[0].clone())
|
||||
@@ -41,7 +49,7 @@ impl Packager {
|
||||
let filename = self.spec.package_filename(arch);
|
||||
let output_path = output_dir.join(&filename);
|
||||
|
||||
println!("Creating package {}...", filename);
|
||||
crate::log_info!("Creating package {}...", filename);
|
||||
|
||||
// Generate .files.yaml
|
||||
self.generate_files_yaml()?;
|
||||
@@ -78,6 +86,9 @@ impl Packager {
|
||||
if rel_path.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if is_internal_staging_rel_path(rel_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_type = entry.file_type();
|
||||
if file_type.is_dir() {
|
||||
@@ -101,7 +112,7 @@ impl Packager {
|
||||
let encoder = tar.into_inner()?;
|
||||
encoder.finish()?;
|
||||
|
||||
println!("Created package: {}", output_path.display());
|
||||
crate::log_info!("Created package: {}", output_path.display());
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
@@ -222,6 +233,9 @@ impl Packager {
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type()?;
|
||||
let relative = path.strip_prefix(base)?.to_string_lossy().to_string();
|
||||
if is_internal_staging_rel_path(Path::new(&relative)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if file_type.is_dir() {
|
||||
self.collect_files(base, &path, files)?;
|
||||
@@ -264,6 +278,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
},
|
||||
destdir,
|
||||
@@ -289,6 +305,23 @@ mod tests {
|
||||
assert!(files.contains(&"etc/config".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_files_skips_internal_output_staging() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path();
|
||||
fs::create_dir_all(dest.join("usr/bin")).unwrap();
|
||||
fs::create_dir_all(dest.join(".depot/outputs/clang/usr/bin")).unwrap();
|
||||
fs::write(dest.join("usr/bin/foo"), "x").unwrap();
|
||||
fs::write(dest.join(".depot/outputs/clang/usr/bin/clang"), "x").unwrap();
|
||||
|
||||
let packager = mk_packager(dest.to_path_buf());
|
||||
let mut files = Vec::new();
|
||||
packager.collect_files(dest, dest, &mut files).unwrap();
|
||||
|
||||
assert!(files.contains(&"usr/bin/foo".to_string()));
|
||||
assert!(!files.iter().any(|f| f.starts_with(".depot/outputs/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_files_yaml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
+855
-11
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
||||
//! Shell helper commands exposed to package build/post-install scripts.
|
||||
|
||||
use crate::builder::{EnvVars, set_env_var};
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Internal staging namespace reserved inside `DESTDIR`.
|
||||
pub const INTERNAL_DEPOT_DIR: &str = ".depot";
|
||||
/// Internal split-output staging root inside `DESTDIR`.
|
||||
pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs";
|
||||
|
||||
/// Ephemeral helper command directory to prepend to PATH while running scripts.
|
||||
pub struct ShellHelpers {
|
||||
_tempdir: TempDir,
|
||||
path_value: String,
|
||||
outputs_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ShellHelpers {
|
||||
/// Create helper commands for a given staging tree (`DESTDIR`).
|
||||
pub fn new(destdir: &Path) -> Result<Self> {
|
||||
let tempdir = tempfile::tempdir().context("Failed to create shell helper tempdir")?;
|
||||
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 haul_path = bin_dir.join("haul");
|
||||
fs::write(&haul_path, HAUL_SCRIPT).with_context(|| {
|
||||
format!(
|
||||
"Failed to write shell helper command: {}",
|
||||
haul_path.display()
|
||||
)
|
||||
})?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&haul_path)
|
||||
.with_context(|| format!("Failed to stat helper: {}", haul_path.display()))?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&haul_path, perms)
|
||||
.with_context(|| format!("Failed to chmod helper: {}", haul_path.display()))?;
|
||||
}
|
||||
|
||||
let subdestdir_path = bin_dir.join("subdestdir");
|
||||
fs::write(&subdestdir_path, SUBDESTDIR_SCRIPT).with_context(|| {
|
||||
format!(
|
||||
"Failed to write shell helper command: {}",
|
||||
subdestdir_path.display()
|
||||
)
|
||||
})?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&subdestdir_path)
|
||||
.with_context(|| format!("Failed to stat helper: {}", subdestdir_path.display()))?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&subdestdir_path, perms).with_context(|| {
|
||||
format!("Failed to chmod helper: {}", subdestdir_path.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let parent_path = std::env::var("PATH").unwrap_or_default();
|
||||
let helper_path = bin_dir.to_string_lossy().into_owned();
|
||||
let path_value = if parent_path.is_empty() {
|
||||
helper_path
|
||||
} else {
|
||||
format!("{}:{}", helper_path, parent_path)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
_tempdir: tempdir,
|
||||
path_value,
|
||||
outputs_dir: destdir.join(INTERNAL_OUTPUTS_DIR),
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply helper-related variables to an environment vector used with `prepare_command`.
|
||||
pub fn apply_to_env_vars(&self, env_vars: &mut EnvVars) {
|
||||
set_env_var(env_vars, "PATH", self.path_value.clone());
|
||||
set_env_var(
|
||||
env_vars,
|
||||
"DEPOT_OUTPUTS_DIR",
|
||||
self.outputs_dir.to_string_lossy().into_owned(),
|
||||
);
|
||||
set_env_var(env_vars, "DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR);
|
||||
}
|
||||
|
||||
/// Apply helper-related variables directly to a `std::process::Command`.
|
||||
pub fn apply_to_command(&self, cmd: &mut std::process::Command) {
|
||||
cmd.env("PATH", &self.path_value)
|
||||
.env("DEPOT_OUTPUTS_DIR", &self.outputs_dir)
|
||||
.env("DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a package name into a safe shell identifier suffix.
|
||||
pub fn shell_ident_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() {
|
||||
out.push(ch.to_ascii_uppercase());
|
||||
} 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
|
||||
}
|
||||
|
||||
const HAUL_SCRIPT: &str = r#"#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "Usage: haul <output-package> <path-pattern> [path-pattern ...]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "haul: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$#" -ge 2 ] || usage
|
||||
|
||||
out_pkg=$1
|
||||
shift
|
||||
|
||||
case "$out_pkg" in
|
||||
""|.|..|*/*) fail "invalid output package name: $out_pkg" ;;
|
||||
esac
|
||||
|
||||
[ "${DESTDIR:-}" != "" ] || fail "DESTDIR is not set"
|
||||
[ "${DEPOT_OUTPUTS_DIR:-}" != "" ] || fail "DEPOT_OUTPUTS_DIR is not set"
|
||||
|
||||
src_root=$DESTDIR
|
||||
out_root=$DEPOT_OUTPUTS_DIR/$out_pkg
|
||||
|
||||
case "$src_root" in
|
||||
*/) src_root=${src_root%/} ;;
|
||||
esac
|
||||
case "$out_root" in
|
||||
*/) out_root=${out_root%/} ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$out_root"
|
||||
|
||||
tmp_matches=$(mktemp "${TMPDIR:-/tmp}/depot-haul.XXXXXX")
|
||||
trap 'rm -f "$tmp_matches"' EXIT HUP INT TERM
|
||||
: > "$tmp_matches"
|
||||
|
||||
collect_matches_for_pattern() {
|
||||
pattern=$1
|
||||
before_count=$(wc -l < "$tmp_matches" | tr -d '[:space:]')
|
||||
case "$pattern" in
|
||||
""|/|/*|../*|*/../*|..)
|
||||
fail "unsafe pattern: $pattern"
|
||||
;;
|
||||
.depot|.depot/*)
|
||||
fail "refusing to haul internal staging paths: $pattern"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -d "$src_root" ]; then
|
||||
fail "DESTDIR does not exist: $src_root"
|
||||
fi
|
||||
|
||||
find "$src_root" -mindepth 1 \
|
||||
! -path "$DEPOT_OUTPUTS_DIR" ! -path "$DEPOT_OUTPUTS_DIR/*" \
|
||||
-print | LC_ALL=C sort |
|
||||
while IFS= read -r abs_path; do
|
||||
rel_path=${abs_path#"$src_root"/}
|
||||
[ "$rel_path" != "$abs_path" ] || continue
|
||||
case "$rel_path" in
|
||||
$pattern)
|
||||
printf '%s\n' "$rel_path" >> "$tmp_matches"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
after_count=$(wc -l < "$tmp_matches" | tr -d '[:space:]')
|
||||
if [ "$before_count" = "$after_count" ]; then
|
||||
fail "no matches for pattern: $pattern"
|
||||
fi
|
||||
}
|
||||
|
||||
for pattern in "$@"; do
|
||||
collect_matches_for_pattern "$pattern"
|
||||
done
|
||||
|
||||
LC_ALL=C sort -u "$tmp_matches" | while IFS= read -r rel_path; do
|
||||
[ -n "$rel_path" ] || continue
|
||||
src_path=$src_root/$rel_path
|
||||
|
||||
if [ ! -e "$src_path" ] && [ ! -L "$src_path" ]; then
|
||||
# Path may already have been moved because an ancestor directory matched.
|
||||
continue
|
||||
fi
|
||||
|
||||
dst_path=$out_root/$rel_path
|
||||
dst_parent=$(dirname "$dst_path")
|
||||
mkdir -p "$dst_parent"
|
||||
|
||||
if [ -e "$dst_path" ] || [ -L "$dst_path" ]; then
|
||||
fail "destination already exists: $dst_path"
|
||||
fi
|
||||
|
||||
mv "$src_path" "$dst_path"
|
||||
done
|
||||
|
||||
# Clean empty directories left behind in the primary staging tree, but never
|
||||
# touch the internal depot namespace.
|
||||
find "$src_root" -depth -type d \
|
||||
! -path "$src_root" \
|
||||
! -path "$src_root/.depot" ! -path "$src_root/.depot/*" \
|
||||
-empty -exec rmdir {} + 2>/dev/null || true
|
||||
"#;
|
||||
|
||||
const SUBDESTDIR_SCRIPT: &str = r#"#!/bin/sh
|
||||
set -eu
|
||||
|
||||
fail() {
|
||||
echo "subdestdir: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$#" -eq 1 ] || fail "usage: subdestdir <output-package>"
|
||||
[ "${DEPOT_OUTPUTS_DIR:-}" != "" ] || fail "DEPOT_OUTPUTS_DIR is not set"
|
||||
|
||||
pkg=$1
|
||||
case "$pkg" in
|
||||
""|.|..|*/*) fail "invalid output package name: $pkg" ;;
|
||||
esac
|
||||
|
||||
path=$DEPOT_OUTPUTS_DIR/$pkg
|
||||
mkdir -p "$path"
|
||||
printf '%s\n' "$path"
|
||||
"#;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::shell_ident_suffix;
|
||||
|
||||
#[test]
|
||||
fn shell_ident_suffix_normalizes_package_names() {
|
||||
assert_eq!(shell_ident_suffix("clang"), "CLANG");
|
||||
assert_eq!(shell_ident_suffix("llvm-libgcc"), "LLVM_LIBGCC");
|
||||
assert_eq!(shell_ident_suffix("3foo"), "_3FOO");
|
||||
assert_eq!(shell_ident_suffix("foo.bar+baz"), "FOO_BAR_BAZ");
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ pub fn extract_archive(
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
println!("Extracting: {}", filename);
|
||||
crate::log_info!("Extracting: {}", filename);
|
||||
|
||||
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
||||
extract_tar_gz(archive_path, &extract_path)?;
|
||||
@@ -70,7 +70,7 @@ pub fn extract_archive(
|
||||
);
|
||||
}
|
||||
|
||||
println!("Extracted to: {}", extract_path.display());
|
||||
crate::log_info!("Extracted to: {}", extract_path.display());
|
||||
Ok(extract_path)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
|
||||
// Check if already cached and verified
|
||||
if dest_path.exists() && verify_checksum(&dest_path, &source.sha256)? {
|
||||
println!("Using cached source: {}", dest_path.display());
|
||||
crate::log_info!("Using cached source: {}", dest_path.display());
|
||||
return Ok(dest_path);
|
||||
}
|
||||
|
||||
println!("Fetching: {}", url);
|
||||
crate::log_info!("Fetching: {}", url);
|
||||
|
||||
// Parse URL early so we can handle non-HTTP schemes (FTP support)
|
||||
let parsed_url = Url::parse(&url).with_context(|| format!("Invalid URL: {}", url))?;
|
||||
@@ -165,7 +165,7 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
if !seen_alts.insert(alt.clone()) {
|
||||
bail!("Mirror retry loop detected for URL: {}", alt);
|
||||
}
|
||||
println!("Retrying download from mirror: {}", alt);
|
||||
crate::log_info!("Retrying download from mirror: {}", alt);
|
||||
fs::remove_file(&dest_path).ok();
|
||||
|
||||
// If mirror URL is FTP -> use suppaftp; otherwise use HTTP retry.
|
||||
@@ -280,7 +280,7 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
bail!("Checksum verification failed for {}", filename);
|
||||
}
|
||||
|
||||
println!("Checksum verified: {}", filename);
|
||||
crate::log_info!("Checksum verified: {}", filename);
|
||||
Ok(dest_path)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ pub fn checkout(
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid mirror path"))?;
|
||||
|
||||
println!("Cloning git source into {}...", checkout_dir.display());
|
||||
crate::log_info!("Cloning git source into {}...", checkout_dir.display());
|
||||
Repository::clone(mirror_url, checkout_dir)
|
||||
.with_context(|| format!("Failed to clone from mirror for {}", url))?;
|
||||
|
||||
@@ -62,7 +62,7 @@ fn mirror_key(url: &str) -> String {
|
||||
|
||||
fn ensure_mirror(url: &str, mirror_dir: &Path, pkgname: &str) -> Result<()> {
|
||||
if !mirror_dir.exists() {
|
||||
println!("Cloning git mirror for {} ({})...", pkgname, url);
|
||||
crate::log_info!("Cloning git mirror for {} ({})...", pkgname, url);
|
||||
let mut fo = FetchOptions::new();
|
||||
fo.remote_callbacks(remote_callbacks());
|
||||
let mut builder = git2::build::RepoBuilder::new();
|
||||
|
||||
+95
-23
@@ -50,13 +50,13 @@ fn apply_patches(
|
||||
)
|
||||
})?;
|
||||
|
||||
println!("Applying {} patch(es)...", source.patches.len());
|
||||
crate::log_info!("Applying {} patch(es)...", source.patches.len());
|
||||
|
||||
for p in &source.patches {
|
||||
let p = spec.expand_vars(p);
|
||||
let patch_path = resolve_patch_path(spec, &p, &patch_cache_dir)?;
|
||||
|
||||
println!(" patch: {}", patch_path.display());
|
||||
crate::log_info!(" patch: {}", patch_path.display());
|
||||
|
||||
// Apply with patch(1). Keep it simple: -p1 is the common case.
|
||||
let status = Command::new("patch")
|
||||
@@ -80,14 +80,14 @@ fn run_post_extract_commands(spec: &PackageSpec, source: &Source, src_dir: &Path
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Running {} post-extract command(s)...",
|
||||
source.post_extract.len()
|
||||
);
|
||||
|
||||
for cmd in &source.post_extract {
|
||||
let cmd = spec.expand_vars(cmd);
|
||||
println!(" post_extract: {}", cmd);
|
||||
crate::log_info!(" post_extract: {}", cmd);
|
||||
|
||||
// Use a shell for convenience; this is a package manager, so specs are trusted input.
|
||||
let status = Command::new("sh")
|
||||
@@ -107,32 +107,75 @@ fn run_post_extract_commands(spec: &PackageSpec, source: &Source, src_dir: &Path
|
||||
}
|
||||
|
||||
/// Run post-compile commands (after make, before make install).
|
||||
pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &Path) -> Result<()> {
|
||||
let commands = &spec.build.flags.post_compile;
|
||||
pub fn run_post_configure_commands(
|
||||
spec: &PackageSpec,
|
||||
src_dir: &Path,
|
||||
destdir: &Path,
|
||||
) -> Result<()> {
|
||||
let commands = &spec.build.flags.post_configure;
|
||||
if commands.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Running {} post-compile command(s)...", commands.len());
|
||||
crate::log_info!("Running {} post-configure command(s)...", commands.len());
|
||||
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
|
||||
|
||||
for cmd in commands {
|
||||
let cmd = spec.expand_vars(cmd);
|
||||
println!(" post_compile: {}", cmd);
|
||||
let cmd_str = spec.expand_vars(cmd);
|
||||
crate::log_info!(" post_configure: {}", cmd_str);
|
||||
|
||||
let status = Command::new("sh")
|
||||
.current_dir(src_dir)
|
||||
let mut shell_cmd = Command::new("sh");
|
||||
shell_cmd.current_dir(src_dir);
|
||||
shell_helpers.apply_to_command(&mut shell_cmd);
|
||||
let status = shell_cmd
|
||||
.env("DEPOT_SPECDIR", &spec.spec_dir)
|
||||
.env("DESTDIR", destdir)
|
||||
.env("DEPOT_ROOTFS", &spec.build.flags.rootfs)
|
||||
.env("CC", &spec.build.flags.cc)
|
||||
.env("AR", &spec.build.flags.ar)
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.arg(&cmd_str)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to run post_compile command: {}", cmd))?;
|
||||
.with_context(|| format!("Failed to run post_configure command: {}", cmd_str))?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("post_compile command failed: {}", cmd);
|
||||
bail!("post_configure command failed: {}", cmd_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run post-compile commands (after make, before make install).
|
||||
pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &Path) -> Result<()> {
|
||||
let commands = &spec.build.flags.post_compile;
|
||||
if commands.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
crate::log_info!("Running {} post-compile command(s)...", commands.len());
|
||||
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
|
||||
|
||||
for cmd in commands {
|
||||
let cmd_str = spec.expand_vars(cmd);
|
||||
crate::log_info!(" post_compile: {}", cmd_str);
|
||||
|
||||
let mut shell_cmd = Command::new("sh");
|
||||
shell_cmd.current_dir(src_dir);
|
||||
shell_helpers.apply_to_command(&mut shell_cmd);
|
||||
let status = shell_cmd
|
||||
.env("DEPOT_SPECDIR", &spec.spec_dir)
|
||||
.env("DESTDIR", destdir)
|
||||
.env("DEPOT_ROOTFS", &spec.build.flags.rootfs)
|
||||
.env("CC", &spec.build.flags.cc)
|
||||
.env("AR", &spec.build.flags.ar)
|
||||
.arg("-c")
|
||||
.arg(&cmd_str)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to run post_compile command: {}", cmd_str))?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("post_compile command failed: {}", cmd_str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,26 +189,29 @@ pub fn run_post_install_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Running {} post-install command(s)...", commands.len());
|
||||
crate::log_info!("Running {} post-install command(s)...", commands.len());
|
||||
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
|
||||
|
||||
for cmd in commands {
|
||||
let cmd = spec.expand_vars(cmd);
|
||||
println!(" post_install: {}", cmd);
|
||||
let cmd_str = spec.expand_vars(cmd);
|
||||
crate::log_info!(" post_install: {}", cmd_str);
|
||||
|
||||
let status = Command::new("sh")
|
||||
.current_dir(src_dir)
|
||||
let mut shell_cmd = Command::new("sh");
|
||||
shell_cmd.current_dir(src_dir);
|
||||
shell_helpers.apply_to_command(&mut shell_cmd);
|
||||
let status = shell_cmd
|
||||
.env("DEPOT_SPECDIR", &spec.spec_dir)
|
||||
.env("DESTDIR", destdir)
|
||||
.env("DEPOT_ROOTFS", &spec.build.flags.rootfs)
|
||||
.env("CC", &spec.build.flags.cc)
|
||||
.env("AR", &spec.build.flags.ar)
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.arg(&cmd_str)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to run post_install command: {}", cmd))?;
|
||||
.with_context(|| format!("Failed to run post_install command: {}", cmd_str))?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("post_install command failed: {}", cmd);
|
||||
bail!("post_install command failed: {}", cmd_str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +286,7 @@ fn download(url: &str, dest: &Path) -> Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::resolve_patch_path;
|
||||
use super::{resolve_patch_path, run_post_install_commands};
|
||||
use crate::package::{
|
||||
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
||||
};
|
||||
@@ -270,6 +316,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: spec_dir.to_path_buf(),
|
||||
}
|
||||
}
|
||||
@@ -308,4 +356,28 @@ mod tests {
|
||||
let resolved = resolve_patch_path(&spec, url, &patch_cache_dir).unwrap();
|
||||
assert_eq!(resolved, cached);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_install_commands_can_haul_into_output_staging() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let spec_dir = tmp.path().join("spec");
|
||||
let src_dir = tmp.path().join("src");
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(&spec_dir).unwrap();
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/lib")).unwrap();
|
||||
std::fs::write(destdir.join("usr/lib/libLLVM.so.1"), "llvm").unwrap();
|
||||
|
||||
let mut spec = dummy_spec(&spec_dir);
|
||||
spec.build.flags.post_install = vec!["haul llvm-libs 'usr/lib/libLLVM*.so*'".into()];
|
||||
|
||||
run_post_install_commands(&spec, &src_dir, &destdir).unwrap();
|
||||
|
||||
assert!(!destdir.join("usr/lib/libLLVM.so.1").exists());
|
||||
assert!(
|
||||
destdir
|
||||
.join(".depot/outputs/llvm-libs/usr/lib/libLLVM.so.1")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+162
-23
@@ -25,12 +25,44 @@ pub fn copy_manual_sources(spec: &PackageSpec, cache_dir: &Path, build_dir: &Pat
|
||||
}
|
||||
|
||||
fs::create_dir_all(build_dir)?;
|
||||
println!("Copying {} manual source(s)...", spec.manual_sources.len());
|
||||
let manual_entry_count: usize = spec
|
||||
.manual_sources
|
||||
.iter()
|
||||
.map(|m| {
|
||||
usize::from(m.file.as_ref().is_some_and(|s| !s.trim().is_empty()))
|
||||
+ m.files.iter().filter(|s| !s.trim().is_empty()).count()
|
||||
+ usize::from(m.url.as_ref().is_some_and(|s| !s.trim().is_empty()))
|
||||
+ m.urls.iter().filter(|s| !s.trim().is_empty()).count()
|
||||
})
|
||||
.sum();
|
||||
crate::log_info!("Copying {} manual source(s)...", manual_entry_count);
|
||||
|
||||
for manual in &spec.manual_sources {
|
||||
let (source_path, source_label, default_dest): (PathBuf, String, String) =
|
||||
let mut local_entries: Vec<String> = Vec::new();
|
||||
if let Some(file) = manual.file.as_ref() {
|
||||
let file = spec.expand_vars(file);
|
||||
if !file.trim().is_empty() {
|
||||
local_entries.push(file.clone());
|
||||
}
|
||||
}
|
||||
local_entries.extend(
|
||||
manual
|
||||
.files
|
||||
.iter()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
let mut url_entries: Vec<String> = Vec::new();
|
||||
if let Some(url) = manual.url.as_ref() {
|
||||
if !url.trim().is_empty() {
|
||||
url_entries.push(url.clone());
|
||||
}
|
||||
}
|
||||
url_entries.extend(manual.urls.iter().filter(|s| !s.trim().is_empty()).cloned());
|
||||
|
||||
if !local_entries.is_empty() {
|
||||
for raw_file in local_entries {
|
||||
let file = spec.expand_vars(&raw_file);
|
||||
let src_path = spec.spec_dir.join(&file);
|
||||
if !src_path.exists() {
|
||||
bail!(
|
||||
@@ -40,19 +72,25 @@ pub fn copy_manual_sources(spec: &PackageSpec, cache_dir: &Path, build_dir: &Pat
|
||||
);
|
||||
}
|
||||
|
||||
// Verify checksum if provided.
|
||||
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
|
||||
&& !verify_file_hash(&src_path, expected_hash)?
|
||||
{
|
||||
bail!("Checksum mismatch for {}: expected {}", file, expected_hash);
|
||||
}
|
||||
|
||||
(src_path, file.clone(), file)
|
||||
} else if let Some(url_raw) = manual.url.as_ref() {
|
||||
let expanded_url = spec.expand_vars(url_raw);
|
||||
let default_dest = file.clone();
|
||||
copy_manual_source_file(spec, build_dir, &src_path, &file, manual, &default_dest)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !url_entries.is_empty() {
|
||||
for raw_url in url_entries {
|
||||
let expanded_url = spec.expand_vars(&raw_url);
|
||||
let parsed = Url::parse(&expanded_url)
|
||||
.with_context(|| format!("Invalid URL: {}", expanded_url))?;
|
||||
|
||||
let (source_path, source_label, default_dest): (PathBuf, String, String) =
|
||||
if parsed.scheme() == "file" {
|
||||
let src_path = parsed
|
||||
.to_file_path()
|
||||
@@ -79,37 +117,57 @@ pub fn copy_manual_sources(spec: &PackageSpec, cache_dir: &Path, build_dir: &Pat
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
};
|
||||
let fetched = fetcher::fetch_archive(spec, &source, &cache_dir.join("manual"))?;
|
||||
let fetched =
|
||||
fetcher::fetch_archive(spec, &source, &cache_dir.join("manual"))?;
|
||||
let default_dest = fetcher::derive_filename_from_url(&expanded_url);
|
||||
(fetched, expanded_url, default_dest)
|
||||
}
|
||||
} else {
|
||||
bail!("Manual source must define either 'file' or 'url'");
|
||||
};
|
||||
|
||||
// Determine destination
|
||||
copy_manual_source_file(
|
||||
spec,
|
||||
build_dir,
|
||||
&source_path,
|
||||
&source_label,
|
||||
manual,
|
||||
&default_dest,
|
||||
)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
bail!("Manual source must define one of 'file', 'files', 'url', or 'urls'");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_manual_source_file(
|
||||
spec: &PackageSpec,
|
||||
build_dir: &Path,
|
||||
source_path: &Path,
|
||||
source_label: &str,
|
||||
manual: &crate::package::ManualSource,
|
||||
default_dest: &str,
|
||||
) -> Result<()> {
|
||||
let dest_name = if let Some(dest) = manual.dest.as_ref() {
|
||||
spec.expand_vars(dest)
|
||||
} else {
|
||||
default_dest
|
||||
default_dest.to_string()
|
||||
};
|
||||
let dest_path = build_dir.join(&dest_name);
|
||||
|
||||
// Create parent directories if needed
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
println!(" {} -> {}", source_label, dest_path.display());
|
||||
fs::copy(&source_path, &dest_path).with_context(|| {
|
||||
crate::log_info!(" {} -> {}", source_label, dest_path.display());
|
||||
fs::copy(source_path, &dest_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy {} to {}",
|
||||
source_path.display(),
|
||||
dest_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -122,7 +180,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
||||
|
||||
let exp = expected.trim();
|
||||
if exp.eq_ignore_ascii_case("skip") {
|
||||
println!("Checksum verification skipped");
|
||||
crate::log_info!("Checksum verification skipped");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -250,7 +308,7 @@ fn prepare_one(
|
||||
if dst.exists() {
|
||||
// If it exists and has a state file, assume we are resuming
|
||||
if dst.join(".depot_state").exists() {
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Resuming build in existing source directory: {}",
|
||||
dst.display()
|
||||
);
|
||||
@@ -265,7 +323,7 @@ fn prepare_one(
|
||||
let src_dir = build_dir.join(&extract_dir_name);
|
||||
if src_dir.exists() {
|
||||
if src_dir.join(".depot_state").exists() {
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Resuming build in existing source directory: {}",
|
||||
src_dir.display()
|
||||
);
|
||||
@@ -288,7 +346,7 @@ fn prepare_one(
|
||||
if let Some((base, rev)) = split_git_url(&url) {
|
||||
let checkout_dir = build_dir.join(&extract_dir_name);
|
||||
if checkout_dir.exists() && checkout_dir.join(".depot_state").exists() {
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Resuming build in existing git directory: {}",
|
||||
checkout_dir.display()
|
||||
);
|
||||
@@ -307,7 +365,7 @@ fn prepare_one(
|
||||
|
||||
let src_dir = build_dir.join(&extract_dir_name);
|
||||
if src_dir.exists() && src_dir.join(".depot_state").exists() {
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Resuming build in existing source directory: {}",
|
||||
src_dir.display()
|
||||
);
|
||||
@@ -406,6 +464,8 @@ mod tests {
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir,
|
||||
}
|
||||
}
|
||||
@@ -437,6 +497,48 @@ mod tests {
|
||||
assert_eq!(rev, "HEAD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_git_url_accepts_expanded_tag_or_hash_revision() {
|
||||
let spec = PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: "json".into(),
|
||||
version: "3.11.3".into(),
|
||||
revision: 1,
|
||||
description: "d".into(),
|
||||
homepage: "h".into(),
|
||||
license: vec!["MIT".into()],
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: vec![Source {
|
||||
url: "https://github.com/nlohmann/json.git#v$version".into(),
|
||||
sha256: "skip".into(),
|
||||
extract_dir: "json-$version".into(),
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
}],
|
||||
build: Build {
|
||||
build_type: BuildType::Custom,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
let expanded = spec.expand_vars(&spec.source[0].url);
|
||||
let (base, rev) = split_git_url(&expanded).unwrap();
|
||||
assert_eq!(base, "https://github.com/nlohmann/json.git");
|
||||
assert_eq!(rev, "v3.11.3");
|
||||
|
||||
let (base, rev) =
|
||||
split_git_url("https://github.com/nlohmann/json.git#0123456789abcdef").unwrap();
|
||||
assert_eq!(base, "https://github.com/nlohmann/json.git");
|
||||
assert_eq!(rev, "0123456789abcdef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_file_hash_accepts_multiple_algorithms() {
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
@@ -489,7 +591,9 @@ mod tests {
|
||||
spec_dir.clone(),
|
||||
vec![ManualSource {
|
||||
file: Some("manual.patch".into()),
|
||||
files: Vec::new(),
|
||||
url: None,
|
||||
urls: Vec::new(),
|
||||
sha256: None,
|
||||
dest: None,
|
||||
}],
|
||||
@@ -517,7 +621,9 @@ mod tests {
|
||||
spec_dir,
|
||||
vec![ManualSource {
|
||||
file: None,
|
||||
files: Vec::new(),
|
||||
url: Some(url),
|
||||
urls: Vec::new(),
|
||||
sha256: Some("skip".into()),
|
||||
dest: Some("assets/manual.txt".into()),
|
||||
}],
|
||||
@@ -529,4 +635,37 @@ mod tests {
|
||||
"remote-data"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_manual_sources_multi_files_in_one_block() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let spec_dir = tmp.path().join("spec");
|
||||
let cache_dir = tmp.path().join("cache");
|
||||
let build_dir = tmp.path().join("build");
|
||||
std::fs::create_dir_all(spec_dir.join("pam")).unwrap();
|
||||
std::fs::write(spec_dir.join("pam/other"), "other").unwrap();
|
||||
std::fs::write(spec_dir.join("pam/system-auth"), "auth").unwrap();
|
||||
|
||||
let spec = mk_spec_with_manuals(
|
||||
spec_dir.clone(),
|
||||
vec![ManualSource {
|
||||
file: None,
|
||||
files: vec!["pam/other".into(), "pam/system-auth".into()],
|
||||
url: None,
|
||||
urls: Vec::new(),
|
||||
sha256: None,
|
||||
dest: None,
|
||||
}],
|
||||
);
|
||||
|
||||
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(build_dir.join("pam/other")).unwrap(),
|
||||
"other"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(build_dir.join("pam/system-auth")).unwrap(),
|
||||
"auth"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+488
-9
@@ -3,23 +3,44 @@
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub const INTERNAL_DEPOT_DIR: &str = ".depot";
|
||||
pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs";
|
||||
|
||||
fn is_skipped_install_path(rel_path: &str) -> bool {
|
||||
let p = rel_path.trim_start_matches('/');
|
||||
p == ".metadata.toml"
|
||||
|| p == ".files.yaml"
|
||||
|| p == INTERNAL_DEPOT_DIR
|
||||
|| p.strip_prefix(INTERNAL_DEPOT_DIR)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
|| p == "scripts"
|
||||
|| p.starts_with("scripts/")
|
||||
|| p == "usr/share/info/dir"
|
||||
|| p.starts_with("usr/share/info/dir.")
|
||||
}
|
||||
|
||||
/// Return the internal split-output staging root inside a package `destdir`.
|
||||
pub fn output_staging_root(destdir: &Path) -> PathBuf {
|
||||
destdir.join(INTERNAL_OUTPUTS_DIR)
|
||||
}
|
||||
|
||||
/// Return the staging directory for an additional output package.
|
||||
pub fn output_staging_dir(destdir: &Path, pkg_name: &str) -> PathBuf {
|
||||
output_staging_root(destdir).join(pkg_name)
|
||||
}
|
||||
|
||||
fn normalize_relative_path(path: &str) -> Result<String> {
|
||||
let trimmed = path.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -57,9 +78,287 @@ fn normalize_relative_path(path: &str) -> Result<String> {
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum KeepMatcher {
|
||||
Exact(String),
|
||||
Pattern(String),
|
||||
}
|
||||
|
||||
impl KeepMatcher {
|
||||
fn from_spec(raw: &str) -> Result<Self> {
|
||||
let normalized = normalize_relative_path(raw)?;
|
||||
if normalized.contains('*') || normalized.contains('?') {
|
||||
Ok(Self::Pattern(normalized))
|
||||
} else {
|
||||
Ok(Self::Exact(normalized))
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(&self, rel_path: &str) -> bool {
|
||||
match self {
|
||||
Self::Exact(p) => p == rel_path,
|
||||
Self::Pattern(p) => glob_match_path(p, rel_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn glob_match_path(pattern: &str, path: &str) -> bool {
|
||||
let p_parts: Vec<&str> = pattern.split('/').collect();
|
||||
let s_parts: Vec<&str> = path.split('/').collect();
|
||||
glob_match_path_parts(&p_parts, &s_parts)
|
||||
}
|
||||
|
||||
fn glob_match_path_parts(pattern_parts: &[&str], path_parts: &[&str]) -> bool {
|
||||
if pattern_parts.is_empty() {
|
||||
return path_parts.is_empty();
|
||||
}
|
||||
|
||||
if pattern_parts[0] == "**" {
|
||||
let mut next = 1usize;
|
||||
while next < pattern_parts.len() && pattern_parts[next] == "**" {
|
||||
next += 1;
|
||||
}
|
||||
let rest = &pattern_parts[next..];
|
||||
if rest.is_empty() {
|
||||
return true;
|
||||
}
|
||||
for skip in 0..=path_parts.len() {
|
||||
if glob_match_path_parts(rest, &path_parts[skip..]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if path_parts.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
glob_match_segment(pattern_parts[0], path_parts[0])
|
||||
&& glob_match_path_parts(&pattern_parts[1..], &path_parts[1..])
|
||||
}
|
||||
|
||||
fn glob_match_segment(pattern: &str, text: &str) -> bool {
|
||||
let p = pattern.as_bytes();
|
||||
let t = text.as_bytes();
|
||||
let (mut pi, mut ti) = (0usize, 0usize);
|
||||
let mut star: Option<usize> = None;
|
||||
let mut star_match_ti = 0usize;
|
||||
|
||||
while ti < t.len() {
|
||||
if pi < p.len() && (p[pi] == b'?' || p[pi] == t[ti]) {
|
||||
pi += 1;
|
||||
ti += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if pi < p.len() && p[pi] == b'*' {
|
||||
star = Some(pi);
|
||||
pi += 1;
|
||||
star_match_ti = ti;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(star_pi) = star {
|
||||
pi = star_pi + 1;
|
||||
star_match_ti += 1;
|
||||
ti = star_match_ti;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
while pi < p.len() && p[pi] == b'*' {
|
||||
pi += 1;
|
||||
}
|
||||
|
||||
pi == p.len()
|
||||
}
|
||||
|
||||
fn has_known_compressed_suffix(path: &str) -> bool {
|
||||
let lower = path.to_ascii_lowercase();
|
||||
lower.ends_with(".zst")
|
||||
|| lower.ends_with(".gz")
|
||||
|| lower.ends_with(".xz")
|
||||
|| lower.ends_with(".bz2")
|
||||
|| lower.ends_with(".lzma")
|
||||
|| lower.ends_with(".z")
|
||||
}
|
||||
|
||||
fn is_manpage_rel_path(rel_path: &str) -> bool {
|
||||
let rel = rel_path.trim_start_matches('/');
|
||||
rel.starts_with("usr/share/man/") && !has_known_compressed_suffix(rel)
|
||||
}
|
||||
|
||||
fn append_os_suffix(path: &Path, suffix: &str) -> PathBuf {
|
||||
let mut s = OsString::from(path.as_os_str());
|
||||
s.push(suffix);
|
||||
PathBuf::from(s)
|
||||
}
|
||||
|
||||
fn is_elf_file(path: &Path) -> Result<bool> {
|
||||
let mut file =
|
||||
fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
|
||||
let mut magic = [0u8; 4];
|
||||
let n = file
|
||||
.read(&mut magic)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
Ok(n == 4 && magic == [0x7F, b'E', b'L', b'F'])
|
||||
}
|
||||
|
||||
fn auto_strip_elf_files(destdir: &Path) -> Result<usize> {
|
||||
let mut stripped = 0usize;
|
||||
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if !is_elf_file(path)? {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = Command::new("strip")
|
||||
.arg("--strip-unneeded")
|
||||
.arg(path)
|
||||
.status()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to execute strip for {} (disable with build.flags.no_strip = true)",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
if !status.success() {
|
||||
anyhow::bail!(
|
||||
"strip failed for {} with status {} (disable with build.flags.no_strip = true)",
|
||||
path.display(),
|
||||
status
|
||||
);
|
||||
}
|
||||
stripped += 1;
|
||||
}
|
||||
Ok(stripped)
|
||||
}
|
||||
|
||||
fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
|
||||
crate::log_info!("Compressing man pages with zstd...");
|
||||
let mut man_files = Vec::new();
|
||||
let mut man_symlinks = Vec::new();
|
||||
|
||||
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path().to_path_buf();
|
||||
let rel = path
|
||||
.strip_prefix(destdir)
|
||||
.context("Failed to strip destdir prefix during manpage compression")?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if !is_manpage_rel_path(&rel) {
|
||||
continue;
|
||||
}
|
||||
if entry.file_type().is_file() {
|
||||
man_files.push(path);
|
||||
} else if entry.file_type().is_symlink() {
|
||||
man_symlinks.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
let mut compressed = 0usize;
|
||||
for path in &man_files {
|
||||
let metadata = fs::metadata(path)
|
||||
.with_context(|| format!("Failed to inspect man page {}", path.display()))?;
|
||||
let out_path = append_os_suffix(path, ".zst");
|
||||
let tmp_path = append_os_suffix(&out_path, ".tmp");
|
||||
|
||||
if tmp_path.exists() {
|
||||
let _ = fs::remove_file(&tmp_path);
|
||||
}
|
||||
|
||||
let mut input = fs::File::open(path)
|
||||
.with_context(|| format!("Failed to open man page {}", path.display()))?;
|
||||
let out_file = fs::File::create(&tmp_path)
|
||||
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
|
||||
|
||||
let mut encoder = zstd::stream::write::Encoder::new(out_file, 22)
|
||||
.with_context(|| format!("Failed to start zstd encoder for {}", path.display()))?;
|
||||
io::copy(&mut input, &mut encoder)
|
||||
.with_context(|| format!("Failed to compress {}", path.display()))?;
|
||||
let mut out_file = encoder
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finish zstd compression for {}", path.display()))?;
|
||||
out_file
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush {}", tmp_path.display()))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&tmp_path)?.permissions();
|
||||
perms.set_mode(metadata.permissions().mode() & 0o777);
|
||||
fs::set_permissions(&tmp_path, perms)?;
|
||||
}
|
||||
|
||||
fs::rename(&tmp_path, &out_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to finalize compressed man page {} -> {}",
|
||||
tmp_path.display(),
|
||||
out_path.display()
|
||||
)
|
||||
})?;
|
||||
fs::remove_file(path)
|
||||
.with_context(|| format!("Failed to remove original man page {}", path.display()))?;
|
||||
compressed += 1;
|
||||
}
|
||||
|
||||
let mut fixed_symlinks = 0usize;
|
||||
for link_path in man_symlinks {
|
||||
let target = fs::read_link(&link_path)
|
||||
.with_context(|| format!("Failed to read manpage symlink {}", link_path.display()))?;
|
||||
let target_s = target.to_string_lossy();
|
||||
if has_known_compressed_suffix(&target_s) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_link_path = append_os_suffix(&link_path, ".zst");
|
||||
let new_target = append_os_suffix(&target, ".zst");
|
||||
|
||||
if new_link_path.symlink_metadata().is_ok() {
|
||||
fs::remove_file(&new_link_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to remove existing compressed manpage symlink {}",
|
||||
new_link_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
std::os::unix::fs::symlink(&new_target, &new_link_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to create compressed manpage symlink {} -> {}",
|
||||
new_link_path.display(),
|
||||
new_target.display()
|
||||
)
|
||||
})?;
|
||||
fs::remove_file(&link_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to remove original manpage symlink {}",
|
||||
link_path.display()
|
||||
)
|
||||
})?;
|
||||
fixed_symlinks += 1;
|
||||
}
|
||||
|
||||
if fixed_symlinks > 0 {
|
||||
crate::log_info!(
|
||||
"Updated {} man page symlink(s) for .zst targets",
|
||||
fixed_symlinks
|
||||
);
|
||||
}
|
||||
|
||||
Ok(compressed)
|
||||
}
|
||||
|
||||
/// Process staged files - remove .la files, strip binaries, etc.
|
||||
pub fn process(destdir: &Path, _spec: &PackageSpec) -> Result<()> {
|
||||
println!("Processing staged files...");
|
||||
pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
|
||||
crate::log_info!("Processing staged files...");
|
||||
|
||||
let mut removed_count = 0;
|
||||
|
||||
@@ -68,14 +367,32 @@ pub fn process(destdir: &Path, _spec: &PackageSpec) -> Result<()> {
|
||||
|
||||
// Remove libtool .la files
|
||||
if path.extension().map(|e| e == "la").unwrap_or(false) {
|
||||
println!(" Removing: {}", path.display());
|
||||
crate::log_info!(" Removing: {}", path.display());
|
||||
fs::remove_file(path)?;
|
||||
removed_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if removed_count > 0 {
|
||||
println!("Removed {} .la file(s)", removed_count);
|
||||
crate::log_info!("Removed {} .la file(s)", removed_count);
|
||||
}
|
||||
|
||||
if spec.build.flags.no_strip {
|
||||
crate::log_info!("Skipping auto-strip: disabled by build.flags.no_strip");
|
||||
} else {
|
||||
let stripped = auto_strip_elf_files(destdir)?;
|
||||
if stripped > 0 {
|
||||
crate::log_info!("Stripped {} ELF file(s)", stripped);
|
||||
}
|
||||
}
|
||||
|
||||
if spec.build.flags.no_compress_man {
|
||||
crate::log_info!("Skipping manpage compression: disabled by build.flags.no_compress_man");
|
||||
} else {
|
||||
let compressed = compress_manpages_zstd(destdir)?;
|
||||
if compressed > 0 {
|
||||
crate::log_info!("Compressed {} man page(s) with zstd -22", compressed);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -127,7 +444,7 @@ pub fn add_licenses(src_dir: &Path, destdir: &Path, pkgname: &str) -> Result<usi
|
||||
}
|
||||
|
||||
if copied > 0 {
|
||||
println!(
|
||||
crate::log_info!(
|
||||
"Copied {} license file(s) to {}/",
|
||||
copied,
|
||||
dst_base.display()
|
||||
@@ -225,10 +542,17 @@ pub fn install_atomic(
|
||||
remove_paths: &[String],
|
||||
keep_paths: &[String],
|
||||
) -> Result<FsTransaction> {
|
||||
let keep_set: HashSet<String> = keep_paths
|
||||
let keep_rules: Vec<KeepMatcher> = keep_paths
|
||||
.iter()
|
||||
.map(|p| normalize_relative_path(p))
|
||||
.collect::<Result<HashSet<_>>>()?;
|
||||
.map(|p| KeepMatcher::from_spec(p))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let keep_set: HashSet<String> = keep_rules
|
||||
.iter()
|
||||
.filter_map(|m| match m {
|
||||
KeepMatcher::Exact(p) => Some(p.clone()),
|
||||
KeepMatcher::Pattern(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
fs::create_dir_all(tx_base_dir)
|
||||
.with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?;
|
||||
@@ -265,6 +589,10 @@ pub fn install_atomic(
|
||||
let rel_path = src_path
|
||||
.strip_prefix(destdir)
|
||||
.context("Failed to strip destdir prefix")?;
|
||||
let rel_path_str = rel_path.to_string_lossy().to_string();
|
||||
if is_skipped_install_path(&rel_path_str) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dest_path = rootfs.join(rel_path);
|
||||
if !dest_path.exists() {
|
||||
@@ -295,7 +623,12 @@ pub fn install_atomic(
|
||||
continue;
|
||||
}
|
||||
|
||||
let keep_as_depotnew = keep_set.contains(&rel_path) && rootfs.join(&rel_path).exists();
|
||||
let keep_match = if keep_set.contains(&rel_path) {
|
||||
true
|
||||
} else {
|
||||
keep_rules.iter().any(|m| m.matches(&rel_path))
|
||||
};
|
||||
let keep_as_depotnew = keep_match && rootfs.join(&rel_path).exists();
|
||||
let install_rel_path = if keep_as_depotnew {
|
||||
format!("{}.depotnew", rel_path)
|
||||
} else {
|
||||
@@ -497,6 +830,115 @@ mod tests {
|
||||
assert!(!rootfs.join("etc/locale.gen.depotnew").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_atomic_keep_wildcard_matches_directory_children() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let rootfs = tmp.path().join("root");
|
||||
let destdir = tmp.path().join("dest");
|
||||
let tx_base = tmp.path().join("tx");
|
||||
std::fs::create_dir_all(rootfs.join("etc/pam.d")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("etc/pam.d")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("etc/pam.d/subdir")).unwrap();
|
||||
|
||||
std::fs::write(rootfs.join("etc/pam.d/system-auth"), "existing-auth").unwrap();
|
||||
std::fs::write(destdir.join("etc/pam.d/system-auth"), "pkg-auth").unwrap();
|
||||
std::fs::write(destdir.join("etc/pam.d/other"), "pkg-other").unwrap();
|
||||
std::fs::write(destdir.join("etc/pam.d/subdir/nested"), "pkg-nested").unwrap();
|
||||
|
||||
let keep = vec!["etc/pam.d/*".to_string()];
|
||||
let tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &keep).unwrap();
|
||||
|
||||
// Existing matched file is preserved and package version becomes .depotnew
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("etc/pam.d/system-auth")).unwrap(),
|
||||
"existing-auth"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("etc/pam.d/system-auth.depotnew")).unwrap(),
|
||||
"pkg-auth"
|
||||
);
|
||||
|
||||
// New matched file installs normally because no existing file is present
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("etc/pam.d/other")).unwrap(),
|
||||
"pkg-other"
|
||||
);
|
||||
|
||||
// Single-segment * does not cross '/'
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("etc/pam.d/subdir/nested")).unwrap(),
|
||||
"pkg-nested"
|
||||
);
|
||||
assert!(!rootfs.join("etc/pam.d/subdir/nested.depotnew").exists());
|
||||
|
||||
tx.rollback().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_glob_matches_question_mark_and_not_path_separator() {
|
||||
assert!(glob_match_path(
|
||||
"etc/pam.d/system-????",
|
||||
"etc/pam.d/system-auth"
|
||||
));
|
||||
assert!(!glob_match_path("etc/pam.d/*", "etc/pam.d/subdir/file"));
|
||||
assert!(glob_match_path("etc/pam.d/*", "etc/pam.d/file"));
|
||||
assert!(glob_match_path("etc/pam.d/**", "etc/pam.d/subdir/file"));
|
||||
assert!(glob_match_path("etc/**/file", "etc/pam.d/subdir/file"));
|
||||
assert!(glob_match_path("etc/pam.d/**", "etc/pam.d"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_manpage_rel_path_detects_uncompressed_manpages() {
|
||||
assert!(is_manpage_rel_path("usr/share/man/man1/ls.1"));
|
||||
assert!(is_manpage_rel_path("/usr/share/man/man5/pam.d.5"));
|
||||
assert!(!is_manpage_rel_path("usr/share/man/man1/ls.1.zst"));
|
||||
assert!(!is_manpage_rel_path("usr/share/doc/readme"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_elf_file_detects_magic_bytes() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let elf = tmp.path().join("elf.bin");
|
||||
let text = tmp.path().join("text.txt");
|
||||
std::fs::write(&elf, [0x7F, b'E', b'L', b'F', 0x02, 0x01]).unwrap();
|
||||
std::fs::write(&text, b"#!/bin/sh\n").unwrap();
|
||||
|
||||
assert!(is_elf_file(&elf).unwrap());
|
||||
assert!(!is_elf_file(&text).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compress_manpages_zstd_rewrites_symlinks() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path().join("dest");
|
||||
let man1 = dest.join("usr/share/man/man1");
|
||||
std::fs::create_dir_all(&man1).unwrap();
|
||||
|
||||
let page = man1.join("foo.1");
|
||||
std::fs::write(&page, b"foo manpage\n").unwrap();
|
||||
std::os::unix::fs::symlink("foo.1", man1.join("bar.1")).unwrap();
|
||||
|
||||
let count = compress_manpages_zstd(&dest).unwrap();
|
||||
assert_eq!(count, 1);
|
||||
assert!(!man1.join("foo.1").exists());
|
||||
assert!(man1.join("foo.1.zst").exists());
|
||||
assert!(!man1.join("bar.1").exists());
|
||||
|
||||
let link_meta = std::fs::symlink_metadata(man1.join("bar.1.zst")).unwrap();
|
||||
assert!(link_meta.file_type().is_symlink());
|
||||
assert_eq!(
|
||||
std::fs::read_link(man1.join("bar.1.zst")).unwrap(),
|
||||
PathBuf::from("foo.1.zst")
|
||||
);
|
||||
|
||||
let file = std::fs::File::open(man1.join("foo.1.zst")).unwrap();
|
||||
let mut decoder = zstd::stream::read::Decoder::new(file).unwrap();
|
||||
let mut out = String::new();
|
||||
use std::io::Read as _;
|
||||
decoder.read_to_string(&mut out).unwrap();
|
||||
assert_eq!(out, "foo manpage\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_atomic_rejects_unsafe_keep_paths() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -671,6 +1113,24 @@ mod tests {
|
||||
assert!(rootfs.join("usr/bin/ok").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_atomic_skips_internal_output_staging_dir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let rootfs = tmp.path().join("root");
|
||||
let destdir = tmp.path().join("dest");
|
||||
let tx_base = tmp.path().join("tx");
|
||||
std::fs::create_dir_all(&rootfs).unwrap();
|
||||
std::fs::create_dir_all(destdir.join(".depot/outputs/clang/usr/bin")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||
std::fs::write(destdir.join(".depot/outputs/clang/usr/bin/clang"), "clang").unwrap();
|
||||
std::fs::write(destdir.join("usr/bin/ok"), "ok").unwrap();
|
||||
|
||||
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||
|
||||
assert!(rootfs.join("usr/bin/ok").exists());
|
||||
assert!(!rootfs.join(".depot").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_manifest_skips_info_dir_index() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -726,6 +1186,25 @@ mod tests {
|
||||
assert!(!manifest.files.contains(&"scripts/pre_install".to_string()));
|
||||
assert!(manifest.files.contains(&"usr/bin/ok".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_manifest_skips_internal_output_staging() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let destdir = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||
std::fs::create_dir_all(destdir.join(".depot/outputs/clang/usr/bin")).unwrap();
|
||||
std::fs::write(destdir.join("usr/bin/llvm-config"), "ok").unwrap();
|
||||
std::fs::write(destdir.join(".depot/outputs/clang/usr/bin/clang"), "clang").unwrap();
|
||||
|
||||
let manifest = generate_manifest_with_dirs(&destdir).unwrap();
|
||||
|
||||
assert!(manifest.files.contains(&"usr/bin/llvm-config".to_string()));
|
||||
assert!(
|
||||
!manifest
|
||||
.files
|
||||
.contains(&".depot/outputs/clang/usr/bin/clang".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Manifest containing files and directories for a package
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Terminal UI helpers (colors + prompts).
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Stream {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
fn colors_enabled(stream: Stream) -> bool {
|
||||
if std::env::var_os("NO_COLOR").is_some() {
|
||||
return false;
|
||||
}
|
||||
if matches!(std::env::var("TERM").as_deref(), Ok("dumb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match stream {
|
||||
Stream::Stdout => io::stdout().is_terminal(),
|
||||
Stream::Stderr => io::stderr().is_terminal(),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(stream: Stream, text: &str, color_code: &str) -> String {
|
||||
if colors_enabled(stream) {
|
||||
format!("\x1b[{}m{}\x1b[0m", color_code, text)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn label(stream: Stream, text: &str, color_code: &str) -> String {
|
||||
format!("[{}]", paint(stream, text, color_code))
|
||||
}
|
||||
|
||||
pub fn info(message: impl AsRef<str>) {
|
||||
println!(
|
||||
"{} {}",
|
||||
label(Stream::Stdout, "INFO", "36"),
|
||||
message.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn success(message: impl AsRef<str>) {
|
||||
println!("{} {}", label(Stream::Stdout, "OK", "32"), message.as_ref());
|
||||
}
|
||||
|
||||
pub fn warn(message: impl AsRef<str>) {
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
label(Stream::Stderr, "WARN", "33"),
|
||||
message.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_info {
|
||||
($($arg:tt)*) => {
|
||||
$crate::ui::info(format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_ok {
|
||||
($($arg:tt)*) => {
|
||||
$crate::ui::success(format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_warn {
|
||||
($($arg:tt)*) => {
|
||||
$crate::ui::warn(format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_yes_no_input(input: &str, default_yes: bool) -> Option<bool> {
|
||||
let trimmed = input.trim().to_ascii_lowercase();
|
||||
if trimmed.is_empty() {
|
||||
return Some(default_yes);
|
||||
}
|
||||
match trimmed.as_str() {
|
||||
"y" | "yes" => Some(true),
|
||||
"n" | "no" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
print!(
|
||||
"{} {} [{}] ",
|
||||
label(Stream::Stdout, "?", "35"),
|
||||
prompt,
|
||||
default_hint
|
||||
);
|
||||
io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush prompt to stdout")?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read user input from stdin")?;
|
||||
|
||||
if let Some(answer) = parse_yes_no_input(&input, default_yes) {
|
||||
return Ok(answer);
|
||||
}
|
||||
|
||||
warn("Please answer with 'y' or 'n'.");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_yes_no_input;
|
||||
|
||||
#[test]
|
||||
fn parse_yes_no_defaults() {
|
||||
assert_eq!(parse_yes_no_input("", true), Some(true));
|
||||
assert_eq!(parse_yes_no_input("", false), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_yes_no_values() {
|
||||
assert_eq!(parse_yes_no_input("y", false), Some(true));
|
||||
assert_eq!(parse_yes_no_input("yes", false), Some(true));
|
||||
assert_eq!(parse_yes_no_input("N", true), Some(false));
|
||||
assert_eq!(parse_yes_no_input("no", true), Some(false));
|
||||
assert_eq!(parse_yes_no_input("maybe", true), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user