Files
depot/src/package/interactive.rs
T

2243 lines
79 KiB
Rust

use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, ManualSource, 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::Perl => write!(f, "Perl"),
BuildType::Custom => write!(f, "Custom"),
BuildType::Python => write!(f, "Python"),
BuildType::Rust => write!(f, "Rust"),
BuildType::Makefile => write!(f, "Makefile"),
BuildType::Bin => write!(f, "Binary installer"),
BuildType::Meta => write!(f, "Metapackage"),
}
}
}
/// 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 ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let client =
crate::source::build_blocking_client(&ua, Some(Duration::from_secs(20)))
.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 = suppaftp::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| {
suppaftp::FtpError::ConnectionError(std::io::Error::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
)
}
fn prompt_repeating_list(item_label: &str, help: &str) -> Result<Vec<String>> {
let mut values = Vec::new();
loop {
let prompt = format!("{} #{} (empty to finish):", item_label, values.len() + 1);
let entry = Text::new(&prompt).with_help_message(help).prompt()?;
let trimmed = entry.trim();
if trimmed.is_empty() {
break;
}
values.push(trimmed.to_string());
}
Ok(values)
}
fn prompt_optional_text(prompt: &str, help: &str) -> Result<String> {
Ok(Text::new(prompt)
.with_help_message(help)
.prompt()?
.trim()
.to_string())
}
fn prompt_manual_sources() -> Result<Vec<ManualSource>> {
let mut out = Vec::new();
loop {
let add = Confirm::new("Add a manual source?")
.with_help_message(
"Use for local files (keys, patches, configs) or direct URLs copied into the build dir",
)
.with_default(out.is_empty())
.prompt()?;
if !add {
break;
}
let modes = vec!["Local file(s)", "Remote URL(s)"];
let mode = Select::new("Manual source mode:", modes)
.with_help_message("Local files are resolved relative to the spec directory")
.prompt()?;
let mut manual = ManualSource {
file: None,
files: Vec::new(),
url: None,
urls: Vec::new(),
sha256: None,
dest: None,
};
let entries = if mode == "Local file(s)" {
prompt_repeating_list(
"Manual source file",
"Path relative to spec dir, e.g. depot.pub, keys/repo.pub",
)?
} else {
prompt_repeating_list(
"Manual source URL",
"Supports http(s), ftp, file://, and git URLs like ...repo.git#rev",
)?
};
if entries.is_empty() {
crate::log_warn!("Manual source block skipped (no entries provided).");
continue;
}
if mode == "Local file(s)" {
if entries.len() == 1 {
manual.file = Some(entries[0].clone());
} else {
manual.files = entries;
}
} else if entries.len() == 1 {
manual.url = Some(entries[0].clone());
} else {
manual.urls = entries;
}
let single_entry = matches!(
(
&manual.file,
manual.files.len(),
&manual.url,
manual.urls.len()
),
(Some(_), 0, None, 0) | (None, 0, Some(_), 0)
);
if single_entry {
let checksum = prompt_optional_text(
"Manual source checksum (optional):",
"sha256:/sha512:/sha1:/md5:/b2:/b2sum:, raw SHA256 hex, or 'skip' (empty omits field)",
)?;
if !checksum.is_empty() {
manual.sha256 = Some(checksum);
}
let dest = prompt_optional_text(
"Manual source destination (optional):",
"Path relative to build work dir (default derives from file/url name)",
)?;
if !dest.is_empty() {
manual.dest = Some(dest);
}
} else {
crate::log_info!(
"Multiple entries in one manual_sources block cannot use a shared checksum or dest; skipping those prompts."
);
}
out.push(manual);
}
Ok(out)
}
fn parse_license_list(input: &str) -> Vec<String> {
input
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}
fn licenses_to_toml_value(licenses: &[String]) -> Option<toml::Value> {
if licenses.is_empty() {
None
} else if licenses.len() == 1 {
Some(toml::Value::String(licenses[0].clone()))
} else {
Some(toml::Value::Array(
licenses
.iter()
.map(|license| toml::Value::String(license.clone()))
.collect(),
))
}
}
fn expand_known_package_vars(input: &str, name: &str, version: &str) -> String {
input
.replace("$name", name)
.replace("${name}", name)
.replace("$version", version)
.replace("${version}", version)
}
pub fn create_interactive() -> Result<PackageSpec> {
crate::log_info!("Interactive Package Specification Creator");
crate::log_info!("-----------------------------------------");
// 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_input = Text::new("License(s):")
.with_help_message("Single SPDX id or comma-separated list, e.g. MIT, Apache-2.0")
.with_placeholder("MIT")
.prompt()?;
let license = parse_license_list(&license_input);
if license.is_empty() {
anyhow::bail!("At least one license is required");
}
// Present all supported build systems.
let build_types = vec![
BuildType::Autotools,
BuildType::CMake,
BuildType::Meson,
BuildType::Perl,
BuildType::Makefile,
BuildType::Python,
BuildType::Rust,
BuildType::Custom,
BuildType::Bin,
BuildType::Meta,
];
let build_type = Select::new("Build System:", build_types)
.with_help_message("Select the build system used by the package (common choices)")
.prompt()?;
let is_metapackage = matches!(build_type, BuildType::Meta);
// 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.
let show_advanced = if is_metapackage {
false
} else {
Confirm::new("Show advanced options?")
.with_help_message(
"Includes source subdir, toolchain overrides, hooks, and build tuning",
)
.with_default(false)
.prompt()?
};
let mut sources = Vec::new();
let mut manual_sources = Vec::new();
if !is_metapackage {
let source_url = Text::new("Source URL:")
.with_help_message(
"URL to the source tarball or git repository (supports file://; leave empty if you will add manual_sources later)",
)
.with_default(gnu_source_default.as_str())
.prompt()?;
if source_url.trim().is_empty() {
crate::log_warn!(
"No source URL provided. Generated spec will omit [source]; add manual_sources or source entries before building (unless this becomes a metapackage)."
);
if Confirm::new("Add manual sources now?")
.with_help_message("Useful for keyrings/patch-only/custom packages")
.with_default(true)
.prompt()?
{
manual_sources = prompt_manual_sources()?;
}
} else {
// Attempt to compute SHA256 automatically when online — use as the default if available.
let checksum_source_url = expand_known_package_vars(&source_url, &name, &version);
let computed_sha_default = match compute_sha256_for_url(&checksum_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:, sha1:, md5:, b2:, b2sum:, 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 source = Source {
url: source_url,
sha256: source_sha256,
extract_dir,
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
};
if show_advanced {
source.patches = prompt_repeating_list(
"Patch path/URL",
"Patch file path (relative to spec dir) or direct URL",
)?;
source.post_extract = prompt_repeating_list(
"post_extract command",
"Runs in extracted source directory using sh -c",
)?;
if Confirm::new("Add manual sources?")
.with_help_message(
"Optional local files or URLs copied into the build dir before source fetching",
)
.with_default(false)
.prompt()?
{
manual_sources = prompt_manual_sources()?;
}
}
sources.push(source);
}
} else {
crate::log_info!(
"Metapackage selected: skipping source/build prompts. Define runtime dependencies to pull in."
);
}
let mut flags = BuildFlags::default();
if !is_metapackage {
if !matches!(build_type, BuildType::Bin) {
flags.prefix = Text::new("Install prefix:")
.with_help_message("Most packages should use /usr")
.with_default(flags.prefix.as_str())
.prompt()?;
}
let supports_separate_build_dir = matches!(
build_type,
BuildType::Autotools | BuildType::CMake | BuildType::Meson | BuildType::Custom
);
if supports_separate_build_dir {
let default_bdir = if matches!(build_type, BuildType::Meson) {
"builddir"
} else {
"build"
};
let default_enabled = matches!(build_type, BuildType::CMake | BuildType::Meson);
if Confirm::new("Use separate build directory?")
.with_help_message("Recommended for CMake/Meson and often useful for Autotools")
.with_default(default_enabled)
.prompt()?
{
flags.build_dir = Some(
Text::new("Build directory name:")
.with_default(default_bdir)
.prompt()?,
);
}
}
if show_advanced {
flags.source_subdir = prompt_optional_text(
"Source subdirectory (optional):",
"Use for monorepos (e.g. llvm-project/clang, src)",
)?;
flags.cc = Text::new("CC:")
.with_help_message("C compiler")
.with_default(flags.cc.as_str())
.prompt()?;
flags.cxx = Text::new("CXX:")
.with_help_message("C++ compiler")
.with_default(flags.cxx.as_str())
.prompt()?;
flags.ar = Text::new("AR:")
.with_help_message("Archiver tool")
.with_default(flags.ar.as_str())
.prompt()?;
flags.chost = prompt_optional_text(
"CHOST target triple (optional):",
"Example: x86_64-sfg-linux-gnu",
)?;
flags.cbuild = prompt_optional_text(
"CBUILD build triple (optional):",
"Example: x86_64-pc-linux-gnu",
)?;
flags.carch = Text::new("CARCH:")
.with_help_message("CPU architecture short name")
.with_default(flags.carch.as_str())
.prompt()?;
flags.cflags = prompt_repeating_list(
"CFLAG",
"One flag per entry, e.g. -O2, -fPIC, -D_GNU_SOURCE",
)?;
flags.ldflags =
prompt_repeating_list("LDFLAG", "One flag per entry, e.g. -Wl,-z,relro")?;
}
if matches!(
build_type,
BuildType::Autotools | BuildType::CMake | BuildType::Meson | BuildType::Perl
) {
let help = match build_type {
BuildType::Autotools => "Examples: --disable-static, --enable-nls, --with-zlib",
BuildType::CMake => "Examples: -DENABLE_TESTS=OFF, -DUSE_SYSTEM_LIBS=ON",
BuildType::Meson => "Examples: -Dtests=false, -Ddefault_library=static",
BuildType::Perl => "Examples: INSTALLDIRS=vendor, INC=-I/usr/include/foo",
_ => "",
};
if Confirm::new("Add configure/setup options?")
.with_help_message("Adds entries to build.flags.configure")
.with_default(show_advanced)
.prompt()?
{
flags.configure = prompt_repeating_list("Configure option", help)?;
}
}
if show_advanced && matches!(build_type, BuildType::Autotools) {
flags.configure_file = prompt_optional_text(
"Configure script path (optional):",
"Relative to source root, e.g. build-aux/configure",
)?;
flags.make_dirs = prompt_repeating_list(
"Make dir (build phase)",
"Relative to build dir, e.g. lib, libelf (empty = build root)",
)?;
flags.make_test_dirs = prompt_repeating_list(
"Make dir (test phase)",
"Relative to build dir, e.g. tests (empty = build root)",
)?;
flags.make_install_dirs = prompt_repeating_list(
"Make dir (install phase)",
"Relative to build dir, e.g. lib, apps (empty = build root)",
)?;
flags.skip_tests = Confirm::new("Skip automatic tests (make check/test)?")
.with_help_message(
"If enabled, Depot will not auto-run detected Autotools test targets",
)
.with_default(false)
.prompt()?;
}
if matches!(build_type, BuildType::Makefile) {
crate::log_info!("Define Makefile build and install commands.");
flags.makefile_commands = prompt_repeating_list(
"makefile build command",
"Examples: make -j$(nproc), make all",
)?;
if flags.makefile_commands.is_empty() {
anyhow::bail!("Makefile build type requires at least one build command");
}
flags.makefile_install_commands = prompt_repeating_list(
"makefile install command",
"Examples: make DESTDIR=$DESTDIR install",
)?;
if flags.makefile_install_commands.is_empty() {
anyhow::bail!("Makefile build type requires at least one install command");
}
}
if matches!(build_type, BuildType::Rust) {
let profiles = vec!["release", "debug"];
let profile = Select::new("Cargo profile:", profiles)
.with_help_message("Release is recommended for production packages")
.prompt()?;
flags.profile = profile.to_string();
flags.target = prompt_optional_text(
"Rust target triple (optional):",
"Example: x86_64-unknown-linux-gnu",
)?;
if show_advanced {
flags.rustflags = prompt_repeating_list(
"RUSTFLAG",
"One flag per entry, e.g. -Ctarget-cpu=native",
)?;
flags.cargs = prompt_repeating_list(
"Extra cargo arg",
"Examples: --locked, --features, serde, --no-default-features",
)?;
}
flags.bindir = Text::new("Binary install dir:")
.with_help_message("Destination inside package image")
.with_default(flags.bindir.as_str())
.prompt()?;
}
if matches!(build_type, BuildType::Python) && show_advanced {
flags.config_settings = prompt_repeating_list(
"PEP 517 config-setting",
"Format: KEY=VALUE (repeat keys to pass multiple values)",
)?;
}
if matches!(build_type, BuildType::Bin) {
flags.binary_type = prompt_optional_text(
"Binary type (optional):",
"Metadata only, e.g. deb, rpm, tarball",
)?;
}
if show_advanced {
flags.post_configure = prompt_repeating_list(
"post_configure command",
"Runs after configure/setup, before build",
)?;
flags.post_compile =
prompt_repeating_list("post_compile command", "Runs after build, before install")?;
flags.post_install =
prompt_repeating_list("post_install command", "Runs after install step")?;
}
}
let runtime_deps =
prompt_repeating_list("Runtime dependency", "Package name (e.g. zlib, openssl)")?;
let build_deps =
prompt_repeating_list("Build dependency", "Package needed only at build time")?;
let test_deps = prompt_repeating_list(
"Test dependency",
"Package needed only for running package test suites",
)?;
let optional_deps = prompt_repeating_list(
"Optional dependency",
"Package that enables optional runtime functionality",
)?;
let alternatives = if show_advanced {
Alternatives {
provides: prompt_repeating_list(
"Alternative provided name",
"Virtual package name(s) this package satisfies, e.g. sh, editor, libfoo",
)?,
conflicts: prompt_repeating_list(
"Conflicting package/provide",
"Installed package or provided name that must be removed before install",
)?,
replaces: prompt_repeating_list(
"Replaced package",
"Package name that should resolve to this package and be replaced during install/update",
)?,
lib32: None,
}
} else {
Alternatives::default()
};
Ok(PackageSpec {
package: PackageInfo {
name,
real_name: None,
version,
revision: 1,
description,
homepage,
abi_breaking: false,
license,
},
packages: Vec::new(),
alternatives,
manual_sources,
source: sources,
build: Build { build_type, flags },
dependencies: Dependencies {
build: build_deps,
runtime: runtime_deps,
test: test_deps,
optional: optional_deps,
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
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()));
if let Some(real_name) = &spec.package.real_name {
pkg.insert("real_name".into(), Value::String(real_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.abi_breaking {
pkg.insert("abi_breaking".into(), Value::Boolean(true));
}
if let Some(v) = licenses_to_toml_value(&spec.package.license) {
pkg.insert("license".into(), v);
}
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()));
if let Some(real_name) = &p.real_name {
pt.insert("real_name".into(), Value::String(real_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.abi_breaking {
pt.insert("abi_breaking".into(), Value::Boolean(true));
}
if let Some(v) = licenses_to_toml_value(&p.license) {
pt.insert("license".into(), v);
}
arr.push(Value::Table(pt));
}
root.insert("packages".into(), Value::Array(arr));
}
// manual sources
if !spec.manual_sources.is_empty() {
let mut arr = Vec::new();
for m in &spec.manual_sources {
let mut mt = Table::new();
if let Some(file) = m.file.as_ref().filter(|s| !s.trim().is_empty()) {
mt.insert("file".into(), Value::String(file.clone()));
}
if !m.files.is_empty() {
mt.insert(
"files".into(),
Value::Array(m.files.iter().map(|s| Value::String(s.clone())).collect()),
);
}
if let Some(url) = m.url.as_ref().filter(|s| !s.trim().is_empty()) {
mt.insert("url".into(), Value::String(url.clone()));
}
if !m.urls.is_empty() {
mt.insert(
"urls".into(),
Value::Array(m.urls.iter().map(|s| Value::String(s.clone())).collect()),
);
}
if let Some(sha256) = m.sha256.as_ref().filter(|s| {
let trimmed = s.trim();
!trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("skip")
}) {
mt.insert("sha256".into(), Value::String(sha256.clone()));
}
if let Some(dest) = m.dest.as_ref().filter(|s| !s.trim().is_empty()) {
mt.insert("dest".into(), Value::String(dest.clone()));
}
if !mt.is_empty() {
arr.push(Value::Table(mt));
}
}
if !arr.is_empty() {
root.insert("manual_sources".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()));
}
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(),
),
);
}
if !s.cherry_pick.is_empty() {
st.insert(
"cherry_pick".into(),
Value::Array(
s.cherry_pick
.iter()
.map(|rev| Value::String(rev.clone()))
.collect(),
),
);
}
arr.push(Value::Table(st));
}
root.insert("source".into(), Value::Array(arr));
}
if !spec.alternatives.provides.is_empty()
|| !spec.alternatives.conflicts.is_empty()
|| !spec.alternatives.replaces.is_empty()
|| spec.alternatives.lib32.is_some()
{
let mut alternatives_tbl = Table::new();
if !spec.alternatives.provides.is_empty() {
alternatives_tbl.insert(
"provides".into(),
Value::Array(
spec.alternatives
.provides
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.alternatives.conflicts.is_empty() {
alternatives_tbl.insert(
"conflicts".into(),
Value::Array(
spec.alternatives
.conflicts
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.alternatives.replaces.is_empty() {
alternatives_tbl.insert(
"replaces".into(),
Value::Array(
spec.alternatives
.replaces
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if let Some(lib32) = &spec.alternatives.lib32 {
let mut lib32_tbl = Table::new();
if !lib32.provides.is_empty() {
lib32_tbl.insert(
"provides".into(),
Value::Array(
lib32
.provides
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !lib32.conflicts.is_empty() {
lib32_tbl.insert(
"conflicts".into(),
Value::Array(
lib32
.conflicts
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !lib32.replaces.is_empty() {
lib32_tbl.insert(
"replaces".into(),
Value::Array(
lib32
.replaces
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !lib32_tbl.is_empty() {
alternatives_tbl.insert("lib32".into(), Value::Table(lib32_tbl));
}
}
root.insert("alternatives".into(), Value::Table(alternatives_tbl));
}
// build (only include set fields)
let mut build_tbl = Table::new();
let build_type = match spec.build.build_type {
BuildType::Autotools => "autotools",
BuildType::CMake => "cmake",
BuildType::Meson => "meson",
BuildType::Perl => "perl",
BuildType::Custom => "custom",
BuildType::Python => "python",
BuildType::Rust => "rust",
BuildType::Makefile => "makefile",
BuildType::Bin => "bin",
BuildType::Meta => "meta",
};
build_tbl.insert("type".into(), Value::String(build_type.to_string()));
let defaults = BuildFlags::default();
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.source_subdir.is_empty() {
flags_tbl.insert(
"source_subdir".into(),
Value::String(spec.build.flags.source_subdir.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.cxxflags.is_empty() {
flags_tbl.insert(
"cxxflags".into(),
Value::Array(
spec.build
.flags
.cxxflags
.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.ltoflags.is_empty() {
flags_tbl.insert(
"ltoflags".into(),
Value::Array(
spec.build
.flags
.ltoflags
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.rustltoflags.is_empty() {
flags_tbl.insert(
"rustltoflags".into(),
Value::Array(
spec.build
.flags
.rustltoflags
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.keep.is_empty() {
flags_tbl.insert(
"keep".into(),
Value::Array(
spec.build
.flags
.keep
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if spec.build.flags.use_lto != defaults.use_lto {
flags_tbl.insert("use_lto".into(), Value::Boolean(spec.build.flags.use_lto));
}
if spec.build.flags.no_flags != defaults.no_flags {
flags_tbl.insert("no_flags".into(), Value::Boolean(spec.build.flags.no_flags));
}
if spec.build.flags.no_strip != defaults.no_strip {
flags_tbl.insert("no_strip".into(), Value::Boolean(spec.build.flags.no_strip));
}
if spec.build.flags.no_delete_static != defaults.no_delete_static {
flags_tbl.insert(
"no_delete_static".into(),
Value::Boolean(spec.build.flags.no_delete_static),
);
}
if spec.build.flags.no_compress_man != defaults.no_compress_man {
flags_tbl.insert(
"no_compress_man".into(),
Value::Boolean(spec.build.flags.no_compress_man),
);
}
if spec.build.flags.skip_tests != defaults.skip_tests {
flags_tbl.insert(
"skip_tests".into(),
Value::Boolean(spec.build.flags.skip_tests),
);
}
if !spec.build.flags.configure.is_empty() {
flags_tbl.insert(
"configure".into(),
Value::Array(
spec.build
.flags
.configure
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.configure_file.is_empty() {
flags_tbl.insert(
"configure_file".into(),
Value::String(spec.build.flags.configure_file.clone()),
);
}
if spec.build.flags.prefix != defaults.prefix {
flags_tbl.insert(
"prefix".into(),
Value::String(spec.build.flags.prefix.clone()),
);
}
if spec.build.flags.cc != defaults.cc {
flags_tbl.insert("cc".into(), Value::String(spec.build.flags.cc.clone()));
}
if spec.build.flags.cxx != defaults.cxx {
flags_tbl.insert("cxx".into(), Value::String(spec.build.flags.cxx.clone()));
}
if spec.build.flags.ar != defaults.ar {
flags_tbl.insert("ar".into(), Value::String(spec.build.flags.ar.clone()));
}
if !spec.build.flags.libc.is_empty() {
flags_tbl.insert("libc".into(), Value::String(spec.build.flags.libc.clone()));
}
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 != defaults.carch {
flags_tbl.insert(
"carch".into(),
Value::String(spec.build.flags.carch.clone()),
);
}
if !spec.build.flags.makeflags.is_empty() {
flags_tbl.insert(
"makeflags".into(),
Value::String(spec.build.flags.makeflags.clone()),
);
}
if !spec.build.flags.make_vars.is_empty() {
flags_tbl.insert(
"make_vars".into(),
Value::Array(
spec.build
.flags
.make_vars
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_exec.is_empty() {
flags_tbl.insert(
"make_exec".into(),
Value::String(spec.build.flags.make_exec.clone()),
);
}
if !spec.build.flags.make_target.is_empty() {
flags_tbl.insert(
"make_target".into(),
Value::String(spec.build.flags.make_target.clone()),
);
}
if !spec.build.flags.make_targets.is_empty() {
flags_tbl.insert(
"make_targets".into(),
Value::Array(
spec.build
.flags
.make_targets
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_dirs.is_empty() {
flags_tbl.insert(
"make_dirs".into(),
Value::Array(
spec.build
.flags
.make_dirs
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_test_vars.is_empty() {
flags_tbl.insert(
"make_test_vars".into(),
Value::Array(
spec.build
.flags
.make_test_vars
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_test_target.is_empty() {
flags_tbl.insert(
"make_test_target".into(),
Value::String(spec.build.flags.make_test_target.clone()),
);
}
if !spec.build.flags.make_test_targets.is_empty() {
flags_tbl.insert(
"make_test_targets".into(),
Value::Array(
spec.build
.flags
.make_test_targets
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_test_dirs.is_empty() {
flags_tbl.insert(
"make_test_dirs".into(),
Value::Array(
spec.build
.flags
.make_test_dirs
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_install_vars.is_empty() {
flags_tbl.insert(
"make_install_vars".into(),
Value::Array(
spec.build
.flags
.make_install_vars
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_install_target.is_empty() {
flags_tbl.insert(
"make_install_target".into(),
Value::String(spec.build.flags.make_install_target.clone()),
);
}
if !spec.build.flags.make_install_targets.is_empty() {
flags_tbl.insert(
"make_install_targets".into(),
Value::Array(
spec.build
.flags
.make_install_targets
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.make_install_dirs.is_empty() {
flags_tbl.insert(
"make_install_dirs".into(),
Value::Array(
spec.build
.flags
.make_install_dirs
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.passthrough_env.is_empty() {
flags_tbl.insert(
"passthrough_env".into(),
Value::Array(
spec.build
.flags
.passthrough_env
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.env_vars.is_empty() {
flags_tbl.insert(
"env_vars".into(),
Value::Array(
spec.build
.flags
.env_vars
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.post_compile.is_empty() {
flags_tbl.insert(
"post_compile".into(),
Value::Array(
spec.build
.flags
.post_compile
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.post_configure.is_empty() {
flags_tbl.insert(
"post_configure".into(),
Value::Array(
spec.build
.flags
.post_configure
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.post_install.is_empty() {
flags_tbl.insert(
"post_install".into(),
Value::Array(
spec.build
.flags
.post_install
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.makefile_commands.is_empty() {
flags_tbl.insert(
"makefile_commands".into(),
Value::Array(
spec.build
.flags
.makefile_commands
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.makefile_install_commands.is_empty() {
flags_tbl.insert(
"makefile_install_commands".into(),
Value::Array(
spec.build
.flags
.makefile_install_commands
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if spec.build.flags.profile != defaults.profile {
flags_tbl.insert(
"profile".into(),
Value::String(spec.build.flags.profile.clone()),
);
}
if !spec.build.flags.target.is_empty() {
flags_tbl.insert(
"target".into(),
Value::String(spec.build.flags.target.clone()),
);
}
if !spec.build.flags.rustflags.is_empty() {
flags_tbl.insert(
"rustflags".into(),
Value::Array(
spec.build
.flags
.rustflags
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.cargs.is_empty() {
flags_tbl.insert(
"cargs".into(),
Value::Array(
spec.build
.flags
.cargs
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.config_settings.is_empty() {
flags_tbl.insert(
"config_setting".into(),
Value::Array(
spec.build
.flags
.config_settings
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if spec.build.flags.bindir != defaults.bindir {
flags_tbl.insert(
"bindir".into(),
Value::String(spec.build.flags.bindir.clone()),
);
}
if !spec.build.flags.sbindir.is_empty() {
flags_tbl.insert(
"sbindir".into(),
Value::String(spec.build.flags.sbindir.clone()),
);
}
if !spec.build.flags.libdir.is_empty() {
flags_tbl.insert(
"libdir".into(),
Value::String(spec.build.flags.libdir.clone()),
);
}
if !spec.build.flags.libexecdir.is_empty() {
flags_tbl.insert(
"libexecdir".into(),
Value::String(spec.build.flags.libexecdir.clone()),
);
}
if !spec.build.flags.sysconfdir.is_empty() {
flags_tbl.insert(
"sysconfdir".into(),
Value::String(spec.build.flags.sysconfdir.clone()),
);
}
if !spec.build.flags.localstatedir.is_empty() {
flags_tbl.insert(
"localstatedir".into(),
Value::String(spec.build.flags.localstatedir.clone()),
);
}
if !spec.build.flags.sharedstatedir.is_empty() {
flags_tbl.insert(
"sharedstatedir".into(),
Value::String(spec.build.flags.sharedstatedir.clone()),
);
}
if !spec.build.flags.includedir.is_empty() {
flags_tbl.insert(
"includedir".into(),
Value::String(spec.build.flags.includedir.clone()),
);
}
if !spec.build.flags.datarootdir.is_empty() {
flags_tbl.insert(
"datarootdir".into(),
Value::String(spec.build.flags.datarootdir.clone()),
);
}
if !spec.build.flags.datadir.is_empty() {
flags_tbl.insert(
"datadir".into(),
Value::String(spec.build.flags.datadir.clone()),
);
}
if !spec.build.flags.mandir.is_empty() {
flags_tbl.insert(
"mandir".into(),
Value::String(spec.build.flags.mandir.clone()),
);
}
if !spec.build.flags.infodir.is_empty() {
flags_tbl.insert(
"infodir".into(),
Value::String(spec.build.flags.infodir.clone()),
);
}
if !spec.build.flags.binary_type.is_empty() {
flags_tbl.insert(
"binary_type".into(),
Value::String(spec.build.flags.binary_type.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()
|| !spec.dependencies.test.is_empty()
|| !spec.dependencies.optional.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(),
),
);
}
if !spec.dependencies.test.is_empty() {
dep_tbl.insert(
"test".into(),
Value::Array(
spec.dependencies
.test
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.dependencies.optional.is_empty() {
dep_tbl.insert(
"optional".into(),
Value::Array(
spec.dependencies
.optional
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
root.insert("dependencies".into(), Value::Table(dep_tbl));
}
if !spec.package_dependencies.is_empty() {
let mut outputs_tbl = Table::new();
for (pkg_name, deps) in &spec.package_dependencies {
let mut dep_tbl = Table::new();
if !deps.build.is_empty() {
dep_tbl.insert(
"build".into(),
Value::Array(
deps.build
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !deps.runtime.is_empty() {
dep_tbl.insert(
"runtime".into(),
Value::Array(
deps.runtime
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !deps.test.is_empty() {
dep_tbl.insert(
"test".into(),
Value::Array(deps.test.iter().map(|s| Value::String(s.clone())).collect()),
);
}
if !deps.optional.is_empty() {
dep_tbl.insert(
"optional".into(),
Value::Array(
deps.optional
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !dep_tbl.is_empty() {
outputs_tbl.insert(pkg_name.clone(), Value::Table(dep_tbl));
}
}
if !outputs_tbl.is_empty() {
root.insert("package_dependencies".into(), Value::Table(outputs_tbl));
}
}
if !spec.package_alternatives.is_empty() {
let mut outputs_tbl = Table::new();
for (pkg_name, alternatives) in &spec.package_alternatives {
let mut alt_tbl = Table::new();
if !alternatives.provides.is_empty() {
alt_tbl.insert(
"provides".into(),
Value::Array(
alternatives
.provides
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !alternatives.conflicts.is_empty() {
alt_tbl.insert(
"conflicts".into(),
Value::Array(
alternatives
.conflicts
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !alternatives.replaces.is_empty() {
alt_tbl.insert(
"replaces".into(),
Value::Array(
alternatives
.replaces
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !alt_tbl.is_empty() {
outputs_tbl.insert(pkg_name.clone(), Value::Table(alt_tbl));
}
}
if !outputs_tbl.is_empty() {
root.insert("package_alternatives".into(), Value::Table(outputs_tbl));
}
}
Ok(toml::to_string_pretty(&Value::Table(root))?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{
BuildFlags, BuildType, Dependencies, ManualSource, PackageInfo, PackageSpec, Source,
};
#[test]
fn spec_to_minimal_toml_omits_defaults() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["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(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::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(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()],
},
packages: vec![PackageInfo {
name: "foo-dev".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "dev files".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["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(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::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 spec_to_minimal_toml_includes_multiple_licenses_as_array() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into(), "Apache-2.0".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(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("license = ["));
assert!(toml.contains("\"MIT\""));
assert!(toml.contains("\"Apache-2.0\""));
}
#[test]
fn spec_to_minimal_toml_includes_extended_build_flags() {
let flags = BuildFlags {
source_subdir: "project/subdir".into(),
configure: vec!["--disable-static".into(), "--enable-foo".into()],
configure_file: "build-aux/configure".into(),
post_configure: vec!["./configure-helper.sh".into()],
post_compile: vec!["make check".into()],
post_install: vec!["strip $DESTDIR/usr/bin/foo".into()],
makefile_commands: vec!["make".into()],
makefile_install_commands: vec!["make DESTDIR=$DESTDIR install".into()],
cargs: vec!["--locked".into()],
config_settings: vec!["editable_mode=compat".into()],
rustflags: vec!["-Ctarget-cpu=native".into()],
cxxflags: vec!["-O2".into(), "-fno-rtti".into()],
ltoflags: vec!["-flto=auto".into()],
target: "x86_64-unknown-linux-gnu".into(),
keep: vec!["etc/locale.gen".into()],
sbindir: "/usr/sbin".into(),
libdir: "/usr/lib64".into(),
libexecdir: "/usr/libexec".into(),
sysconfdir: "/etc/custom".into(),
localstatedir: "/var/custom".into(),
sharedstatedir: "/var/lib/custom".into(),
includedir: "/usr/include/custom".into(),
datarootdir: "/usr/share/root".into(),
datadir: "/usr/share/custom".into(),
mandir: "/usr/share/custom/man".into(),
infodir: "/usr/share/custom/info".into(),
use_lto: false,
no_flags: true,
no_strip: true,
no_delete_static: true,
no_compress_man: true,
skip_tests: true,
makeflags: "-j12 --output-sync=target".into(),
make_vars: vec!["V=1".into()],
make_dirs: vec!["lib".into(), "libelf".into()],
make_test_vars: vec!["TESTS=unit".into()],
make_test_dirs: vec!["tests".into()],
make_install_vars: vec!["STRIPPROG=true".into()],
make_install_dirs: vec!["lib".into(), "apps".into()],
env_vars: vec![
"SETUPTOOLS_SCM_PRETEND_VERSION=$version".into(),
"PYO3_CONFIG_FILE=$specdir/pyo3.toml".into(),
],
..BuildFlags::default()
};
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["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!["fix.patch".into()],
post_extract: vec!["autoreconf -fi".into()],
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("source_subdir = \"project/subdir\""));
assert!(toml.contains("configure = ["));
assert!(toml.contains("configure_file = \"build-aux/configure\""));
assert!(toml.contains("post_configure = ["));
assert!(toml.contains("post_compile = ["));
assert!(toml.contains("post_install = ["));
assert!(toml.contains("makefile_commands = ["));
assert!(toml.contains("makefile_install_commands = ["));
assert!(toml.contains("cargs = ["));
assert!(toml.contains("config_setting = ["));
assert!(toml.contains("rustflags = ["));
assert!(toml.contains("cxxflags = ["));
assert!(toml.contains("ltoflags = ["));
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
assert!(toml.contains("keep = ["));
assert!(toml.contains("\"etc/locale.gen\""));
assert!(toml.contains("sbindir = \"/usr/sbin\""));
assert!(toml.contains("libdir = \"/usr/lib64\""));
assert!(toml.contains("libexecdir = \"/usr/libexec\""));
assert!(toml.contains("sysconfdir = \"/etc/custom\""));
assert!(toml.contains("localstatedir = \"/var/custom\""));
assert!(toml.contains("sharedstatedir = \"/var/lib/custom\""));
assert!(toml.contains("includedir = \"/usr/include/custom\""));
assert!(toml.contains("datarootdir = \"/usr/share/root\""));
assert!(toml.contains("datadir = \"/usr/share/custom\""));
assert!(toml.contains("mandir = \"/usr/share/custom/man\""));
assert!(toml.contains("infodir = \"/usr/share/custom/info\""));
assert!(toml.contains("use_lto = false"));
assert!(toml.contains("no_flags = true"));
assert!(toml.contains("no_strip = true"));
assert!(toml.contains("no_delete_static = true"));
assert!(toml.contains("no_compress_man = true"));
assert!(toml.contains("skip_tests = true"));
assert!(toml.contains("makeflags = \"-j12 --output-sync=target\""));
assert!(toml.contains("make_vars = ["));
assert!(toml.contains("make_dirs = ["));
assert!(toml.contains("make_test_vars = ["));
assert!(toml.contains("make_test_dirs = ["));
assert!(toml.contains("make_install_vars = ["));
assert!(toml.contains("make_install_dirs = ["));
assert!(toml.contains("env_vars = ["));
assert!(toml.contains("patches = ["));
assert!(toml.contains("post_extract = ["));
}
#[test]
fn spec_to_minimal_toml_includes_extract_dir_for_variable_default() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["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: "$name-$version".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("extract_dir = \"$name-$version\""));
}
#[test]
fn spec_to_minimal_toml_includes_test_and_optional_dependencies() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["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(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: vec![],
runtime: vec![],
test: vec!["python".into(), "bats".into()],
optional: vec!["gtk-doc".into()],
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
let val: toml::Value = toml::from_str(&toml).unwrap();
let test_deps = val
.get("dependencies")
.and_then(|d| d.get("test"))
.and_then(|t| t.as_array())
.expect("expected dependencies.test array");
assert_eq!(test_deps.len(), 2);
assert_eq!(test_deps[0].as_str(), Some("python"));
assert_eq!(test_deps[1].as_str(), Some("bats"));
let optional_deps = val
.get("dependencies")
.and_then(|d| d.get("optional"))
.and_then(|t| t.as_array())
.expect("expected dependencies.optional array");
assert_eq!(optional_deps.len(), 1);
assert_eq!(optional_deps[0].as_str(), Some("gtk-doc"));
}
#[test]
fn spec_to_minimal_toml_includes_alternatives_conflicts_and_provides() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "A test".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives {
provides: vec!["editor".into(), "sh".into()],
conflicts: vec!["nano".into(), "busybox-sh".into()],
replaces: vec!["vi".into()],
lib32: Some(crate::package::AlternativeGroup {
provides: Vec::new(),
conflicts: Vec::new(),
replaces: vec!["lib32-vi".into()],
}),
},
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(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
let val: toml::Value = toml::from_str(&toml).unwrap();
let alternatives = val
.get("alternatives")
.and_then(|v| v.as_table())
.expect("expected alternatives table");
let provides = alternatives
.get("provides")
.and_then(|v| v.as_array())
.expect("expected alternatives.provides array");
let conflicts = alternatives
.get("conflicts")
.and_then(|v| v.as_array())
.expect("expected alternatives.conflicts array");
let replaces = alternatives
.get("replaces")
.and_then(|v| v.as_array())
.expect("expected alternatives.replaces array");
let lib32 = alternatives
.get("lib32")
.and_then(|v| v.as_table())
.expect("expected alternatives.lib32 table");
let lib32_replaces = lib32
.get("replaces")
.and_then(|v| v.as_array())
.expect("expected alternatives.lib32.replaces array");
assert_eq!(provides.len(), 2);
assert_eq!(provides[0].as_str(), Some("editor"));
assert_eq!(provides[1].as_str(), Some("sh"));
assert_eq!(conflicts.len(), 2);
assert_eq!(conflicts[0].as_str(), Some("nano"));
assert_eq!(conflicts[1].as_str(), Some("busybox-sh"));
assert_eq!(replaces.len(), 1);
assert_eq!(replaces[0].as_str(), Some("vi"));
assert_eq!(lib32_replaces.len(), 1);
assert_eq!(lib32_replaces[0].as_str(), Some("lib32-vi"));
}
#[test]
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "foo-meta".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "Meta package".into(),
homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: Build {
build_type: BuildType::Meta,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: Vec::new(),
runtime: vec!["foo".into(), "bar".into()],
test: Vec::new(),
optional: Vec::new(),
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("type = \"meta\""));
assert!(!toml.contains("[[source]]"));
let val: toml::Value = toml::from_str(&toml).unwrap();
assert!(val.get("source").is_none());
}
#[test]
fn spec_to_minimal_toml_includes_manual_sources() {
let spec = PackageSpec {
package: PackageInfo {
name: "vertex-keyring".into(),
real_name: None,
version: "1.0.0".into(),
revision: 1,
description: "keyring".into(),
homepage: "https://www.vertexlinux.net".into(),
abi_breaking: false,
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: vec![
ManualSource {
file: Some("vertex.pub".into()),
files: Vec::new(),
url: None,
urls: Vec::new(),
sha256: None,
dest: Some("usr/share/depot/keys/public/vertex.pub".into()),
},
ManualSource {
file: None,
files: Vec::new(),
url: Some("file:///tmp/vertex.minisig".into()),
urls: Vec::new(),
sha256: Some("skip".into()),
dest: Some("usr/share/depot/keys/sign/vertex.minisig".into()),
},
],
source: 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("."),
};
let toml = spec_to_minimal_toml(&spec).unwrap();
assert!(toml.contains("[[manual_sources]]"));
assert!(toml.contains("file = \"vertex.pub\""));
assert!(toml.contains("url = \"file:///tmp/vertex.minisig\""));
assert!(toml.contains("dest = \"usr/share/depot/keys/public/vertex.pub\""));
assert!(!toml.contains("sha256 = \"skip\""));
let val: toml::Value = toml::from_str(&toml).unwrap();
let arr = val
.get("manual_sources")
.and_then(|v| v.as_array())
.expect("expected manual_sources array");
assert_eq!(arr.len(), 2);
}
#[test]
fn compute_sha256_for_local_path_and_file_url() {
use sha2::Digest as TestDigest;
use sha2::Sha256 as TestSha256;
use tempfile::NamedTempFile;
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);
}
#[test]
fn expand_known_package_vars_replaces_name_and_version_patterns() {
let input = "https://example.com/$name/${name}-$version-${version}.tar.xz";
let out = expand_known_package_vars(input, "python", "3.13.1");
assert_eq!(
out,
"https://example.com/python/python-3.13.1-3.13.1.tar.xz"
);
}
}