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:
Executable
+209
@@ -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)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Executable
+179
@@ -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)
|
||||
}
|
||||
Executable
+103
@@ -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(())
|
||||
}
|
||||
Executable
+146
@@ -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)
|
||||
}
|
||||
Executable
+101
@@ -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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+179
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user