chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 14:51:59 +08:00
commit 14dcf8c9bf
321 changed files with 76893 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "xtask"
version.workspace = true
edition.workspace = true
license.workspace = true
description.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
rust-version.workspace = true
[[bin]]
name = "xtask"
path = "src/main.rs"
[dependencies]
anyhow = "1.0.100"
cargo_metadata = "0.23.1"
clap = { version = "4.5.53", features = ["derive"] }
flate2 = "1.1.5"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
sha2 = "0.10.9"
tar = "0.4.44"
tempfile = "3.23.0"
time = { version = "0.3.47", features = ["formatting"] }
walkdir = "2.5.0"
zip = { version = "2.4.2", default-features = false, features = ["deflate"] }
[lints]
workspace = true
+237
View File
@@ -0,0 +1,237 @@
#![expect(
clippy::redundant_pub_crate,
reason = "the cli module stays private while sibling xtask modules need crate-visible CLI types"
)]
use std::path::PathBuf;
use clap::{ArgAction, Args, Parser, Subcommand};
/// Top-level xtask CLI entrypoint.
#[derive(Debug, Parser)]
#[command(
author,
version,
about = "Cargo-native workflow entrypoints for aria2-rust-pro"
)]
pub(crate) struct Cli {
/// Selected top-level subcommand.
#[command(subcommand)]
pub(crate) command: TopLevelCommand,
}
/// Supported top-level xtask command groups.
#[derive(Debug, Subcommand)]
pub(crate) enum TopLevelCommand {
/// Compatibility and golden-output workflows.
Compat(CompatCommand),
/// Docker packaging and smoke-test workflows.
Docker(DockerCommand),
/// Performance collection workflows.
Perf(PerfCommand),
/// Release packaging and validation workflows.
Release(ReleaseCommand),
/// Aggregated testing and lint-style sweeps.
Testing(TestingCommand),
}
/// Compatibility command group.
#[derive(Debug, Args)]
pub(crate) struct CompatCommand {
/// Selected compatibility subcommand.
#[command(subcommand)]
pub(crate) command: CompatSubcommand,
}
/// Compatibility-focused subcommands.
#[derive(Debug, Subcommand)]
pub(crate) enum CompatSubcommand {
/// Capture CLI output goldens from upstream and Rust binaries.
CaptureCliGoldens(CaptureCliGoldensArgs),
}
/// Arguments for capturing CLI golden outputs.
#[derive(Debug, Args)]
pub(crate) struct CaptureCliGoldensArgs {
/// Optional path to the upstream reference binary.
#[arg(long)]
pub(crate) upstream_binary: Option<PathBuf>,
/// Optional path to the Rust implementation binary.
#[arg(long)]
pub(crate) rust_binary: Option<PathBuf>,
/// Optional root directory where captured goldens are written.
#[arg(long)]
pub(crate) output_root: Option<PathBuf>,
}
/// Release command group.
#[derive(Debug, Args)]
pub(crate) struct ReleaseCommand {
/// Selected release subcommand.
#[command(subcommand)]
pub(crate) command: ReleaseSubcommand,
}
/// Release-oriented subcommands.
#[derive(Debug, Subcommand)]
pub(crate) enum ReleaseSubcommand {
/// Validate the release binary reports the expected version string.
SmokeVersion(ReleaseSmokeVersionArgs),
/// Package a local release artifact layout.
PackageLocal(PackageLocalArgs),
}
/// Arguments for the release version smoke test.
#[derive(Debug, Args)]
pub(crate) struct ReleaseSmokeVersionArgs {
/// Build the binary before running the smoke test.
#[arg(long)]
pub(crate) build: bool,
/// Optional target triple used when locating or building the binary.
#[arg(long)]
pub(crate) target_triple: Option<String>,
/// Optional explicit path to the binary under test.
#[arg(long)]
pub(crate) binary_path: Option<PathBuf>,
}
/// Arguments for packaging a local release bundle.
#[derive(Debug, Args)]
pub(crate) struct PackageLocalArgs {
/// Build the binary before packaging it.
#[arg(long)]
pub(crate) build: bool,
/// Optional target triple used when locating or building the binary.
#[arg(long)]
pub(crate) target_triple: Option<String>,
/// Optional explicit path to the binary that should be packaged.
#[arg(long)]
pub(crate) source_binary: Option<PathBuf>,
/// Optional root directory where packaged artifacts are written.
#[arg(long)]
pub(crate) output_root: Option<PathBuf>,
/// Skip the version smoke test that normally runs before packaging.
#[arg(long)]
pub(crate) skip_version_smoke: bool,
}
/// Testing command group.
#[derive(Debug, Args)]
pub(crate) struct TestingCommand {
/// Selected testing subcommand.
#[command(subcommand)]
pub(crate) command: TestingSubcommand,
}
/// Testing-oriented subcommands.
#[derive(Debug, Subcommand)]
pub(crate) enum TestingSubcommand {
/// Run the strict sweep across the xtask quality gates.
StrictSweep(StrictSweepArgs),
}
/// Arguments for the strict sweep workflow.
#[derive(Debug, Args)]
pub(crate) struct StrictSweepArgs {
/// Include release smoke checks as part of the sweep.
#[arg(long)]
pub(crate) include_release_smoke: bool,
}
/// Docker command group.
#[derive(Debug, Args)]
pub(crate) struct DockerCommand {
/// Selected Docker subcommand.
#[command(subcommand)]
pub(crate) command: DockerSubcommand,
}
/// Docker-oriented subcommands.
#[derive(Debug, Subcommand)]
pub(crate) enum DockerSubcommand {
/// Export a locally built Docker image as a tarball.
ExportLocal(ExportLocalArgs),
/// Run the Docker smoke test against a tagged image.
Smoke(DockerSmokeArgs),
/// Run the Docker smoke test using the local defaults.
SmokeLocal(DockerSmokeLocalArgs),
}
/// Arguments for exporting a local Docker image.
#[derive(Debug, Args)]
pub(crate) struct ExportLocalArgs {
/// Docker image tag to build or export.
#[arg(long, default_value = "aria2-rust-pro:local")]
pub(crate) tag: String,
/// Optional root directory where exported artifacts are written.
#[arg(long)]
pub(crate) output_root: Option<PathBuf>,
/// Build the Docker image before exporting it.
#[arg(long)]
pub(crate) build: bool,
}
/// Arguments for running the Docker smoke test.
#[derive(Debug, Args)]
pub(crate) struct DockerSmokeArgs {
/// Docker image tag to build or run during the smoke test.
#[arg(long, default_value = "aria2-rust-pro:smoke")]
pub(crate) tag: String,
/// Build the Docker image before running the smoke test.
#[arg(long, default_value_t = true, action = ArgAction::Set)]
pub(crate) build: bool,
/// Host RPC port mapped into the smoke-test container.
#[arg(long, default_value_t = 26_800)]
pub(crate) host_rpc_port: u16,
/// RPC secret used by the smoke-test container.
#[arg(long, default_value = "smoke-secret")]
pub(crate) rpc_secret: String,
/// Special mode passed into the smoke-test scenario.
#[arg(long, default_value = "move")]
pub(crate) special_mode: String,
}
/// Arguments for the local Docker smoke shortcut.
#[derive(Debug, Args)]
pub(crate) struct DockerSmokeLocalArgs {}
/// Performance command group.
#[derive(Debug, Args)]
pub(crate) struct PerfCommand {
/// Selected performance subcommand.
#[command(subcommand)]
pub(crate) command: PerfSubcommand,
}
/// Performance-oriented subcommands.
#[derive(Debug, Subcommand)]
pub(crate) enum PerfSubcommand {
/// Collect local upstream-vs-Rust comparison data.
CollectLocalComparison(CollectLocalComparisonArgs),
}
/// Arguments for local comparison data collection.
#[derive(Debug, Args)]
pub(crate) struct CollectLocalComparisonArgs {
/// Optional output file for the comparison report.
#[arg(long)]
pub(crate) output_path: Option<PathBuf>,
/// Number of samples collected per benchmark case.
#[arg(long, default_value_t = 10.0)]
pub(crate) sample_size: f64,
/// Measurement duration, in seconds, for each sample.
#[arg(long, default_value_t = 0.05)]
pub(crate) measurement_seconds: f64,
/// Warmup duration, in seconds, before measurements begin.
#[arg(long, default_value_t = 0.05)]
pub(crate) warmup_seconds: f64,
/// Transfer size, in bytes, used by the benchmark.
#[arg(long, default_value_t = 8 * 1024 * 1024)]
pub(crate) transfer_bytes: usize,
/// Timed same-host transfer samples collected per binary and scenario after warmup.
#[arg(long, default_value_t = 5)]
pub(crate) same_host_runs: usize,
/// Skip bench execution and only reuse existing output data.
#[arg(long)]
pub(crate) skip_bench_execution: bool,
}
+168
View File
@@ -0,0 +1,168 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Serialize;
use crate::{
cli::CaptureCliGoldensArgs,
workspace::{
capture_stdout_lines, default_binary_path, load_workspace_metadata, utc_now_rfc3339,
write_utf8_lines, write_utf8_text,
},
};
#[derive(Debug, Serialize)]
/// JSON manifest written beside captured CLI compatibility goldens.
struct CompatManifest {
/// UTC generation timestamp for the manifest.
generated_at_utc: String,
/// Relative paths captured by the workflow.
captured: Vec<&'static str>,
}
#[derive(Debug, Serialize)]
/// Human-readable JSON summary printed after capture.
struct CompatSummary {
#[serde(rename = "Captured")]
/// Relative paths captured by the workflow.
captured: Vec<&'static str>,
}
/// Captures upstream and Rust CLI help/version output as compatibility goldens.
pub fn run_capture_cli_goldens(args: &CaptureCliGoldensArgs) -> Result<()> {
let metadata = load_workspace_metadata()?;
let host_triple = crate::workspace::host_triple()?;
let upstream_binary = args
.upstream_binary
.clone()
.unwrap_or_else(default_upstream_binary);
let rust_binary = args
.rust_binary
.clone()
.unwrap_or_else(|| default_binary_path(&metadata, &host_triple, &host_triple));
let output_root = args.output_root.clone().unwrap_or_else(|| {
metadata
.workspace_root
.join("docs/compatibility/goldens/cli")
});
ensure_path_exists(&upstream_binary, "upstream binary")?;
ensure_path_exists(&rust_binary, "rust binary")?;
let upstream_dir = output_root.join("upstream");
let rust_dir = output_root.join("rust");
std::fs::create_dir_all(&upstream_dir)
.with_context(|| format!("failed to create {}", upstream_dir.display()))?;
std::fs::create_dir_all(&rust_dir)
.with_context(|| format!("failed to create {}", rust_dir.display()))?;
let upstream_version = capture_stdout_lines(&upstream_binary, &["-v"])?;
let upstream_help =
redact_windows_home_paths(capture_stdout_lines(&upstream_binary, &["--help=#all"])?);
let rust_version = capture_stdout_lines(&rust_binary, &["--version"])?;
let rust_help =
redact_windows_home_paths(capture_stdout_lines(&rust_binary, &["--help=#all"])?);
write_utf8_lines(&upstream_dir.join("version.txt"), &upstream_version)?;
write_utf8_lines(&upstream_dir.join("help-all.txt"), &upstream_help)?;
write_utf8_lines(&rust_dir.join("version.txt"), &rust_version)?;
write_utf8_lines(&rust_dir.join("help-all.txt"), &rust_help)?;
let captured = captured_paths();
let manifest = CompatManifest {
generated_at_utc: utc_now_rfc3339()?,
captured: captured.to_vec(),
};
let manifest_text = serde_json::to_string_pretty(&manifest)
.context("failed to serialize compatibility manifest")?;
write_utf8_text(&output_root.join("manifest.json"), &(manifest_text + "\n"))?;
let summary = CompatSummary {
captured: summary_paths().to_vec(),
};
println!(
"{}",
serde_json::to_string_pretty(&summary)
.context("failed to serialize compatibility summary")?
);
Ok(())
}
/// Returns the repository-local default upstream aria2 binary path.
fn default_upstream_binary() -> PathBuf {
crate::workspace::repo_root()
.join("dist")
.join("upstream")
.join("aria2-1.37.0-win-64bit-build1")
.join("aria2-1.37.0-win-64bit-build1")
.join("aria2c.exe")
}
/// Ensures an expected binary path exists.
fn ensure_path_exists(path: &Path, label: &str) -> Result<()> {
if path.is_file() {
Ok(())
} else {
bail!("{label} not found: {}", path.display())
}
}
/// Replaces a Windows account name embedded in captured CLI help text.
fn redact_windows_home_paths(lines: Vec<String>) -> Vec<String> {
lines
.into_iter()
.map(|line| redact_windows_home_path(&line))
.collect()
}
fn redact_windows_home_path(line: &str) -> String {
const PREFIX: &str = "C:/Users/";
let Some(start) = line.find(PREFIX) else {
return line.to_owned();
};
let user_start = start + PREFIX.len();
let Some(user_length) = line[user_start..].find('/') else {
return line.to_owned();
};
let suffix_start = user_start + user_length + 1;
format!(
"{}C:/Users/<user>/{}",
&line[..start],
&line[suffix_start..]
)
}
#[cfg(test)]
mod tests {
use super::redact_windows_home_path;
#[test]
fn redacts_a_windows_home_path_in_captured_help() {
assert_eq!(
redact_windows_home_path("Default: C:/Users/alice/.netrc"),
"Default: C:/Users/<user>/.netrc"
);
}
}
/// Returns the golden files produced by the capture workflow.
const fn captured_paths() -> &'static [&'static str] {
&[
"upstream/version.txt",
"upstream/help-all.txt",
"rust/version.txt",
"rust/help-all.txt",
]
}
/// Returns all paths reported in the workflow summary.
const fn summary_paths() -> &'static [&'static str] {
&[
"upstream/version.txt",
"upstream/help-all.txt",
"rust/version.txt",
"rust/help-all.txt",
"manifest.json",
]
}
+986
View File
@@ -0,0 +1,986 @@
use std::{
env,
ffi::{OsStr, OsString},
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
thread::sleep,
time::Duration,
};
use anyhow::{Context, Result, anyhow, bail};
use serde::Serialize;
use sha2::{Digest, Sha256};
use crate::{
cli::{DockerSmokeArgs, DockerSmokeLocalArgs, ExportLocalArgs},
workspace::{
load_workspace_metadata, repo_root, run_command, run_command_with_env, utc_now_rfc3339,
workspace_relative_or_external, write_utf8_text,
},
};
#[derive(Debug, Serialize)]
/// Machine-readable manifest written beside a Docker image archive.
struct DockerArchiveManifest {
/// Package version used for the archive directory.
version: String,
/// Docker image tag exported by the workflow.
tag: String,
/// Docker image identifier reported by `docker image inspect`.
image_id: String,
/// Exported archive artifact metadata.
archive: DockerArchiveArtifact,
/// Path to the aggregate checksum list.
checksum_list: String,
/// UTC generation timestamp for the manifest.
generated_at_utc: String,
}
#[derive(Debug, Serialize)]
/// Metadata for a Docker archive artifact.
struct DockerArchiveArtifact {
/// Archive file name.
file_name: String,
/// Full archive path.
path: String,
/// Hex-encoded SHA-256 digest.
sha256: String,
}
#[derive(Debug, Serialize)]
/// Summary printed after exporting a local Docker image.
struct DockerExportSummary {
#[serde(rename = "Version")]
/// Package version used for the archive directory.
version: String,
#[serde(rename = "Tag")]
/// Docker image tag exported by the workflow.
tag: String,
#[serde(rename = "ArchivePath")]
/// Path to the exported Docker archive.
archive_path: String,
#[serde(rename = "ManifestPath")]
/// Path to the generated manifest.
manifest_path: String,
#[serde(rename = "ChecksumList")]
/// Path to the aggregate checksum list.
checksum_list: String,
}
#[derive(Debug, Serialize)]
/// Summary printed after running the Docker container smoke test.
struct DockerSmokeSummary {
#[serde(rename = "Dockerfile")]
/// Dockerfile used to build the image.
dockerfile: String,
#[serde(rename = "ComposeFile")]
/// Compose file shipped with the image.
compose_file: String,
#[serde(rename = "VersionOutput")]
/// Captured `aria2c --version` output.
version_output: String,
#[serde(rename = "RuntimeConfigPreview")]
/// Captured generated runtime config.
runtime_config_preview: String,
#[serde(rename = "RpcResponsePreview")]
/// Captured JSON-RPC probe response.
rpc_response_preview: String,
}
#[derive(Debug, Serialize)]
/// Summary printed after running the shell-only Docker entrypoint smoke.
struct DockerSmokeLocalSummary {
#[serde(rename = "Entrypoint")]
/// Entrypoint script exercised by the smoke.
entrypoint: String,
#[serde(rename = "RuntimeConfig")]
/// Generated runtime config path.
runtime_config: String,
#[serde(rename = "BaseConfig")]
/// Materialized base config path.
base_config: String,
#[serde(rename = "CapturedArgv")]
/// Arguments captured by the fake aria2 binary.
captured_argv: Vec<String>,
}
/// Exports a local Docker image archive and checksum manifest.
pub fn run_export_local(args: &ExportLocalArgs) -> Result<()> {
let metadata = load_workspace_metadata()?;
let repo_root = repo_root();
let dockerfile = dockerfile_path(&repo_root);
let output_root = args
.output_root
.clone()
.unwrap_or_else(|| repo_root.join("dist").join("docker"));
if args.build {
build_image(&args.tag, &dockerfile, &repo_root)?;
}
assert_image_available(&args.tag)?;
let version_dir = output_root.join(format!("v{}", metadata.package_version));
std::fs::create_dir_all(&version_dir)
.with_context(|| format!("failed to create {}", version_dir.display()))?;
let archive_base_name = format!("aria2-rust-pro-docker-v{}", metadata.package_version);
let archive_name = format!("{archive_base_name}.tar");
let archive_path = version_dir.join(&archive_name);
if archive_path.exists() {
std::fs::remove_file(&archive_path)
.with_context(|| format!("failed to remove {}", archive_path.display()))?;
}
run_command(
"docker",
&[
os("save"),
os("-o"),
archive_path.clone().into_os_string(),
os(&args.tag),
],
)?;
let checksum_list = write_checksum_files(std::slice::from_ref(&archive_path), &version_dir)?;
let archive_hash = sha256_hex(&archive_path)?;
let image_id = capture_command_stdout(
"docker",
&[
os("image"),
os("inspect"),
os("--format"),
os("{{.Id}}"),
os(&args.tag),
],
)?;
let archive_path_text = workspace_relative_or_external(&archive_path);
let checksum_list_path = workspace_relative_or_external(&checksum_list);
let manifest_path = version_dir.join(format!("{archive_base_name}.manifest.json"));
let manifest_path_text = workspace_relative_or_external(&manifest_path);
let manifest = DockerArchiveManifest {
version: metadata.package_version.clone(),
tag: args.tag.clone(),
image_id: trim_owned(&image_id),
archive: DockerArchiveArtifact {
file_name: archive_name,
path: archive_path_text.clone(),
sha256: archive_hash,
},
checksum_list: checksum_list_path.clone(),
generated_at_utc: utc_now_rfc3339()?,
};
let manifest_text = serde_json::to_string_pretty(&manifest)
.context("failed to serialize docker export manifest")?;
write_utf8_text(&manifest_path, &(manifest_text + "\n"))?;
let summary = DockerExportSummary {
version: metadata.package_version,
tag: args.tag.clone(),
archive_path: archive_path_text,
manifest_path: manifest_path_text,
checksum_list: checksum_list_path,
};
println!(
"{}",
serde_json::to_string_pretty(&summary)
.context("failed to serialize docker export summary")?
);
Ok(())
}
/// Runs the Docker image smoke workflow against a live container.
pub fn run_smoke(args: &DockerSmokeArgs) -> Result<()> {
let repo_root = repo_root();
let dockerfile = dockerfile_path(&repo_root);
let compose_file = compose_file_path(&repo_root);
if args.build {
build_image(&args.tag, &dockerfile, &repo_root)?;
}
let version_output = capture_command_stdout(
"docker",
&[
os("run"),
os("--rm"),
os(&args.tag),
os("aria2c"),
os("--version"),
],
)?;
let container_id = start_smoke_container(args)?;
let _guard = DockerContainerGuard::new(container_id.clone());
sleep(Duration::from_secs(3));
docker_exec_ok(&container_id, &["test", "-f", "/config/aria2.conf"])
.context("container did not materialize /config/aria2.conf")?;
let runtime_config = capture_command_stdout(
"docker",
&[
os("exec"),
os(&container_id),
os("cat"),
os("/run/aria2-rust-pro/aria2.generated.conf"),
],
)
.context("docker exec runtime config read failed")?;
verify_container_special_mode(&container_id, &runtime_config, &args.special_mode)?;
let rpc_payload = format!(
"{{\"jsonrpc\":\"2.0\",\"id\":\"smoke\",\"method\":\"aria2.getVersion\",\"params\":[\"token:{}\"]}}",
args.rpc_secret
);
let rpc_response = capture_command_stdout_with_stdin(
"docker",
&[
os("exec"),
os("-i"),
os(&container_id),
os("curl"),
os("-fsS"),
os("-H"),
os("Content-Type: application/json"),
os("--data-binary"),
os("@-"),
os("http://127.0.0.1:6800/jsonrpc"),
],
&rpc_payload,
)
.context("docker exec rpc probe failed")?;
ensure_contains(
&rpc_response,
"\"version\"",
"rpc probe did not return version payload",
)?;
verify_container_runtime_config(&runtime_config)?;
let dockerfile_path = workspace_relative_or_external(&dockerfile);
let compose_file_path = workspace_relative_or_external(&compose_file);
let summary = DockerSmokeSummary {
dockerfile: dockerfile_path,
compose_file: compose_file_path,
version_output: trim_owned(&version_output),
runtime_config_preview: trim_owned(&redact_runtime_config(&runtime_config)),
rpc_response_preview: trim_owned(&rpc_response),
};
println!(
"{}",
serde_json::to_string_pretty(&summary)
.context("failed to serialize docker smoke summary")?
);
Ok(())
}
/// Starts the container used by the Docker smoke test and returns its id.
fn start_smoke_container(args: &DockerSmokeArgs) -> Result<String> {
let port_mapping = format!("{}:6800", args.host_rpc_port);
let container_id = capture_command_stdout(
"docker",
&[
os("run"),
os("-d"),
os("-e"),
os(&format!("RPC_SECRET={}", args.rpc_secret)),
os("-e"),
os("UPDATE_TRACKERS=false"),
os("-e"),
os(&format!("SPECIAL_MODE={}", args.special_mode)),
os("-p"),
os(&port_mapping),
os(&args.tag),
],
)?;
let container_id = trim_owned(&container_id);
if container_id.is_empty() {
bail!("docker run -d returned an empty container id");
}
Ok(container_id)
}
/// Verifies special-mode side effects inside the smoke-test container.
fn verify_container_special_mode(
container_id: &str,
runtime_config: &str,
special_mode: &str,
) -> Result<()> {
if special_mode == "move" {
docker_exec_ok(container_id, &["test", "-f", "/config/script/move.sh"])
.context("container did not materialize move special-mode script")?;
ensure_contains(
runtime_config,
"on-download-complete=/config/script/move.sh",
"runtime config did not apply SPECIAL_MODE=move hook override",
)?;
}
Ok(())
}
/// Verifies common generated runtime config snippets for the container smoke.
fn verify_container_runtime_config(runtime_config: &str) -> Result<()> {
ensure_contains(
runtime_config,
"bt-tracker=",
"runtime config did not seed bundled bt-tracker snapshot",
)
}
/// Runs the Docker entrypoint smoke locally with a fake aria2 binary.
pub fn run_smoke_local(_args: &DockerSmokeLocalArgs) -> Result<()> {
let workspace = SmokeLocalWorkspace::new()?;
let shell = find_posix_shell()
.ok_or_else(|| anyhow!("no POSIX shell was found for docker smoke-local"))?;
run_shell_script(&shell, SMOKE_LOCAL_SCRIPT, &workspace.envs)?;
let downloads_dir_unix = capture_unix_path(&shell, "DOWNLOAD_DIR_WIN", &workspace.envs)?;
let config_dir_unix = capture_unix_path(&shell, "CONFIG_DIR_WIN", &workspace.envs)?;
let runtime_config = workspace.runtime_dir.join("aria2.generated.conf");
validate_smoke_local_outputs(&runtime_config, &workspace.capture_path)?;
let runtime_config_text = std::fs::read_to_string(&runtime_config)
.with_context(|| format!("failed to read {}", runtime_config.display()))?;
let captured_argv_text = std::fs::read_to_string(&workspace.capture_path)
.with_context(|| format!("failed to read {}", workspace.capture_path.display()))?;
let captured_argv = captured_argv_text
.lines()
.map(str::to_owned)
.collect::<Vec<_>>();
let base_config = workspace.config_dir.join("aria2.conf");
let session_file = workspace.config_dir.join("aria2.session");
validate_smoke_local_files(&workspace.config_dir, &base_config, &session_file)?;
validate_smoke_local_config(&runtime_config_text, &downloads_dir_unix, &config_dir_unix)?;
validate_smoke_local_argv(&captured_argv)?;
let entrypoint_path = workspace_relative_or_external(&workspace.entrypoint);
let runtime_config_path = workspace_relative_or_external(&runtime_config);
let base_config_path = workspace_relative_or_external(&base_config);
let summary = DockerSmokeLocalSummary {
entrypoint: entrypoint_path,
runtime_config: runtime_config_path,
base_config: base_config_path,
captured_argv,
};
println!(
"{}",
serde_json::to_string_pretty(&summary)
.context("failed to serialize docker smoke-local summary")?
);
Ok(())
}
/// Temporary workspace for the local Docker entrypoint smoke.
struct SmokeLocalWorkspace {
/// Temporary directory whose lifetime owns all smoke files.
_work_root: tempfile::TempDir,
/// Entrypoint script under test.
entrypoint: PathBuf,
/// Temporary configuration directory.
config_dir: PathBuf,
/// Temporary runtime directory.
runtime_dir: PathBuf,
/// File where the fake aria2 binary records argv.
capture_path: PathBuf,
/// Environment variables passed to the smoke shell.
envs: Vec<(&'static str, OsString)>,
}
impl SmokeLocalWorkspace {
/// Creates the temporary workspace and fake aria2 executable.
fn new() -> Result<Self> {
let repo_root = repo_root();
let entrypoint = repo_root.join("docker").join("entrypoint.sh");
let defaults_dir = repo_root.join("docker").join("defaults");
let work_root =
tempfile::tempdir().context("failed to create temporary smoke-local root")?;
let config_dir = work_root.path().join("config");
let downloads_dir = work_root.path().join("downloads");
let runtime_dir = work_root.path().join("run");
let bin_dir = work_root.path().join("bin");
for directory in [&config_dir, &downloads_dir, &runtime_dir, &bin_dir] {
std::fs::create_dir_all(directory)
.with_context(|| format!("failed to create {}", directory.display()))?;
}
let capture_path = work_root.path().join("captured-argv.txt");
let fake_aria2 = bin_dir.join("aria2c");
write_utf8_text(
&fake_aria2,
"#!/usr/bin/env bash\nset -eu\nprintf '%s\\n' \"$@\" > \"$CAPTURE_PATH\"\n",
)?;
let envs = smoke_local_envs(
&entrypoint,
&defaults_dir,
&config_dir,
&downloads_dir,
&runtime_dir,
&fake_aria2,
&capture_path,
);
Ok(Self {
_work_root: work_root,
entrypoint,
config_dir,
runtime_dir,
capture_path,
envs,
})
}
}
/// Verifies that the smoke script wrote its primary outputs.
fn validate_smoke_local_outputs(runtime_config: &Path, capture_path: &Path) -> Result<()> {
if !runtime_config.is_file() {
bail!("entrypoint did not generate runtime config");
}
if !capture_path.is_file() {
bail!("fake aria2 binary did not capture argv");
}
Ok(())
}
/// Verifies files materialized by the local Docker entrypoint smoke.
fn validate_smoke_local_files(
config_dir: &Path,
base_config: &Path,
session_file: &Path,
) -> Result<()> {
if !base_config.is_file() {
bail!("entrypoint did not materialize base aria2.conf");
}
if !session_file.is_file() {
bail!("entrypoint did not materialize aria2.session");
}
let move_script = config_dir.join("script").join("move.sh");
if !move_script.is_file() {
bail!("entrypoint did not materialize move special-mode script");
}
Ok(())
}
/// Verifies expected generated runtime config snippets.
fn validate_smoke_local_config(
runtime_config_text: &str,
downloads_dir_unix: &str,
config_dir_unix: &str,
) -> Result<()> {
let session_file_unix = format!("{config_dir_unix}/aria2.session");
for snippet in [
format!("dir={downloads_dir_unix}"),
format!("input-file={session_file_unix}"),
format!("save-session={session_file_unix}"),
"save-session-interval=60".to_owned(),
"enable-rpc=true".to_owned(),
"rpc-listen-port=16800".to_owned(),
"listen-port=51413".to_owned(),
"dht-listen-port=51413".to_owned(),
"disable-ipv6=false".to_owned(),
"rpc-secret=smoke-secret".to_owned(),
"rpc-listen-all=true".to_owned(),
"disk-cache=32M".to_owned(),
format!("on-download-complete={config_dir_unix}/script/move.sh"),
] {
ensure_contains(
runtime_config_text,
&snippet,
"runtime config missing expected snippet",
)?;
}
ensure_contains(
runtime_config_text,
"bt-tracker=",
"runtime config did not seed bundled bt-tracker snapshot",
)
}
/// Verifies the fake aria2 binary observed the expected argument flow.
fn validate_smoke_local_argv(captured_argv: &[String]) -> Result<()> {
if captured_argv.len() < 3 {
bail!("captured argv is incomplete: {captured_argv:?}");
}
if captured_argv
.first()
.is_none_or(|value| value != "--enable-rpc")
{
bail!("captured argv missing --enable-rpc bootstrap: {captured_argv:?}");
}
if captured_argv
.get(1)
.is_none_or(|value| !value.starts_with("--conf-path="))
{
bail!("captured argv missing conf-path: {captured_argv:?}");
}
if captured_argv
.last()
.is_none_or(|value| value != "--version")
{
bail!("captured argv did not preserve passthrough args: {captured_argv:?}");
}
Ok(())
}
/// POSIX shell script used by the local Docker entrypoint smoke.
const SMOKE_LOCAL_SCRIPT: &str = r#"
set -eu
to_unix_path() {
if command -v cygpath >/dev/null 2>&1; then
cygpath -u "$1"
else
printf '%s\n' "$1"
fi
}
entrypoint=$(to_unix_path "$ENTRYPOINT_WIN")
defaults_dir=$(to_unix_path "$DEFAULTS_DIR_WIN")
config_dir=$(to_unix_path "$CONFIG_DIR_WIN")
downloads_dir=$(to_unix_path "$DOWNLOAD_DIR_WIN")
runtime_dir=$(to_unix_path "$RUNTIME_DIR_WIN")
fake_aria2=$(to_unix_path "$FAKE_ARIA2_WIN")
capture_path=$(to_unix_path "$CAPTURE_PATH_WIN")
chmod +x "$fake_aria2"
CONFIG_DIR="$config_dir" \
DOWNLOAD_DIR="$downloads_dir" \
RUNTIME_DIR="$runtime_dir" \
DEFAULTS_DIR="$defaults_dir" \
ARIA2_BIN="$fake_aria2" \
CAPTURE_PATH="$capture_path" \
RPC_SECRET='smoke-secret' \
RPC_PORT='16800' \
DISK_CACHE='32M' \
LISTEN_PORT='51413' \
IPV6_MODE='true' \
UPDATE_TRACKERS='false' \
CUSTOM_TRACKER_URL='https://example.invalid/trackers.txt' \
SPECIAL_MODE='move' \
UMASK_SET='027' \
"$entrypoint" --version
"#;
/// Drop guard that force-removes a smoke-test container.
struct DockerContainerGuard {
/// Container id returned by `docker run -d`.
container_id: String,
}
impl DockerContainerGuard {
/// Builds a container cleanup guard.
const fn new(container_id: String) -> Self {
Self { container_id }
}
}
impl Drop for DockerContainerGuard {
fn drop(&mut self) {
let _ = Command::new("docker")
.args(["rm", "-f", &self.container_id])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
/// Returns the repository Dockerfile path.
fn dockerfile_path(repo_root: &Path) -> PathBuf {
repo_root.join("docker").join("Dockerfile")
}
/// Returns the repository Docker Compose file path.
fn compose_file_path(repo_root: &Path) -> PathBuf {
repo_root.join("docker").join("docker-compose.yml")
}
/// Builds the Docker image used by export and smoke workflows.
fn build_image(tag: &str, dockerfile: &Path, repo_root: &Path) -> Result<()> {
let envs = if env::var_os("DOCKER_BUILDKIT").is_none() {
// Synology Docker 24 can leave buildx sessions idle on this full release build.
vec![("DOCKER_BUILDKIT", os("0"))]
} else {
Vec::new()
};
run_command_with_env(
"docker",
&[
os("build"),
os("-t"),
os(tag),
os("-f"),
dockerfile.into(),
repo_root.into(),
],
&envs,
None,
)
}
/// Ensures the requested Docker image tag exists locally.
fn assert_image_available(tag: &str) -> Result<()> {
run_command("docker", &[os("image"), os("inspect"), os(tag)]).map_err(|_| {
anyhow!("docker image `{tag}` is not available; build it first or pass --build")
})
}
/// Runs a Docker exec command that is expected to succeed.
fn docker_exec_ok(container_id: &str, tail: &[&str]) -> Result<()> {
let mut args = vec![os("exec"), os(container_id)];
args.extend(tail.iter().map(|value| os(value)));
run_command("docker", &args)
}
/// Runs a command and captures UTF-8 stdout.
fn capture_command_stdout(program: &str, args: &[OsString]) -> Result<String> {
let output = Command::new(program)
.args(args)
.output()
.with_context(|| format!("failed to launch `{program}`"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"`{program}` failed with status {}: {}",
render_exit_status(output.status),
stderr.trim()
);
}
String::from_utf8(output.stdout).context("command emitted non-UTF-8 stdout")
}
/// Runs a command with UTF-8 stdin and captures UTF-8 stdout.
fn capture_command_stdout_with_stdin(
program: &str,
args: &[OsString],
stdin_text: &str,
) -> Result<String> {
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("failed to launch `{program}`"))?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("failed to open stdin for `{program}`"))?;
stdin
.write_all(stdin_text.as_bytes())
.with_context(|| format!("failed to write stdin for `{program}`"))?;
drop(stdin);
let output = child
.wait_with_output()
.with_context(|| format!("failed to wait for `{program}`"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"`{program}` failed with status {}: {}",
render_exit_status(output.status),
stderr.trim()
);
}
String::from_utf8(output.stdout).context("command emitted non-UTF-8 stdout")
}
/// Builds Windows-path environment variables consumed by the local smoke script.
fn smoke_local_envs(
entrypoint: &Path,
defaults_dir: &Path,
config_dir: &Path,
downloads_dir: &Path,
runtime_dir: &Path,
fake_aria2: &Path,
capture_path: &Path,
) -> Vec<(&'static str, OsString)> {
vec![
("ENTRYPOINT_WIN", entrypoint.as_os_str().to_os_string()),
("DEFAULTS_DIR_WIN", defaults_dir.as_os_str().to_os_string()),
("CONFIG_DIR_WIN", config_dir.as_os_str().to_os_string()),
("DOWNLOAD_DIR_WIN", downloads_dir.as_os_str().to_os_string()),
("RUNTIME_DIR_WIN", runtime_dir.as_os_str().to_os_string()),
("FAKE_ARIA2_WIN", fake_aria2.as_os_str().to_os_string()),
("CAPTURE_PATH_WIN", capture_path.as_os_str().to_os_string()),
]
}
/// Captures a workspace path converted for the selected POSIX shell.
fn capture_unix_path(
shell: &Path,
env_name: &'static str,
envs: &[(&str, OsString)],
) -> Result<String> {
let script = format!(
r#"
set -eu
to_unix_path() {{
if command -v cygpath >/dev/null 2>&1; then
cygpath -u "$1"
else
printf '%s\n' "$1"
fi
}}
to_unix_path "${{{env_name}}}"
"#
);
Ok(capture_shell_stdout(shell, &script, envs)?
.trim()
.to_string())
}
/// Runs a POSIX shell script with the provided environment variables.
fn run_shell_script(shell: &Path, script: &str, envs: &[(&str, OsString)]) -> Result<()> {
let mut command = Command::new(shell);
command.arg("-lc").arg(script);
for (key, value) in envs {
command.env(key, value);
}
let status = command
.status()
.with_context(|| format!("failed to launch {}", shell.display()))?;
if status.success() {
Ok(())
} else {
bail!(
"{} failed with status {}",
shell.display(),
render_exit_status(status)
)
}
}
/// Runs a POSIX shell script and captures UTF-8 stdout.
fn capture_shell_stdout(shell: &Path, script: &str, envs: &[(&str, OsString)]) -> Result<String> {
let mut command = Command::new(shell);
command.arg("-lc").arg(script);
for (key, value) in envs {
command.env(key, value);
}
let output = command
.output()
.with_context(|| format!("failed to launch {}", shell.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"{} failed with status {}: {}",
shell.display(),
render_exit_status(output.status),
stderr.trim()
);
}
String::from_utf8(output.stdout).context("shell emitted non-UTF-8 stdout")
}
/// Locates a POSIX shell suitable for the local Docker entrypoint smoke.
fn find_posix_shell() -> Option<PathBuf> {
let names: &[&str] = if cfg!(windows) {
&["bash.exe", "bash", "sh.exe", "sh"]
} else {
&["bash", "sh"]
};
let mut candidates = Vec::new();
if let Some(path_var) = env::var_os("PATH") {
for directory in env::split_paths(&path_var) {
for name in names {
let candidate = directory.join(name);
if candidate.is_file() {
candidates.push(candidate);
}
}
}
}
if cfg!(windows) {
for candidate in [
r"C:\Program Files\Git\bin\bash.exe",
r"C:\Program Files\Git\usr\bin\bash.exe",
r"C:\Program Files\Git\bin\sh.exe",
r"C:\Program Files\Git\usr\bin\sh.exe",
] {
let path = PathBuf::from(candidate);
if path.is_file() {
candidates.push(path);
}
}
}
choose_posix_shell_candidate(&candidates)
}
/// Chooses the best available POSIX shell candidate for local smoke checks.
fn choose_posix_shell_candidate(candidates: &[PathBuf]) -> Option<PathBuf> {
candidates
.iter()
.find(|candidate| !is_windowsapps_bash(candidate))
.cloned()
.or_else(|| candidates.first().cloned())
}
/// Returns whether the shell path resolves to the `WindowsApps` WSL launcher.
fn is_windowsapps_bash(path: &Path) -> bool {
let normalized = path
.to_string_lossy()
.replace('/', "\\")
.to_ascii_lowercase();
normalized.contains("\\windowsapps\\bash.exe")
}
/// Ensures a captured text payload contains an expected snippet.
fn ensure_contains(text: &str, needle: &str, context: &str) -> Result<()> {
if text.contains(needle) {
Ok(())
} else {
bail!("{context}: {needle}");
}
}
/// Writes per-artifact and aggregate SHA-256 checksum files.
fn write_checksum_files(paths: &[PathBuf], output_directory: &Path) -> Result<PathBuf> {
let mut archive_candidates = std::fs::read_dir(output_directory)
.with_context(|| format!("failed to read {}", output_directory.display()))?
.filter_map(std::result::Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_file() && is_archive_path(path))
.collect::<Vec<_>>();
archive_candidates.sort();
for path in paths {
let hash = sha256_hex(path)?;
let file_name = file_name_string_lossy(path)?;
let line = format!("{hash} *{file_name}\n");
write_utf8_text(&PathBuf::from(format!("{}.sha256", path.display())), &line)?;
}
let mut lines = Vec::with_capacity(archive_candidates.len());
for path in archive_candidates {
let hash = sha256_hex(&path)?;
let file_name = file_name_string_lossy(&path)?;
lines.push(format!("{hash} *{file_name}"));
}
let checksum_list_path = output_directory.join("SHA256SUMS.txt");
write_utf8_text(&checksum_list_path, &(lines.join("\n") + "\n"))?;
Ok(checksum_list_path)
}
/// Computes a hex-encoded SHA-256 digest for a file.
fn sha256_hex(path: &Path) -> Result<String> {
let mut file =
File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let mut hasher = Sha256::new();
let mut buffer = Box::new([0_u8; 64 * 1024]);
loop {
let read = file
.read(&mut buffer[..])
.with_context(|| format!("failed to read {}", path.display()))?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
/// Returns whether a path looks like a Docker archive.
fn is_archive_path(path: &Path) -> bool {
let file_name = path
.file_name()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
file_name.ends_with(".zip") || file_name.ends_with(".tar.gz") || file_name.ends_with(".tar")
}
/// Trims surrounding whitespace and returns an owned string.
fn trim_owned(text: &str) -> String {
text.trim().to_owned()
}
/// Redacts generated runtime config lines that may carry local secrets.
fn redact_runtime_config(text: &str) -> String {
text.lines()
.map(|line| {
let trimmed = line.trim_start();
if trimmed.starts_with("rpc-secret=") {
let indent = line.strip_suffix(trimmed).unwrap_or_default();
format!("{indent}rpc-secret=<redacted>")
} else {
line.to_owned()
}
})
.collect::<Vec<_>>()
.join("\n")
}
/// Renders a process exit status for diagnostics.
fn render_exit_status(status: ExitStatus) -> String {
status
.code()
.map_or_else(|| String::from("signal"), |code| code.to_string())
}
/// Returns the trailing file name as UTF-8 text for archive reporting.
fn file_name_string_lossy(path: &Path) -> Result<String> {
path.file_name()
.ok_or_else(|| {
anyhow!(
"archive path did not include a file name: {}",
path.display()
)
})
.map(|name| OsStr::to_string_lossy(name).into_owned())
}
/// Converts a string slice into an owned OS string for command arguments.
fn os(value: &str) -> OsString {
OsString::from(value)
}
#[cfg(test)]
mod tests {
use super::{choose_posix_shell_candidate, redact_runtime_config};
use std::path::PathBuf;
#[test]
fn prefers_git_bash_over_windowsapps_bash_on_windows() {
let candidates = vec![
PathBuf::from(r"C:\Users\example\AppData\Local\Microsoft\WindowsApps\bash.exe"),
PathBuf::from(r"C:\Program Files\Git\bin\bash.exe"),
];
let selected = choose_posix_shell_candidate(&candidates);
assert_eq!(
selected,
Some(PathBuf::from(r"C:\Program Files\Git\bin\bash.exe"))
);
}
#[test]
fn falls_back_to_windowsapps_bash_when_no_better_shell_exists() {
let candidates = vec![PathBuf::from(
r"C:\Users\example\AppData\Local\Microsoft\WindowsApps\bash.exe",
)];
let selected = choose_posix_shell_candidate(&candidates);
assert_eq!(
selected,
Some(PathBuf::from(
r"C:\Users\example\AppData\Local\Microsoft\WindowsApps\bash.exe",
))
);
}
#[test]
fn redacts_runtime_config_rpc_secret_preview() {
let config = "enable-rpc=true\nrpc-secret=super-secret\n rpc-secret=indented\n";
let redacted = redact_runtime_config(config);
assert_eq!(
redacted,
"enable-rpc=true\nrpc-secret=<redacted>\n rpc-secret=<redacted>"
);
}
}
+68
View File
@@ -0,0 +1,68 @@
//! Cargo-native workflow entrypoints for `aria2-rust-pro`.
#![expect(
unreachable_pub,
clippy::indexing_slicing,
clippy::integer_division,
clippy::large_stack_arrays,
clippy::redundant_pub_crate,
clippy::too_many_lines,
reason = "xtask is an internal workflow binary; product-facing runtime crates keep the stricter public-surface and implementation discipline"
)]
/// `clap` command-line model for the cargo-native workflow entrypoint.
mod cli;
/// Compatibility-oriented maintenance workflows.
mod compat;
/// Local Docker export and smoke workflows.
mod docker;
/// Performance collection and same-host comparison workflows.
mod perf;
/// Release packaging and version-smoke workflows.
mod release;
/// Strict local quality-sweep workflows.
mod testing;
/// Shared workspace discovery and command helpers.
mod workspace;
use anyhow::Result;
use clap::Parser;
use crate::cli::{
Cli, CompatSubcommand, DockerSubcommand, PerfSubcommand, ReleaseSubcommand, TestingSubcommand,
TopLevelCommand,
};
/// Runs the `xtask` entrypoint and converts structured failures into a non-zero exit code.
fn main() {
if let Err(error) = run() {
eprintln!("{error:#}");
std::process::exit(1);
}
}
/// Dispatches the requested top-level `xtask` workflow.
fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
TopLevelCommand::Compat(command) => match command.command {
CompatSubcommand::CaptureCliGoldens(args) => compat::run_capture_cli_goldens(&args),
},
TopLevelCommand::Docker(command) => match command.command {
DockerSubcommand::ExportLocal(args) => docker::run_export_local(&args),
DockerSubcommand::Smoke(args) => docker::run_smoke(&args),
DockerSubcommand::SmokeLocal(args) => docker::run_smoke_local(&args),
},
TopLevelCommand::Perf(command) => match command.command {
PerfSubcommand::CollectLocalComparison(args) => {
perf::run_collect_local_comparison(&args)
}
},
TopLevelCommand::Release(command) => match command.command {
ReleaseSubcommand::SmokeVersion(args) => release::run_release_smoke_version(&args),
ReleaseSubcommand::PackageLocal(args) => release::run_package_local(&args),
},
TopLevelCommand::Testing(command) => match command.command {
TestingSubcommand::StrictSweep(args) => testing::run_strict_sweep(&args),
},
}
}
+1622
View File
File diff suppressed because it is too large Load Diff
+610
View File
@@ -0,0 +1,610 @@
use std::{
ffi::{OsStr, OsString},
fs::File,
io::{BufWriter, Read},
path::{Path, PathBuf},
process::ExitStatus,
};
use anyhow::{Context, Result, anyhow, bail};
use serde::Serialize;
use sha2::{Digest, Sha256};
use tempfile::TempDir;
use walkdir::WalkDir;
use zip::write::SimpleFileOptions;
use crate::{
cli::{PackageLocalArgs, ReleaseSmokeVersionArgs},
workspace::{
archive_extension_for_target, default_binary_path, host_triple, load_workspace_metadata,
root_manifest_path, run_command, utc_now_rfc3339, workspace_relative_or_external,
write_utf8_text,
},
};
#[derive(Debug, Serialize)]
/// Summary printed after validating a release binary version.
struct ReleaseSmokeSummary {
#[serde(rename = "BinaryPath")]
/// Binary that was executed for the smoke check.
binary_path: String,
#[serde(rename = "HostTriple")]
/// Build host target triple.
host_triple: String,
#[serde(rename = "TargetTriple")]
/// Release artifact target triple.
target_triple: String,
#[serde(rename = "Version")]
/// Expected package version.
version: String,
#[serde(rename = "VersionOutput")]
/// Captured `--version` output.
version_output: String,
}
#[derive(Debug, Serialize)]
/// Machine-readable manifest written beside a release archive.
struct ReleaseArchiveManifest {
/// Package version in the archive.
version: String,
/// Build host target triple.
host_triple: String,
/// Release artifact target triple.
target_triple: String,
/// Packaged archive metadata.
archive: ManifestArtifact,
/// Source binary metadata.
binary: ManifestArtifact,
/// Path to the aggregate checksum list.
checksum_list: String,
/// Version smoke result included in the package manifest.
version_smoke: VersionSmokeManifest,
/// Files included in the staged archive.
included_files: Vec<IncludedFile>,
/// UTC generation timestamp for the manifest.
generated_at_utc: String,
}
#[derive(Debug, Serialize)]
/// File artifact metadata embedded in release manifests.
struct ManifestArtifact {
/// Artifact file name.
file_name: String,
/// Full artifact path.
path: String,
/// Hex-encoded SHA-256 digest.
sha256: String,
}
#[derive(Debug, Serialize)]
/// Captured release version-smoke output.
struct VersionSmokeManifest {
/// Whether the smoke check was intentionally skipped.
skipped: bool,
/// Captured output lines from the smoke check.
output: Vec<String>,
}
#[derive(Debug, Serialize)]
/// File included in the staged release archive.
struct IncludedFile {
#[serde(rename = "RelativePath")]
/// Archive-relative file path.
relative_path: String,
#[serde(rename = "Size")]
/// File size in bytes.
size: u64,
}
#[derive(Debug, Serialize)]
/// Summary printed after creating a local release package.
struct PackageSummary {
#[serde(rename = "Version")]
/// Package version in the archive.
version: String,
#[serde(rename = "HostTriple")]
/// Build host target triple.
host_triple: String,
#[serde(rename = "TargetTriple")]
/// Release artifact target triple.
target_triple: String,
#[serde(rename = "SourceBinary")]
/// Binary copied into the archive.
source_binary: String,
#[serde(rename = "ArchivePath")]
/// Path to the generated archive.
archive_path: String,
#[serde(rename = "ManifestPath")]
/// Path to the generated manifest.
manifest_path: String,
#[serde(rename = "ChecksumList")]
/// Path to the aggregate checksum list.
checksum_list: String,
}
/// Runs the release binary version smoke workflow.
pub fn run_release_smoke_version(args: &ReleaseSmokeVersionArgs) -> Result<()> {
let metadata = load_workspace_metadata()?;
let host_triple = host_triple()?;
let target_triple = args
.target_triple
.clone()
.unwrap_or_else(|| host_triple.clone());
if args.build {
invoke_release_build(&target_triple, &host_triple)?;
}
let binary_path = args
.binary_path
.clone()
.unwrap_or_else(|| default_binary_path(&metadata, &target_triple, &host_triple));
let version_output = invoke_version_smoke(&binary_path, &metadata.package_version)?;
let binary_path_text = workspace_relative_or_external(&binary_path);
let summary = ReleaseSmokeSummary {
binary_path: binary_path_text,
host_triple,
target_triple,
version: metadata.package_version,
version_output: version_output.join("\n"),
};
println!(
"{}",
serde_json::to_string_pretty(&summary)
.context("failed to serialize release smoke summary")?
);
Ok(())
}
/// Builds a local release archive and manifest from a compiled binary.
pub fn run_package_local(args: &PackageLocalArgs) -> Result<()> {
let metadata = load_workspace_metadata()?;
let host_triple = host_triple()?;
let target_triple = args
.target_triple
.clone()
.unwrap_or_else(|| host_triple.clone());
let output_root = args
.output_root
.clone()
.unwrap_or_else(|| metadata.workspace_root.join("dist").join("release"));
if args.build {
invoke_release_build(&target_triple, &host_triple)?;
}
let source_binary = args
.source_binary
.clone()
.unwrap_or_else(|| default_binary_path(&metadata, &target_triple, &host_triple));
ensure_file_exists(&source_binary, "release packaging could not find binary")?;
let artifact_base_name = format!(
"aria2-rust-pro-v{}-{}",
metadata.package_version, target_triple
);
let archive_extension = archive_extension_for_target(&target_triple);
let version_dir = output_root.join(format!("v{}", metadata.package_version));
std::fs::create_dir_all(&version_dir)
.with_context(|| format!("failed to create {}", version_dir.display()))?;
let version_output = if args.skip_version_smoke {
Vec::new()
} else {
invoke_version_smoke(&source_binary, &metadata.package_version)?
};
let stage = StageDirectory::new(&artifact_base_name)?;
let binary_name = file_name_os_string(&source_binary)?;
std::fs::copy(&source_binary, stage.stage_root.join(&binary_name))
.with_context(|| format!("failed to stage {}", source_binary.display()))?;
if target_triple.contains("windows")
&& let Some(pdb_path) = find_windows_pdb_path(&source_binary)
{
let pdb_name = file_name_os_string(&pdb_path)?;
std::fs::copy(&pdb_path, stage.stage_root.join(&pdb_name))
.with_context(|| format!("failed to stage {}", pdb_path.display()))?;
}
add_optional_file(
&metadata.workspace_root.join("README.md"),
&stage.stage_root,
Path::new("README.md"),
)?;
add_optional_file(
&metadata
.workspace_root
.join("docs")
.join("release")
.join("README.md"),
&stage.stage_root,
Path::new("docs")
.join("release")
.join("README.md")
.as_path(),
)?;
let archive_path = version_dir.join(format!("{artifact_base_name}{archive_extension}"));
if archive_extension == ".zip" {
create_zip_archive(&stage.stage_root, &archive_path)?;
} else {
create_tar_gz_archive(&stage.stage_root, &archive_path)?;
}
let checksum_list = write_checksum_files(std::slice::from_ref(&archive_path), &version_dir)?;
let binary_hash = sha256_hex(&source_binary)?;
let archive_hash = sha256_hex(&archive_path)?;
let manifest_path = version_dir.join(format!("{artifact_base_name}.manifest.json"));
let included_files = collect_included_files(&stage.stage_root)?;
let archive_path_text = workspace_relative_or_external(&archive_path);
let source_binary_text = workspace_relative_or_external(&source_binary);
let checksum_list_text = workspace_relative_or_external(&checksum_list);
let manifest_path_text = workspace_relative_or_external(&manifest_path);
let manifest = ReleaseArchiveManifest {
version: metadata.package_version.clone(),
host_triple: host_triple.clone(),
target_triple: target_triple.clone(),
archive: ManifestArtifact {
file_name: file_name_string(&archive_path)?,
path: archive_path_text.clone(),
sha256: archive_hash,
},
binary: ManifestArtifact {
file_name: file_name_string(&source_binary)?,
path: source_binary_text.clone(),
sha256: binary_hash,
},
checksum_list: checksum_list_text.clone(),
version_smoke: VersionSmokeManifest {
skipped: args.skip_version_smoke,
output: version_output,
},
included_files,
generated_at_utc: utc_now_rfc3339()?,
};
let manifest_text =
serde_json::to_string_pretty(&manifest).context("failed to serialize release manifest")?;
write_utf8_text(&manifest_path, &(manifest_text + "\n"))?;
let summary = PackageSummary {
version: metadata.package_version,
host_triple,
target_triple,
source_binary: source_binary_text,
archive_path: archive_path_text,
manifest_path: manifest_path_text,
checksum_list: checksum_list_text,
};
println!(
"{}",
serde_json::to_string_pretty(&summary)
.context("failed to serialize release package summary")?
);
Ok(())
}
/// Executes a binary and validates its aria2-compatible version banner.
fn invoke_version_smoke(binary_path: &Path, expected_version: &str) -> Result<Vec<String>> {
ensure_file_exists(binary_path, "version smoke could not find binary")?;
let output = std::process::Command::new(binary_path)
.arg("--version")
.output()
.with_context(|| format!("failed to launch {}", binary_path.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"version smoke failed with status {}: {}",
render_exit_status(output.status),
stderr.trim()
);
}
let stdout = String::from_utf8(output.stdout)
.with_context(|| format!("{} emitted non-UTF-8 stdout", binary_path.display()))?;
let lines = stdout.lines().map(str::to_owned).collect::<Vec<_>>();
let first_line = lines
.first()
.ok_or_else(|| anyhow!("version smoke output was empty"))?;
let expected_compat_line = "aria2 version 1.37.0";
if !first_line.starts_with(expected_compat_line) {
bail!("version smoke output did not start with `{expected_compat_line}`: {first_line}");
}
let expected_package_line = format!("Rust rewrite package: aria2-rust-pro {expected_version}");
let package_line = lines
.iter()
.find(|line| line.starts_with("Rust rewrite package:"))
.ok_or_else(|| {
anyhow!("version smoke output did not include a Rust rewrite package line")
})?;
if package_line != &expected_package_line {
bail!("version smoke package line did not match `{expected_package_line}`: {package_line}");
}
Ok(lines)
}
/// Invokes the release build for the requested target triple.
fn invoke_release_build(target_triple: &str, host_triple: &str) -> Result<()> {
let manifest_path = root_manifest_path();
let mut args = vec![
os("+nightly"),
os("build"),
os("-Zbuild-std=std,panic_abort"),
os("--manifest-path"),
manifest_path.into_os_string(),
os("-p"),
os("aria2-rust-pro-cli"),
os("--bin"),
os("aria2-rust-pro"),
os("--release"),
];
if target_triple != host_triple {
args.push(os("--target"));
args.push(os(target_triple));
}
run_command("cargo", &args)
}
/// Temporary release staging directory.
struct StageDirectory {
/// Temporary directory whose lifetime keeps the staging tree alive.
_temp_dir: TempDir,
/// Root directory containing staged archive contents.
stage_root: PathBuf,
}
impl StageDirectory {
/// Creates a new temporary staging directory for one artifact.
fn new(artifact_base_name: &str) -> Result<Self> {
let temp_dir =
TempDir::new().context("failed to create temporary release staging directory")?;
let stage_root = temp_dir.path().join(artifact_base_name);
std::fs::create_dir_all(&stage_root)
.with_context(|| format!("failed to create {}", stage_root.display()))?;
Ok(Self {
_temp_dir: temp_dir,
stage_root,
})
}
}
/// Ensures an expected release input file exists.
fn ensure_file_exists(path: &Path, label: &str) -> Result<()> {
if path.is_file() {
Ok(())
} else {
bail!("{label}: {}", path.display())
}
}
/// Finds the optional Windows PDB companion for a release binary.
fn find_windows_pdb_path(binary_path: &Path) -> Option<PathBuf> {
let mut candidates = Vec::new();
let mut direct = binary_path.to_path_buf();
direct.set_extension("pdb");
candidates.push(direct);
let alt = binary_path
.parent()
.map(|parent| parent.join("aria2_rust_pro.pdb"));
if let Some(path) = alt {
candidates.push(path);
}
candidates.into_iter().find(|candidate| candidate.is_file())
}
/// Copies an optional repository file into the staging tree.
fn add_optional_file(
source_path: &Path,
destination_root: &Path,
relative_path: &Path,
) -> Result<bool> {
if !source_path.is_file() {
return Ok(false);
}
let destination_path = destination_root.join(relative_path);
if let Some(parent) = destination_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::copy(source_path, &destination_path).with_context(|| {
format!(
"failed to copy {} to {}",
source_path.display(),
destination_path.display()
)
})?;
Ok(true)
}
/// Creates a zip archive from the staged release tree.
fn create_zip_archive(stage_root: &Path, archive_path: &Path) -> Result<()> {
if archive_path.exists() {
std::fs::remove_file(archive_path)
.with_context(|| format!("failed to remove {}", archive_path.display()))?;
}
let archive_root = file_name_string(stage_root)?;
let file = File::create(archive_path)
.with_context(|| format!("failed to create {}", archive_path.display()))?;
let writer = BufWriter::new(file);
let mut zip = zip::ZipWriter::new(writer);
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for entry in WalkDir::new(stage_root).sort_by_file_name() {
let entry = entry.with_context(|| format!("failed to walk {}", stage_root.display()))?;
let path = entry.path();
let relative = path.strip_prefix(stage_root).with_context(|| {
format!(
"failed to strip {} from {}",
stage_root.display(),
path.display()
)
})?;
let archive_name = if relative.as_os_str().is_empty() {
archive_root.clone()
} else {
format!(
"{archive_root}/{}",
relative.to_string_lossy().replace('\\', "/")
)
};
if entry.file_type().is_dir() {
if !relative.as_os_str().is_empty() {
zip.add_directory(format!("{archive_name}/"), options)
.with_context(|| {
format!("failed to add directory {} to zip", path.display())
})?;
}
continue;
}
zip.start_file(&archive_name, options)
.with_context(|| format!("failed to add file {} to zip", path.display()))?;
let mut input =
File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
std::io::copy(&mut input, &mut zip)
.with_context(|| format!("failed to copy {} into zip", path.display()))?;
}
zip.finish().context("failed to finalize zip archive")?;
Ok(())
}
/// Creates a gzip-compressed tar archive from the staged release tree.
fn create_tar_gz_archive(stage_root: &Path, archive_path: &Path) -> Result<()> {
if archive_path.exists() {
std::fs::remove_file(archive_path)
.with_context(|| format!("failed to remove {}", archive_path.display()))?;
}
let file = File::create(archive_path)
.with_context(|| format!("failed to create {}", archive_path.display()))?;
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut builder = tar::Builder::new(encoder);
let archive_root = stage_root.file_name().ok_or_else(|| {
anyhow!(
"stage root did not include a directory name: {}",
stage_root.display()
)
})?;
builder
.append_dir_all(archive_root, stage_root)
.with_context(|| format!("failed to package {}", stage_root.display()))?;
builder
.finish()
.context("failed to finalize tar.gz archive")?;
Ok(())
}
/// Writes per-artifact and aggregate SHA-256 checksum files.
fn write_checksum_files(paths: &[PathBuf], output_directory: &Path) -> Result<PathBuf> {
let mut archive_candidates = std::fs::read_dir(output_directory)
.with_context(|| format!("failed to read {}", output_directory.display()))?
.filter_map(std::result::Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_file() && is_archive_path(path))
.collect::<Vec<_>>();
archive_candidates.sort();
for path in paths {
let hash = sha256_hex(path)?;
let file_name = file_name_string(path)?;
let line = format!("{hash} *{file_name}\n");
write_utf8_text(&PathBuf::from(format!("{}.sha256", path.display())), &line)?;
}
let mut lines = Vec::with_capacity(archive_candidates.len());
for path in archive_candidates {
let hash = sha256_hex(&path)?;
let file_name = file_name_string(&path)?;
lines.push(format!("{hash} *{file_name}"));
}
let checksum_list_path = output_directory.join("SHA256SUMS.txt");
write_utf8_text(&checksum_list_path, &(lines.join("\n") + "\n"))?;
Ok(checksum_list_path)
}
/// Computes a hex-encoded SHA-256 digest for a file.
fn sha256_hex(path: &Path) -> Result<String> {
let mut file =
File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let mut hasher = Sha256::new();
let mut buffer = Box::new([0_u8; 64 * 1024]);
loop {
let read = file
.read(&mut buffer[..])
.with_context(|| format!("failed to read {}", path.display()))?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
/// Collects files included in the staged release tree.
fn collect_included_files(stage_root: &Path) -> Result<Vec<IncludedFile>> {
WalkDir::new(stage_root)
.sort_by_file_name()
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_file())
.map(|entry| {
let relative = entry.path().strip_prefix(stage_root).with_context(|| {
format!(
"failed to strip {} from {}",
stage_root.display(),
entry.path().display()
)
})?;
let metadata = entry.metadata().with_context(|| {
format!("failed to read metadata for {}", entry.path().display())
})?;
Ok(IncludedFile {
relative_path: relative.to_string_lossy().replace('\\', "/"),
size: metadata.len(),
})
})
.collect()
}
/// Returns the trailing file name as UTF-8 text for manifest entries.
fn file_name_string(path: &Path) -> Result<String> {
path.file_name()
.ok_or_else(|| anyhow!("path did not include a file name: {}", path.display()))
.map(|name| OsStr::to_string_lossy(name).into_owned())
}
/// Returns the trailing file name as an owned OS string.
fn file_name_os_string(path: &Path) -> Result<OsString> {
path.file_name()
.ok_or_else(|| anyhow!("path did not include a file name: {}", path.display()))
.map(OsStr::to_os_string)
}
/// Converts a string slice into an owned OS string for command arguments.
fn os(value: &str) -> OsString {
OsString::from(value)
}
/// Renders a process exit status for release diagnostics.
fn render_exit_status(status: ExitStatus) -> String {
status
.code()
.map_or_else(|| String::from("signal"), |code| code.to_string())
}
/// Returns whether a path looks like a release archive.
fn is_archive_path(path: &Path) -> bool {
let file_name = path
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_default();
file_name.ends_with(".zip") || file_name.ends_with(".tar.gz") || file_name.ends_with(".tar")
}
+172
View File
@@ -0,0 +1,172 @@
use std::{
ffi::OsString,
path::{Path, PathBuf},
};
use anyhow::Result;
use crate::{
cli::{ReleaseSmokeVersionArgs, StrictSweepArgs},
release::run_release_smoke_version,
workspace::{repo_root, root_manifest_path, run_command_with_env},
};
/// Runs the local strict quality sweep used before release or handoff.
pub fn run_strict_sweep(args: &StrictSweepArgs) -> Result<()> {
let repo_root = repo_root();
let manifest_path = root_manifest_path();
let deny_config_path = repo_root.join("deny.toml");
let commands = vec![
SweepCommand::new(
"fmt",
Some(repo_root.join("target-gate-fmt")),
vec![
os("fmt"),
os("--all"),
os("--check"),
os("--manifest-path"),
path_arg(&manifest_path),
],
),
SweepCommand::new(
"check",
Some(repo_root.join("target-gate-check")),
vec![
os("check"),
os("--workspace"),
os("--all-targets"),
os("--all-features"),
os("--locked"),
os("--manifest-path"),
path_arg(&manifest_path),
],
),
SweepCommand::new(
"nextest",
Some(repo_root.join("target-gate-nextest")),
vec![
os("nextest"),
os("run"),
os("--workspace"),
os("--all-targets"),
os("--all-features"),
os("--locked"),
os("--manifest-path"),
path_arg(&manifest_path),
],
),
SweepCommand::new(
"clippy",
Some(repo_root.join("target-gate-clippy")),
vec![
os("clippy"),
os("--workspace"),
os("--all-targets"),
os("--all-features"),
os("--locked"),
os("--no-deps"),
os("--manifest-path"),
path_arg(&manifest_path),
os("--"),
os("-D"),
os("warnings"),
os("-D"),
os("clippy::pedantic"),
os("-D"),
os("clippy::nursery"),
],
),
SweepCommand::new(
"udeps",
Some(repo_root.join("target-gate-udeps")),
vec![
os("+nightly"),
os("udeps"),
os("--workspace"),
os("--all-targets"),
os("--all-features"),
os("--locked"),
os("--manifest-path"),
path_arg(&manifest_path),
],
),
SweepCommand::new(
"deny",
None,
vec![
os("deny"),
os("--manifest-path"),
path_arg(&manifest_path),
os("--locked"),
os("check"),
os("--config"),
path_arg(&deny_config_path),
],
),
];
for command in commands {
println!("==> {}", command.name);
let envs = build_envs(command.target_dir.as_deref(), command.name == "deny");
run_command_with_env("cargo", &command.args, &envs, Some(&repo_root))?;
}
if args.include_release_smoke {
println!("==> release-smoke");
run_release_smoke_version(&ReleaseSmokeVersionArgs {
build: true,
target_triple: None,
binary_path: None,
})?;
}
Ok(())
}
#[derive(Debug)]
/// A cargo command executed as part of the strict sweep.
struct SweepCommand {
/// Short label printed before executing the command.
name: &'static str,
/// Optional target directory isolating build artifacts.
target_dir: Option<PathBuf>,
/// Arguments passed to cargo.
args: Vec<OsString>,
}
impl SweepCommand {
/// Builds a sweep command descriptor.
const fn new(name: &'static str, target_dir: Option<PathBuf>, args: Vec<OsString>) -> Self {
Self {
name,
target_dir,
args,
}
}
}
/// Builds environment overrides for an individual sweep command.
fn build_envs(target_dir: Option<&Path>, cargo_deny: bool) -> Vec<(&'static str, OsString)> {
let mut envs = vec![("CARGO_INCREMENTAL", os("0"))];
if let Some(target_dir) = target_dir {
envs.push(("CARGO_TARGET_DIR", target_dir.as_os_str().to_os_string()));
}
if cargo_deny {
envs.extend([
("GIT_CONFIG_COUNT", os("1")),
("GIT_CONFIG_KEY_0", os("http.sslbackend")),
("GIT_CONFIG_VALUE_0", os("openssl")),
]);
}
envs
}
/// Converts a string literal into an owned OS argument.
fn os(value: &str) -> OsString {
OsString::from(value)
}
/// Converts the root manifest path into a fresh cargo argument.
fn path_arg(path: &Path) -> OsString {
path.as_os_str().to_os_string()
}
+267
View File
@@ -0,0 +1,267 @@
use std::{
ffi::{OsStr, OsString},
path::{Path, PathBuf},
process::{Command, ExitStatus},
};
use anyhow::{Context, Result, anyhow, bail};
use cargo_metadata::MetadataCommand;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
#[derive(Debug, Clone)]
/// Shared workspace metadata discovered from `cargo metadata`.
pub struct WorkspaceMetadata {
/// Absolute repository root of the workspace.
pub workspace_root: PathBuf,
/// Resolved Cargo target directory for the workspace.
pub target_directory: PathBuf,
/// Current package version of the CLI package.
pub package_version: String,
/// Canonical binary name produced by the workspace.
pub binary_name: String,
}
/// Returns the repository root that contains the `xtask` crate.
pub fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.map(Path::to_path_buf)
.expect("xtask should live directly under the workspace root")
}
/// Returns the absolute path to the workspace root manifest.
pub fn root_manifest_path() -> PathBuf {
repo_root().join("Cargo.toml")
}
/// Renders a path for public manifests without exposing the local workspace.
pub fn workspace_relative_or_external(path: &Path) -> String {
match path.strip_prefix(repo_root()) {
Ok(relative) => relative.to_string_lossy().replace('\\', "/"),
Err(_) => path
.file_name()
.map(|name| format!("<external>/{}", name.to_string_lossy()))
.unwrap_or_else(|| "<external>".to_owned()),
}
}
#[cfg(test)]
mod tests {
use super::{Path, repo_root, workspace_relative_or_external};
#[test]
fn renders_workspace_paths_relative_and_external_paths_anonymous() {
assert_eq!(
workspace_relative_or_external(&repo_root().join("Cargo.toml")),
"Cargo.toml"
);
assert_eq!(
workspace_relative_or_external(Path::new("outside.bin")),
"<external>/outside.bin"
);
}
}
/// Loads shared workspace metadata used by multiple `xtask` workflows.
pub fn load_workspace_metadata() -> Result<WorkspaceMetadata> {
let manifest_path = root_manifest_path();
let metadata = MetadataCommand::new()
.manifest_path(&manifest_path)
.no_deps()
.exec()
.with_context(|| {
format!(
"failed to run cargo metadata for {}",
manifest_path.display()
)
})?;
let cli_package = metadata
.packages
.iter()
.find(|package| package.name == "aria2-rust-pro-cli")
.ok_or_else(|| anyhow!("cargo metadata did not return aria2-rust-pro-cli"))?;
Ok(WorkspaceMetadata {
workspace_root: metadata.workspace_root.into_std_path_buf(),
target_directory: metadata.target_directory.into_std_path_buf(),
package_version: cli_package.version.to_string(),
binary_name: String::from("aria2-rust-pro"),
})
}
/// Returns the Rust host target triple reported by the current toolchain.
pub fn host_triple() -> Result<String> {
let output = Command::new("rustc")
.arg("-vV")
.output()
.context("failed to launch rustc -vV")?;
if !output.status.success() {
bail!(
"rustc -vV failed with status {}",
render_exit_status(output.status)
);
}
let stdout = String::from_utf8(output.stdout).context("rustc -vV emitted non-UTF-8 stdout")?;
stdout
.lines()
.find_map(|line| line.strip_prefix("host:").map(str::trim))
.map(str::to_owned)
.ok_or_else(|| anyhow!("rustc -vV did not report a host triple"))
}
/// Returns the binary filename for a target triple, including `.exe` on Windows.
pub fn binary_name_for_target(binary_name: &str, target_triple: &str) -> String {
if target_triple.contains("windows") {
format!("{binary_name}.exe")
} else {
String::from(binary_name)
}
}
/// Returns the release archive extension for a target triple.
pub fn archive_extension_for_target(target_triple: &str) -> &'static str {
if target_triple.contains("windows") {
".zip"
} else {
".tar.gz"
}
}
/// Returns the default release binary path for a target triple.
pub fn default_binary_path(
metadata: &WorkspaceMetadata,
target_triple: &str,
host_triple: &str,
) -> PathBuf {
let binary_name = binary_name_for_target(&metadata.binary_name, target_triple);
if target_triple == host_triple {
metadata.target_directory.join("release").join(binary_name)
} else {
metadata
.target_directory
.join(target_triple)
.join("release")
.join(binary_name)
}
}
/// Runs a command and returns an error with a rendered command line when it fails.
pub fn run_command(program: &str, args: &[OsString]) -> Result<()> {
let rendered = render_command(program, args);
let status = Command::new(program)
.args(args)
.status()
.with_context(|| format!("failed to launch `{rendered}`"))?;
if status.success() {
Ok(())
} else {
bail!(
"`{rendered}` failed with status {}",
render_exit_status(status)
)
}
}
/// Runs a command with additional environment variables and reports contextual failures.
pub fn run_command_with_env(
program: &str,
args: &[OsString],
envs: &[(&str, OsString)],
working_dir: Option<&Path>,
) -> Result<()> {
let rendered = render_command(program, args);
let mut command = Command::new(program);
command.args(args);
if let Some(working_dir) = working_dir {
command.current_dir(working_dir);
}
for (key, value) in envs {
command.env(key, value);
}
let status = command
.status()
.with_context(|| format!("failed to launch `{rendered}`"))?;
if status.success() {
Ok(())
} else {
bail!(
"`{rendered}` failed with status {}",
render_exit_status(status)
)
}
}
/// Captures UTF-8 stdout lines from a process that is expected to succeed.
pub fn capture_stdout_lines(program: &Path, args: &[&str]) -> Result<Vec<String>> {
let output = Command::new(program)
.args(args)
.output()
.with_context(|| format!("failed to launch {}", program.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"{} failed with status {}: {}",
program.display(),
render_exit_status(output.status),
stderr.trim()
);
}
let stdout = String::from_utf8(output.stdout)
.with_context(|| format!("{} emitted non-UTF-8 stdout", program.display()))?;
Ok(stdout.lines().map(str::to_owned).collect())
}
/// Writes newline-delimited UTF-8 text lines to a file, creating parent directories first.
pub fn write_utf8_lines(path: &Path, lines: &[String]) -> Result<()> {
let mut text = lines.join("\n");
text.push('\n');
write_utf8_text(path, &text)
}
/// Writes UTF-8 text to a file, creating parent directories first.
pub fn write_utf8_text(path: &Path, text: &str) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::write(path, text).with_context(|| format!("failed to write {}", path.display()))
}
/// Returns the current UTC timestamp formatted as `RFC 3339`.
pub fn utc_now_rfc3339() -> Result<String> {
OffsetDateTime::now_utc()
.format(&Rfc3339)
.context("failed to format current UTC timestamp")
}
/// Renders a shell-style command preview for error messages.
fn render_command(program: &str, args: &[OsString]) -> String {
let rendered_args = args
.iter()
.map(|arg| quote_os(arg.as_os_str()))
.collect::<Vec<_>>()
.join(" ");
if rendered_args.is_empty() {
String::from(program)
} else {
format!("{program} {rendered_args}")
}
}
/// Quotes an OS string for readable command rendering when it contains spaces.
fn quote_os(value: &OsStr) -> String {
let text = value.to_string_lossy();
if text.contains(' ') {
format!("\"{text}\"")
} else {
text.into_owned()
}
}
/// Renders a process exit status for workspace command failures.
fn render_exit_status(status: ExitStatus) -> String {
status
.code()
.map_or_else(|| "signal".to_owned(), |code| code.to_string())
}