use {
crate::{
archive::{Archive, ArchiveArgs, OptionalArchiveArgs, SnapshotId, SnapshotIdArgParser},
content::ContentProvider,
packages::{InstallPriority, Package},
version::{Constraint, Dependency, Version},
StagingFileSystem,
},
anyhow::Result,
indicatif::ProgressBar,
std::{
ffi::OsString,
io::{self, Write},
num::NonZero,
path::{Component, Path},
str::FromStr,
sync::{
atomic::{AtomicU64, AtomicU8},
Arc,
},
},
};
pub trait Config {
type FS: StagingFileSystem;
type Cache: ContentProvider<Target = Self::FS>;
fn log_level(&self) -> i32 {
0
}
fn arch(&self) -> &str;
fn manifest(&self) -> &Path;
fn concurrency(&self) -> NonZero<usize>;
fn fetcher(&self) -> io::Result<&Self::Cache>;
}
pub trait Command<C> {
fn exec(&self, conf: &C) -> Result<()>;
}
#[derive(Clone)]
pub enum StageProgress {
Indicatif(ProgressBar),
Percent(PercentProgress),
}
impl StageProgress {
pub fn from_progress_bar(pb: ProgressBar) -> Self {
Self::Indicatif(pb)
}
pub fn percent(total: u64) -> Self {
Self::Percent(PercentProgress::new(total))
}
pub(crate) fn inc(&self, delta: u64) {
match self {
StageProgress::Indicatif(pb) => pb.inc(delta),
StageProgress::Percent(progress) => progress.inc(delta),
}
}
pub(crate) fn finish(&self) {
match self {
StageProgress::Indicatif(pb) => pb.finish_using_style(),
StageProgress::Percent(progress) => progress.finish(),
}
}
}
pub struct PercentProgress {
inner: Arc<PercentProgressInner>,
}
impl Clone for PercentProgress {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
struct PercentProgressInner {
total: u64,
current: AtomicU64,
last: AtomicU8,
}
impl PercentProgress {
fn new(total: u64) -> Self {
{
use std::io::Write as _;
let mut stdout = std::io::stdout();
let _ = stdout.write_all(b"... ");
let _ = stdout.flush();
}
Self {
inner: Arc::new(PercentProgressInner {
total,
current: AtomicU64::new(0),
last: AtomicU8::new(0),
}),
}
}
fn inc(&self, delta: u64) {
use std::sync::atomic::Ordering;
let current = self
.inner
.current
.fetch_add(delta, Ordering::Relaxed)
.saturating_add(delta);
self.print_thresholds(self.percent(current));
}
fn finish(&self) {
self.print_thresholds(100);
}
fn percent(&self, current: u64) -> u8 {
current
.saturating_mul(100)
.checked_div(self.inner.total)
.unwrap_or(100)
.min(100) as u8
}
fn print_thresholds(&self, percent: u8) {
use std::sync::atomic::Ordering;
let last = self.inner.last.load(Ordering::Relaxed);
let next = last + 10;
if percent > next
&& self
.inner
.last
.compare_exchange(last, next, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
use std::io::Write as _;
let mut stdout = std::io::stdout();
let _ = writeln!(stdout, "{}%", next);
if next != 100 {
let _ = write!(stdout, "... ");
}
let _ = stdout.flush();
}
}
}
pub mod cmd {
pub use crate::cli_edit::Edit;
use {
super::*,
crate::{
archive::SignedBy,
artifact::{hash_directory, ArtifactArg},
builder::ExecutorKind,
comp::is_comp_ext,
content::{ContentProviderGuard, HostCache},
git::{parse_import_target, ImportTarget},
hash::{Hash, HashAlgo},
manifest::Manifest,
staging::HostFileSystem,
version::ProvidedName,
},
anyhow::{anyhow, Result},
clap::Parser,
indicatif::ProgressBar,
itertools::Itertools,
rustix::path::Arg,
std::{
io::IsTerminal,
path::{Path, PathBuf},
},
};
#[derive(Parser)]
#[command(
about = "Create a new manifest file",
long_about = r#"Create a new manifest file from an archive definition, vendor preset, or imported manifest.
If a vendor name is provided as the archive URL, default archives and packages are derived from it.
Use --import to bootstrap from another already-locked manifest. The target can be a local
path or a `git+<scheme>://...` URL referencing a manifest hosted in a git repository.
Accepted --import shapes:
path/to/manifest.toml
file:///absolute/path/manifest.toml
git+ssh://[git@]host/repo[.git]?rev=<sha>|ref=<name>#path/to/manifest.toml
git+https://host/repo[.git]?rev=<sha>|ref=<name>#path/to/manifest.toml
git+http://host/repo[.git]?rev=<sha>|ref=<name>#path/to/manifest.toml
git+file:///absolute/path/to/repo?rev=<sha>|ref=<name>#path/to/manifest.toml
Examples:
rdebootstrap init debian --package mc --package libcom-err2
rdebootstrap init --import ../base/Manifest.toml --spec base
rdebootstrap init --import 'git+https://gitlab.com/org/sys.git?rev=v1.2.3#sys/Manifest.toml' --spec base"#
)]
pub struct Init {
#[arg(long)]
pub force: bool,
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(short = 'r', long = "package", value_name = "PACKAGE")]
requirements: Vec<String>,
#[arg(
long = "meta",
value_names = ["NAME", "VALUE"],
num_args = 2,
action = clap::ArgAction::Append
)]
meta: Vec<String>,
#[arg(
long = "import",
value_name = "PATH-OR-URL",
conflicts_with_all = [
"url",
"arch",
"allow_insecure",
"signed_by",
"snapshots",
"snapshot",
"suites",
"components",
"hash",
"priority",
]
)]
import: Option<String>,
#[arg(
long = "spec",
value_name = "SPEC",
value_delimiter = ',',
requires = "import"
)]
specs: Vec<String>,
#[command(flatten)]
archive: OptionalArchiveArgs,
#[arg(
long = "no-verify",
display_order = 0,
action,
conflicts_with = "import"
)]
insecure_release: bool,
}
impl<C: Config> Command<C> for Init {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let mut mf = if let Some(target_str) = self.import.as_deref() {
let target = parse_import_target(target_str)?;
let fetcher = conf.fetcher()?;
let mut mf =
Manifest::new(conf.manifest(), conf.arch(), self.comment.as_deref());
match target {
ImportTarget::LocalPath(path) => {
let path = rebase_pathbuf_input(&path, conf.manifest())?;
mf.set_import(&path, &self.specs, fetcher).await?;
}
ImportTarget::Git(arg) => {
mf.set_import_git(
fetcher,
arg.repo.remote,
arg.repo.revspec,
&arg.path,
&self.specs,
)
.await?;
}
}
mf
} else {
let mut archive = self.archive.to_archive().ok_or_else(|| {
anyhow!(
"either an archive/vendor template or --import PATH-OR-URL is required"
)
})?;
archive.signed_by =
rebase_signed_by_input(&archive.signed_by, conf.manifest())?;
let (archives, packages, comment) =
if let Some((archives, mut packages)) = archive.as_vendor() {
if !self.requirements.is_empty() {
packages.extend(self.requirements.iter().cloned());
packages = packages.into_iter().unique().collect();
}
(
archives,
packages,
Some(format!("default manifest file for {}", &archive.url)),
)
} else {
(vec![archive], self.requirements.clone(), None)
};
for a in &archives {
a.validate()
.map_err(|err| anyhow!("invalid archive {}: {}", a.url, err))?;
}
let mut mf = Manifest::from_archives(
conf.manifest(),
conf.arch(),
archives.iter().cloned(),
self.comment.as_deref().or(comment.as_deref()),
);
mf.add_requirements(None, packages.iter(), None)?;
mf
};
for pair in self.meta.chunks_exact(2) {
mf.set_spec_meta(None, &pair[0], &pair[1])?;
}
if self.import.is_some() {
mf.add_requirements(None, self.requirements.iter(), None)?;
}
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
if self.import.is_some() {
mf.resolve(conf.concurrency(), fetcher).await?;
} else {
mf.update(
true,
false,
self.insecure_release,
conf.concurrency(),
fetcher,
)
.await?;
}
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Add an archive",
long_about = "Add an archive definition to the manifest file."
)]
pub struct ArchiveAdd {
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(long = "no-verify", display_order = 0, action)]
insecure_release: bool,
#[command(flatten)]
archive: ArchiveArgs,
}
impl<C: Config> Command<C> for ArchiveAdd {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let mut archive = Archive::from(&self.archive);
archive.signed_by = rebase_signed_by_input(&archive.signed_by, conf.manifest())?;
if let Some((archives, _)) = archive.as_vendor() {
for archive in archives {
mf.add_archive(archive, self.comment.as_deref())?;
}
} else {
mf.add_archive(archive, self.comment.as_deref())?;
}
mf.update(false, false, false, conf.concurrency(), fetcher)
.await?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Add a local package",
long_about = "Add a local .deb package to the manifest file so it can be staged alongside archives."
)]
pub struct DebAdd {
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(value_name = "PATH")]
path: PathBuf,
}
impl<C: Config> Command<C> for DebAdd {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let path = self
.path
.as_str()
.map_err(|err| anyhow!("invalid path: {}", err))?;
let path = rebase_cli_local_input(path, conf.manifest())?;
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let base = mf.local_path(&path);
let (file, ctrl) = fetcher.ensure_deb(&path, &base).await?;
mf.add_local_package(file, ctrl, self.comment.as_deref())?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Add an artifact",
long_about = "Add an artifact definition to the manifest file. Use --stage to attach it to a spec."
)]
pub struct ArtifactAdd {
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(long = "stage", action)]
stage: bool,
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[command(flatten)]
artifact: ArtifactArg,
}
impl<C: Config> Command<C> for ArtifactAdd {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
if let Some(path) = self.artifact.url.strip_prefix('@') {
if path.is_empty() {
return Err(anyhow!("text artifact path is empty"));
}
let name = Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| anyhow!("text artifact path has no filename: {path}"))?;
let text = std::fs::read(path)
.map_err(|err| anyhow!("failed to read text artifact {}: {}", path, err))
.and_then(|bytes| {
String::from_utf8(bytes)
.map_err(|_| anyhow!("text artifact {} is not valid utf-8", path))
})?;
let target =
self.artifact.target.clone().ok_or_else(|| {
anyhow!("text artifact {} requires TARGET_PATH", path)
})?;
mf.upsert_text_artifact(
name,
target,
text,
self.artifact.mode,
self.artifact.target_arch.clone(),
)?;
if self.stage {
mf.add_stage_items(self.spec.as_deref(), vec![name.to_string()], None)?;
}
} else {
let mut artifact = ArtifactArg {
mode: self.artifact.mode,
do_not_unpack: self.artifact.do_not_unpack,
target_arch: self.artifact.target_arch.clone(),
url: self.artifact.url.clone(),
target: self.artifact.target.clone(),
};
if !crate::is_url(&artifact.url) {
artifact.url = rebase_cli_local_input(&artifact.url, conf.manifest())?;
}
mf.upsert_artifact_only(&artifact, self.comment.as_deref(), fetcher)
.await?;
if self.stage {
mf.add_stage_items(self.spec.as_deref(), vec![artifact.url], None)?;
}
}
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Remove an archive",
long_about = "Remove an archive reference from the manifest file."
)]
pub struct ArchiveRemove {
#[command(flatten)]
archive: ArchiveArgs,
}
impl<C: Config> Command<C> for ArchiveRemove {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
let archive = Archive::from(&self.archive);
if let Some((archives, _)) = archive.as_vendor() {
for archive in archives {
mf.drop_archive(&archive.url)?;
}
} else {
mf.drop_archive(&archive.url)?;
}
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Remove local package",
long_about = "Remove a local package from the manifest file."
)]
pub struct DebRemove {
#[arg(value_name = "PATH")]
path: PathBuf,
}
impl<C: Config> Command<C> for DebRemove {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let path = self
.path
.as_str()
.map_err(|err| anyhow!("invalid path: {}", err))?;
let path = rebase_cli_local_input(path, conf.manifest())?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
mf.drop_local_package(&path)?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
alias = "drop",
about = "Remove requirements or constraints from a spec",
long_about = r#"Remove requirements and/or constraints from a spec.
Use --requirements-only or --constraints-only to limit the operation scope."#
)]
pub struct Remove {
#[arg(
short = 'R',
long = "requirements-only",
conflicts_with = "constraints_only"
)]
requirements_only: bool,
#[arg(
short = 'C',
long = "constraints-only",
conflicts_with = "requirements_only"
)]
constraints_only: bool,
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(value_name = "PACKAGE_OR_SET")]
cons: Vec<String>,
}
impl<C: Config> Command<C> for Remove {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
if !self.constraints_only {
mf.remove_requirements(self.spec.as_deref(), self.cons.iter())?;
}
if !self.requirements_only {
mf.remove_constraints(self.spec.as_deref(), self.cons.iter())?;
}
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Stage an artifact into a spec",
long_about = "Add an external artifact (URL or file path) to a spec so it is included in the system tree."
)]
pub struct Stage {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(value_name = "ARTIFACT")]
artifact: String,
}
impl<C: Config> Command<C> for Stage {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let artifact = if crate::is_url(&self.artifact) {
self.artifact.clone()
} else {
rebase_cli_local_input(&self.artifact, conf.manifest())?
};
mf.add_stage_items(
self.spec.as_deref(),
vec![artifact],
self.comment.as_deref(),
)?;
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Remove a staged artifact from a spec",
long_about = "Remove a previously staged artifact (by URL or file path) from the given spec."
)]
pub struct Unstage {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(value_name = "URL")]
url: String,
}
impl<C: Config> Command<C> for Unstage {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let url = if crate::is_url(&self.url) {
self.url.clone()
} else {
rebase_cli_local_input(&self.url, conf.manifest())?
};
mf.remove_artifact(self.spec.as_deref(), &url)?;
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Add package requirements to a spec",
alias = "include",
long_about = r#"Add one or more package requirements to a spec. Each requirement can be a bare package name or a set, and can include a version relation, e.g.:
foo
foo (= 1.2.3)
bar (>= 2.0)
foo | bar (<< 3.0)
Alternatively, requirement might be a path to .deb file to include directly."#
)]
pub struct Require {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(value_name = "REQUIREMENT", value_parser = DependencyParser)]
reqs: Vec<Dependency<String>>,
}
impl<C: Config> Command<C> for Require {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
mf.add_requirements(
self.spec.as_deref(),
self.reqs.iter(),
self.comment.as_deref(),
)?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Add package constraints to a spec",
alias = "exclude",
long_about = r#"Add one or more constraints to restrict resolution. Examples:
foo (= 1.2.3)
foo (<< 2.0)
bar (<= 3.4)"#
)]
pub struct Forbid {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(short = 'c', long = "comment", value_name = "COMMENT")]
comment: Option<String>,
#[arg(value_name = "CONSTRAINT", value_parser = ConstraintParser)]
reqs: Vec<Constraint<String>>,
}
impl<C: Config> Command<C> for Forbid {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
mf.add_constraints(
self.spec.as_deref(),
self.reqs.iter(),
self.comment.as_deref(),
)?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Update archives and snapshot/lock state",
long_about = "Refresh archive metadata, solve specs, rewrite the lock file, and refresh stored import fingerprints when [import] is present."
)]
pub struct Update {
#[arg(short = 'A', long = "archives", action)]
archives: bool,
#[arg(short = 'L', long = "locals", action)]
locals: bool,
#[arg(short = 's', long = "snapshot", value_name = "SNAPSHOT_ID", value_parser = SnapshotIdArgParser)]
snapshot: Option<SnapshotId>,
#[arg(long = "no-verify", display_order = 0, action)]
insecure_release: bool,
}
impl<C: Config> Command<C> for Update {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
let force_archives = self.archives || self.snapshot.is_some();
let force = force_archives || self.locals;
let mut git_refreshed = false;
if self.locals {
git_refreshed = mf.refresh_git_import(fetcher).await?;
}
if has_valid_lock && !force && !git_refreshed {
tracing::debug!("Lock file is up to date, nothing to do");
return Ok(());
}
if let Some(snapshot) = &self.snapshot {
mf.set_snapshot(*snapshot).await;
}
mf.update(
force_archives,
self.locals,
self.insecure_release,
conf.concurrency(),
fetcher,
)
.await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Add or replace an imported manifest",
long_about = r#"Configure a manifest import: validates an already-locked imported manifest, optionally
exports selected named specs for downstream extends, and writes [import] to the
downstream manifest.
The first positional argument is either a local manifest path or a `git+<scheme>://`
URL referencing a manifest hosted in a git repository. When a git URL is given the
manifest is fetched, validated, and pinned with the resolved commit SHA.
Accepted shapes (identical to `init --import`):
path/to/manifest.toml
file:///absolute/path/manifest.toml
git+ssh://[git@]host/repo[.git]?rev=<sha>|ref=<name>#path/to/manifest.toml
git+https://host/repo[.git]?rev=<sha>|ref=<name>#path/to/manifest.toml
git+http://host/repo[.git]?rev=<sha>|ref=<name>#path/to/manifest.toml
git+file:///absolute/path/to/repo?rev=<sha>|ref=<name>#path/to/manifest.toml
Examples:
rdebootstrap import ../base/Manifest.toml --spec base
rdebootstrap import 'git+https://gitlab.com/org/sys.git?rev=v1.2.3#sys/Manifest.toml' --spec base
rdebootstrap import 'git+ssh://git@gitlab.com/org/sys.git?ref=main#sys/Manifest.toml' --spec base"#
)]
pub struct ImportCmd {
#[arg(value_name = "PATH-OR-URL")]
target: String,
#[arg(long = "spec", value_name = "SPEC", value_delimiter = ',')]
specs: Vec<String>,
}
impl<C: Config> Command<C> for ImportCmd {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
let target = parse_import_target(&self.target)?;
let fetcher = conf.fetcher()?;
match target {
ImportTarget::Git(arg) => {
mf.set_import_git(
fetcher,
arg.repo.remote,
arg.repo.revspec,
&arg.path,
&self.specs,
)
.await?;
}
ImportTarget::LocalPath(path) => {
let path = rebase_pathbuf_input(&path, conf.manifest())?;
mf.set_import(&path, &self.specs, fetcher).await?;
}
}
let guard = fetcher.init().await?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Search packages in archives",
long_about = "Search for packages matching the given regex pattern(s)."
)]
pub struct Search {
#[arg(short = 'p', long = "names-only")]
names_only: bool,
#[arg(value_name = "PATTERN")]
pattern: Vec<String>,
}
impl<C: Config> Command<C> for Search {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
mf.load_universe(conf.concurrency(), fetcher).await?;
guard.commit().await?;
let res = self
.pattern
.iter()
.map(|p| {
regex::RegexBuilder::new(p)
.unicode(true)
.case_insensitive(true)
.build()
.map_err(|err| anyhow!("invalid regex: {}", err))
})
.collect::<Result<Vec<_>>>()?;
let mut pkgs = mf
.universe_packages()?
.filter(|p| {
res.is_empty()
|| res.iter().any(|re| {
re.is_match(p.name())
|| (!self.names_only
&& re.is_match(p.field("Description").unwrap_or("")))
})
})
.collect::<Vec<_>>();
pkgs.sort_by_key(|&pkg| pkg.name());
let out = pretty_print_packages(pkgs, false)?;
std::io::stdout().lock().write_all(&out)?;
Ok(())
})
}
}
#[derive(Parser)]
pub enum SpecCommands {
Meta(SpecMeta),
Hash(SpecHash),
List(SpecList),
Packages(SpecPackages),
Require(SpecRequire),
Forbid(SpecForbid),
Remove(SpecRemove),
Extend(SpecExtend),
Artifact(SpecArtifact),
InstallOrder(SpecInstallOrder),
}
#[derive(Parser)]
#[command(about = "Inspect or update specs")]
pub struct Spec {
#[command(subcommand)]
cmd: SpecCommands,
}
impl<C: Config> Command<C> for Spec {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
SpecCommands::Meta(cmd) => cmd.exec(conf),
SpecCommands::Hash(cmd) => cmd.exec(conf),
SpecCommands::List(cmd) => cmd.exec(conf),
SpecCommands::Packages(cmd) => cmd.exec(conf),
SpecCommands::Require(cmd) => cmd.exec(conf),
SpecCommands::Forbid(cmd) => cmd.exec(conf),
SpecCommands::Remove(cmd) => cmd.exec(conf),
SpecCommands::Extend(cmd) => cmd.exec(conf),
SpecCommands::Artifact(cmd) => cmd.exec(conf),
SpecCommands::InstallOrder(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
#[command(about = "List packages installation order for a spec", hide = true)]
pub struct SpecInstallOrder {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(long = "flat", action)]
flat: bool,
}
impl<C: Config> Command<C> for SpecInstallOrder {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let spec = mf.lookup_spec(self.spec.as_deref())?;
let (_, essentials, other) = if self.flat {
spec.effective_staging_installables()?
} else {
spec.staging_installables()?
};
std::iter::once(essentials)
.chain(other)
.for_each(|group| println!("{}", group.iter().join(" ")));
Ok(())
})
}
}
#[derive(Parser)]
#[command(about = "Inspect or update spec metadata")]
pub struct SpecMeta {
#[command(subcommand)]
cmd: SpecMetaCommands,
}
impl<C: Config> Command<C> for SpecMeta {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
SpecMetaCommands::Get(cmd) => cmd.exec(conf),
SpecMetaCommands::Set(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum SpecMetaCommands {
#[command(name = "get", alias = "get-meta")]
Get(SpecGetMeta),
#[command(name = "set", alias = "set-meta")]
Set(SpecSetMeta),
}
#[derive(Parser)]
#[command(about = "List spec names")]
pub struct SpecList;
impl<C: Config> Command<C> for SpecList {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
for spec in mf.specs() {
println!("{}", spec.display_name());
}
Ok(())
})
}
}
#[derive(Parser)]
#[command(about = "List packages for a spec")]
pub struct SpecPackages {
#[command(flatten)]
inner: List,
}
impl<C: Config> Command<C> for SpecPackages {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(about = "Add package requirements to a spec")]
pub struct SpecRequire {
#[command(flatten)]
inner: Require,
}
impl<C: Config> Command<C> for SpecRequire {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(about = "Add package constraints to a spec")]
pub struct SpecForbid {
#[command(flatten)]
inner: Forbid,
}
impl<C: Config> Command<C> for SpecForbid {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(about = "Remove requirements or constraints from a spec")]
pub struct SpecRemove {
#[command(flatten)]
inner: Remove,
}
impl<C: Config> Command<C> for SpecRemove {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(about = "Manage spec artifacts")]
pub struct SpecArtifact {
#[command(subcommand)]
cmd: SpecArtifactCommands,
}
impl<C: Config> Command<C> for SpecArtifact {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
SpecArtifactCommands::Add(cmd) => cmd.exec(conf),
SpecArtifactCommands::Remove(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum SpecArtifactCommands {
#[command(name = "add", alias = "stage")]
Add(SpecArtifactAdd),
#[command(name = "remove", alias = "unstage")]
Remove(SpecArtifactRemove),
}
#[derive(Parser)]
#[command(about = "Stage an artifact into a spec")]
pub struct SpecArtifactAdd {
#[command(flatten)]
inner: Stage,
}
impl<C: Config> Command<C> for SpecArtifactAdd {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(about = "Remove a staged artifact from a spec")]
pub struct SpecArtifactRemove {
#[command(flatten)]
inner: Unstage,
}
impl<C: Config> Command<C> for SpecArtifactRemove {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(about = "Get a spec meta value")]
pub struct SpecGetMeta {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(value_name = "NAME")]
name: String,
}
impl<C: Config> Command<C> for SpecGetMeta {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
let spec = mf.lookup_spec(self.spec.as_deref())?;
match spec.get_meta(&self.name)? {
Some(value) => {
println!("{}", value);
Ok(())
}
None => Err(anyhow!(
"meta {} not found in spec {}",
self.name,
spec.display_name()
)),
}
})
}
}
#[derive(Parser)]
#[command(about = "Set a spec meta value")]
pub struct SpecSetMeta {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(value_name = "NAME")]
name: String,
#[arg(value_name = "VALUE")]
value: String,
}
impl<C: Config> Command<C> for SpecSetMeta {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
mf.set_spec_meta(self.spec.as_deref(), &self.name, &self.value)?;
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Set, modify or clear a spec's parents",
long_about = "Manage the `extends` list of a spec.\n\
\n\
With one or more positional PARENT arguments, the entire parent set is \
replaced. Use --add to append, --remove to drop a single parent without \
otherwise touching the list, and --clear to remove the relationship \
entirely. Multi-parent inheritance is strictly additive: a child spec \
cannot remove packages, build-env entries, meta entries, or stage \
artifacts contributed by any ancestor."
)]
pub struct SpecExtend {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(long = "clear", conflicts_with_all = ["parents", "add", "remove"])]
clear: bool,
#[arg(long = "add", value_name = "PARENT", conflicts_with_all = ["parents", "remove"])]
add: Vec<String>,
#[arg(long = "remove", value_name = "PARENT", conflicts_with_all = ["parents", "add"])]
remove: Vec<String>,
#[arg(value_name = "PARENT")]
parents: Vec<String>,
}
impl<C: Config> Command<C> for SpecExtend {
fn exec(&self, conf: &C) -> Result<()> {
if self.parents.is_empty()
&& self.add.is_empty()
&& self.remove.is_empty()
&& !self.clear
{
return Err(anyhow!(
"specify one or more PARENT arguments, --add, --remove, or --clear"
));
}
smol::block_on(async move {
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let new_parents: Vec<String> = if self.clear {
Vec::new()
} else if !self.add.is_empty() || !self.remove.is_empty() {
let current = mf
.lookup_spec(self.spec.as_deref())
.ok()
.map(|spec| {
spec.parents()
.map(|parents| {
parents
.into_iter()
.map(|p| {
p.name().map(|n| n.to_string()).unwrap_or_default()
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
})
.unwrap_or_default();
let mut next: Vec<String> =
current.into_iter().filter(|p| !p.is_empty()).collect();
for parent in &self.remove {
match next.iter().position(|p| p == parent) {
Some(idx) => {
next.remove(idx);
}
None => {
return Err(anyhow!("spec does not currently extend '{}'", parent));
}
}
}
for parent in &self.add {
if next.iter().any(|p| p == parent) {
return Err(anyhow!("'{}' is already a parent", parent));
}
next.push(parent.clone());
}
next
} else {
self.parents.clone()
};
let parent_refs: Vec<&str> = new_parents.iter().map(String::as_str).collect();
mf.set_extends(self.spec.as_deref(), &parent_refs)?;
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
mf.resolve(conf.concurrency(), fetcher).await?;
mf.store().await?;
guard.commit().await?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
name = "spec-hash",
about = "Show a spec hash",
long_about = "Print the hash of a spec definition. By default the hash is printed as a hexadecimal string; use --sri to emit Subresource Integrity format."
)]
pub struct SpecHash {
#[arg(long = "sri", action)]
sri: bool,
#[arg(value_name = "SPEC")]
spec: Option<String>,
}
impl<C: Config> Command<C> for SpecHash {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let (mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), conf.fetcher()?).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let hash = mf.lookup_spec(self.spec.as_deref())?.locked_hash()?;
if self.sri {
println!("{}", hash.to_sri());
} else {
println!("{}", hash.to_hex());
}
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Show a package's control record",
long_about = "Print the raw control record for the given package."
)]
pub struct PackageShow {
#[arg(value_name = "PACKAGE")]
package: String,
}
impl<C: Config> Command<C> for PackageShow {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
mf.load_universe(conf.concurrency(), fetcher).await?;
guard.commit().await?;
let pkg = mf.universe_packages()?.find(|p| self.package == p.name());
if let Some(pkg) = pkg {
std::io::stdout().lock().write_all(pkg.src().as_bytes())?;
Ok(())
} else {
Err(anyhow!("package {} not found", &self.package))
}
})
}
}
#[derive(Parser)]
#[command(
about = "Show a package's source control record",
long_about = "Print the raw source control record for the given package or source package."
)]
pub struct SourceShow {
#[arg(long = "stage-to", value_name = "PATH")]
stage_to: Option<String>,
#[arg(value_name = "PACKAGE")]
package: String,
}
impl<C: Config> Command<C> for SourceShow {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let name = ProvidedName::try_parse_display(&self.package).map_err(|err| {
anyhow!("invalid package/source name '{}': {}", &self.package, err)
})?;
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, _) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
mf.load_source_universe(conf.concurrency(), fetcher).await?;
guard.commit().await?;
let found = mf.find_source(&name)?;
if found.is_empty() {
return Err(anyhow!("package/source {} not found", &self.package));
}
let mut out = std::io::stdout().lock();
if let Some(target) = self.stage_to.as_deref() {
if found.len() > 1 {
return Err(anyhow!(
"multiple source packages found for {}, cannot show as artifacts",
&self.package
));
}
let src = found.first().unwrap();
let mut first = true;
let mut staged = vec![];
for artifact in src.files() {
if first {
first = false;
} else {
out.write_all(b"\n")?;
}
let file_name = match artifact.path.rsplit('/').next() {
Some(name) => name,
None => &artifact.path,
};
out.write_all(
format!(
"[artifact.\"{}\"]
type = \"file\"
target = \"{}/{}\"
size = {}
hash = \"{}\"
",
&artifact.path,
target.trim_end_matches('/'),
file_name,
artifact.size,
artifact.hash.to_sri(),
)
.as_bytes(),
)?;
if is_comp_ext(&artifact.path) {
out.write_all(b"unpack = false\n")?;
}
staged.push(&artifact.path);
}
out.write_all(b"\nstage = [\n")?;
for staged in staged {
out.write_all(format!(" \"{}\",\n", staged).as_bytes())?;
}
out.write_all(b"]\n")?;
} else {
for src in found.iter() {
out.write_all(src.as_ref().as_bytes())?;
out.write_all(b"\n")?;
}
}
out.flush()?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(name = "package", about = "Inspect packages")]
pub struct PackageCmd {
#[command(subcommand)]
cmd: PackageCommands,
}
impl<C: Config> Command<C> for PackageCmd {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
PackageCommands::Show(cmd) => cmd.exec(conf),
PackageCommands::Search(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum PackageCommands {
Show(PackageShow),
Search(PackageSearch),
}
#[derive(Parser)]
#[command(about = "Search for packages in the universe")]
pub struct PackageSearch {
#[command(flatten)]
inner: Search,
}
impl<C: Config> Command<C> for PackageSearch {
fn exec(&self, conf: &C) -> Result<()> {
self.inner.exec(conf)
}
}
#[derive(Parser)]
#[command(name = "source", about = "Inspect source packages")]
pub struct SourceCmd {
#[command(subcommand)]
cmd: SourceCommands,
}
impl<C: Config> Command<C> for SourceCmd {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
SourceCommands::Show(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum SourceCommands {
Show(SourceShow),
}
#[derive(Parser)]
#[command(name = "archive", about = "Manage archives")]
pub struct ArchiveCmd {
#[command(subcommand)]
cmd: ArchiveCommands,
}
impl<C: Config> Command<C> for ArchiveCmd {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
ArchiveCommands::Add(cmd) => cmd.exec(conf),
ArchiveCommands::Remove(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum ArchiveCommands {
Add(ArchiveAdd),
Remove(ArchiveRemove),
}
#[derive(Parser)]
#[command(name = "artifact", about = "Manage artifacts")]
pub struct ArtifactCmd {
#[command(subcommand)]
cmd: ArtifactCommands,
}
impl<C: Config> Command<C> for ArtifactCmd {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
ArtifactCommands::Add(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum ArtifactCommands {
Add(ArtifactAdd),
}
#[derive(Parser)]
#[command(name = "deb", about = "Manage local .deb files")]
pub struct DebCmd {
#[command(subcommand)]
cmd: DebCommands,
}
impl<C: Config> Command<C> for DebCmd {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
DebCommands::Add(cmd) => cmd.exec(conf),
DebCommands::Remove(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
pub enum DebCommands {
Add(DebAdd),
Remove(DebRemove),
}
fn normalize_hash_name(name: &str) -> Result<&'static str> {
let name = name.trim();
if name.is_empty() {
return Err(anyhow!("hash name cannot be empty"));
}
match name.to_ascii_lowercase().as_str() {
"md5" | "md5sum" => Ok(md5::Md5::NAME),
"sha1" => Ok(sha1::Sha1::NAME),
"sha256" => Ok(sha2::Sha256::NAME),
"sha512" => Ok(sha2::Sha512::NAME),
"blake3" => Ok(blake3::Hasher::NAME),
_ => Err(anyhow!(
"unsupported hash {}, expected one of: md5, sha1, sha256, sha512, blake3",
name
)),
}
}
#[derive(Parser)]
pub enum ToolCommands {
HexToSri(ToolHexToSri),
SriToHex(ToolSriToHex),
Hash(ToolHash),
}
#[derive(Parser)]
#[command(about = "Internal tooling helpers", hide = true)]
pub struct Tool {
#[command(subcommand)]
cmd: ToolCommands,
}
impl<C: Config> Command<C> for Tool {
fn exec(&self, conf: &C) -> Result<()> {
match &self.cmd {
ToolCommands::HexToSri(cmd) => cmd.exec(conf),
ToolCommands::SriToHex(cmd) => cmd.exec(conf),
ToolCommands::Hash(cmd) => cmd.exec(conf),
}
}
}
#[derive(Parser)]
#[command(
name = "hex-to-sri",
about = "Convert a hex digest to SRI format",
long_about = "Convert a hex digest and hash name to SRI (Subresource Integrity) format."
)]
pub struct ToolHexToSri {
#[arg(value_name = "HASH_NAME")]
hash: String,
#[arg(value_name = "HEX_DIGEST")]
digest: String,
}
impl<C: Config> Command<C> for ToolHexToSri {
fn exec(&self, _conf: &C) -> Result<()> {
let name = normalize_hash_name(&self.hash)?;
let hash = Hash::from_hex(name, &self.digest)
.map_err(|err| anyhow!("error decoding hex digest: {}", err))?;
println!("{}", hash.to_sri());
Ok(())
}
}
#[derive(Parser)]
#[command(
name = "sri-to-hex",
about = "Convert an SRI digest to hex",
long_about = "Convert an SRI (Subresource Integrity) digest to hexadecimal."
)]
pub struct ToolSriToHex {
#[arg(value_name = "SRI_DIGEST")]
digest: String,
}
impl<C: Config> Command<C> for ToolSriToHex {
fn exec(&self, _conf: &C) -> Result<()> {
let hash = Hash::from_sri(&self.digest)
.map_err(|err| anyhow!("error decoding SRI digest: {}", err))?;
println!("{}", hash.to_hex());
Ok(())
}
}
#[derive(Parser)]
#[command(
name = "hash",
about = "Hash a file, directory, or stdin",
long_about = "Hash a file, directory, or stdin. For directories the artifact tree hash (blake3) is used."
)]
pub struct ToolHash {
#[arg(value_name = "HASH_NAME")]
hash: String,
#[arg(long = "sri", conflicts_with = "hex", action)]
sri: bool,
#[arg(long = "hex", conflicts_with = "sri", action)]
hex: bool,
#[arg(value_name = "PATH")]
path: Option<PathBuf>,
}
impl<C: Config> Command<C> for ToolHash {
fn exec(&self, _conf: &C) -> Result<()> {
let name = normalize_hash_name(&self.hash)?;
let use_sri = self.sri;
smol::block_on(async move {
let hash = if let Some(path) = self.path.as_ref() {
let meta = smol::fs::metadata(path)
.await
.map_err(|err| anyhow!("failed to stat {}: {}", path.display(), err))?;
if meta.is_dir() {
let (hash, _) = hash_directory(path, name).await.map_err(|err| {
anyhow!("failed to hash directory {}: {}", path.display(), err)
})?;
hash
} else {
let file = smol::fs::File::open(path)
.await
.map_err(|err| anyhow!("failed to open {}: {}", path.display(), err))?;
let mut hasher = Hash::hashing_reader_for(name, file)?;
smol::io::copy(&mut hasher, &mut smol::io::sink())
.await
.map_err(|err| anyhow!("failed to read stdin: {}", err))?;
hasher.as_mut().hash()
}
} else {
let stdin = async_io::Async::new(std::io::stdin())
.map_err(|err| anyhow!("failed to read stdin: {}", err))?;
let mut hasher = Hash::hashing_reader_for(name, stdin)?;
smol::io::copy(&mut hasher, &mut smol::io::sink())
.await
.map_err(|err| anyhow!("failed to read stdin: {}", err))?;
hasher.as_mut().hash()
};
if use_sri {
println!("{}", hash.to_sri());
} else {
println!("{}", hash.to_hex());
}
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "List spec packages",
long_about = "List spec packages. Use -e to see only essential packages, or -s to specify a non-default spec."
)]
pub struct List {
#[arg(short = 'e', long = "only-essential", hide = true)]
only_essential: bool,
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(long = "flat", action)]
flat: bool,
}
impl<C: Config> Command<C> for List {
fn exec(&self, conf: &C) -> Result<()> {
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let (mut mf, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher).await?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
mf.load_universe(conf.concurrency(), fetcher).await?;
guard.commit().await?;
let spec = mf.lookup_spec(self.spec.as_deref())?;
let mut pkgs = if self.flat {
spec.effective_packages()?
} else {
spec.packages()?
};
pkgs.sort_by_key(|&pkg| pkg.name());
let out = pretty_print_packages(pkgs, false)?;
std::io::stdout().lock().write_all(&out)?;
Ok(())
})
}
}
#[derive(Parser)]
#[command(
about = "Build an installable root for a spec",
long_about = "Build an installable root filesystem tree for a spec."
)]
pub struct Build {
#[arg(short = 's', long = "spec", value_name = "SPEC")]
spec: Option<String>,
#[arg(
long = "executor",
value_name = "EXECUTOR",
value_enum,
default_value = "sandbox"
)]
executor: ExecutorKind,
#[arg(short, long, value_name = "PATH")]
path: PathBuf,
}
impl<C: Config<FS = HostFileSystem, Cache = HostCache>> Command<C> for Build {
fn exec(&self, conf: &C) -> Result<()> {
let mut builder = self.executor.build_executor(&self.path)?;
tracing::debug!("current process state {}", current_process_state(),);
smol::block_on(async move {
let fetcher = conf.fetcher()?;
let guard = fetcher.init().await?;
let fs = HostFileSystem::new(&self.path, rustix::process::geteuid().is_root())
.await
.map_err(|err| {
anyhow!(
"failed to initialize staging filesystem at {}: {}",
self.path.display(),
err
)
})?;
let (manifest, has_valid_lock) =
Manifest::from_file(conf.manifest(), conf.arch(), fetcher)
.await
.map_err(|err| {
anyhow!(
"failed to load manifest from {}: {}",
conf.manifest().display(),
err
)
})?;
if !has_valid_lock {
return Err(anyhow!("manifest lock is not live; run update first"));
}
let pb = if conf.log_level() == 0 {
let use_tty = std::io::stderr().is_terminal();
Some(move |size| {
if use_tty {
StageProgress::from_progress_bar(
ProgressBar::new(size).with_style(
indicatif::ProgressStyle::with_template(
"staging files: {spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta}) {msg}",
)
.unwrap()
.progress_chars("#>-"),
).with_finish(indicatif::ProgressFinish::AndClear),
)
} else {
StageProgress::percent(size)
}
})
} else {
None
};
tracing::debug!(
"Staging spec '{}' into '{}'",
self.spec.as_deref().unwrap_or("default"),
self.path.display()
);
let (essentials, other, scripts, build_env) = manifest
.stage(
self.spec.as_deref(),
None,
&fs,
conf.concurrency(),
fetcher,
pb,
)
.await?;
builder
.build(&fs, essentials, other, scripts, build_env)
.await?;
guard.commit().await?;
Ok(())
})
}
}
fn manifest_dir_for(manifest: &Path, cwd: &Path) -> PathBuf {
let manifest = if manifest.is_absolute() {
manifest.to_path_buf()
} else {
cwd.join(manifest)
};
let manifest = normalize_path(&manifest);
manifest
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.to_path_buf()
}
fn normalize_path(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
let mut normal_parts: Vec<OsString> = Vec::new();
for component in path.components() {
match component {
Component::Prefix(prefix) => out.push(prefix.as_os_str()),
Component::RootDir => out.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
if !normal_parts.is_empty() {
normal_parts.pop();
} else if out.as_os_str().is_empty() {
normal_parts.push(component.as_os_str().to_os_string());
}
}
Component::Normal(part) => normal_parts.push(part.to_os_string()),
}
}
for part in normal_parts {
out.push(part);
}
if out.as_os_str().is_empty() {
PathBuf::from(".")
} else {
out
}
}
fn diff_relative_path(path: &Path, base: &Path) -> Option<PathBuf> {
let path = normalize_path(path);
let base = normalize_path(base);
let path_components = path.components().collect::<Vec<_>>();
let base_components = base.components().collect::<Vec<_>>();
if path_components
.first()
.zip(base_components.first())
.map(|(lhs, rhs)| lhs != rhs)
.unwrap_or(false)
{
return None;
}
let common_len = path_components
.iter()
.zip(base_components.iter())
.take_while(|(lhs, rhs)| lhs == rhs)
.count();
let mut relative = PathBuf::new();
for component in &base_components[common_len..] {
match component {
Component::Normal(_) | Component::ParentDir => relative.push(".."),
Component::CurDir => {}
Component::RootDir | Component::Prefix(_) => return None,
}
}
for component in &path_components[common_len..] {
relative.push(component.as_os_str());
}
if relative.as_os_str().is_empty() {
Some(PathBuf::from("."))
} else {
Some(relative)
}
}
fn rebase_local_path_for_manifest(path: &Path, manifest: &Path, cwd: &Path) -> PathBuf {
if path.is_absolute() {
return path.to_path_buf();
}
let cwd = normalize_path(cwd);
let manifest_dir = manifest_dir_for(manifest, &cwd);
if manifest_dir == cwd {
return path.to_path_buf();
}
let resolved = normalize_path(&cwd.join(path));
diff_relative_path(&resolved, &manifest_dir).unwrap_or(resolved)
}
fn rebase_local_input_for_manifest(path: &str, manifest: &Path, cwd: &Path) -> String {
rebase_local_path_for_manifest(Path::new(path), manifest, cwd)
.to_string_lossy()
.into_owned()
}
fn rebase_cli_local_input(path: &str, manifest: &Path) -> io::Result<String> {
let cwd = std::env::current_dir()?;
Ok(rebase_local_input_for_manifest(path, manifest, &cwd))
}
fn rebase_pathbuf_input(path: &Path, manifest: &Path) -> io::Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(PathBuf::from(rebase_cli_local_input(
&path.to_string_lossy(),
manifest,
)?))
}
}
fn rebase_signed_by_input(
signed_by: &Option<SignedBy>,
manifest: &Path,
) -> io::Result<Option<SignedBy>> {
match signed_by {
Some(SignedBy::Keyring(path)) => Ok(Some(SignedBy::Keyring(rebase_pathbuf_input(
path, manifest,
)?))),
Some(SignedBy::Builtin) => Ok(Some(SignedBy::Builtin)),
Some(SignedBy::Key(key)) => Ok(Some(SignedBy::Key(key.clone()))),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn normalize_path_handles_curdir_parentdir_and_empty_collapse() {
assert_eq!(
normalize_path(Path::new("./foo/./bar")),
PathBuf::from("foo/bar")
);
assert_eq!(
normalize_path(Path::new("foo/../bar")),
PathBuf::from("bar")
);
assert_eq!(
normalize_path(Path::new("foo/bar/../../baz")),
PathBuf::from("baz")
);
assert_eq!(normalize_path(Path::new("foo/..")), PathBuf::from("."));
assert_eq!(normalize_path(Path::new(".")), PathBuf::from("."));
assert_eq!(normalize_path(Path::new("../foo")), PathBuf::from("../foo"));
}
#[test]
fn diff_relative_path_edge_cases() {
assert_eq!(
diff_relative_path(Path::new("foo/bar"), Path::new("foo/bar")),
Some(PathBuf::from("."))
);
assert_eq!(
diff_relative_path(Path::new("/abs/path"), Path::new("rel/path")),
None,
);
}
#[test]
fn rebase_local_path_for_manifest_short_circuits_absolute() {
let abs = Path::new("/absolute/path/file.txt");
let manifest = Path::new("/some/manifest.toml");
let cwd = Path::new("/working");
let result = rebase_local_path_for_manifest(abs, manifest, cwd);
assert_eq!(result, PathBuf::from("/absolute/path/file.txt"));
}
#[test]
fn rebase_signed_by_covers_builtin_and_key() {
let manifest = Path::new("/tmp/manifest.toml");
let builtin =
rebase_signed_by_input(&Some(SignedBy::Builtin), manifest).expect("builtin");
assert_eq!(builtin, Some(SignedBy::Builtin));
let key = rebase_signed_by_input(
&Some(SignedBy::Key("-----BEGIN PGP PUBLIC KEY BLOCK-----".into())),
manifest,
)
.expect("key");
assert_eq!(
key,
Some(SignedBy::Key("-----BEGIN PGP PUBLIC KEY BLOCK-----".into()))
);
let none = rebase_signed_by_input(&None, manifest).expect("none");
assert_eq!(none, None);
}
#[test]
fn manifest_dir_for_joins_cwd_for_relative_manifest() {
let dir = manifest_dir_for(Path::new("manifest.toml"), Path::new("/cwd"));
assert_eq!(dir, PathBuf::from("/cwd"));
let dir = manifest_dir_for(Path::new("/etc/manifest.toml"), Path::new("/cwd"));
assert_eq!(dir, PathBuf::from("/etc"));
}
}
}
#[derive(Clone)]
pub struct DependencyParser;
impl clap::builder::TypedValueParser for DependencyParser {
type Value = Dependency<String>;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value.to_str().ok_or_else(|| {
let mut err = clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err
})?;
Self::Value::from_str(value).map_err(|e| {
let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(format!("{}", e)),
);
err
})
}
}
struct PackageDisplay<'a> {
name: &'a str,
arch: &'a str,
ver: Version<&'a str>,
desc: &'a str,
prio: InstallPriority,
}
impl<'a> From<&'a Package<'a>> for PackageDisplay<'a> {
fn from(pkg: &'a Package<'a>) -> Self {
Self {
name: pkg.name(),
arch: pkg.arch(),
ver: pkg.raw_version(),
desc: pkg
.field("Description")
.map_or("", |d| d.split('\n').next().unwrap_or("")),
prio: pkg.install_priority(),
}
}
}
pub fn pretty_print_packages<'a>(
iter: impl IntoIterator<Item = &'a Package<'a>>,
sort: bool,
) -> Result<Vec<u8>> {
let mut w0 = 0usize;
let mut w1 = 0usize;
let mut w2 = 0usize;
let mut w3 = 0usize;
let mut w4 = 0usize;
let mut packages = iter
.into_iter()
.map(PackageDisplay::try_from)
.map(|pkg| {
let pkg = pkg?;
w0 = std::cmp::max(w0, pkg.arch.len());
w1 = std::cmp::max(w1, pkg.prio.as_ref().len());
w2 = std::cmp::max(w2, pkg.name.len());
w3 = std::cmp::max(w3, pkg.ver.as_ref().len());
w4 = std::cmp::max(w4, pkg.desc.len());
Ok(pkg)
})
.collect::<Result<Vec<_>>>()?;
if sort {
packages.sort_by(|this, that| match this.name.cmp(that.name) {
std::cmp::Ordering::Equal => this.ver.cmp(&that.ver),
other => other,
});
}
let mut buf = Vec::new();
for p in packages.iter() {
use std::io::Write;
writeln!(
&mut buf,
"{:>w0$} {:<w2$} {:>w3$} {:<w4$}",
p.arch, p.name, p.ver, p.desc
)?;
}
Ok(buf)
}
#[derive(Clone)]
pub struct ConstraintParser;
impl clap::builder::TypedValueParser for ConstraintParser {
type Value = Constraint<String>;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value.to_str().ok_or_else(|| {
let mut err = clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err
})?;
Self::Value::from_str(value).map_err(|e| {
let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(format!("{}", e)),
);
err
})
}
}
pub fn current_process_state() -> impl std::fmt::Display {
ProcessState::collect()
}
struct ProcessState {
uid: u32,
euid: u32,
gid: u32,
egid: u32,
uid_map: String,
gid_map: String,
setgroups: String,
status_filtered: String,
chown_test_result: String,
}
impl ProcessState {
fn read_file(path: &str) -> String {
std::fs::read_to_string(path).unwrap_or_else(|e| format!("(failed to read {path}: {e})"))
}
fn collect() -> Self {
use rustix::process::{getegid, geteuid, getgid, getuid};
let uid = getuid().as_raw();
let euid = geteuid().as_raw();
let gid = getgid().as_raw();
let egid = getegid().as_raw();
let uid_map = Self::read_file("/proc/self/uid_map");
let gid_map = Self::read_file("/proc/self/gid_map");
let setgroups = Self::read_file("/proc/self/setgroups");
let status = Self::read_file("/proc/self/status");
let mut status_filtered = String::new();
for line in status.lines() {
if line.starts_with("Uid:")
|| line.starts_with("Gid:")
|| line.starts_with("Groups:")
|| line.starts_with("CapEff:")
|| line.starts_with("CapPrm:")
|| line.starts_with("CapBnd:")
|| line.starts_with("NoNewPrivs:")
|| line.starts_with("Seccomp:")
{
status_filtered.push_str(line);
status_filtered.push('\n');
}
}
let chown_test_result = match Self::chown_test() {
Ok(msg) => msg,
Err(e) => format!("chown test failed to run: {e}"),
};
Self {
uid,
euid,
gid,
egid,
uid_map,
gid_map,
setgroups,
status_filtered,
chown_test_result,
}
}
fn chown_test() -> io::Result<String> {
use rustix::fd::OwnedFd;
use rustix::fs::{fchown, openat, unlinkat, AtFlags, Mode, OFlags};
use rustix::process::{Gid, Uid};
let path = "rdebootstrap_chown_test";
let _ = unlinkat(rustix::fs::CWD, path, AtFlags::empty());
let fd: OwnedFd = openat(
rustix::fs::CWD,
path,
OFlags::CREATE | OFlags::WRONLY,
Mode::from_bits_truncate(0o644),
)?;
let result = match fchown(&fd, Some(Uid::from_raw(0)), Some(Gid::from_raw(42))) {
Ok(()) => "fchown(fd, 0, 42) succeeded".to_string(),
Err(e) => format!("fchown(fd, 0, 42) failed: {e}"),
};
drop(fd);
let _ = unlinkat(rustix::fs::CWD, path, AtFlags::empty());
Ok(result)
}
}
impl std::fmt::Display for ProcessState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "== ids ==")?;
writeln!(
f,
"uid={} euid={} gid={} egid={}",
self.uid, self.euid, self.gid, self.egid
)?;
writeln!(f, "\n== /proc/self/uid_map ==")?;
writeln!(f, "{}", self.uid_map)?;
writeln!(f, "== /proc/self/gid_map ==")?;
writeln!(f, "{}", self.gid_map)?;
writeln!(f, "== /proc/self/setgroups ==")?;
writeln!(f, "{}", self.setgroups)?;
writeln!(f, "== filtered /proc/self/status ==")?;
writeln!(f, "{}", self.status_filtered)?;
writeln!(f, "== chown test ==")?;
writeln!(f, "{}", self.chown_test_result)?;
Ok(())
}
}