feat: Implement build state tracking and interactive package specification creation
- Added StateTracker to manage build steps and allow resuming interrupted builds. - Updated post_extract function to utilize StateTracker for patch and command execution. - Introduced makefile.rs to handle building and installing packages via Makefile. - Created interactive.rs for an interactive package specification creator with SHA256 computation for source URLs. - Enhanced checksum verification to support multiple algorithms (SHA256, SHA512, MD5). - Added example configuration files in contrib for system-wide and user-level Depot configurations. - Updated tests to cover new functionality and ensure correctness of state tracking and interactive creation.
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
use crate::package::{
|
||||
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use inquire::{Confirm, Select, Text};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
impl fmt::Display for BuildType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BuildType::Autotools => write!(f, "Autotools"),
|
||||
BuildType::CMake => write!(f, "CMake"),
|
||||
BuildType::Meson => write!(f, "Meson"),
|
||||
BuildType::Custom => write!(f, "Custom"),
|
||||
BuildType::Rust => write!(f, "Rust"),
|
||||
BuildType::Makefile => write!(f, "Makefile"),
|
||||
BuildType::Bin => write!(f, "Binary installer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to compute the SHA256 hex for the given URL or local path.
|
||||
///
|
||||
/// Supports: `http`, `https`, `ftp`, `file`, and plain local filesystem paths.
|
||||
/// Returns Ok(hex) on success or Err on any failure (caller should treat failure as "no default").
|
||||
fn compute_sha256_for_url(u: &str) -> anyhow::Result<String> {
|
||||
// helper to hash a Read
|
||||
fn hash_reader<R: Read>(r: &mut R) -> anyhow::Result<String> {
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
let n = r.read(&mut buf)?;
|
||||
if n == 0 { break; }
|
||||
hasher.update(&buf[..n]);
|
||||
}
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
// Try to parse as URL first; if parsing fails, treat as local path.
|
||||
if let Ok(parsed) = Url::parse(u) {
|
||||
match parsed.scheme() {
|
||||
"http" | "https" => {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(20))
|
||||
.build()
|
||||
.with_context(|| "failed to build http client")?;
|
||||
let mut resp = client.get(u).send().with_context(|| format!("failed to GET {}", u))?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("HTTP status {}", resp.status());
|
||||
}
|
||||
return hash_reader(&mut resp);
|
||||
}
|
||||
"ftp" => {
|
||||
let host = parsed.host_str().context("ftp url missing host")?;
|
||||
let port = parsed.port_or_known_default().unwrap_or(21);
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let mut ftp_stream = ftp::FtpStream::connect(addr.as_str())
|
||||
.with_context(|| format!("failed to connect to {}", addr))?;
|
||||
let user = if parsed.username().is_empty() { "anonymous" } else { parsed.username() };
|
||||
let pass = parsed.password().unwrap_or("anonymous@");
|
||||
ftp_stream.login(user, pass).with_context(|| "ftp login failed")?;
|
||||
let mut result_hex = None;
|
||||
let path = parsed.path();
|
||||
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
|
||||
for p in candidates.iter().filter(|s| !s.is_empty()) {
|
||||
if let Ok(res) = ftp_stream.retr(p, |reader| {
|
||||
// reuse hash_reader by adapting reader to trait object
|
||||
let mut r = reader;
|
||||
hash_reader(&mut r).map_err(|e| ftp::FtpError::ConnectionError(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))
|
||||
}) {
|
||||
result_hex = Some(res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ftp_stream.quit().ok();
|
||||
if let Some(h) = result_hex { return Ok(h); }
|
||||
anyhow::bail!("ftp retrieval failed")
|
||||
}
|
||||
"file" => {
|
||||
if let Ok(fp) = parsed.to_file_path() {
|
||||
let mut f = std::fs::File::open(fp)?;
|
||||
return hash_reader(&mut f);
|
||||
}
|
||||
anyhow::bail!("invalid file URL")
|
||||
}
|
||||
_ => anyhow::bail!("unsupported URL scheme")
|
||||
}
|
||||
}
|
||||
|
||||
// Treat as local path if it exists
|
||||
let p = std::path::Path::new(u);
|
||||
if p.exists() {
|
||||
let mut f = std::fs::File::open(p)?;
|
||||
return hash_reader(&mut f);
|
||||
}
|
||||
|
||||
anyhow::bail!("could not compute sha256 for '{}': unsupported or unreachable", u)
|
||||
}
|
||||
|
||||
pub fn create_interactive() -> Result<PackageSpec> {
|
||||
println!("Interactive Package Specification Creator");
|
||||
println!("-----------------------------------------");
|
||||
|
||||
// Ask early whether this is a GNU project so we can pre-fill homepage and
|
||||
// avoid repeating the same question later for autotools sources.
|
||||
let is_gnu_project = Confirm::new("Is this a GNU project?")
|
||||
.with_help_message("Automatically fill GNU-specific defaults (homepage, mirrors)")
|
||||
.with_default(false)
|
||||
.prompt()?;
|
||||
|
||||
// Default package name to the current directory basename when possible.
|
||||
let default_name = std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()))
|
||||
.unwrap_or_else(|| "zlib".into());
|
||||
|
||||
let name = Text::new("Package Name:")
|
||||
.with_help_message("e.g. zlib, bash, linux")
|
||||
.with_default(default_name.as_str())
|
||||
.prompt()?;
|
||||
|
||||
if name.is_empty() {
|
||||
anyhow::bail!("Package name cannot be empty");
|
||||
}
|
||||
|
||||
let version = Text::new("Version:")
|
||||
.with_help_message("e.g. 1.2.11, 5.1")
|
||||
.with_placeholder("1.0.0")
|
||||
.prompt()?;
|
||||
|
||||
let description = Text::new("Description:")
|
||||
.with_help_message("A short description of the package")
|
||||
.prompt()?;
|
||||
|
||||
let homepage_default = if is_gnu_project {
|
||||
format!("https://www.gnu.org/software/{}/", name)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let homepage = Text::new("Homepage:")
|
||||
.with_help_message("Project website URL")
|
||||
.with_default(homepage_default.as_str())
|
||||
.prompt()?;
|
||||
|
||||
let license = Text::new("License:")
|
||||
.with_help_message("e.g. MIT, GPL-3.0, Apache-2.0")
|
||||
.with_placeholder("MIT")
|
||||
.prompt()?;
|
||||
|
||||
// Present core build system choices only (keep interactive concise)
|
||||
let build_types = vec![
|
||||
BuildType::Autotools,
|
||||
BuildType::CMake,
|
||||
BuildType::Meson,
|
||||
BuildType::Makefile,
|
||||
BuildType::Rust,
|
||||
];
|
||||
|
||||
let build_type = Select::new("Build System:", build_types)
|
||||
.with_help_message("Select the build system used by the package (common choices)")
|
||||
.prompt()?;
|
||||
|
||||
// If the build system is Autotools, offer a convenience prompt for GNU-hosted tarballs.
|
||||
let mut gnu_source_default = String::new();
|
||||
if let BuildType::Autotools = build_type {
|
||||
// Rely on the initial `is_gnu_project` answer (do not re-ask).
|
||||
if is_gnu_project {
|
||||
let suffix = Text::new("GNU tarball suffix:")
|
||||
.with_help_message("e.g. .tar.gz, .tar.xz")
|
||||
.with_default(".tar.gz")
|
||||
.prompt()?;
|
||||
gnu_source_default = format!(
|
||||
"https://mirrors.kernel.org/gnu/{name}/{name}-{version}{suffix}",
|
||||
name = name,
|
||||
version = version,
|
||||
suffix = suffix
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask whether to show advanced fields — if yes, prompt for them later.
|
||||
let show_advanced = Confirm::new("Show advanced options?")
|
||||
.with_help_message("Enable prompts for optional build flags and advanced fields")
|
||||
.with_default(false)
|
||||
.prompt()?;
|
||||
|
||||
let build_dir = if Confirm::new("Use separate build directory?")
|
||||
.with_help_message("e.g., build/ or out/")
|
||||
.with_default(false)
|
||||
.prompt()?
|
||||
{
|
||||
Some(
|
||||
Text::new("Build directory name:")
|
||||
.with_default("build")
|
||||
.prompt()?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Advanced fields (only shown when requested)
|
||||
let mut cflags = Vec::new();
|
||||
let mut ldflags = Vec::new();
|
||||
let mut chost = String::new();
|
||||
let mut cbuild = String::new();
|
||||
let mut carch = String::new();
|
||||
let mut bindir = String::new();
|
||||
|
||||
if show_advanced {
|
||||
let c = Text::new("CFLAGS (optional, empty to skip):")
|
||||
.with_help_message("Space-separated CFLAGS")
|
||||
.prompt()?;
|
||||
if !c.trim().is_empty() {
|
||||
cflags = c.split_whitespace().map(String::from).collect();
|
||||
}
|
||||
let l = Text::new("LDFLAGS (optional, empty to skip):")
|
||||
.with_help_message("Space-separated LDFLAGS")
|
||||
.prompt()?;
|
||||
if !l.trim().is_empty() {
|
||||
ldflags = l.split_whitespace().map(String::from).collect();
|
||||
}
|
||||
chost = Text::new("CHOST (optional):").prompt()?;
|
||||
cbuild = Text::new("CBUILD (optional):").prompt()?;
|
||||
carch = Text::new("CARCH (optional):").prompt()?;
|
||||
bindir = Text::new("Binary install dir (optional, default /usr/bin):")
|
||||
.with_default("/usr/bin")
|
||||
.prompt()?;
|
||||
}
|
||||
|
||||
let source_url = Text::new("Source URL:")
|
||||
.with_help_message("URL to the source tarball or git repository")
|
||||
.with_default(gnu_source_default.as_str())
|
||||
.prompt()?;
|
||||
|
||||
// Attempt to compute SHA256 automatically when online — use as the default if available.
|
||||
let computed_sha_default = match compute_sha256_for_url(&source_url) {
|
||||
Ok(hex) => {
|
||||
// Use raw hex as the default so pressing Enter accepts it
|
||||
hex
|
||||
}
|
||||
Err(_) => "skip".to_string(),
|
||||
};
|
||||
|
||||
let source_sha256 = Text::new("Source checksum:")
|
||||
.with_help_message("Accepts sha256:, sha512:, md5:, or raw SHA256 hex (use 'skip' to bypass)")
|
||||
.with_default(computed_sha_default.as_str())
|
||||
.prompt()?;
|
||||
|
||||
let extract_dir = Text::new("Extract Directory:")
|
||||
.with_help_message("Directory created after extraction (supports $name, $version)")
|
||||
.with_default("$name-$version")
|
||||
.prompt()?;
|
||||
|
||||
let mut sources = Vec::new();
|
||||
if !source_url.is_empty() {
|
||||
sources.push(Source {
|
||||
url: source_url,
|
||||
sha256: source_sha256,
|
||||
extract_dir,
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut runtime_deps = Vec::new();
|
||||
loop {
|
||||
let dep = Text::new("Runtime Dependency (empty to finish):").prompt()?;
|
||||
if dep.is_empty() {
|
||||
break;
|
||||
}
|
||||
runtime_deps.push(dep);
|
||||
}
|
||||
|
||||
let mut build_deps = Vec::new();
|
||||
loop {
|
||||
let dep = Text::new("Build-time Dependency (empty to finish):").prompt()?;
|
||||
if dep.is_empty() {
|
||||
break;
|
||||
}
|
||||
build_deps.push(dep);
|
||||
}
|
||||
|
||||
Ok(PackageSpec {
|
||||
package: PackageInfo {
|
||||
name,
|
||||
version,
|
||||
revision: 1,
|
||||
description,
|
||||
homepage,
|
||||
license,
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: sources,
|
||||
build: Build {
|
||||
build_type,
|
||||
flags: BuildFlags {
|
||||
build_dir,
|
||||
cflags,
|
||||
ldflags,
|
||||
chost,
|
||||
cbuild,
|
||||
carch,
|
||||
bindir: if bindir.is_empty() { BuildFlags::default().bindir } else { bindir },
|
||||
..BuildFlags::default()
|
||||
},
|
||||
},
|
||||
dependencies: Dependencies {
|
||||
build: build_deps,
|
||||
runtime: runtime_deps,
|
||||
},
|
||||
spec_dir: PathBuf::from("."),
|
||||
})
|
||||
}
|
||||
|
||||
/// Produce a compact TOML string for a spec created interactively.
|
||||
/// This omits default/empty fields so generated `pkg.toml` is concise.
|
||||
pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
let mut root = Table::new();
|
||||
|
||||
// package block
|
||||
let mut pkg = Table::new();
|
||||
pkg.insert("name".into(), Value::String(spec.package.name.clone()));
|
||||
pkg.insert("version".into(), Value::String(spec.package.version.clone()));
|
||||
if !spec.package.description.is_empty() {
|
||||
pkg.insert("description".into(), Value::String(spec.package.description.clone()));
|
||||
}
|
||||
if !spec.package.homepage.is_empty() {
|
||||
pkg.insert("homepage".into(), Value::String(spec.package.homepage.clone()));
|
||||
}
|
||||
if !spec.package.license.is_empty() {
|
||||
pkg.insert("license".into(), Value::String(spec.package.license.clone()));
|
||||
}
|
||||
root.insert("package".into(), Value::Table(pkg));
|
||||
|
||||
// additional package outputs (if any)
|
||||
if !spec.packages.is_empty() {
|
||||
let mut arr = Vec::new();
|
||||
for p in &spec.packages {
|
||||
let mut pt = Table::new();
|
||||
pt.insert("name".into(), Value::String(p.name.clone()));
|
||||
pt.insert("version".into(), Value::String(p.version.clone()));
|
||||
if !p.description.is_empty() {
|
||||
pt.insert("description".into(), Value::String(p.description.clone()));
|
||||
}
|
||||
if !p.homepage.is_empty() {
|
||||
pt.insert("homepage".into(), Value::String(p.homepage.clone()));
|
||||
}
|
||||
if !p.license.is_empty() {
|
||||
pt.insert("license".into(), Value::String(p.license.clone()));
|
||||
}
|
||||
arr.push(Value::Table(pt));
|
||||
}
|
||||
root.insert("packages".into(), Value::Array(arr));
|
||||
}
|
||||
|
||||
// sources
|
||||
if !spec.source.is_empty() {
|
||||
let mut arr = Vec::new();
|
||||
for s in &spec.source {
|
||||
let mut st = Table::new();
|
||||
st.insert("url".into(), Value::String(s.url.clone()));
|
||||
if !s.sha256.is_empty() && s.sha256.to_lowercase() != "skip" {
|
||||
st.insert("sha256".into(), Value::String(s.sha256.clone()));
|
||||
}
|
||||
let default_extract = format!("{}-{}", spec.package.name, spec.package.version);
|
||||
if s.extract_dir != default_extract {
|
||||
st.insert("extract_dir".into(), Value::String(s.extract_dir.clone()));
|
||||
}
|
||||
if !s.patches.is_empty() {
|
||||
st.insert(
|
||||
"patches".into(),
|
||||
Value::Array(s.patches.iter().map(|p| Value::String(p.clone())).collect()),
|
||||
);
|
||||
}
|
||||
if !s.post_extract.is_empty() {
|
||||
st.insert(
|
||||
"post_extract".into(),
|
||||
Value::Array(s.post_extract.iter().map(|p| Value::String(p.clone())).collect()),
|
||||
);
|
||||
}
|
||||
arr.push(Value::Table(st));
|
||||
}
|
||||
root.insert("source".into(), Value::Array(arr));
|
||||
}
|
||||
|
||||
// build (only include set fields)
|
||||
let mut build_tbl = Table::new();
|
||||
build_tbl.insert("type".into(), Value::String(format!("{}", format!("{:?}", spec.build.build_type).to_lowercase())));
|
||||
|
||||
let mut flags_tbl = Table::new();
|
||||
if let Some(bdir) = &spec.build.flags.build_dir {
|
||||
flags_tbl.insert("build_dir".into(), Value::String(bdir.clone()));
|
||||
}
|
||||
if !spec.build.flags.cflags.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"cflags".into(),
|
||||
Value::Array(spec.build.flags.cflags.iter().map(|s| Value::String(s.clone())).collect()),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.ldflags.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"ldflags".into(),
|
||||
Value::Array(spec.build.flags.ldflags.iter().map(|s| Value::String(s.clone())).collect()),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.chost.is_empty() {
|
||||
flags_tbl.insert("chost".into(), Value::String(spec.build.flags.chost.clone()));
|
||||
}
|
||||
if !spec.build.flags.cbuild.is_empty() {
|
||||
flags_tbl.insert("cbuild".into(), Value::String(spec.build.flags.cbuild.clone()));
|
||||
}
|
||||
if !spec.build.flags.carch.is_empty() && spec.build.flags.carch != BuildFlags::default().carch {
|
||||
flags_tbl.insert("carch".into(), Value::String(spec.build.flags.carch.clone()));
|
||||
}
|
||||
if spec.build.flags.bindir != BuildFlags::default().bindir {
|
||||
flags_tbl.insert("bindir".into(), Value::String(spec.build.flags.bindir.clone()));
|
||||
}
|
||||
|
||||
if !flags_tbl.is_empty() {
|
||||
build_tbl.insert("flags".into(), Value::Table(flags_tbl));
|
||||
}
|
||||
root.insert("build".into(), Value::Table(build_tbl));
|
||||
|
||||
// dependencies
|
||||
if !spec.dependencies.build.is_empty() || !spec.dependencies.runtime.is_empty() {
|
||||
let mut dep_tbl = Table::new();
|
||||
if !spec.dependencies.build.is_empty() {
|
||||
dep_tbl.insert(
|
||||
"build".into(),
|
||||
Value::Array(spec.dependencies.build.iter().map(|s| Value::String(s.clone())).collect()),
|
||||
);
|
||||
}
|
||||
if !spec.dependencies.runtime.is_empty() {
|
||||
dep_tbl.insert(
|
||||
"runtime".into(),
|
||||
Value::Array(spec.dependencies.runtime.iter().map(|s| Value::String(s.clone())).collect()),
|
||||
);
|
||||
}
|
||||
root.insert("dependencies".into(), Value::Table(dep_tbl));
|
||||
}
|
||||
|
||||
Ok(toml::to_string_pretty(&Value::Table(root))?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::package::{BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source};
|
||||
|
||||
#[test]
|
||||
fn spec_to_minimal_toml_omits_defaults() {
|
||||
let spec = PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: "foo".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
description: "A test".into(),
|
||||
homepage: "".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: vec![Source {
|
||||
url: "https://example.com/foo-1.0.tar.gz".into(),
|
||||
sha256: "skip".into(),
|
||||
extract_dir: "foo-1.0".into(),
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
}],
|
||||
build: Build {
|
||||
build_type: BuildType::Autotools,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
let toml = spec_to_minimal_toml(&spec).unwrap();
|
||||
assert!(toml.contains("name = \"foo\""));
|
||||
assert!(toml.contains("version = \"1.0\""));
|
||||
assert!(toml.contains("description = \"A test\""));
|
||||
// defaults should not be present
|
||||
assert!(!toml.contains("cflags"));
|
||||
assert!(!toml.contains("rustflags"));
|
||||
// sha256="skip" should be omitted
|
||||
assert!(!toml.contains("sha256"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_to_minimal_toml_includes_additional_packages() {
|
||||
let spec = PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: "foo".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
description: "A test".into(),
|
||||
homepage: "".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
packages: vec![PackageInfo {
|
||||
name: "foo-dev".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
description: "dev files".into(),
|
||||
homepage: "".into(),
|
||||
license: "MIT".into(),
|
||||
}],
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: vec![Source {
|
||||
url: "https://example.com/foo-1.0.tar.gz".into(),
|
||||
sha256: "skip".into(),
|
||||
extract_dir: "foo-1.0".into(),
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
}],
|
||||
build: Build {
|
||||
build_type: BuildType::Autotools,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
let toml = spec_to_minimal_toml(&spec).unwrap();
|
||||
assert!(toml.contains("[[packages]]"));
|
||||
assert!(toml.contains("name = \"foo-dev\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_sha256_for_local_path_and_file_url() {
|
||||
use tempfile::NamedTempFile;
|
||||
use sha2::Sha256 as TestSha256;
|
||||
use sha2::Digest as TestDigest;
|
||||
|
||||
let mut tmp = NamedTempFile::new().unwrap();
|
||||
std::io::Write::write_all(&mut tmp, b"abc").unwrap();
|
||||
let expected = {
|
||||
let mut h = TestSha256::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
};
|
||||
|
||||
// plain path
|
||||
let p = tmp.path().to_str().unwrap().to_string();
|
||||
assert_eq!(compute_sha256_for_url(&p).unwrap(), expected);
|
||||
|
||||
// file:// URL
|
||||
let file_url = format!("file://{}", tmp.path().to_str().unwrap());
|
||||
assert_eq!(compute_sha256_for_url(&file_url).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Package specification parsing
|
||||
|
||||
mod interactive;
|
||||
mod packager;
|
||||
mod spec;
|
||||
|
||||
pub use interactive::*;
|
||||
pub use packager::Packager;
|
||||
pub use spec::*;
|
||||
|
||||
@@ -40,13 +40,13 @@ impl Packager {
|
||||
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)
|
||||
// Respect zstd level from config (default to 19 if not specified)
|
||||
let level = self
|
||||
.config
|
||||
.package_overrides
|
||||
.get("compression_level")
|
||||
.and_then(|v| v.as_integer())
|
||||
.unwrap_or(3) as i32;
|
||||
.unwrap_or(19) as i32;
|
||||
|
||||
let mut encoder = Encoder::new(file, level)?;
|
||||
let _ = encoder.multithread(num_cpus() as u32);
|
||||
@@ -231,6 +231,7 @@ mod tests {
|
||||
homepage: "h".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: Vec::new(),
|
||||
|
||||
+140
-13
@@ -9,9 +9,12 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Complete package specification from TOML
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[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.
|
||||
@@ -60,13 +63,21 @@ impl PackageSpec {
|
||||
.replace("$name", &self.package.name)
|
||||
.replace("$version", &self.package.version)
|
||||
.replace("$specdir", &specdir)
|
||||
.replace("$NYAPM_SPECDIR", &specdir)
|
||||
.replace("$DEPOT_SPECDIR", &specdir)
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[Source] {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -140,6 +151,11 @@ impl PackageSpec {
|
||||
self.build.flags.cbuild = s.to_string();
|
||||
}
|
||||
}
|
||||
"carch" => {
|
||||
if let Some(s) = v.as_str() {
|
||||
self.build.flags.carch = s.to_string();
|
||||
}
|
||||
}
|
||||
// Add more fields as needed
|
||||
_ => {}
|
||||
}
|
||||
@@ -233,6 +249,11 @@ impl PackageSpec {
|
||||
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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -400,6 +421,8 @@ cbuild = "x86_64-pc-linux-gnu"
|
||||
.unwrap(),
|
||||
package_overrides: toml::Value::Table(toml::map::Map::new()),
|
||||
appends: std::collections::HashMap::new(),
|
||||
mirrors: std::collections::HashMap::new(),
|
||||
repo_clone_dir: PathBuf::from("/tmp"),
|
||||
};
|
||||
|
||||
spec.apply_config(&config);
|
||||
@@ -407,6 +430,21 @@ cbuild = "x86_64-pc-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");
|
||||
@@ -417,6 +455,46 @@ cbuild = "x86_64-pc-linux-gnu"
|
||||
);
|
||||
}
|
||||
|
||||
#[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 {
|
||||
@@ -427,6 +505,7 @@ cbuild = "x86_64-pc-linux-gnu"
|
||||
homepage: "h".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives::default(),
|
||||
manual_sources: Vec::new(),
|
||||
source: vec![Source {
|
||||
@@ -493,21 +572,22 @@ impl PackageSpec {
|
||||
}
|
||||
|
||||
/// Package alternatives (provides/replaces)
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Alternatives {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub provides: Vec<String>,
|
||||
/// Reserved for future package replacement feature
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[allow(dead_code)]
|
||||
pub replaces: Vec<String>,
|
||||
}
|
||||
|
||||
/// Source tarball information
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Source {
|
||||
pub url: String,
|
||||
/// SHA256 checksum or "skip" to bypass
|
||||
/// 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,
|
||||
@@ -528,7 +608,7 @@ pub struct Source {
|
||||
}
|
||||
|
||||
/// Manual (local) source file to copy before fetching remote sources.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ManualSource {
|
||||
/// Filename in the spec directory
|
||||
pub file: String,
|
||||
@@ -561,7 +641,7 @@ where
|
||||
}
|
||||
|
||||
/// Build configuration
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Build {
|
||||
#[serde(rename = "type")]
|
||||
pub build_type: BuildType,
|
||||
@@ -570,7 +650,7 @@ pub struct Build {
|
||||
}
|
||||
|
||||
/// Supported build systems
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BuildType {
|
||||
Autotools,
|
||||
@@ -578,12 +658,12 @@ pub enum BuildType {
|
||||
Meson,
|
||||
Custom,
|
||||
Rust,
|
||||
/// Binary distribution installer (e.g., .deb, .rpm)
|
||||
Makefile,
|
||||
Bin,
|
||||
}
|
||||
|
||||
/// Build flags and toolchain configuration
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct BuildFlags {
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub cflags: Vec<String>,
|
||||
@@ -611,6 +691,12 @@ pub struct BuildFlags {
|
||||
#[serde(default)]
|
||||
pub post_install: 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,
|
||||
@@ -623,6 +709,10 @@ pub struct BuildFlags {
|
||||
#[serde(default)]
|
||||
pub cbuild: String,
|
||||
|
||||
/// CPU architecture short name (CARCH equivalent), e.g. "x86_64", "aarch64"
|
||||
#[serde(default = "default_carch")]
|
||||
pub carch: String,
|
||||
|
||||
// Rust-specific fields
|
||||
/// Rust build profile: "debug" or "release" (default: release)
|
||||
#[serde(default = "default_profile")]
|
||||
@@ -644,11 +734,44 @@ pub struct BuildFlags {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Default for BuildFlags {
|
||||
fn default() -> Self {
|
||||
BuildFlags {
|
||||
cflags: Vec::new(),
|
||||
ldflags: Vec::new(),
|
||||
configure: Vec::new(),
|
||||
cc: default_cc(),
|
||||
ar: default_ar(),
|
||||
libc: String::new(),
|
||||
rootfs: default_rootfs(),
|
||||
post_compile: Vec::new(),
|
||||
post_install: Vec::new(),
|
||||
makefile_commands: Vec::new(),
|
||||
makefile_install_commands: Vec::new(),
|
||||
prefix: default_prefix(),
|
||||
chost: String::new(),
|
||||
cbuild: String::new(),
|
||||
carch: default_carch(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_string_or_array<'de, D>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<Vec<String>, D::Error>
|
||||
@@ -702,8 +825,12 @@ fn default_prefix() -> String {
|
||||
"/usr".to_string()
|
||||
}
|
||||
|
||||
fn default_carch() -> String {
|
||||
std::env::consts::ARCH.to_string()
|
||||
}
|
||||
|
||||
/// Package dependencies
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Dependencies {
|
||||
#[serde(default)]
|
||||
pub build: Vec<String>,
|
||||
|
||||
Reference in New Issue
Block a user