2243 lines
79 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|