feat: add support for Mercurial (hg) source handling in mod.rs

- Implemented `split_hg_url` to parse Mercurial URLs and revisions.
- Added `checkout_hg` function to clone Mercurial repositories.
- Integrated Mercurial handling into the `prepare_one` function to support resuming builds.
- Added tests for `split_hg_url` to ensure correct parsing of URLs and revisions.

---

feat: introduce legacy STARBUILD conversion support in starbuild.rs

- Created `ConvertedStarbuild` and `ParsedStarbuild` structs to manage conversion data.
- Implemented `convert_starbuild_file` to parse STARBUILD files and generate package specifications.
- Added functions to handle parsing of various STARBUILD constructs, including dependencies and build phases.
- Developed a build script generator for STARBUILD, supporting custom install functions.
- Included tests to validate conversion of single and multi-output STARBUILD files.
This commit is contained in:
2026-03-23 15:13:07 -05:00
parent 19da5b960c
commit fd7873995a
10 changed files with 1653 additions and 9 deletions
Generated
+3 -3
View File
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]] [[package]]
name = "depot" name = "depot"
version = "0.31.0" version = "0.31.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ar", "ar",
@@ -3090,9 +3090,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "8.3.1" version = "8.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c546feb4481b0fbafb4ef0d79b6204fc41c6f9884b1b73b1d73f82442fc0845" checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9"
dependencies = [ dependencies = [
"aes", "aes",
"bzip2", "bzip2",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "depot" name = "depot"
version = "0.31.0" version = "0.31.1"
edition = "2024" edition = "2024"
[lints.rust] [lints.rust]
@@ -27,7 +27,7 @@ toml = "1.0.6"
url = "2.5.8" url = "2.5.8"
walkdir = "2.5.0" walkdir = "2.5.0"
xz2 = "0.1.7" xz2 = "0.1.7"
zip = "8.3.1" zip = "8.4.0"
zstd = { version = "0.13.3", features = ["zstdmt"] } zstd = { version = "0.13.3", features = ["zstdmt"] }
inquire = "0.9.4" inquire = "0.9.4"
md5 = "0.8.0" md5 = "0.8.0"
+1 -1
View File
@@ -1,6 +1,6 @@
project( project(
'depot', 'depot',
version: '0.31.0', version: '0.31.1',
meson_version: '>=0.60.0', meson_version: '>=0.60.0',
default_options: ['buildtype=release'], default_options: ['buildtype=release'],
) )
+13
View File
@@ -117,6 +117,19 @@ pub fn build(
"DEPOT_PRIMARY_DESTDIR", "DEPOT_PRIMARY_DESTDIR",
install_destdir.to_string_lossy().into_owned(), install_destdir.to_string_lossy().into_owned(),
); );
let starbuild_workdir = if spec.sources().is_empty() {
src_dir.to_path_buf()
} else {
src_dir
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| src_dir.to_path_buf())
};
crate::builder::set_env_var(
&mut env_vars,
"DEPOT_STARBUILD_WORKDIR",
starbuild_workdir.to_string_lossy().into_owned(),
);
add_output_destdir_envs(spec, &install_destdir, &mut env_vars); add_output_destdir_envs(spec, &install_destdir, &mut env_vars);
// Ensure build script path is absolute for when we are in a sub-build-dir // Ensure build script path is absolute for when we are in a sub-build-dir
+40 -1
View File
@@ -240,6 +240,17 @@ pub struct MakeSpecArgs {
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
} }
#[derive(Debug, Clone, Args)]
pub struct ConvertArgs {
/// Path to the legacy STARBUILD file
#[arg(default_value = "STARBUILD")]
pub input: PathBuf,
/// Output TOML file path (defaults to <mainpkgname>.toml beside the STARBUILD)
#[arg(short, long)]
pub output: Option<PathBuf>,
}
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct InternalArgs { pub struct InternalArgs {
#[command(subcommand)] #[command(subcommand)]
@@ -277,6 +288,8 @@ pub enum Commands {
GenerateArtifacts(GenerateArtifactsArgs), GenerateArtifacts(GenerateArtifactsArgs),
/// Create a new package specification interactively /// Create a new package specification interactively
MakeSpec(MakeSpecArgs), MakeSpec(MakeSpecArgs),
/// Convert a legacy STARBUILD into a Depot package spec
Convert(ConvertArgs),
#[command(hide = true)] #[command(hide = true)]
Internal(InternalArgs), Internal(InternalArgs),
} }
@@ -430,7 +443,8 @@ pub enum RepoKindArg {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
BuildArgs, Cli, Commands, InstallArgs, RepoArgs, RepoCommands, SearchArgs, UpdateArgs, BuildArgs, Cli, Commands, ConvertArgs, InstallArgs, RepoArgs, RepoCommands, SearchArgs,
UpdateArgs,
}; };
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use std::path::PathBuf; use std::path::PathBuf;
@@ -517,6 +531,31 @@ mod tests {
} }
} }
#[test]
fn convert_accepts_default_input() {
let cli = Cli::try_parse_from(["depot", "convert"]).unwrap();
match cli.command {
Commands::Convert(ConvertArgs { input, output }) => {
assert_eq!(input, PathBuf::from("STARBUILD"));
assert!(output.is_none());
}
_ => panic!("expected convert command"),
}
}
#[test]
fn convert_accepts_custom_output() {
let cli = Cli::try_parse_from(["depot", "convert", "legacy/STARBUILD", "-o", "pkg.toml"])
.unwrap();
match cli.command {
Commands::Convert(ConvertArgs { input, output }) => {
assert_eq!(input, PathBuf::from("legacy/STARBUILD"));
assert_eq!(output, Some(PathBuf::from("pkg.toml")));
}
_ => panic!("expected convert command"),
}
}
#[test] #[test]
fn build_cleanup_deps_flag_is_parsed() { fn build_cleanup_deps_flag_is_parsed() {
let cli = Cli::try_parse_from(["depot", "build", "--cleanup-deps", "pkg.toml"]).unwrap(); let cli = Cli::try_parse_from(["depot", "build", "--cleanup-deps", "pkg.toml"]).unwrap();
+58 -2
View File
@@ -1,6 +1,7 @@
use crate::cli::{ use crate::cli::{
BuildArgs, Cli, Commands, ConfigArgs, InfoArgs, InstallArgs, InternalCommands, ListArgs, BuildArgs, Cli, Commands, ConfigArgs, ConvertArgs, InfoArgs, InstallArgs, InternalCommands,
OwnsArgs, RemoveArgs, RepoArgs, RepoCommands, RepoKindArg, SearchArgs, SignArgs, UpdateArgs, ListArgs, OwnsArgs, RemoveArgs, RepoArgs, RepoCommands, RepoKindArg, SearchArgs, SignArgs,
UpdateArgs,
}; };
use crate::{ use crate::{
builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner, builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner,
@@ -63,6 +64,7 @@ fn command_rootfs(command: &Commands) -> Option<&Path> {
Commands::Repo(args) => Some(repo_command_rootfs(&args.command)), Commands::Repo(args) => Some(repo_command_rootfs(&args.command)),
Commands::Config(args) => Some(&args.rootfs_args.rootfs), Commands::Config(args) => Some(&args.rootfs_args.rootfs),
Commands::Check(_) Commands::Check(_)
| Commands::Convert(_)
| Commands::GenerateArtifacts(_) | Commands::GenerateArtifacts(_)
| Commands::MakeSpec(_) | Commands::MakeSpec(_)
| Commands::Internal(_) => None, | Commands::Internal(_) => None,
@@ -76,6 +78,7 @@ fn command_assume_yes(command: &Commands) -> bool {
Commands::Build(args) => args.prompt_args.yes, Commands::Build(args) => args.prompt_args.yes,
Commands::Update(args) => args.prompt_args.yes, Commands::Update(args) => args.prompt_args.yes,
Commands::Check(_) Commands::Check(_)
| Commands::Convert(_)
| Commands::Info(_) | Commands::Info(_)
| Commands::Search(_) | Commands::Search(_)
| Commands::Owns(_) | Commands::Owns(_)
@@ -4669,6 +4672,7 @@ pub fn run(cli: Cli) -> Result<()> {
| Commands::Repo(_) | Commands::Repo(_)
| Commands::Config(_) | Commands::Config(_)
| Commands::GenerateArtifacts(_) | Commands::GenerateArtifacts(_)
| Commands::Convert(_)
| Commands::MakeSpec(_) | Commands::MakeSpec(_)
| Commands::Internal(_) => false, | Commands::Internal(_) => false,
}; };
@@ -5901,6 +5905,58 @@ pub fn run(cli: Cli) -> Result<()> {
output_path.display() output_path.display()
)); ));
} }
Commands::Convert(ConvertArgs { input, output }) => {
let converted = package::convert_starbuild_file(&input, output.as_deref())?;
let mut outputs = vec![converted.output_path.clone()];
if let Some(build_script_path) = &converted.build_script_path {
outputs.push(build_script_path.clone());
}
let existing: Vec<_> = outputs
.iter()
.filter(|path| path.exists())
.map(|path| path.display().to_string())
.collect();
if !existing.is_empty() {
ui::warn(format!(
"Generated files already exist: {}",
existing.join(", ")
));
if !ui::prompt_yes_no("Overwrite them?", false)? {
anyhow::bail!("Aborted");
}
}
fs::write(&converted.output_path, converted.toml)
.with_context(|| format!("Failed to write {}", converted.output_path.display()))?;
if let (Some(build_script), Some(build_script_path)) =
(converted.build_script, converted.build_script_path)
{
fs::write(&build_script_path, build_script)
.with_context(|| format!("Failed to write {}", build_script_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&build_script_path)
.with_context(|| format!("Failed to stat {}", build_script_path.display()))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&build_script_path, perms).with_context(|| {
format!("Failed to chmod {}", build_script_path.display())
})?;
}
ui::success(format!(
"Converted STARBUILD into {} and {}",
converted.output_path.display(),
build_script_path.display()
));
} else {
ui::success(format!(
"Converted STARBUILD into {}",
converted.output_path.display()
));
}
}
Commands::Internal(args) => { Commands::Internal(args) => {
let command = args.command; let command = args.command;
run_internal_command(command)?; run_internal_command(command)?;
+101
View File
@@ -1544,6 +1544,107 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
root.insert("dependencies".into(), Value::Table(dep_tbl)); 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))?) Ok(toml::to_string_pretty(&Value::Table(root))?)
} }
+2
View File
@@ -3,7 +3,9 @@
mod interactive; mod interactive;
mod packager; mod packager;
mod spec; mod spec;
mod starbuild;
pub use interactive::*; pub use interactive::*;
pub use packager::Packager; pub use packager::Packager;
pub use spec::*; pub use spec::*;
pub(crate) use starbuild::*;
File diff suppressed because it is too large Load Diff
+65
View File
@@ -13,6 +13,7 @@ use sha2::{Digest, Sha256};
use std::fs; use std::fs;
use std::os::unix::fs as unix_fs; use std::os::unix::fs as unix_fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
use url::Url; use url::Url;
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -418,6 +419,20 @@ fn prepare_one(
// Treat `<url>#<rev>` and bare `*.git` sources as git before local file:// // Treat `<url>#<rev>` and bare `*.git` sources as git before local file://
// handling so `file://...repo.git#tag` resolves through the git checkout path. // handling so `file://...repo.git#tag` resolves through the git checkout path.
if let Some((base, rev)) = split_hg_url(&url) {
let checkout_dir = build_dir.join(&extract_dir_name);
if checkout_dir.exists() && checkout_dir.join(".depot_state").exists() {
crate::log_info!(
"Resuming build in existing hg directory: {}",
checkout_dir.display()
);
return Ok(checkout_dir);
}
checkout_hg(&base, &rev, &checkout_dir)?;
hooks::post_extract(spec, source, &checkout_dir, cache_dir)?;
return Ok(checkout_dir);
}
if let Some((base, rev)) = split_git_url(&url) { if let Some((base, rev)) = split_git_url(&url) {
let checkout_dir = build_dir.join(&extract_dir_name); let checkout_dir = build_dir.join(&extract_dir_name);
if checkout_dir.exists() && checkout_dir.join(".depot_state").exists() { if checkout_dir.exists() && checkout_dir.join(".depot_state").exists() {
@@ -556,6 +571,45 @@ fn split_git_url(url: &str) -> Option<(String, String)> {
None None
} }
fn split_hg_url(url: &str) -> Option<(String, String)> {
let rest = url.strip_prefix("hg+")?;
if let Some((base, rev)) = rest.split_once('#') {
let revision = if rev.trim().is_empty() {
"tip"
} else {
rev.trim()
};
return Some((base.to_string(), revision.to_string()));
}
Some((rest.to_string(), "tip".to_string()))
}
fn checkout_hg(url: &str, rev: &str, checkout_dir: &Path) -> Result<()> {
if checkout_dir.exists() {
fs::remove_dir_all(checkout_dir).with_context(|| {
format!(
"Failed to remove existing Mercurial checkout dir: {}",
checkout_dir.display()
)
})?;
}
crate::log_info!("Cloning Mercurial source {} @ {}...", url, rev);
let mut cmd = Command::new("hg");
cmd.arg("clone")
.arg("-u")
.arg(rev)
.arg(url)
.arg(checkout_dir);
cmd.env("PATH", crate::runtime_env::safe_script_path());
let status = crate::interrupts::command_status(&mut cmd)
.with_context(|| format!("Failed to run hg clone for {}", url))?;
if !status.success() {
bail!("Mercurial clone failed for {} @ {}", url, rev);
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -755,6 +809,17 @@ mod tests {
assert_eq!(rev, "0123456789abcdef"); assert_eq!(rev, "0123456789abcdef");
} }
#[test]
fn split_hg_url_accepts_revision_and_default_tip() {
let (base, rev) = split_hg_url("hg+https://hg.example.test/repo#v1").unwrap();
assert_eq!(base, "https://hg.example.test/repo");
assert_eq!(rev, "v1");
let (base, rev) = split_hg_url("hg+https://hg.example.test/repo").unwrap();
assert_eq!(base, "https://hg.example.test/repo");
assert_eq!(rev, "tip");
}
#[test] #[test]
fn prepare_one_rejects_cherry_pick_for_non_git_sources() { fn prepare_one_rejects_cherry_pick_for_non_git_sources() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();