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
+7
@@ -0,0 +1,7 @@
|
||||
//! Package specification parsing
|
||||
|
||||
mod packager;
|
||||
mod spec;
|
||||
|
||||
pub use packager::Packager;
|
||||
pub use spec::*;
|
||||
@@ -0,0 +1,304 @@
|
||||
//! Package creation and archive management
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tar::Builder;
|
||||
use zstd::stream::write::Encoder;
|
||||
|
||||
pub struct Packager {
|
||||
pub spec: PackageSpec,
|
||||
pub destdir: PathBuf,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
impl Packager {
|
||||
pub fn new(spec: PackageSpec, destdir: PathBuf, config: Config) -> Self {
|
||||
Self {
|
||||
spec,
|
||||
destdir,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a package archive (.depot.pkg.tar.zst) from the destdir
|
||||
pub fn create_package(&self, output_dir: &Path, arch: &str) -> Result<PathBuf> {
|
||||
let filename = self.spec.package_filename(arch);
|
||||
let output_path = output_dir.join(&filename);
|
||||
|
||||
println!("Creating package {}...", filename);
|
||||
|
||||
// Generate .files.yaml
|
||||
self.generate_files_yaml()?;
|
||||
|
||||
// Generate .metadata.toml
|
||||
self.generate_metadata_toml()?;
|
||||
|
||||
// Create tar.zst
|
||||
let file = fs::File::create(&output_path)
|
||||
.with_context(|| format!("Failed to create output file: {}", output_path.display()))?;
|
||||
|
||||
// Respect zstd level from config (default to 3 if not specified)
|
||||
let level = self
|
||||
.config
|
||||
.package_overrides
|
||||
.get("compression_level")
|
||||
.and_then(|v| v.as_integer())
|
||||
.unwrap_or(3) as i32;
|
||||
|
||||
let mut encoder = Encoder::new(file, level)?;
|
||||
let _ = encoder.multithread(num_cpus() as u32);
|
||||
|
||||
let mut tar = Builder::new(encoder);
|
||||
|
||||
// Manual walk to ensure symlinks aren't followed (preserving them as links)
|
||||
for entry in walkdir::WalkDir::new(&self.destdir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
let rel_path = path.strip_prefix(&self.destdir)?;
|
||||
|
||||
// Skip the root of the destdir
|
||||
if rel_path.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_type = entry.file_type();
|
||||
if file_type.is_dir() {
|
||||
tar.append_dir(rel_path, path)?;
|
||||
} else if file_type.is_symlink() {
|
||||
// For symlinks, we need to read the link and append to tar correctly
|
||||
let target = fs::read_link(path)?;
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_metadata_in_mode(
|
||||
&fs::symlink_metadata(path)?,
|
||||
tar::HeaderMode::Deterministic,
|
||||
);
|
||||
tar.append_link(&mut header, rel_path, target)?;
|
||||
} else {
|
||||
// Files
|
||||
let mut file = fs::File::open(path)?;
|
||||
tar.append_file(rel_path, &mut file)?;
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = tar.into_inner()?;
|
||||
encoder.finish()?;
|
||||
|
||||
println!("Created package: {}", output_path.display());
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
fn generate_metadata_toml(&self) -> Result<()> {
|
||||
let metadata_path = self.destdir.join(".metadata.toml");
|
||||
|
||||
// Construct a simple metadata structure
|
||||
let mut map = toml::map::Map::new();
|
||||
map.insert(
|
||||
"name".to_string(),
|
||||
toml::Value::String(self.spec.package.name.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"version".to_string(),
|
||||
toml::Value::String(self.spec.package.version.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"revision".to_string(),
|
||||
toml::Value::Integer(self.spec.package.revision as i64),
|
||||
);
|
||||
map.insert(
|
||||
"description".to_string(),
|
||||
toml::Value::String(self.spec.package.description.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"homepage".to_string(),
|
||||
toml::Value::String(self.spec.package.homepage.clone()),
|
||||
);
|
||||
map.insert(
|
||||
"license".to_string(),
|
||||
toml::Value::String(self.spec.package.license.clone()),
|
||||
);
|
||||
|
||||
// Add provides
|
||||
map.insert(
|
||||
"provides".to_string(),
|
||||
toml::Value::Array(
|
||||
self.spec
|
||||
.alternatives
|
||||
.provides
|
||||
.iter()
|
||||
.map(|s| toml::Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
||||
// Add dependencies if useful for repo indexing
|
||||
let mut build_deps = toml::map::Map::new();
|
||||
build_deps.insert(
|
||||
"build".to_string(),
|
||||
toml::Value::Array(
|
||||
self.spec
|
||||
.dependencies
|
||||
.build
|
||||
.iter()
|
||||
.map(|s| toml::Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
build_deps.insert(
|
||||
"runtime".to_string(),
|
||||
toml::Value::Array(
|
||||
self.spec
|
||||
.dependencies
|
||||
.runtime
|
||||
.iter()
|
||||
.map(|s| toml::Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
map.insert("dependencies".to_string(), toml::Value::Table(build_deps));
|
||||
|
||||
let toml_str = toml::to_string(&toml::Value::Table(map))
|
||||
.context("Failed to serialize metadata to TOML")?;
|
||||
|
||||
fs::write(&metadata_path, toml_str)
|
||||
.with_context(|| format!("Failed to write metadata: {}", metadata_path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_files_yaml(&self) -> Result<()> {
|
||||
let files_path = self.destdir.join(".files.yaml");
|
||||
let mut files = Vec::new();
|
||||
|
||||
// Recursively list all files in destdir
|
||||
self.collect_files(&self.destdir, &self.destdir, &mut files)?;
|
||||
|
||||
let mut out_str = String::new();
|
||||
{
|
||||
let mut emitter = yaml_rust2::YamlEmitter::new(&mut out_str);
|
||||
let yaml_vec: Vec<yaml_rust2::Yaml> =
|
||||
files.into_iter().map(yaml_rust2::Yaml::String).collect();
|
||||
emitter
|
||||
.dump(&yaml_rust2::Yaml::Array(yaml_vec))
|
||||
.context("Failed to emit YAML")?;
|
||||
}
|
||||
|
||||
fs::write(&files_path, out_str)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_files(&self, base: &Path, current: &Path, files: &mut Vec<String>) -> Result<()> {
|
||||
for entry in fs::read_dir(current)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type()?;
|
||||
let relative = path.strip_prefix(base)?.to_string_lossy().to_string();
|
||||
|
||||
if file_type.is_dir() {
|
||||
self.collect_files(base, &path, files)?;
|
||||
} else {
|
||||
files.push(relative);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn num_cpus() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::package::{Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo};
|
||||
|
||||
fn mk_packager(destdir: PathBuf) -> Packager {
|
||||
Packager::new(
|
||||
PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: "test".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
description: "d".into(),
|
||||
homepage: "h".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: Vec::new(),
|
||||
build: Build {
|
||||
build_type: BuildType::Custom,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
},
|
||||
destdir,
|
||||
Config::for_rootfs(Path::new("/tmp/nonexistent")),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path();
|
||||
fs::create_dir_all(dest.join("usr/bin")).unwrap();
|
||||
fs::write(dest.join("usr/bin/foo"), "x").unwrap();
|
||||
fs::create_dir_all(dest.join("etc")).unwrap();
|
||||
fs::write(dest.join("etc/config"), "y").unwrap();
|
||||
|
||||
let packager = mk_packager(dest.to_path_buf());
|
||||
let mut files = Vec::new();
|
||||
packager.collect_files(dest, dest, &mut files).unwrap();
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
assert!(files.contains(&"usr/bin/foo".to_string()));
|
||||
assert!(files.contains(&"etc/config".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_files_yaml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path();
|
||||
fs::create_dir_all(dest.join("usr/bin")).unwrap();
|
||||
fs::write(dest.join("usr/bin/foo"), "x").unwrap();
|
||||
|
||||
let packager = mk_packager(dest.to_path_buf());
|
||||
packager.generate_files_yaml().unwrap();
|
||||
|
||||
let yaml_path = dest.join(".files.yaml");
|
||||
assert!(yaml_path.exists());
|
||||
let yaml_content = fs::read_to_string(yaml_path).unwrap();
|
||||
assert!(yaml_content.contains("usr/bin/foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_metadata_toml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path();
|
||||
|
||||
let packager = mk_packager(dest.to_path_buf());
|
||||
packager.generate_metadata_toml().unwrap();
|
||||
|
||||
let meta_path = dest.join(".metadata.toml");
|
||||
assert!(meta_path.exists());
|
||||
let content = fs::read_to_string(meta_path).unwrap();
|
||||
let val: toml::Value = toml::from_str(&content).unwrap();
|
||||
|
||||
assert_eq!(val.get("name").and_then(|v| v.as_str()), Some("test"));
|
||||
assert_eq!(val.get("version").and_then(|v| v.as_str()), Some("1.0"));
|
||||
assert_eq!(val.get("revision").and_then(|v| v.as_integer()), Some(1));
|
||||
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
||||
|
||||
let deps = val.get("dependencies").unwrap();
|
||||
assert!(deps.get("build").unwrap().as_array().unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
Executable
+712
@@ -0,0 +1,712 @@
|
||||
//! Package specification structures and TOML parsing
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Complete package specification from TOML
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PackageSpec {
|
||||
pub package: 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,
|
||||
|
||||
/// 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 mut spec: PackageSpec = toml::from_str(&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("."));
|
||||
|
||||
// Require at least one source (remote or manual)
|
||||
if spec.source.is_empty() && spec.manual_sources.is_empty() {
|
||||
anyhow::bail!("Package must have at least one source or manual_sources entry");
|
||||
}
|
||||
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
/// 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("$NYAPM_SPECDIR", &specdir)
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[Source] {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// 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 k.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()];
|
||||
}
|
||||
}
|
||||
"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()];
|
||||
}
|
||||
}
|
||||
"cc" => {
|
||||
if let Some(s) = v.as_str() {
|
||||
self.build.flags.cc = 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();
|
||||
}
|
||||
}
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
"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());
|
||||
}
|
||||
}
|
||||
}
|
||||
"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());
|
||||
}
|
||||
}
|
||||
}
|
||||
"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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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_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_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 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"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
config.appends.insert(
|
||||
"build.flags.cflags".to_string(),
|
||||
vec![toml::Value::String("-g".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()),
|
||||
])],
|
||||
);
|
||||
|
||||
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.rustflags.contains(&"-C".to_string()));
|
||||
assert!(
|
||||
spec.build
|
||||
.flags
|
||||
.rustflags
|
||||
.contains(&"opt-level=3".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[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(),
|
||||
};
|
||||
|
||||
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_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"
|
||||
);
|
||||
}
|
||||
|
||||
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: "MIT".into(),
|
||||
},
|
||||
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(),
|
||||
}],
|
||||
build: Build {
|
||||
build_type: BuildType::Custom,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::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)?;
|
||||
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,
|
||||
pub license: String,
|
||||
}
|
||||
|
||||
fn default_revision() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
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, Default, Deserialize)]
|
||||
pub struct Alternatives {
|
||||
#[serde(default)]
|
||||
pub provides: Vec<String>,
|
||||
/// Reserved for future package replacement feature
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
pub replaces: Vec<String>,
|
||||
}
|
||||
|
||||
/// Source tarball information
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Source {
|
||||
pub url: String,
|
||||
/// SHA256 checksum or "skip" to bypass
|
||||
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>,
|
||||
}
|
||||
|
||||
/// Manual (local) source file to copy before fetching remote sources.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ManualSource {
|
||||
/// Filename in the spec directory
|
||||
pub file: String,
|
||||
/// SHA256 checksum (optional, use "skip" to bypass verification)
|
||||
#[serde(default)]
|
||||
pub sha256: Option<String>,
|
||||
/// Destination path relative to build work directory (default: same as file)
|
||||
#[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, Deserialize)]
|
||||
pub struct Build {
|
||||
#[serde(rename = "type")]
|
||||
pub build_type: BuildType,
|
||||
#[serde(default)]
|
||||
pub flags: BuildFlags,
|
||||
}
|
||||
|
||||
/// Supported build systems
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BuildType {
|
||||
Autotools,
|
||||
CMake,
|
||||
Meson,
|
||||
Custom,
|
||||
Rust,
|
||||
/// Binary distribution installer (e.g., .deb, .rpm)
|
||||
Bin,
|
||||
}
|
||||
|
||||
/// Build flags and toolchain configuration
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct BuildFlags {
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub cflags: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub ldflags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub configure: Vec<String>,
|
||||
/// C compiler
|
||||
#[serde(default = "default_cc")]
|
||||
pub cc: 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 compile (after make, before make install)
|
||||
#[serde(default)]
|
||||
pub post_compile: Vec<String>,
|
||||
/// Commands to run after install (after make install)
|
||||
#[serde(default)]
|
||||
pub post_install: 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,
|
||||
|
||||
// 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,
|
||||
/// Binary package type when using BuildType::Bin (e.g. "deb")
|
||||
#[serde(default)]
|
||||
pub binary_type: String,
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
|
||||
/// Package dependencies
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct Dependencies {
|
||||
#[serde(default)]
|
||||
pub build: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub runtime: Vec<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user