Files
depot/src/package/spec.rs
T

3309 lines
104 KiB
Rust
Executable File

//! Package specification structures and TOML parsing
use anyhow::{Context, Result};
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
/// Complete package specification from TOML
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct PackageSpec {
pub package: PackageInfo,
/// Optional additional package outputs produced from the same spec/destdir
#[serde(default)]
pub packages: Vec<PackageInfo>,
#[serde(default)]
pub alternatives: Alternatives,
/// Manual (local) sources to copy before fetching remote sources.
#[serde(default)]
pub manual_sources: Vec<ManualSource>,
#[serde(default, deserialize_with = "deserialize_sources")]
pub source: Vec<Source>,
pub build: Build,
#[serde(default)]
pub dependencies: Dependencies,
/// Optional per-output alternatives/provides overrides keyed by package name.
///
/// Example:
/// [package_alternatives.clang]
/// provides = ["cc", "c++", "gcc"]
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub package_alternatives: BTreeMap<String, Alternatives>,
/// Optional per-output dependency overrides keyed by package name.
///
/// Example:
/// [package_dependencies.clang]
/// runtime = ["llvm-libs", "llvm-libgcc"]
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub package_dependencies: BTreeMap<String, Dependencies>,
/// Directory containing the spec file (used to resolve relative paths such as patches).
#[serde(skip)]
pub spec_dir: PathBuf,
}
impl PackageSpec {
/// Load package spec from a TOML file
pub fn from_file(path: &Path) -> Result<Self> {
// Canonicalize path to ensure spec_dir is absolute
let abs_path = path
.canonicalize()
.with_context(|| format!("Failed to resolve path: {}", path.display()))?;
let content = fs::read_to_string(&abs_path)
.with_context(|| format!("Failed to read package spec: {}", abs_path.display()))?;
let (base_content, appends) =
preprocess_spec_toml_appends(&content).with_context(|| {
format!("Failed to preprocess package spec: {}", abs_path.display())
})?;
let mut spec: PackageSpec = toml::from_str(&base_content)
.with_context(|| format!("Failed to parse package spec: {}", abs_path.display()))?;
spec.spec_dir = abs_path
.parent()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
spec.apply_spec_appends(&appends)?;
// Require at least one source (remote or manual) unless this is a metapackage.
if spec.source.is_empty() && spec.manual_sources.is_empty() && !spec.is_metapackage() {
anyhow::bail!(
"Package must have at least one source or manual_sources entry (except build.type = \"meta\")"
);
}
spec.validate_manual_sources()?;
Ok(spec)
}
fn apply_spec_appends(
&mut self,
appends: &std::collections::HashMap<String, Vec<toml::Value>>,
) -> Result<()> {
for (key, values) in appends {
if let Some(subkey) = key.strip_prefix("build.flags.") {
self.apply_append(subkey, values);
continue;
}
if let Some(subkey) = key.strip_prefix("flags.") {
self.apply_append(subkey, values);
continue;
}
if !key.contains('.') {
self.apply_append(key, values);
continue;
}
anyhow::bail!("Unsupported '+=' key in package spec: {}", key);
}
Ok(())
}
fn validate_manual_sources(&self) -> Result<()> {
for (idx, manual) in self.manual_sources.iter().enumerate() {
let has_file = manual
.file
.as_ref()
.map(|s| !s.trim().is_empty())
.unwrap_or(false);
let has_url = manual
.url
.as_ref()
.map(|s| !s.trim().is_empty())
.unwrap_or(false);
let file_count = manual.files.iter().filter(|s| !s.trim().is_empty()).count();
let url_count = manual.urls.iter().filter(|s| !s.trim().is_empty()).count();
let local_count = usize::from(has_file) + file_count;
let remote_count = usize::from(has_url) + url_count;
if local_count == 0 && remote_count == 0 {
anyhow::bail!(
"manual_sources[{}] must specify one of 'file', 'files', 'url', or 'urls'",
idx
);
}
if local_count > 0 && remote_count > 0 {
anyhow::bail!(
"manual_sources[{}] cannot mix local ('file'/'files') and remote ('url'/'urls') entries",
idx
);
}
if (local_count > 1 || remote_count > 1)
&& manual.dest.as_ref().is_some_and(|d| !d.trim().is_empty())
{
anyhow::bail!(
"manual_sources[{}] cannot use 'dest' with multiple entries in one block",
idx
);
}
if (local_count > 1 || remote_count > 1)
&& manual
.sha256
.as_ref()
.is_some_and(|h| !h.trim().is_empty() && h.trim() != "skip")
{
anyhow::bail!(
"manual_sources[{}] cannot use one 'sha256' for multiple entries in one block",
idx
);
}
}
Ok(())
}
/// Expand variables like `$name` and `$version` in a string.
pub fn expand_vars(&self, input: &str) -> String {
let specdir = self.spec_dir.to_string_lossy();
input
.replace("$name", &self.package.name)
.replace("$version", &self.package.version)
.replace("$specdir", &specdir)
.replace("$DEPOT_SPECDIR", &specdir)
}
pub fn sources(&self) -> &[Source] {
&self.source
}
/// Returns true when this spec is a metadata-only package that exists to pull dependencies.
pub fn is_metapackage(&self) -> bool {
matches!(self.build.build_type, BuildType::Meta)
}
/// Return all package outputs this spec will produce (primary + any extras)
pub fn outputs(&self) -> Vec<PackageInfo> {
let mut v = Vec::new();
v.push(self.package.clone());
v.extend(self.packages.clone());
v
}
/// Return dependencies for a specific output package name.
///
/// If no per-output override exists, returns the top-level dependencies.
pub fn dependencies_for_output(&self, pkg_name: &str) -> Dependencies {
self.package_dependencies
.get(pkg_name)
.cloned()
.unwrap_or_else(|| self.dependencies.clone())
}
/// Return all package names/provided features produced by this spec.
///
/// This includes all output package names and per-output `provides` entries.
pub fn local_dependency_provides(&self) -> HashSet<String> {
let mut names = HashSet::new();
for output in self.outputs() {
let output_name = output.name.clone();
names.insert(output_name.clone());
let alternatives = self.alternatives_for_output(&output_name);
for provided in alternatives.provides {
names.insert(provided);
}
}
names
}
/// Return alternatives/provides for a specific output package name.
///
/// If no per-output override exists, returns the top-level alternatives.
pub fn alternatives_for_output(&self, pkg_name: &str) -> Alternatives {
self.package_alternatives
.get(pkg_name)
.cloned()
.unwrap_or_else(|| self.alternatives.clone())
}
/// Apply system configuration overrides and appends
pub fn apply_config(&mut self, config: &crate::config::Config) {
// Apply build overrides from /etc/depot.d/build.toml
self.apply_toml_overrides(&config.build_overrides, "build");
// Apply appends from /etc/depot.d/build.toml (e.g. build.flags.cflags += ["-O3"])
for (key, values) in &config.appends {
if let Some(subkey) = key.strip_prefix("build.flags.") {
self.apply_append(subkey, values);
}
}
}
fn apply_toml_overrides(&mut self, overrides: &toml::Value, _prefix: &str) {
// Support both [build.flags] and top-level [build] fields
if let Some(table) = overrides.as_table() {
self.apply_flags_table(table);
}
if let Some(table) = overrides.get("flags").and_then(|f| f.as_table()) {
self.apply_flags_table(table);
}
}
fn apply_flags_table(&mut self, table: &toml::map::Map<String, toml::Value>) {
for (k, v) in table {
// match case-insensitively for common keys (allow CXX/Cc etc.)
match k.to_lowercase().as_str() {
"cflags" => {
if let Some(arr) = v.as_array() {
self.build.flags.cflags = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.cflags = vec![s.to_string()];
}
}
"cflags-lib32" | "cflags_lib32" => {
if let Some(arr) = v.as_array() {
self.build.flags.cflags_lib32 = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.cflags_lib32 = vec![s.to_string()];
}
}
"cxxflags" => {
if let Some(arr) = v.as_array() {
self.build.flags.cxxflags = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.cxxflags = vec![s.to_string()];
}
}
"cxxflags-lib32" | "cxxflags_lib32" => {
if let Some(arr) = v.as_array() {
self.build.flags.cxxflags_lib32 = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.cxxflags_lib32 = vec![s.to_string()];
}
}
"ldflags" => {
if let Some(arr) = v.as_array() {
self.build.flags.ldflags = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.ldflags = vec![s.to_string()];
}
}
"keep" => {
if let Some(arr) = v.as_array() {
self.build.flags.keep = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.keep = vec![s.to_string()];
}
}
"cc" => {
if let Some(s) = v.as_str() {
self.build.flags.cc = s.to_string();
}
}
"cxx" => {
if let Some(s) = v.as_str() {
self.build.flags.cxx = s.to_string();
}
}
"ar" => {
if let Some(s) = v.as_str() {
self.build.flags.ar = s.to_string();
}
}
"prefix" => {
if let Some(s) = v.as_str() {
self.build.flags.prefix = s.to_string();
}
}
"chost" => {
if let Some(s) = v.as_str() {
self.build.flags.chost = s.to_string();
}
}
"cbuild" => {
if let Some(s) = v.as_str() {
self.build.flags.cbuild = s.to_string();
}
}
"carch" => {
if let Some(s) = v.as_str() {
self.build.flags.carch = s.to_string();
}
}
"makeflags" | "make_flags" | "make-flags" => {
if let Some(s) = v.as_str() {
self.build.flags.makeflags = s.to_string();
} else if let Some(arr) = v.as_array() {
self.build.flags.makeflags = arr
.iter()
.filter_map(|x| x.as_str())
.map(str::trim)
.filter(|x| !x.is_empty())
.collect::<Vec<_>>()
.join(" ");
}
}
"make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_vars = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_vars =
s.split_whitespace().map(String::from).collect();
}
}
"make_exec" | "make-exec" => {
if let Some(s) = v.as_str() {
self.build.flags.make_exec = s.to_string();
}
}
"make_target" | "make-target" | "make_build_target" | "make-build-target" => {
if let Some(s) = v.as_str() {
self.build.flags.make_target = s.to_string();
}
}
"make_targets" | "make-targets" | "make_build_targets" | "make-build-targets" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_targets = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_targets =
s.split_whitespace().map(String::from).collect();
}
}
"make_dirs" | "make-dirs" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_dirs = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_dirs =
s.split_whitespace().map(String::from).collect();
}
}
"make_test_vars" | "make-test-vars" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_test_vars = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_test_vars =
s.split_whitespace().map(String::from).collect();
}
}
"make_test_target" | "make-test-target" => {
if let Some(s) = v.as_str() {
self.build.flags.make_test_target = s.to_string();
}
}
"make_test_targets" | "make-test-targets" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_test_targets = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_test_targets =
s.split_whitespace().map(String::from).collect();
}
}
"make_test_dirs" | "make-test-dirs" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_test_dirs = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_test_dirs =
s.split_whitespace().map(String::from).collect();
}
}
"make_install_vars" | "make-install-vars" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_install_vars = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_install_vars =
s.split_whitespace().map(String::from).collect();
}
}
"make_install_target" | "make-install-target" => {
if let Some(s) = v.as_str() {
self.build.flags.make_install_target = s.to_string();
}
}
"make_install_targets" | "make-install-targets" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_install_targets = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_install_targets =
s.split_whitespace().map(String::from).collect();
}
}
"make_install_dirs" | "make-install-dirs" => {
if let Some(arr) = v.as_array() {
self.build.flags.make_install_dirs = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.make_install_dirs =
s.split_whitespace().map(String::from).collect();
}
}
"passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env"
| "export-env" => {
if let Some(arr) = v.as_array() {
self.build.flags.passthrough_env = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.passthrough_env =
s.split_whitespace().map(String::from).collect();
}
}
"no_flags" | "no-flags" => {
if let Some(b) = v.as_bool() {
self.build.flags.no_flags = b;
}
}
"no_strip" | "no-strip" => {
if let Some(b) = v.as_bool() {
self.build.flags.no_strip = b;
}
}
"no_delete_static" | "no-delete-static" => {
if let Some(b) = v.as_bool() {
self.build.flags.no_delete_static = b;
}
}
"no_compress_man"
| "no-compress-man"
| "no_compress_manpages"
| "no-compress-manpages" => {
if let Some(b) = v.as_bool() {
self.build.flags.no_compress_man = b;
}
}
"skip_tests" | "skip-tests" => {
if let Some(b) = v.as_bool() {
self.build.flags.skip_tests = b;
}
}
"build_32" | "build-32" => {
if let Some(b) = toml_value_as_boolish(v) {
self.build.flags.build_32 = b;
}
}
"configure_lib32" | "configure-lib32" => {
if let Some(arr) = v.as_array() {
self.build.flags.configure_lib32 = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.configure_lib32 = vec![s.to_string()];
}
}
"config_setting" | "config_settings" | "config-setting" | "config-settings" => {
if let Some(arr) = v.as_array() {
self.build.flags.config_settings = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.config_settings = vec![s.to_string()];
}
}
"configure_file" | "configure-file" => {
if let Some(s) = v.as_str() {
self.build.flags.configure_file = s.to_string();
}
}
"post_configure" | "post-configure" => {
if let Some(arr) = v.as_array() {
self.build.flags.post_configure = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.post_configure = vec![s.to_string()];
}
}
"post_configure_lib32" | "post_configure-lib32" | "post-configure-lib32" => {
if let Some(arr) = v.as_array() {
self.build.flags.post_configure_lib32 = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.post_configure_lib32 = vec![s.to_string()];
}
}
"post_compile" | "post-compile" => {
if let Some(arr) = v.as_array() {
self.build.flags.post_compile = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.post_compile = vec![s.to_string()];
}
}
"post_compile_lib32" | "post_compile-lib32" | "post-compile-lib32" => {
if let Some(arr) = v.as_array() {
self.build.flags.post_compile_lib32 = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.post_compile_lib32 = vec![s.to_string()];
}
}
"post_install" | "post-install" => {
if let Some(arr) = v.as_array() {
self.build.flags.post_install = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.post_install = vec![s.to_string()];
}
}
"post_install_lib32" | "post_install-lib32" | "post-install-lib32" => {
if let Some(arr) = v.as_array() {
self.build.flags.post_install_lib32 = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.post_install_lib32 = vec![s.to_string()];
}
}
// Add more fields as needed
_ => {}
}
}
}
fn apply_append(&mut self, key: &str, values: &[toml::Value]) {
match key {
"cflags" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.cflags
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.cflags.push(s.to_string());
}
}
}
"cflags-lib32" | "cflags_lib32" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.cflags_lib32
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.cflags_lib32.push(s.to_string());
}
}
}
"cxxflags" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.cxxflags
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.cxxflags.push(s.to_string());
}
}
}
"cxxflags-lib32" | "cxxflags_lib32" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.cxxflags_lib32
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.cxxflags_lib32.push(s.to_string());
}
}
}
"ldflags" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.ldflags
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.ldflags.push(s.to_string());
}
}
}
"keep" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.keep
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.keep.push(s.to_string());
}
}
}
"configure" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.configure
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.configure.push(s.to_string());
}
}
}
"configure_lib32" | "configure-lib32" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.configure_lib32
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.configure_lib32.push(s.to_string());
}
}
}
"config_setting" | "config_settings" | "config-setting" | "config-settings" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.config_settings
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.config_settings.push(s.to_string());
}
}
}
"configure_file" | "configure-file" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.configure_file = s.to_string();
}
}
"post_configure" | "post-configure" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.post_configure
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.post_configure.push(s.to_string());
}
}
}
"post_configure_lib32" | "post_configure-lib32" | "post-configure-lib32" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.post_configure_lib32
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.post_configure_lib32.push(s.to_string());
}
}
}
"post_compile" | "post-compile" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.post_compile
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.post_compile.push(s.to_string());
}
}
}
"post_compile_lib32" | "post_compile-lib32" | "post-compile-lib32" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.post_compile_lib32
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.post_compile_lib32.push(s.to_string());
}
}
}
"post_install" | "post-install" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.post_install
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.post_install.push(s.to_string());
}
}
}
"post_install_lib32" | "post_install-lib32" | "post-install-lib32" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.post_install_lib32
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.post_install_lib32.push(s.to_string());
}
}
}
"cargs" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.cargs
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.cargs.push(s.to_string());
}
}
}
"rustflags" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.rustflags
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.rustflags.push(s.to_string());
}
}
}
"cc" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.cc = s.to_string();
}
}
"ar" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.ar = s.to_string();
}
}
"prefix" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.prefix = s.to_string();
}
}
"chost" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.chost = s.to_string();
}
}
"cbuild" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.cbuild = s.to_string();
}
}
"carch" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.carch = s.to_string();
}
}
"makeflags" | "make_flags" | "make-flags" | "MAKEFLAGS" => {
for v in values {
if let Some(arr) = v.as_array() {
let joined = arr
.iter()
.filter_map(|x| x.as_str())
.map(str::trim)
.filter(|x| !x.is_empty())
.collect::<Vec<_>>()
.join(" ");
append_whitespace_separated(&mut self.build.flags.makeflags, &joined);
} else if let Some(s) = v.as_str() {
append_whitespace_separated(&mut self.build.flags.makeflags, s);
}
}
}
"make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_vars
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_vars
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_exec" | "make-exec" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.make_exec = s.to_string();
}
}
"make_target" | "make-target" | "make_build_target" | "make-build-target" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.make_target = s.to_string();
}
}
"make_targets" | "make-targets" | "make_build_targets" | "make-build-targets" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_targets
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_targets
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_dirs" | "make-dirs" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_dirs
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_dirs
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_test_vars" | "make-test-vars" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_test_vars
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_test_vars
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_test_target" | "make-test-target" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.make_test_target = s.to_string();
}
}
"make_test_targets" | "make-test-targets" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_test_targets
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_test_targets
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_test_dirs" | "make-test-dirs" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_test_dirs
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_test_dirs
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_install_vars" | "make-install-vars" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_install_vars
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_install_vars
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_install_target" | "make-install-target" => {
if let Some(s) = values.last().and_then(|v| v.as_str()) {
self.build.flags.make_install_target = s.to_string();
}
}
"make_install_targets" | "make-install-targets" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_install_targets
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_install_targets
.extend(s.split_whitespace().map(String::from));
}
}
}
"make_install_dirs" | "make-install-dirs" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.make_install_dirs
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.make_install_dirs
.extend(s.split_whitespace().map(String::from));
}
}
}
"passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env"
| "export-env" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.passthrough_env
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build
.flags
.passthrough_env
.extend(s.split_whitespace().map(String::from));
}
}
}
"no_flags" | "no-flags" => {
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
self.build.flags.no_flags = b;
}
}
"no_strip" | "no-strip" => {
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
self.build.flags.no_strip = b;
}
}
"no_delete_static" | "no-delete-static" => {
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
self.build.flags.no_delete_static = b;
}
}
"no_compress_man"
| "no-compress-man"
| "no_compress_manpages"
| "no-compress-manpages" => {
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
self.build.flags.no_compress_man = b;
}
}
"skip_tests" | "skip-tests" => {
if let Some(b) = values.last().and_then(toml_value_as_boolish) {
self.build.flags.skip_tests = b;
}
}
"build_32" | "build-32" => {
if let Some(b) = values.last().and_then(toml_value_as_boolish) {
self.build.flags.build_32 = b;
}
}
_ => {}
}
}
}
fn preprocess_spec_toml_appends(
input: &str,
) -> Result<(String, std::collections::HashMap<String, Vec<toml::Value>>)> {
let mut base_text = String::new();
let mut appends = std::collections::HashMap::new();
let mut current_table: Option<String> = None;
let mut in_array_table = false;
for line in input.lines() {
let trimmed = line.trim();
if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 {
current_table = Some(trimmed[2..trimmed.len() - 2].trim().to_string());
in_array_table = true;
base_text.push_str(line);
base_text.push('\n');
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
current_table = Some(trimmed[1..trimmed.len() - 1].trim().to_string());
in_array_table = false;
base_text.push_str(line);
base_text.push('\n');
continue;
}
if trimmed.is_empty() || trimmed.starts_with('#') {
base_text.push_str(line);
base_text.push('\n');
continue;
}
if let Some(plus_idx) = trimmed.find("+=") {
if in_array_table {
anyhow::bail!(
"'+=' is not supported inside array-of-table sections ({})",
current_table.as_deref().unwrap_or("")
);
}
let key = trimmed[..plus_idx].trim();
let val_str = trimmed[plus_idx + 2..].trim();
let val: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str))
.context("Failed to parse append value")?
.get("v")
.cloned()
.unwrap();
let full_key = if key.contains('.') {
key.to_string()
} else if let Some(table) = current_table.as_deref() {
format!("{}.{}", table, key)
} else {
key.to_string()
};
appends.entry(full_key).or_insert_with(Vec::new).push(val);
// Preserve line numbering for parser diagnostics.
base_text.push('\n');
continue;
}
base_text.push_str(line);
base_text.push('\n');
}
Ok((base_text, appends))
}
#[cfg(test)]
mod spec_tests {
use super::*;
#[test]
fn parse_single_source_table() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo-$version.tar.gz"
sha256 = "skip"
extract_dir = "foo-$version"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.package.name, "foo");
assert_eq!(spec.sources().len(), 1);
assert_eq!(
spec.expand_vars(&spec.sources()[0].url),
"https://example.com/foo-1.0.tar.gz"
);
assert!(spec.sources()[0].patches.is_empty());
assert!(spec.sources()[0].post_extract.is_empty());
assert!(spec.sources()[0].cherry_pick.is_empty());
assert_eq!(spec.spec_dir, tmp.path());
}
#[test]
fn parse_source_array() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[source]]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[[source]]
url = "https://example.com/bar.tar.gz"
sha256 = "skip"
extract_dir = "bar"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.sources().len(), 2);
assert_eq!(spec.sources()[0].extract_dir, "foo");
assert_eq!(spec.sources()[1].extract_dir, "bar");
}
#[test]
fn parse_git_source_with_cherry_pick() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.git#main"
sha256 = "skip"
extract_dir = "foo"
cherry_pick = ["deadbeef", "cafebabe"]
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.sources()[0].cherry_pick,
vec!["deadbeef".to_string(), "cafebabe".to_string()]
);
}
#[test]
fn parse_package_dependencies_overrides() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/llvm.tar.gz"
sha256 = "skip"
extract_dir = "llvm"
[build]
type = "custom"
[dependencies]
runtime = ["base"]
[package_dependencies.clang]
runtime = ["llvm-libs", "llvm-libgcc"]
[package_dependencies.llvm-libs]
runtime = ["llvm-libgcc", "zstd"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.dependencies_for_output("llvm").runtime,
vec!["base".to_string()]
);
assert_eq!(
spec.dependencies_for_output("clang").runtime,
vec!["llvm-libs".to_string(), "llvm-libgcc".to_string()]
);
assert_eq!(
spec.dependencies_for_output("llvm-libs").runtime,
vec!["llvm-libgcc".to_string(), "zstd".to_string()]
);
}
#[test]
fn parse_package_alternatives_overrides() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/llvm.tar.gz"
sha256 = "skip"
extract_dir = "llvm"
[build]
type = "custom"
[alternatives]
provides = ["toolchain"]
[package_alternatives.clang]
provides = ["cc", "c++", "gcc"]
[package_alternatives.llvm]
provides = ["binutils"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.alternatives_for_output("llvm").provides,
vec!["binutils".to_string()]
);
assert_eq!(
spec.alternatives_for_output("clang").provides,
vec!["cc".to_string(), "c++".to_string(), "gcc".to_string()]
);
assert_eq!(
spec.alternatives_for_output("other").provides,
vec!["toolchain".to_string()]
);
}
#[test]
fn parse_python_build_type() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "python"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(matches!(spec.build.build_type, BuildType::Python));
}
#[test]
fn parse_python_config_settings_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "python"
[build.flags]
config-setting = ["editable_mode=compat", "setup-args=--plat-name=x86_64"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.config_settings,
vec![
"editable_mode=compat".to_string(),
"setup-args=--plat-name=x86_64".to_string()
]
);
}
#[test]
fn parse_multiple_licenses() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = ["MIT", "Apache-2.0"]
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.package.license,
vec!["MIT".to_string(), "Apache-2.0".to_string()]
);
}
#[test]
fn parse_rejects_empty_sources() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
// `source = []` is not accepted (must have at least one entry)
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
source = []
[build]
type = "custom"
"#,
)
.unwrap();
assert!(PackageSpec::from_file(&path).is_err());
}
#[test]
fn parse_allows_metapackage_without_sources() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo-meta"
version = "1.0"
description = "metapackage"
homepage = "https://example.com"
license = "MIT"
[build]
type = "meta"
[dependencies]
runtime = ["foo", "bar"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.source.is_empty());
assert!(spec.manual_sources.is_empty());
assert!(spec.is_metapackage());
assert_eq!(spec.dependencies.runtime, vec!["foo", "bar"]);
}
#[test]
fn parse_manual_source_with_url() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
url = "https://example.com/manual.patch"
sha256 = "skip"
dest = "patches/manual.patch"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.manual_sources.len(), 1);
assert_eq!(
spec.manual_sources[0].url.as_deref(),
Some("https://example.com/manual.patch")
);
assert_eq!(spec.manual_sources[0].file, None);
}
#[test]
fn parse_manual_source_rejects_missing_file_and_url() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
sha256 = "skip"
[build]
type = "custom"
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).expect_err("spec should be rejected");
assert!(
err.to_string()
.contains("must specify one of 'file', 'files', 'url', or 'urls'")
);
}
#[test]
fn parse_manual_source_rejects_file_and_url_together() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
file = "manual.patch"
url = "https://example.com/manual.patch"
[build]
type = "custom"
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).expect_err("spec should be rejected");
assert!(
err.to_string()
.contains("cannot mix local ('file'/'files') and remote ('url'/'urls') entries")
);
}
#[test]
fn parse_manual_source_with_files_array() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[manual_sources]]
files = ["other", "system-auth"]
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.manual_sources.len(), 1);
assert_eq!(spec.manual_sources[0].files, vec!["other", "system-auth"]);
assert!(spec.manual_sources[0].urls.is_empty());
}
#[test]
fn test_apply_config() {
let mut spec = mk_spec("foo", "1.0");
let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent"));
// Mock some overrides and appends
config.build_overrides = toml::from_str(
r#"
[flags]
cc = "my-cc"
cflags = ["-O2"]
cxxflags = ["-O2", "-pipe"]
passthrough_env = ["RUSTFLAGS"]
makeflags = "-j8"
make_vars = ["V=1"]
make_dirs = ["lib"]
make_test_dirs = ["tests"]
make_install_dirs = ["lib"]
no_flags = true
no_strip = true
no_delete_static = true
no_compress_man = true
skip_tests = true
keep = ["etc/locale.gen"]
configure_file = "configure.gnu"
config-setting = ["editable_mode=compat"]
post_configure = ["echo configured"]
"#,
)
.unwrap();
config.appends.insert(
"build.flags.cflags".to_string(),
vec![toml::Value::String("-g".to_string())],
);
config.appends.insert(
"build.flags.cxxflags".to_string(),
vec![toml::Value::String("-stdlib=libc++".to_string())],
);
config.appends.insert(
"build.flags.rustflags".to_string(),
vec![toml::Value::Array(vec![
toml::Value::String("-C".to_string()),
toml::Value::String("opt-level=3".to_string()),
])],
);
config.appends.insert(
"build.flags.keep".to_string(),
vec![toml::Value::Array(vec![toml::Value::String(
"etc/locale.gen".to_string(),
)])],
);
config.appends.insert(
"build.flags.no_strip".to_string(),
vec![toml::Value::Boolean(false)],
);
config.appends.insert(
"build.flags.no_compress_man".to_string(),
vec![toml::Value::Boolean(false)],
);
config.appends.insert(
"build.flags.no_delete_static".to_string(),
vec![toml::Value::Boolean(false)],
);
config.appends.insert(
"build.flags.passthrough_env".to_string(),
vec![toml::Value::String("CARGO_HOME".to_string())],
);
config.appends.insert(
"build.flags.make_test_vars".to_string(),
vec![toml::Value::String("TESTS=smoke".to_string())],
);
config.appends.insert(
"build.flags.makeflags".to_string(),
vec![toml::Value::String("--output-sync=target".to_string())],
);
config.appends.insert(
"build.flags.make_dirs".to_string(),
vec![toml::Value::String("libelf".to_string())],
);
config.appends.insert(
"build.flags.make_test_dirs".to_string(),
vec![toml::Value::String("fuzz".to_string())],
);
config.appends.insert(
"build.flags.make_install_dirs".to_string(),
vec![toml::Value::String("tools".to_string())],
);
config.appends.insert(
"build.flags.make_install_vars".to_string(),
vec![toml::Value::String("DESTDIR=/tmp/pkg".to_string())],
);
config.appends.insert(
"build.flags.configure_file".to_string(),
vec![toml::Value::String("build-aux/configure".to_string())],
);
config.appends.insert(
"build.flags.config-setting".to_string(),
vec![toml::Value::String(
"setup-args=--plat-name=x86_64".to_string(),
)],
);
config.appends.insert(
"build.flags.post_configure".to_string(),
vec![toml::Value::String("touch configured.stamp".to_string())],
);
spec.apply_config(&config);
assert_eq!(spec.build.flags.cc, "my-cc");
assert!(spec.build.flags.cflags.contains(&"-O2".to_string()));
assert!(spec.build.flags.cflags.contains(&"-g".to_string()));
assert!(spec.build.flags.cxxflags.contains(&"-O2".to_string()));
assert!(spec.build.flags.cxxflags.contains(&"-pipe".to_string()));
assert!(
spec.build
.flags
.cxxflags
.contains(&"-stdlib=libc++".to_string())
);
assert!(spec.build.flags.rustflags.contains(&"-C".to_string()));
assert!(
spec.build
.flags
.rustflags
.contains(&"opt-level=3".to_string())
);
assert!(spec.build.flags.no_flags);
assert!(!spec.build.flags.no_strip);
assert!(!spec.build.flags.no_delete_static);
assert!(!spec.build.flags.no_compress_man);
assert!(
spec.build
.flags
.keep
.contains(&"etc/locale.gen".to_string())
);
assert!(
spec.build
.flags
.passthrough_env
.contains(&"RUSTFLAGS".to_string())
);
assert!(
spec.build
.flags
.passthrough_env
.contains(&"CARGO_HOME".to_string())
);
assert_eq!(spec.build.flags.makeflags, "-j8 --output-sync=target");
assert!(spec.build.flags.make_vars.contains(&"V=1".to_string()));
assert!(spec.build.flags.make_dirs.contains(&"lib".to_string()));
assert!(spec.build.flags.make_dirs.contains(&"libelf".to_string()));
assert!(spec.build.flags.skip_tests);
assert!(
spec.build
.flags
.make_test_vars
.contains(&"TESTS=smoke".to_string())
);
assert!(
spec.build
.flags
.make_test_dirs
.contains(&"tests".to_string())
);
assert!(
spec.build
.flags
.make_test_dirs
.contains(&"fuzz".to_string())
);
assert!(
spec.build
.flags
.make_install_vars
.contains(&"DESTDIR=/tmp/pkg".to_string())
);
assert!(
spec.build
.flags
.make_install_dirs
.contains(&"lib".to_string())
);
assert!(
spec.build
.flags
.make_install_dirs
.contains(&"tools".to_string())
);
assert_eq!(spec.build.flags.configure_file, "build-aux/configure");
assert!(
spec.build
.flags
.config_settings
.contains(&"editable_mode=compat".to_string())
);
assert!(
spec.build
.flags
.config_settings
.contains(&"setup-args=--plat-name=x86_64".to_string())
);
assert!(
spec.build
.flags
.post_configure
.contains(&"echo configured".to_string())
);
assert!(
spec.build
.flags
.post_configure
.contains(&"touch configured.stamp".to_string())
);
}
#[test]
fn parse_no_flags_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
no_flags = true
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.no_flags);
}
#[test]
fn parse_no_strip_no_delete_static_and_no_compress_man_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
no_strip = true
"no-delete-static" = true
no-compress-man = true
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.no_strip);
assert!(spec.build.flags.no_delete_static);
assert!(spec.build.flags.no_compress_man);
}
#[test]
fn parse_no_flags_alias_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
"no-flags" = true
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.no_flags);
}
#[test]
fn parse_skip_tests_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[build.flags]
skip_tests = true
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.skip_tests);
}
#[test]
fn parse_skip_tests_alias_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[build.flags]
"skip-tests" = true
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.skip_tests);
}
#[test]
fn parse_configure_file_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[build.flags]
configure_file = "build-aux/configure"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.build.flags.configure_file, "build-aux/configure");
}
#[test]
fn parse_lib32_build_flags_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[build.flags]
"build-32" = "true"
"CFLAGS-lib32" = ["-mstackrealign"]
"CXXFLAGS-lib32" = ["-fno-rtti"]
"configure-lib32" = ["--disable-static"]
"post_configure-lib32" = ["echo configured lib32"]
"post_compile-lib32" = ["echo compiled lib32"]
"post_install-lib32" = ["echo lib32"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(spec.build.flags.build_32);
assert_eq!(spec.build.flags.cflags_lib32, vec!["-mstackrealign"]);
assert_eq!(spec.build.flags.cxxflags_lib32, vec!["-fno-rtti"]);
assert_eq!(spec.build.flags.configure_lib32, vec!["--disable-static"]);
assert_eq!(
spec.build.flags.post_configure_lib32,
vec!["echo configured lib32"]
);
assert_eq!(
spec.build.flags.post_compile_lib32,
vec!["echo compiled lib32"]
);
assert_eq!(spec.build.flags.post_install_lib32, vec!["echo lib32"]);
}
#[test]
fn parse_post_configure_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "cmake"
[build.flags]
post_configure = ["cmake -L . > cmake-options.txt"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.post_configure,
vec!["cmake -L . > cmake-options.txt".to_string()]
);
}
#[test]
fn parse_keep_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
keep = ["etc/locale.gen", "etc/resolv.conf"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.keep,
vec!["etc/locale.gen".to_string(), "etc/resolv.conf".to_string()]
);
}
#[test]
fn parse_build_flags_appends_from_spec_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
cxxflags = ["-O2"]
cxxflags += [ "-Wno-gnu-statement-expression-from-macro-expansion" ]
ldflags += "-Wl,--as-needed"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.cxxflags,
vec![
"-O2".to_string(),
"-Wno-gnu-statement-expression-from-macro-expansion".to_string()
]
);
assert_eq!(
spec.build.flags.ldflags,
vec!["-Wl,--as-needed".to_string()]
);
}
#[test]
fn parse_passthrough_env_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
[build.flags]
passthrough_env = ["RUSTFLAGS", "CARGO_HOME"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.passthrough_env,
vec!["RUSTFLAGS".to_string(), "CARGO_HOME".to_string()]
);
}
#[test]
fn parse_test_dependencies_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[dependencies]
build = ["make"]
test = ["python", "bats"]
optional = ["gtk-doc"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.dependencies.test,
vec!["python".to_string(), "bats".to_string()]
);
assert_eq!(spec.dependencies.optional, vec!["gtk-doc".to_string()]);
}
#[test]
fn parse_make_var_overrides_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[build.flags]
make_vars = ["V=1", "CC=clang"]
make_exec = "ninja"
make_target = "bootstrap"
make_targets = ["stage1", "stage2"]
make_dirs = ["lib", "libelf"]
make_test_vars = ["TESTS=unit"]
make_test_target = "test"
make_test_targets = ["test-unit", "test-integration"]
make_test_dirs = ["tests"]
make_install_vars = ["STRIPPROG=true"]
make_install_target = "install/strip"
make_install_targets = ["install-runtime", "install-devel"]
make_install_dirs = ["lib", "apps"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(
spec.build.flags.make_vars,
vec!["V=1".to_string(), "CC=clang".to_string()]
);
assert_eq!(spec.build.flags.make_exec, "ninja");
assert_eq!(spec.build.flags.make_target, "bootstrap");
assert_eq!(
spec.build.flags.make_targets,
vec!["stage1".to_string(), "stage2".to_string()]
);
assert_eq!(
spec.build.flags.make_dirs,
vec!["lib".to_string(), "libelf".to_string()]
);
assert_eq!(
spec.build.flags.make_test_vars,
vec!["TESTS=unit".to_string()]
);
assert_eq!(spec.build.flags.make_test_target, "test".to_string());
assert_eq!(
spec.build.flags.make_test_targets,
vec!["test-unit".to_string(), "test-integration".to_string()]
);
assert_eq!(spec.build.flags.make_test_dirs, vec!["tests".to_string()]);
assert_eq!(
spec.build.flags.make_install_vars,
vec!["STRIPPROG=true".to_string()]
);
assert_eq!(
spec.build.flags.make_install_target,
"install/strip".to_string()
);
assert_eq!(
spec.build.flags.make_install_targets,
vec!["install-runtime".to_string(), "install-devel".to_string()]
);
assert_eq!(
spec.build.flags.make_install_dirs,
vec!["lib".to_string(), "apps".to_string()]
);
}
#[test]
fn parse_makeflags_from_spec() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "autotools"
[build.flags]
MAKEFLAGS = ["-j12", "--output-sync=target"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.build.flags.makeflags, "-j12 --output-sync=target");
}
#[test]
fn test_chost_cbuild_overrides() {
let mut spec = mk_spec("foo", "1.0");
let config = crate::config::Config {
cache_dir: "/tmp".into(),
build_dir: "/tmp".into(),
db_dir: "/tmp".into(),
build_overrides: toml::from_str(
r#"
chost = "x86_64-sfg-linux-gnu"
cbuild = "x86_64-pc-linux-gnu"
"#,
)
.unwrap(),
package_overrides: toml::Value::Table(toml::map::Map::new()),
appends: std::collections::HashMap::new(),
repo_settings: crate::config::RepoSettings::default(),
source_repos: std::collections::BTreeMap::new(),
binary_repos: std::collections::BTreeMap::new(),
mirrors: std::collections::HashMap::new(),
repo_clone_dir: PathBuf::from("/tmp"),
package_cache_dir: PathBuf::from("/tmp"),
};
spec.apply_config(&config);
assert_eq!(spec.build.flags.chost, "x86_64-sfg-linux-gnu");
assert_eq!(spec.build.flags.cbuild, "x86_64-pc-linux-gnu");
}
#[test]
fn test_default_and_override_carch() {
let mut spec = mk_spec("foo", "1.0");
// Default should be host arch
assert_eq!(spec.build.flags.carch, std::env::consts::ARCH.to_string());
// Override via config
let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent"));
config.build_overrides = toml::from_str(
r#"[flags]
carch = "armv7"
"#,
)
.unwrap();
spec.apply_config(&config);
assert_eq!(spec.build.flags.carch, "armv7");
}
#[test]
fn test_package_filename() {
let mut spec = mk_spec("foo", "1.0");
spec.package.revision = 2;
assert_eq!(
spec.package_filename("x86_64"),
"foo-1.0-2-x86_64.depot.pkg.tar.zst"
);
}
#[test]
fn parse_packages_array() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[[packages]]
name = "foo-dev"
version = "1.0"
description = "development files"
homepage = "h"
license = "MIT"
[[source]]
url = "https://example.com/foo-1.0.tar.gz"
sha256 = "skip"
extract_dir = "foo-1.0"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
let outputs = spec.outputs();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].name, "foo");
assert_eq!(outputs[1].name, "foo-dev");
}
fn mk_spec(name: &str, version: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: name.into(),
version: version.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: "h".into(),
sha256: "s".into(),
extract_dir: "e".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: 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("."),
}
}
}
impl fmt::Display for PackageSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"Package: {} v{}",
self.package.name, self.package.version
)?;
writeln!(f, "Description: {}", self.package.description)?;
writeln!(f, "Homepage: {}", self.package.homepage)?;
writeln!(f, "License: {}", self.package.license.join(", "))?;
writeln!(f, "Sources: {}", self.source.len())?;
writeln!(f, "Build Type: {:?}", self.build.build_type)?;
if !self.alternatives.provides.is_empty() {
writeln!(f, "Provides: {}", self.alternatives.provides.join(", "))?;
}
Ok(())
}
}
/// Package metadata
#[derive(Debug, Deserialize, serde::Serialize, Clone)]
pub struct PackageInfo {
pub name: String,
pub version: String,
/// Maintenance revision of the package (defaults to 1)
#[serde(default = "default_revision")]
pub revision: u32,
pub description: String,
pub homepage: String,
#[serde(
deserialize_with = "deserialize_licenses",
serialize_with = "serialize_licenses"
)]
pub license: Vec<String>,
}
fn default_revision() -> u32 {
1
}
fn deserialize_licenses<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrArray {
String(String),
Array(Vec<String>),
}
match StringOrArray::deserialize(deserializer)? {
StringOrArray::String(s) => Ok(vec![s]),
StringOrArray::Array(v) => Ok(v),
}
}
fn serialize_licenses<S>(licenses: &[String], serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
if licenses.len() == 1 {
serializer.serialize_str(&licenses[0])
} else {
licenses.serialize(serializer)
}
}
impl PackageSpec {
/// Generate the standard package filename: <name>-<version>-<revision>-<arch>.depot.pkg.tar.zst
pub fn package_filename(&self, arch: &str) -> String {
format!(
"{}-{}-{}-{}.depot.pkg.tar.zst",
self.package.name, self.package.version, self.package.revision, arch
)
}
}
/// Package alternatives (provides/replaces)
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct Alternatives {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub provides: Vec<String>,
/// Reserved for future package replacement feature
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[allow(dead_code)]
pub replaces: Vec<String>,
}
/// Source tarball information
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Source {
pub url: String,
/// Checksum for the source (e.g. `sha256:...`, `sha512:...`, `md5:...`, or raw SHA256 hex).
/// Use `skip` to bypass verification.
pub sha256: String,
/// Directory name after extraction (supports $name, $version)
pub extract_dir: String,
/// Patch files or URLs to apply after extraction.
///
/// Example:
/// patches = ["fix-build.patch", "<https://example.com/patches/foo.patch>"]
#[serde(default)]
pub patches: Vec<String>,
/// Commands to run after extraction (and after patches), executed in the source directory.
///
/// Example:
/// post_extract = ["autoreconf -fi"]
#[serde(default)]
pub post_extract: Vec<String>,
/// Optional list of git commit hashes/revs to cherry-pick after checkout.
///
/// This is only valid for git sources (`*.git` URL or `url#rev` git form).
/// Example:
/// cherry_pick = ["a1b2c3d4", "deadbeef"]
#[serde(
default,
alias = "cherry-pick",
deserialize_with = "deserialize_string_or_array"
)]
pub cherry_pick: Vec<String>,
}
/// Manual source copied before standard source fetching.
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ManualSource {
/// Filename in the spec directory (local manual source mode).
#[serde(default)]
pub file: Option<String>,
/// Multiple filenames in the spec directory (local manual source mode).
#[serde(default)]
pub files: Vec<String>,
/// Remote URL to fetch (remote manual source mode).
#[serde(default)]
pub url: Option<String>,
/// Multiple remote URLs to fetch (remote manual source mode).
#[serde(default)]
pub urls: Vec<String>,
/// Checksum (optional, use "skip" to bypass verification).
#[serde(default)]
pub sha256: Option<String>,
/// Destination path relative to build work directory.
/// Defaults to `file` for local mode or a derived filename for URL mode.
#[serde(default)]
pub dest: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum OneOrManySources {
One(Source),
Many(Vec<Source>),
}
fn deserialize_sources<'de, D>(deserializer: D) -> std::result::Result<Vec<Source>, D::Error>
where
D: Deserializer<'de>,
{
// Try to deserialize; if the field is missing/null, return empty vec
let parsed = Option::<OneOrManySources>::deserialize(deserializer)?;
match parsed {
Some(OneOrManySources::One(s)) => Ok(vec![s]),
Some(OneOrManySources::Many(v)) => Ok(v),
None => Ok(Vec::new()),
}
}
/// Build configuration
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Build {
#[serde(rename = "type")]
pub build_type: BuildType,
#[serde(default)]
pub flags: BuildFlags,
}
/// Supported build systems
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum BuildType {
Autotools,
CMake,
Meson,
Custom,
Python,
Rust,
Makefile,
Bin,
Meta,
}
/// Build flags and toolchain configuration
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct BuildFlags {
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub cflags: Vec<String>,
/// Extra flags exported to `CFLAGS` only for the lib32 build variant.
#[serde(
default,
alias = "cflags-lib32",
alias = "cflags_lib32",
alias = "CFLAGS-lib32",
alias = "CFLAGS_lib32",
deserialize_with = "deserialize_string_or_array"
)]
pub cflags_lib32: Vec<String>,
/// Extra flags exported to `CXXFLAGS`.
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub cxxflags: Vec<String>,
/// Extra flags exported to `CXXFLAGS` only for the lib32 build variant.
#[serde(
default,
alias = "cxxflags-lib32",
alias = "cxxflags_lib32",
alias = "CXXFLAGS-lib32",
alias = "CXXFLAGS_lib32",
deserialize_with = "deserialize_string_or_array"
)]
pub cxxflags_lib32: Vec<String>,
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub ldflags: Vec<String>,
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub keep: Vec<String>,
/// Disable exporting CFLAGS/CXXFLAGS/LDFLAGS for this package build.
#[serde(default, alias = "no-flags")]
pub no_flags: bool,
/// Disable automatic stripping of ELF files during staging.
#[serde(default, alias = "no-strip")]
pub no_strip: bool,
/// Disable automatic deletion of static libraries (`*.a`) during staging.
#[serde(
default,
alias = "no-delete-static",
alias = "no_remove_static",
alias = "no-remove-static"
)]
pub no_delete_static: bool,
/// Disable automatic zstd compression of man pages during staging.
#[serde(
default,
alias = "no-compress-man",
alias = "no_compress_manpages",
alias = "no-compress-manpages"
)]
pub no_compress_man: bool,
/// Skip automatic build-system test execution (e.g. Autotools `make check`/`make test`).
#[serde(default, alias = "skip-tests")]
pub skip_tests: bool,
/// Run an additional lib32 build pass and emit a `lib32-*` package.
#[serde(
default,
alias = "build-32",
alias = "build_32",
deserialize_with = "deserialize_boolish"
)]
pub build_32: bool,
#[serde(default)]
pub configure: Vec<String>,
/// PEP 517 config settings for Python builds (each entry is `KEY=VALUE` or `KEY`).
#[serde(
default,
alias = "config-setting",
alias = "config-settings",
alias = "config_setting",
alias = "config_settings",
deserialize_with = "deserialize_string_or_array_no_split"
)]
pub config_settings: Vec<String>,
/// Configure arguments used only for the lib32 build variant (replaces `configure` when set).
#[serde(default, alias = "configure-lib32", alias = "configure_lib32")]
pub configure_lib32: Vec<String>,
/// Autotools configure script path, relative to source root or absolute.
#[serde(default, alias = "configure-file")]
pub configure_file: String,
/// C compiler
#[serde(default = "default_cc")]
pub cc: String,
/// C++ compiler
#[serde(default = "default_cxx")]
pub cxx: String,
/// Archiver
#[serde(default = "default_ar")]
pub ar: String,
/// Dynamic loader path
#[serde(default)]
pub libc: String,
/// Root filesystem for installation (per-package override)
#[serde(default = "default_rootfs")]
#[allow(dead_code)]
pub rootfs: String,
/// Commands to run after configure/setup step, before compile/build step.
#[serde(default, alias = "post-configure")]
pub post_configure: Vec<String>,
/// Commands to run after configure/setup for the lib32 build variant.
#[serde(
default,
alias = "post-configure-lib32",
alias = "post_configure-lib32",
alias = "post_configure_lib32"
)]
pub post_configure_lib32: Vec<String>,
/// Commands to run after compile (after make, before make install).
#[serde(default, alias = "post-compile")]
pub post_compile: Vec<String>,
/// Commands to run after compile for the lib32 build variant.
#[serde(
default,
alias = "post-compile-lib32",
alias = "post_compile-lib32",
alias = "post_compile_lib32"
)]
pub post_compile_lib32: Vec<String>,
/// Commands to run after install (after make install)
#[serde(default, alias = "post-install")]
pub post_install: Vec<String>,
/// Commands to run after the lib32 install step (replaces `post_install` when set).
#[serde(
default,
alias = "post-install-lib32",
alias = "post_install-lib32",
alias = "post_install_lib32"
)]
pub post_install_lib32: Vec<String>,
/// Specific commands for 'makefile' build type
#[serde(default)]
pub makefile_commands: Vec<String>,
#[serde(default)]
pub makefile_install_commands: Vec<String>,
/// Installation prefix (default: /usr)
#[serde(default = "default_prefix")]
pub prefix: String,
/// Target architecture triple (CHOST equivalent)
#[serde(default)]
pub chost: String,
/// Build architecture triple (CBUILD equivalent)
#[serde(default)]
pub cbuild: String,
/// CPU architecture short name (CARCH equivalent), e.g. "x86_64", "aarch64"
#[serde(default = "default_carch")]
pub carch: String,
/// MAKEFLAGS environment variable passed to build commands.
#[serde(
default,
alias = "make-flags",
alias = "make_flags",
alias = "MAKEFLAGS",
deserialize_with = "deserialize_string_or_array_joined"
)]
pub makeflags: String,
/// Variable overrides passed directly to `make` (compile step), e.g. ["V=1", "CC=clang"].
#[serde(
default,
alias = "make-vars",
alias = "make_build_vars",
alias = "make-build-vars",
deserialize_with = "deserialize_string_or_array"
)]
pub make_vars: Vec<String>,
/// Make-like executable for build/test/install phases (default: `make`), e.g. `ninja`.
#[serde(default, alias = "make-exec")]
pub make_exec: String,
/// Target for the compile/build phase (e.g. `all`, `bootstrap`).
#[serde(
default,
alias = "make-target",
alias = "make_build_target",
alias = "make-build-target"
)]
pub make_target: String,
/// Targets for the compile/build phase (e.g. `["all", "bootstrap"]`).
#[serde(
default,
alias = "make-targets",
alias = "make_build_targets",
alias = "make-build-targets",
deserialize_with = "deserialize_string_or_array"
)]
pub make_targets: Vec<String>,
/// Subdirectories (relative to build directory) where `make` should run.
#[serde(
default,
alias = "make-dirs",
deserialize_with = "deserialize_string_or_array"
)]
pub make_dirs: Vec<String>,
/// Variable overrides passed directly to `make check` / `make test`.
#[serde(
default,
alias = "make-test-vars",
deserialize_with = "deserialize_string_or_array"
)]
pub make_test_vars: Vec<String>,
/// Target for the test phase, passed to the make-like executable.
#[serde(default, alias = "make-test-target")]
pub make_test_target: String,
/// Targets for the test phase, passed to the make-like executable.
#[serde(
default,
alias = "make-test-targets",
deserialize_with = "deserialize_string_or_array"
)]
pub make_test_targets: Vec<String>,
/// Subdirectories (relative to build directory) where test targets should run.
#[serde(
default,
alias = "make-test-dirs",
deserialize_with = "deserialize_string_or_array"
)]
pub make_test_dirs: Vec<String>,
/// Variable overrides passed directly to `make install`.
#[serde(
default,
alias = "make-install-vars",
deserialize_with = "deserialize_string_or_array"
)]
pub make_install_vars: Vec<String>,
/// Target for the install phase (default: `install`).
#[serde(default, alias = "make-install-target")]
pub make_install_target: String,
/// Targets for the install phase.
#[serde(
default,
alias = "make-install-targets",
deserialize_with = "deserialize_string_or_array"
)]
pub make_install_targets: Vec<String>,
/// Subdirectories (relative to build directory) where `make install` should run.
#[serde(
default,
alias = "make-install-dirs",
deserialize_with = "deserialize_string_or_array"
)]
pub make_install_dirs: Vec<String>,
/// Additional host environment variable names to export unchanged to build commands.
/// Example: ["RUSTFLAGS", "CARGO_HOME"].
#[serde(
default,
alias = "passthrough-env",
alias = "pass_env",
alias = "pass-env",
alias = "export_env",
alias = "export-env",
deserialize_with = "deserialize_string_or_array"
)]
pub passthrough_env: Vec<String>,
// Rust-specific fields
/// Rust build profile: "debug" or "release" (default: release)
#[serde(default = "default_profile")]
pub profile: String,
/// Rust target triple (e.g., x86_64-unknown-linux-musl). Optional.
#[serde(default)]
pub target: String,
/// RUSTFLAGS environment variable
#[serde(default, deserialize_with = "deserialize_string_or_array")]
pub rustflags: Vec<String>,
/// Additional cargo arguments (short name)
#[serde(default)]
pub cargs: Vec<String>,
/// Binary installation directory relative to DESTDIR (default: /usr/bin)
#[serde(default = "default_bindir")]
pub bindir: String,
/// Subdirectory within extracted source to use as the actual source root.
/// Useful for monorepos like llvm-project where you want to build just one component.
#[serde(default)]
pub source_subdir: String,
/// Build directory relative to source root (e.g. "build")
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_dir: Option<String>,
/// Binary package type when using BuildType::Bin (e.g. "deb")
#[serde(default)]
pub binary_type: String,
/// Internal runtime marker used to adjust builder behavior for the lib32 variant.
#[serde(skip)]
pub lib32_variant: bool,
}
impl Default for BuildFlags {
fn default() -> Self {
BuildFlags {
cflags: Vec::new(),
cflags_lib32: Vec::new(),
cxxflags: Vec::new(),
cxxflags_lib32: Vec::new(),
ldflags: Vec::new(),
keep: Vec::new(),
no_flags: false,
no_strip: false,
no_delete_static: false,
no_compress_man: false,
skip_tests: false,
build_32: false,
configure: Vec::new(),
config_settings: Vec::new(),
configure_lib32: Vec::new(),
configure_file: String::new(),
cc: default_cc(),
cxx: default_cxx(),
ar: default_ar(),
libc: String::new(),
rootfs: default_rootfs(),
post_configure: Vec::new(),
post_configure_lib32: Vec::new(),
post_compile: Vec::new(),
post_compile_lib32: Vec::new(),
post_install: Vec::new(),
post_install_lib32: Vec::new(),
makefile_commands: Vec::new(),
makefile_install_commands: Vec::new(),
prefix: default_prefix(),
chost: String::new(),
cbuild: String::new(),
carch: default_carch(),
makeflags: String::new(),
make_vars: Vec::new(),
make_exec: String::new(),
make_target: String::new(),
make_targets: Vec::new(),
make_dirs: Vec::new(),
make_test_vars: Vec::new(),
make_test_target: String::new(),
make_test_targets: Vec::new(),
make_test_dirs: Vec::new(),
make_install_vars: Vec::new(),
make_install_target: String::new(),
make_install_targets: Vec::new(),
make_install_dirs: Vec::new(),
passthrough_env: Vec::new(),
profile: default_profile(),
target: String::new(),
rustflags: Vec::new(),
cargs: Vec::new(),
bindir: default_bindir(),
source_subdir: String::new(),
build_dir: None,
binary_type: String::new(),
lib32_variant: false,
}
}
}
fn deserialize_string_or_array<'de, D>(
deserializer: D,
) -> std::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrArray {
String(String),
Array(Vec<String>),
}
match Option::<StringOrArray>::deserialize(deserializer)? {
Some(StringOrArray::String(s)) => Ok(s.split_whitespace().map(String::from).collect()),
Some(StringOrArray::Array(a)) => Ok(a),
None => Ok(Vec::new()),
}
}
fn deserialize_string_or_array_no_split<'de, D>(
deserializer: D,
) -> std::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrArray {
String(String),
Array(Vec<String>),
}
match Option::<StringOrArray>::deserialize(deserializer)? {
Some(StringOrArray::String(s)) => Ok(vec![s]),
Some(StringOrArray::Array(a)) => Ok(a),
None => Ok(Vec::new()),
}
}
fn deserialize_string_or_array_joined<'de, D>(
deserializer: D,
) -> std::result::Result<String, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrArray {
String(String),
Array(Vec<String>),
}
match Option::<StringOrArray>::deserialize(deserializer)? {
Some(StringOrArray::String(s)) => Ok(s),
Some(StringOrArray::Array(a)) => Ok(a
.iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")),
None => Ok(String::new()),
}
}
fn deserialize_boolish<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Boolish {
Bool(bool),
String(String),
}
match Option::<Boolish>::deserialize(deserializer)? {
Some(Boolish::Bool(v)) => Ok(v),
Some(Boolish::String(s)) => match s.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Ok(true),
"false" | "0" | "no" | "off" => Ok(false),
other => Err(serde::de::Error::custom(format!(
"expected boolean string for lib32 flag, got '{}'",
other
))),
},
None => Ok(false),
}
}
fn toml_value_as_boolish(value: &toml::Value) -> Option<bool> {
if let Some(b) = value.as_bool() {
return Some(b);
}
value
.as_str()
.and_then(|s| match s.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
})
}
fn append_whitespace_separated(dst: &mut String, value: &str) {
let trimmed = value.trim();
if trimmed.is_empty() {
return;
}
if dst.is_empty() {
dst.push_str(trimmed);
} else {
dst.push(' ');
dst.push_str(trimmed);
}
}
fn default_cc() -> String {
// Prefer clang if available (supports -print-resource-dir and other useful flags)
if std::process::Command::new("which")
.arg("clang")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return "clang".to_string();
}
"gcc".to_string()
}
fn default_ar() -> String {
"ar".to_string()
}
fn default_rootfs() -> String {
"/".to_string()
}
fn default_profile() -> String {
"release".to_string()
}
fn default_bindir() -> String {
"/usr/bin".to_string()
}
fn default_prefix() -> String {
"/usr".to_string()
}
fn default_carch() -> String {
std::env::consts::ARCH.to_string()
}
fn default_cxx() -> String {
// Infer a sensible C++ compiler name from default_cc()
let cc = default_cc();
if cc.contains("clang") {
"clang++".to_string()
} else {
"g++".to_string()
}
}
/// Package dependencies
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct Dependencies {
/// Dependencies required for building packages.
#[serde(default)]
pub build: Vec<String>,
/// Dependencies required at runtime.
#[serde(default)]
pub runtime: Vec<String>,
/// Dependencies required to run package test suites.
#[serde(default)]
pub test: Vec<String>,
/// Optional runtime integrations that enhance functionality when installed.
#[serde(default)]
pub optional: Vec<String>,
}