feat: initial implementation of Depot package manager

- Implement source-based package management with support for archives and Git
- Add multi-system build support (Autotools, CMake, Meson, Rust/Cargo, Custom)
- Implement atomic installation logic using transactions and fakeroot
- Add dependency resolution for build-time and runtime requirements
- Implement package indexing and local repository management
- Add comprehensive configuration system with system/package overrides
- Include Project guidelines (AGENTS.md) and README.md
This commit is contained in:
2026-02-16 11:35:23 -06:00
parent 94044a48c3
commit 00ca2ebac6
29 changed files with 9385 additions and 69 deletions
+209
View File
@@ -0,0 +1,209 @@
//! GNU Autotools build system (configure && make && make install)
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,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
) -> Result<()> {
let flags = &spec.build.flags;
// Determine actual source directory (support source_subdir)
let actual_src = if !flags.source_subdir.is_empty() {
src_dir.join(&flags.source_subdir)
} else {
src_dir.to_path_buf()
};
if !actual_src.exists() {
anyhow::bail!(
"Source directory not found: {} (source_subdir: {})",
actual_src.display(),
flags.source_subdir
);
}
// Create destdir
fs::create_dir_all(destdir)?;
// Build environment variables
let mut env_vars: Vec<(&str, String)> = vec![];
// Use cross-compilation tools if configured
let (cc, ar) = if let Some(cc_cfg) = cross {
(cc_cfg.cc.clone(), cc_cfg.ar.clone())
} else {
(flags.cc.clone(), flags.ar.clone())
};
if !flags.cflags.is_empty() {
// Expand shell command substitutions like $($CC -print-resource-dir)
let cflags_str = flags.cflags.join(" ");
let expanded = expand_shell_commands(&cflags_str, &cc)?;
env_vars.push(("CFLAGS", expanded));
}
if !flags.ldflags.is_empty() {
env_vars.push(("LDFLAGS", flags.ldflags.join(" ")));
}
env_vars.push(("CC", cc.clone()));
env_vars.push(("AR", ar));
// Add cross-compilation environment
if let Some(cc_cfg) = cross {
env_vars.push(("CXX", cc_cfg.cxx.clone()));
env_vars.push(("RANLIB", cc_cfg.ranlib.clone()));
env_vars.push(("STRIP", cc_cfg.strip.clone()));
env_vars.push(("LD", cc_cfg.ld.clone()));
env_vars.push(("NM", cc_cfg.nm.clone()));
}
// Add dynamic loader flag if specified
if !flags.libc.is_empty() {
let ldflags = if flags.ldflags.is_empty() {
format!("-Wl,--dynamic-linker={}", flags.libc)
} else {
format!(
"{} -Wl,--dynamic-linker={}",
flags.ldflags.join(" "),
flags.libc
)
};
env_vars.push(("LDFLAGS", ldflags));
}
// Run configure
println!("Running configure...");
let mut configure_cmd = Command::new("./configure");
configure_cmd.current_dir(&actual_src);
crate::builder::prepare_command(&mut configure_cmd, &env_vars);
configure_cmd.arg(format!("--prefix={}", flags.prefix));
if !flags.chost.is_empty() {
configure_cmd.arg(format!("--host={}", flags.chost));
}
if !flags.cbuild.is_empty() {
configure_cmd.arg(format!("--build={}", flags.cbuild));
}
// Add cross-compilation flags
if let Some(cc_cfg) = cross {
configure_cmd.arg(format!("--host={}", cc_cfg.host_triple()));
if let Ok(build) = CrossConfig::build_triple() {
configure_cmd.arg(format!("--build={}", build));
}
}
for arg in &flags.configure {
configure_cmd.arg(arg);
}
let status = configure_cmd
.status()
.with_context(|| format!("Failed to run configure in {}", actual_src.display()))?;
if !status.success() {
anyhow::bail!("configure failed with status: {}", status);
}
// Run make
println!("Running make...");
let mut make_cmd = Command::new("make");
make_cmd.current_dir(&actual_src);
make_cmd.arg("-j").arg(num_cpus().to_string());
crate::builder::prepare_command(&mut make_cmd, &env_vars);
let status = make_cmd
.status()
.with_context(|| format!("Failed to run make in {}", actual_src.display()))?;
if !status.success() {
anyhow::bail!("make failed with status: {}", status);
}
// Run post-compile hooks (after make, before make install)
hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
// Run make install with fakeroot if not root
println!(
"Running make install{}...",
if fakeroot::is_root() {
""
} else {
" (with internal fakeroot for build)"
}
);
let mut install_cmd = fakeroot::wrap_install_command("make", destdir);
install_cmd.current_dir(&actual_src);
install_cmd.arg("install");
let mut install_env = env_vars.clone();
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
crate::builder::prepare_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
.with_context(|| format!("Failed to run make install for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("make install failed with status: {}", status);
}
// Run post-install hooks (after make install)
hooks::run_post_install_commands(spec, &actual_src, destdir)?;
Ok(())
}
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
/// Expand shell command substitutions like $($CC -print-resource-dir) in a string
fn expand_shell_commands(input: &str, cc: &str) -> Result<String> {
let mut result = input.to_string();
// Find and expand $(...) patterns
while let Some(start) = result.find("$(") {
let rest = &result[start + 2..];
if let Some(end) = rest.find(')') {
let cmd = &rest[..end];
// Replace $CC with actual compiler
let cmd = cmd.replace("$CC", cc);
// Execute the command via shell
let output = Command::new("sh").arg("-c").arg(&cmd).output();
let replacement = match output {
Ok(out) if out.status.success() => {
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
_ => {
// Silently skip failed commands (e.g., gcc doesn't support -print-resource-dir)
eprintln!("Warning: shell command '{}' failed, skipping", cmd);
String::new()
}
};
result = format!("{}{}{}", &result[..start], replacement, &rest[end + 1..]);
} else {
break; // Malformed, no closing paren
}
}
Ok(result)
}
+42
View File
@@ -0,0 +1,42 @@
//! Binary package "build" system — used when package supplies a prebuilt binary installer
use anyhow::{Context, Result};
use std::fs;
use std::os::unix::fs as unix_fs;
use std::path::Path;
use walkdir::WalkDir;
use crate::package::PackageSpec;
use crate::cross::CrossConfig;
/// For binary packages we simply copy the extracted files into DESTDIR (preserving
/// directory structure). This is useful for .deb packages where extract step
/// already unpacked the data payload into the source directory.
pub fn build(
_spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
_cross: Option<&CrossConfig>,
) -> Result<()> {
println!("Binary install: copying files from {} to {} (pkg type={})", src_dir.display(), destdir.display(), _spec.build.flags.binary_type);
fs::create_dir_all(destdir).with_context(|| format!("Failed to create destdir: {}", destdir.display()))?;
for entry in WalkDir::new(src_dir) {
let entry = entry?;
let rel = entry.path().strip_prefix(src_dir).unwrap();
let target = destdir.join(rel);
if entry.file_type().is_dir() {
fs::create_dir_all(&target)?;
} else if entry.file_type().is_symlink() {
let link_target = fs::read_link(entry.path())?;
if let Some(parent) = target.parent() { fs::create_dir_all(parent)?; }
// overwrite existing links/files
if target.exists() { let _ = fs::remove_file(&target); }
unix_fs::symlink(link_target, &target)?;
} else {
if let Some(parent) = target.parent() { fs::create_dir_all(parent)?; }
fs::copy(entry.path(), &target)?;
}
}
Ok(())
}
+179
View File
@@ -0,0 +1,179 @@
//! CMake build system
use crate::cross::CrossConfig;
use crate::fakeroot;
use crate::package::PackageSpec;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(
spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
) -> Result<()> {
let flags = &spec.build.flags;
// Determine actual source directory (support source_subdir)
let actual_src = if !flags.source_subdir.is_empty() {
src_dir.join(&flags.source_subdir)
} else {
src_dir.to_path_buf()
};
if !actual_src.exists() {
anyhow::bail!(
"Source directory not found: {} (source_subdir: {})",
actual_src.display(),
flags.source_subdir
);
}
let build_dir = actual_src.join("build");
// Create directories
fs::create_dir_all(&build_dir)?;
fs::create_dir_all(destdir)?;
// Environment variables
let mut env_vars: Vec<(&str, String)> = vec![];
// Use cross-compilation tools if configured
let cc = if let Some(cc_cfg) = cross {
cc_cfg.cc.clone()
} else {
flags.cc.clone()
};
if !flags.cflags.is_empty() {
env_vars.push(("CFLAGS", flags.cflags.join(" ")));
}
if !flags.chost.is_empty() {
env_vars.push(("CHOST", flags.chost.clone()));
}
if !flags.cbuild.is_empty() {
env_vars.push(("CBUILD", flags.cbuild.clone()));
}
if !flags.ldflags.is_empty() {
let ldflags = if flags.libc.is_empty() {
flags.ldflags.join(" ")
} else {
format!(
"{} -Wl,--dynamic-linker={}",
flags.ldflags.join(" "),
flags.libc
)
};
env_vars.push(("LDFLAGS", ldflags));
}
env_vars.push(("CC", cc));
// Add cross-compilation env
if let Some(cc_cfg) = cross {
env_vars.push(("CXX", cc_cfg.cxx.clone()));
env_vars.push(("AR", cc_cfg.ar.clone()));
}
// Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=)
let prefix = flags
.configure
.iter()
.find(|s| s.contains("CMAKE_INSTALL_PREFIX="))
.and_then(|s| s.split('=').nth(1))
.unwrap_or(&flags.prefix);
// Generate toolchain file if cross-compiling
let toolchain_file = if let Some(cc_cfg) = cross {
Some(cc_cfg.generate_cmake_toolchain(&build_dir)?)
} else {
None
};
// Run cmake configure
println!("Running cmake configure...");
let mut cmake_cmd = Command::new("cmake");
cmake_cmd.current_dir(&build_dir);
cmake_cmd.arg("-S").arg(&actual_src);
cmake_cmd.arg("-B").arg(&build_dir);
cmake_cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix));
cmake_cmd.arg("-DCMAKE_BUILD_TYPE=Release");
// Add toolchain file for cross-compilation
if let Some(ref tf) = toolchain_file {
cmake_cmd.arg(format!("-DCMAKE_TOOLCHAIN_FILE={}", tf.display()));
}
// Add custom configure flags from spec (supports cross-compilation overrides)
for flag in &flags.configure {
// Expand environment variables in the flag
let expanded = expand_env_vars(flag);
cmake_cmd.arg(&expanded);
}
crate::builder::prepare_command(&mut cmake_cmd, &env_vars);
let status = cmake_cmd.status().context("Failed to run cmake")?;
if !status.success() {
anyhow::bail!("cmake configure failed");
}
// Run cmake build
println!("Running cmake build...");
let mut build_cmd = Command::new("cmake");
build_cmd.arg("--build").arg(&build_dir);
build_cmd.arg("-j").arg(num_cpus().to_string());
crate::builder::prepare_command(&mut build_cmd, &env_vars);
let status = build_cmd
.status()
.with_context(|| format!("Failed to run cmake build for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("cmake build failed");
}
// Run cmake install with fakeroot if not root
println!(
"Running cmake install{}...",
if fakeroot::is_root() {
""
} else {
" (with fakeroot)"
}
);
let mut install_cmd = fakeroot::wrap_install_command("cmake", destdir);
install_cmd.arg("--install").arg(&build_dir);
let mut install_env = env_vars.clone();
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
crate::builder::prepare_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
.with_context(|| format!("Failed to run cmake install for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("cmake install failed");
}
Ok(())
}
/// 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
for (key, value) in std::env::vars() {
result = result.replace(&format!("${}", key), &value);
result = result.replace(&format!("${{{}}}", key), &value);
}
result
}
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
+103
View File
@@ -0,0 +1,103 @@
//! Custom build scripts
use crate::cross::CrossConfig;
use crate::fakeroot;
use crate::package::PackageSpec;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub fn build(
spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
) -> Result<()> {
let flags = &spec.build.flags;
// Create destdir
fs::create_dir_all(destdir)?;
let mut env_vars: Vec<(&str, String)> = vec![];
// For custom builds, look for a build.sh script in the source directory
let build_script = src_dir.join("build.sh");
if !build_script.exists() {
anyhow::bail!(
"Custom build type requires build.sh in source directory: {}",
src_dir.display()
);
}
println!(
"Running custom build script{}...",
if fakeroot::is_root() {
""
} else {
" (with fakeroot)"
}
);
let mut cmd = fakeroot::wrap_install_command("bash", destdir);
cmd.current_dir(src_dir);
cmd.arg(&build_script);
if !flags.cflags.is_empty() {
env_vars.push(("CFLAGS", flags.cflags.join(" ")));
}
if !flags.ldflags.is_empty() {
let ldflags = if flags.libc.is_empty() {
flags.ldflags.join(" ")
} else {
format!(
"{} -Wl,--dynamic-linker={}",
flags.ldflags.join(" "),
flags.libc
)
};
env_vars.push(("LDFLAGS", ldflags));
}
env_vars.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
env_vars.push(("PREFIX", spec.build.flags.prefix.clone()));
env_vars.push((
"NYAPM_SPECDIR",
spec.spec_dir.to_string_lossy().into_owned(),
));
if !flags.chost.is_empty() {
env_vars.push(("CHOST", flags.chost.clone()));
}
if !flags.cbuild.is_empty() {
env_vars.push(("CBUILD", flags.cbuild.clone()));
}
// Use cross-compilation tools if configured
if let Some(cc_cfg) = cross {
env_vars.push(("CC", cc_cfg.cc.clone()));
env_vars.push(("CXX", cc_cfg.cxx.clone()));
env_vars.push(("AR", cc_cfg.ar.clone()));
env_vars.push(("RANLIB", cc_cfg.ranlib.clone()));
env_vars.push(("STRIP", cc_cfg.strip.clone()));
env_vars.push(("LD", cc_cfg.ld.clone()));
env_vars.push(("NM", cc_cfg.nm.clone()));
env_vars.push(("CROSS_PREFIX", cc_cfg.prefix.clone()));
env_vars.push(("CROSS_COMPILE", format!("{}-", cc_cfg.prefix)));
} else {
env_vars.push(("CC", flags.cc.clone()));
env_vars.push(("AR", flags.ar.clone()));
}
crate::builder::prepare_command(&mut cmd, &env_vars);
let status = cmd
.status()
.with_context(|| format!("Failed to run build script: {}", build_script.display()))?;
if !status.success() {
anyhow::bail!("Custom build script failed with status: {}", status);
}
Ok(())
}
+146
View File
@@ -0,0 +1,146 @@
//! Meson build system
use crate::cross::CrossConfig;
use crate::fakeroot;
use crate::package::PackageSpec;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(
spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
) -> Result<()> {
let flags = &spec.build.flags;
let build_dir = src_dir.join("builddir");
// Create directories
fs::create_dir_all(&build_dir)?;
fs::create_dir_all(destdir)?;
// Environment variables
let mut env_vars: Vec<(&str, String)> = vec![];
// Use cross-compilation tools if configured
let cc = if let Some(cc_cfg) = cross {
cc_cfg.cc.clone()
} else {
flags.cc.clone()
};
if !flags.cflags.is_empty() {
env_vars.push(("CFLAGS", flags.cflags.join(" ")));
}
if !flags.chost.is_empty() {
env_vars.push(("CHOST", flags.chost.clone()));
}
if !flags.cbuild.is_empty() {
env_vars.push(("CBUILD", flags.cbuild.clone()));
}
if !flags.ldflags.is_empty() {
let ldflags = if flags.libc.is_empty() {
flags.ldflags.join(" ")
} else {
format!(
"{} -Wl,--dynamic-linker={}",
flags.ldflags.join(" "),
flags.libc
)
};
env_vars.push(("LDFLAGS", ldflags));
}
env_vars.push(("CC", cc));
// Add cross-compilation env
if let Some(cc_cfg) = cross {
env_vars.push(("CXX", cc_cfg.cxx.clone()));
env_vars.push(("AR", cc_cfg.ar.clone()));
}
// 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)?)
} else {
None
};
// Run meson setup
println!("Running meson setup...");
let mut meson_cmd = Command::new("meson");
meson_cmd.current_dir(src_dir);
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()));
}
crate::builder::prepare_command(&mut meson_cmd, &env_vars);
let status = meson_cmd.status().context("Failed to run meson setup")?;
if !status.success() {
anyhow::bail!("meson setup failed");
}
// Run ninja build
println!("Running ninja...");
let mut ninja_cmd = Command::new("ninja");
ninja_cmd.current_dir(&build_dir);
ninja_cmd.arg("-j").arg(num_cpus().to_string());
crate::builder::prepare_command(&mut ninja_cmd, &env_vars);
let status = ninja_cmd
.status()
.with_context(|| format!("Failed to run ninja for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("ninja build failed");
}
// Run meson install with fakeroot if not root
println!(
"Running meson install{}...",
if fakeroot::is_root() {
""
} else {
" (with fakeroot)"
}
);
let mut install_cmd = fakeroot::wrap_install_command("meson", destdir);
install_cmd.arg("install");
install_cmd.arg("-C").arg(&build_dir);
let mut install_env = env_vars.clone();
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
crate::builder::prepare_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
.with_context(|| format!("Failed to run meson install for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("meson install failed");
}
Ok(())
}
fn num_cpus() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
+101
View File
@@ -0,0 +1,101 @@
//! Build system abstraction
mod autotools;
mod bin;
mod cmake;
mod custom;
mod meson;
mod rust;
use crate::cross::CrossConfig;
use crate::package::{BuildType, PackageSpec};
use anyhow::Result;
use std::path::Path;
use std::process::Command;
/// Prepare a Command with a hermetic environment and some essential variables preserved.
pub fn prepare_command(cmd: &mut Command, env_vars: &[(&str, String)]) {
cmd.env_clear();
// Preserve essential environment variables
for var in &["PATH", "LANG", "HOME", "SHELL", "DESTDIR"] {
if let Ok(val) = std::env::var(var) {
cmd.env(var, val);
}
}
// Set requested environment variables
for (key, val) in env_vars {
cmd.env(key, val);
}
}
/// Build a package using the appropriate build system
pub fn build(
spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
) -> Result<()> {
if let Some(cc) = cross {
println!(
"Cross-compiling for {} with {:?}...",
cc.prefix, spec.build.build_type
);
} else {
println!("Building with {:?}...", spec.build.build_type);
}
// Clean destdir to prevent stale files/directories (e.g., directories where symlinks should be)
if destdir.exists() {
std::fs::remove_dir_all(destdir)?;
}
match spec.build.build_type {
BuildType::Autotools => autotools::build(spec, src_dir, destdir, cross),
BuildType::CMake => cmake::build(spec, src_dir, destdir, cross),
BuildType::Meson => meson::build(spec, src_dir, destdir, cross),
BuildType::Custom => custom::build(spec, src_dir, destdir, cross),
BuildType::Rust => rust::build(spec, src_dir, destdir, cross),
BuildType::Bin => bin::build(spec, src_dir, destdir, cross),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prepare_command() {
let mut cmd = Command::new("ls");
// Set an env var that should be cleared
cmd.env("FORBIDDEN", "value");
// Set PATH manually in the current process to ensure it's picked up if it exists
unsafe {
std::env::set_var("PATH", "/usr/bin");
std::env::set_var("HOME", "/home/test");
}
prepare_command(&mut cmd, &[("MYVAR", "myval".to_string())]);
let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect();
assert!(envs.get(std::ffi::OsStr::new("PATH")).is_some());
assert!(envs.get(std::ffi::OsStr::new("HOME")).is_some());
assert!(envs.get(std::ffi::OsStr::new("FORBIDDEN")).is_none());
assert_eq!(
envs.get(std::ffi::OsStr::new("MYVAR")),
Some(&Some(std::ffi::OsString::from("myval").as_os_str()))
);
}
#[test]
fn test_prepare_command_preserves_destdir() {
let mut cmd = std::process::Command::new("ls");
unsafe {
std::env::set_var("DESTDIR", "/tmp/dest");
}
prepare_command(&mut cmd, &[]);
let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect();
assert_eq!(
envs.get(std::ffi::OsStr::new("DESTDIR")),
Some(&Some(std::ffi::OsString::from("/tmp/dest").as_os_str()))
);
}
}
+179
View File
@@ -0,0 +1,179 @@
//! Rust/Cargo build system
use crate::cross::CrossConfig;
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,
src_dir: &Path,
destdir: &Path,
cross: Option<&CrossConfig>,
) -> Result<()> {
let flags = &spec.build.flags;
// Create destdir
fs::create_dir_all(destdir)?;
// Isolate from parent workspace by adding empty [workspace] if not present
// This prevents "believes it's in a workspace when it's not" errors
let cargo_toml = src_dir.join("Cargo.toml");
if cargo_toml.exists() {
let contents = fs::read_to_string(&cargo_toml)
.with_context(|| format!("Failed to read {}", cargo_toml.display()))?;
if !contents.contains("[workspace]") {
let isolated = format!("{}\n\n[workspace]\n", contents);
fs::write(&cargo_toml, isolated).with_context(|| {
format!("Failed to isolate Cargo.toml: {}", cargo_toml.display())
})?;
}
}
// Determine profile
let is_release = flags.profile == "release";
let profile_dir = if is_release { "release" } else { "debug" };
// Determine target triple
let target = if !flags.target.is_empty() {
Some(flags.target.clone())
} else {
cross.map(|cc_cfg| cc_cfg.prefix.clone())
};
// Build environment
let mut env_vars: Vec<(&str, String)> = vec![];
// RUSTFLAGS
if !flags.rustflags.is_empty() {
env_vars.push(("RUSTFLAGS", flags.rustflags.join(" ")));
}
// If cross-compiling, set linker via CARGO_TARGET_*_LINKER
if let Some(cc_cfg) = cross {
// Convert target triple to uppercase with underscores for env var
let target_env = target
.as_ref()
.unwrap_or(&cc_cfg.prefix)
.to_uppercase()
.replace('-', "_");
let linker_var = format!("CARGO_TARGET_{}_LINKER", target_env);
env_vars.push((linker_var.leak(), cc_cfg.cc.clone()));
env_vars.push(("CC".to_string().leak(), cc_cfg.cc.clone()));
env_vars.push(("AR".to_string().leak(), cc_cfg.ar.clone()));
}
// Set default rustup toolchain if not already set
if std::env::var("RUSTUP_TOOLCHAIN").is_err() {
env_vars.push(("RUSTUP_TOOLCHAIN", "stable".to_string()));
}
// Run cargo build
println!(
"Running cargo build ({})...",
if is_release { "release" } else { "debug" }
);
let mut cargo_cmd = Command::new("cargo");
cargo_cmd.current_dir(src_dir);
cargo_cmd.arg("build");
if is_release {
cargo_cmd.arg("--release");
}
// Add target if specified
if let Some(ref t) = target {
cargo_cmd.arg("--target").arg(t);
}
// Add additional cargo args
for arg in &flags.cargs {
cargo_cmd.arg(arg);
}
// Set environment
crate::builder::prepare_command(&mut cargo_cmd, &env_vars);
let status = cargo_cmd
.status()
.with_context(|| format!("Failed to run cargo build for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("cargo build failed");
}
// Run post-compile hooks
hooks::run_post_compile_commands(spec, src_dir, destdir)?;
// Install binaries to destdir
println!("Installing binaries to DESTDIR...");
// Determine target directory
let target_dir = if let Some(ref t) = target {
src_dir.join("target").join(t).join(profile_dir)
} else {
src_dir.join("target").join(profile_dir)
};
// Use bindir from flags (default: /usr/bin)
let bin_dir = destdir.join(flags.bindir.trim_start_matches('/'));
fs::create_dir_all(&bin_dir)?;
// Find and copy executable files
if target_dir.exists() {
for entry in fs::read_dir(&target_dir)
.with_context(|| format!("Failed to read target directory: {}", target_dir.display()))?
{
let entry = entry?;
let path = entry.path();
// Skip directories and non-executable files
if path.is_dir() {
continue;
}
// Check if it's an executable (no extension on Linux, or special check)
let file_name = path.file_name().unwrap().to_string_lossy();
// Skip common non-binary files
if file_name.ends_with(".d")
|| file_name.ends_with(".rlib")
|| file_name.ends_with(".rmeta")
|| file_name.contains(".so")
|| file_name.starts_with("lib")
{
continue;
}
// Check if file is executable
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if path
.metadata()
.ok()
.filter(|m| m.permissions().mode() & 0o111 != 0 && m.is_file())
.is_some()
{
let dest = bin_dir.join(&*file_name);
println!(" Installing: {}", file_name);
fs::copy(&path, &dest).with_context(|| {
format!("Failed to copy {} to {}", path.display(), dest.display())
})?;
// Preserve executable permission
let mut perms = fs::metadata(&dest)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&dest, perms)?;
}
}
}
}
// Run post-install hooks
hooks::run_post_install_commands(spec, src_dir, destdir)?;
Ok(())
}