use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use clap::{Parser, Subcommand};
use fn_error_context::context;
use ostree::{cap_std, gio, glib};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::io::BufWriter;
use std::path::PathBuf;
use std::process::Command;
use tokio::sync::mpsc::Receiver;
use crate::commit::container_commit;
use crate::container::store::{ImportProgress, LayerProgress, PreparedImport};
use crate::container::{self as ostree_container};
use crate::container::{Config, ImageReference, OstreeImageReference};
use crate::sysroot::SysrootLock;
use ostree_container::store::{ImageImporter, PrepareResult};
pub fn parse_imgref(s: &str) -> Result<OstreeImageReference> {
OstreeImageReference::try_from(s)
}
pub fn parse_base_imgref(s: &str) -> Result<ImageReference> {
ImageReference::try_from(s)
}
pub fn parse_repo(s: &Utf8Path) -> Result<ostree::Repo> {
let repofd = cap_std::fs::Dir::open_ambient_dir(s, cap_std::ambient_authority())
.with_context(|| format!("Opening directory at '{s}'"))?;
ostree::Repo::open_at_dir(&repofd, ".")
.with_context(|| format!("Opening ostree repository at '{s}'"))
}
#[derive(Debug, Parser)]
pub(crate) struct ImportOpts {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
path: Option<String>,
}
#[derive(Debug, Parser)]
pub(crate) struct ExportOpts {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
#[clap(long, hide(true))]
format_version: u32,
rev: String,
}
#[derive(Debug, Subcommand)]
pub(crate) enum TarOpts {
Import(ImportOpts),
Export(ExportOpts),
}
#[derive(Debug, Subcommand)]
pub(crate) enum ContainerOpts {
#[clap(alias = "import")]
Unencapsulate {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
#[clap(value_parser = parse_imgref)]
imgref: OstreeImageReference,
#[clap(long)]
write_ref: Option<String>,
#[clap(long)]
quiet: bool,
},
Info {
#[clap(value_parser = parse_imgref)]
imgref: OstreeImageReference,
},
#[clap(alias = "export")]
Encapsulate {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
rev: String,
#[clap(value_parser = parse_base_imgref)]
imgref: ImageReference,
#[clap(name = "label", long, short)]
labels: Vec<String>,
#[clap(name = "copymeta", long)]
copy_meta_keys: Vec<String>,
#[clap(name = "copymeta-opt", long)]
copy_meta_opt_keys: Vec<String>,
#[clap(long)]
cmd: Option<Vec<String>>,
#[clap(long)]
compression_fast: bool,
},
Commit,
#[clap(subcommand)]
Image(ContainerImageOpts),
Compare {
#[clap(value_parser = parse_imgref)]
imgref_old: OstreeImageReference,
#[clap(value_parser = parse_imgref)]
imgref_new: OstreeImageReference,
},
}
#[derive(Debug, Parser)]
pub(crate) struct ContainerProxyOpts {
#[clap(long)]
auth_anonymous: bool,
#[clap(long)]
authfile: Option<PathBuf>,
#[clap(long)]
cert_dir: Option<PathBuf>,
#[clap(long)]
insecure_skip_tls_verification: bool,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ContainerImageOpts {
List {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
},
Pull {
#[clap(value_parser)]
repo: Utf8PathBuf,
#[clap(value_parser = parse_imgref)]
imgref: OstreeImageReference,
#[clap(flatten)]
proxyopts: ContainerProxyOpts,
#[clap(long)]
quiet: bool,
},
History {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
#[clap(value_parser = parse_base_imgref)]
imgref: ImageReference,
},
Copy {
#[clap(long, value_parser)]
src_repo: Utf8PathBuf,
#[clap(long, value_parser)]
dest_repo: Utf8PathBuf,
#[clap(value_parser = parse_imgref)]
imgref: OstreeImageReference,
},
ReplaceDetachedMetadata {
#[clap(long)]
#[clap(value_parser = parse_base_imgref)]
src: ImageReference,
#[clap(long)]
#[clap(value_parser = parse_base_imgref)]
dest: ImageReference,
contents: Option<Utf8PathBuf>,
},
Remove {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
#[clap(value_parser = parse_base_imgref)]
imgrefs: Vec<ImageReference>,
#[clap(long)]
skip_gc: bool,
},
PruneLayers {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
},
PruneImages {
#[clap(long)]
sysroot: Utf8PathBuf,
#[clap(long)]
and_layers: bool,
},
Deploy {
#[clap(long)]
sysroot: String,
#[clap(long, default_value = ostree_container::deploy::STATEROOT_DEFAULT)]
stateroot: String,
#[clap(long, required_unless_present = "image")]
imgref: Option<String>,
#[clap(long, required_unless_present = "imgref")]
image: Option<String>,
#[clap(long)]
transport: Option<String>,
#[clap(long)]
no_signature_verification: bool,
#[clap(long)]
ostree_remote: Option<String>,
#[clap(flatten)]
proxyopts: ContainerProxyOpts,
#[clap(long)]
#[clap(value_parser = parse_imgref)]
target_imgref: Option<OstreeImageReference>,
#[clap(long)]
no_imgref: bool,
#[clap(long)]
karg: Option<Vec<String>>,
#[clap(long)]
write_commitid_to: Option<Utf8PathBuf>,
},
}
#[derive(Debug, Parser)]
pub(crate) enum ProvisionalRepairOpts {
AnalyzeInodes {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
#[clap(long)]
verbose: bool,
#[clap(long)]
write_result_to: Option<Utf8PathBuf>,
},
Repair {
#[clap(long, value_parser)]
sysroot: Utf8PathBuf,
#[clap(long)]
dry_run: bool,
#[clap(long)]
write_result_to: Option<Utf8PathBuf>,
#[clap(long)]
verbose: bool,
},
}
#[derive(Debug, Parser)]
pub(crate) struct ImaSignOpts {
#[clap(long, value_parser)]
repo: Utf8PathBuf,
src_rev: String,
target_ref: String,
algorithm: String,
key: Utf8PathBuf,
#[clap(long)]
overwrite: bool,
}
#[derive(Debug, Subcommand)]
pub(crate) enum TestingOpts {
DetectEnv,
CreateFixture,
Run,
RunIMA,
FilterTar,
}
#[derive(Debug, Parser)]
pub(crate) struct ManOpts {
#[clap(long)]
directory: Utf8PathBuf,
}
#[derive(Debug, Parser)]
#[clap(name = "ostree-ext")]
#[clap(rename_all = "kebab-case")]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Opt {
#[clap(subcommand)]
Tar(TarOpts),
#[clap(subcommand)]
Container(ContainerOpts),
ImaSign(ImaSignOpts),
#[clap(hide(true), subcommand)]
#[cfg(feature = "internal-testing-api")]
InternalOnlyForTesting(TestingOpts),
#[clap(hide(true))]
#[cfg(feature = "docgen")]
Man(ManOpts),
#[clap(hide = true, subcommand)]
ProvisionalRepair(ProvisionalRepairOpts),
}
#[allow(clippy::from_over_into)]
impl Into<ostree_container::store::ImageProxyConfig> for ContainerProxyOpts {
fn into(self) -> ostree_container::store::ImageProxyConfig {
ostree_container::store::ImageProxyConfig {
auth_anonymous: self.auth_anonymous,
authfile: self.authfile,
certificate_directory: self.cert_dir,
insecure_skip_tls_verification: Some(self.insecure_skip_tls_verification),
..Default::default()
}
}
}
async fn tar_import(opts: &ImportOpts) -> Result<()> {
let repo = parse_repo(&opts.repo)?;
let imported = if let Some(path) = opts.path.as_ref() {
let instream = tokio::fs::File::open(path).await?;
crate::tar::import_tar(&repo, instream, None).await?
} else {
let stdin = tokio::io::stdin();
crate::tar::import_tar(&repo, stdin, None).await?
};
println!("Imported: {}", imported);
Ok(())
}
fn tar_export(opts: &ExportOpts) -> Result<()> {
let repo = parse_repo(&opts.repo)?;
#[allow(clippy::needless_update)]
let subopts = crate::tar::ExportOptions {
..Default::default()
};
crate::tar::export_commit(&repo, opts.rev.as_str(), std::io::stdout(), Some(subopts))?;
Ok(())
}
pub fn layer_progress_format(p: &ImportProgress) -> String {
let (starting, s, layer) = match p {
ImportProgress::OstreeChunkStarted(v) => (true, "ostree chunk", v),
ImportProgress::OstreeChunkCompleted(v) => (false, "ostree chunk", v),
ImportProgress::DerivedLayerStarted(v) => (true, "layer", v),
ImportProgress::DerivedLayerCompleted(v) => (false, "layer", v),
};
let short_digest = layer.digest().chars().take(12 + 7).collect::<String>();
if starting {
let size = glib::format_size(layer.size() as u64);
format!("Fetching {s} {short_digest} ({size})")
} else {
format!("Fetched {s} {short_digest}")
}
}
pub async fn handle_layer_progress_print(
mut layers: Receiver<ImportProgress>,
mut layer_bytes: tokio::sync::watch::Receiver<Option<LayerProgress>>,
) {
let style = indicatif::ProgressStyle::default_bar();
let pb = indicatif::ProgressBar::new(100);
pb.set_style(
style
.template("{prefix} {bytes} [{bar:20}] ({eta}) {msg}")
.unwrap(),
);
loop {
tokio::select! {
biased;
layer = layers.recv() => {
if let Some(l) = layer {
if l.is_starting() {
pb.set_position(0);
} else {
pb.finish();
}
pb.set_message(layer_progress_format(&l));
} else {
break
};
},
r = layer_bytes.changed() => {
if r.is_err() {
break
}
let bytes = layer_bytes.borrow();
if let Some(bytes) = &*bytes {
pb.set_length(bytes.total);
pb.set_position(bytes.fetched);
}
}
}
}
}
pub fn print_layer_status(prep: &PreparedImport) {
if let Some(status) = prep.format_layer_status() {
println!("{status}");
}
}
pub async fn print_deprecated_warning(msg: &str) {
eprintln!("warning: {msg}");
tokio::time::sleep(std::time::Duration::from_secs(3)).await
}
async fn container_import(
repo: &ostree::Repo,
imgref: &OstreeImageReference,
write_ref: Option<&str>,
quiet: bool,
) -> Result<()> {
let target = indicatif::ProgressDrawTarget::stdout();
let style = indicatif::ProgressStyle::default_bar();
let pb = (!quiet).then(|| {
let pb = indicatif::ProgressBar::new_spinner();
pb.set_draw_target(target);
pb.set_style(style.template("{spinner} {prefix} {msg}").unwrap());
pb.enable_steady_tick(std::time::Duration::from_millis(200));
pb.set_message("Downloading...");
pb
});
let importer = ImageImporter::new(repo, imgref, Default::default()).await?;
let import = importer.unencapsulate().await;
if let Some(pb) = pb.as_ref() {
pb.finish();
}
let import = import?;
if let Some(warning) = import.deprecated_warning.as_deref() {
print_deprecated_warning(warning).await;
}
if let Some(write_ref) = write_ref {
repo.set_ref_immediate(
None,
write_ref,
Some(import.ostree_commit.as_str()),
gio::Cancellable::NONE,
)?;
println!(
"Imported: {} => {}",
write_ref,
import.ostree_commit.as_str()
);
} else {
println!("Imported: {}", import.ostree_commit);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn container_export(
repo: &ostree::Repo,
rev: &str,
imgref: &ImageReference,
labels: BTreeMap<String, String>,
copy_meta_keys: Vec<String>,
copy_meta_opt_keys: Vec<String>,
cmd: Option<Vec<String>>,
compression_fast: bool,
) -> Result<()> {
let config = Config {
labels: Some(labels),
cmd,
};
let opts = crate::container::ExportOpts {
copy_meta_keys,
copy_meta_opt_keys,
skip_compression: compression_fast, ..Default::default()
};
let pushed =
crate::container::encapsulate(repo, rev, &config, None, Some(opts), None, imgref).await?;
println!("{}", pushed);
Ok(())
}
async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
let (_, digest) = crate::container::fetch_manifest(imgref).await?;
println!("{} digest: {}", imgref, digest);
Ok(())
}
async fn container_store(
repo: &ostree::Repo,
imgref: &OstreeImageReference,
proxyopts: ContainerProxyOpts,
quiet: bool,
) -> Result<()> {
let mut imp = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
let prep = match imp.prepare().await? {
PrepareResult::AlreadyPresent(c) => {
println!("No changes in {} => {}", imgref, c.merge_commit);
return Ok(());
}
PrepareResult::Ready(r) => r,
};
if let Some(warning) = prep.deprecated_warning() {
print_deprecated_warning(warning).await;
}
print_layer_status(&prep);
let printer = (!quiet).then(|| {
let layer_progress = imp.request_progress();
let layer_byte_progress = imp.request_layer_progress();
tokio::task::spawn(async move {
handle_layer_progress_print(layer_progress, layer_byte_progress).await
})
});
let import = imp.import(prep).await;
if let Some(printer) = printer {
let _ = printer.await;
}
let import = import?;
if let Some(msg) =
ostree_container::store::image_filtered_content_warning(repo, &imgref.imgref)?
{
eprintln!("{msg}")
}
println!("Wrote: {} => {}", imgref, import.merge_commit);
Ok(())
}
fn print_column(s: &str, clen: usize, remaining: &mut usize) {
let l = s.len().min(*remaining);
print!("{}", &s[0..l]);
if clen > 0 {
let pad = clen.saturating_sub(l) + 2;
for _ in 0..pad {
print!(" ");
}
*remaining = remaining.checked_sub(l + pad).unwrap();
}
}
async fn container_history(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
let img = crate::container::store::query_image_ref(repo, imgref)?
.ok_or_else(|| anyhow::anyhow!("No such image: {}", imgref))?;
let columns = [("ID", 20), ("SIZE", 10), ("CREATED BY", 0usize)];
let width = term_size::dimensions().map(|x| x.0).unwrap_or(80);
if let Some(config) = img.configuration.as_ref() {
{
let mut remaining = width;
for (name, width) in columns.iter() {
print_column(name, *width, &mut remaining);
}
println!();
}
let mut history = config.history().iter();
let layers = img.manifest.layers().iter();
for layer in layers {
let histent = history.next();
let created_by = histent
.and_then(|s| s.created_by().as_deref())
.unwrap_or("");
let mut remaining = width;
let digest = layer.digest().as_str();
assert!(digest.chars().all(|c| c.is_ascii()));
let digest_max = columns[0].1;
let digest = &digest[0..digest_max];
print_column(digest, digest_max, &mut remaining);
let size = glib::format_size(layer.size() as u64);
print_column(size.as_str(), columns[1].1, &mut remaining);
print_column(created_by, columns[2].1, &mut remaining);
println!();
}
Ok(())
} else {
anyhow::bail!("v0 image does not have fetched configuration");
}
}
fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> {
let cancellable = gio::Cancellable::NONE;
let signopts = crate::ima::ImaOpts {
algorithm: cmdopts.algorithm.clone(),
key: cmdopts.key.clone(),
overwrite: cmdopts.overwrite,
};
let repo = parse_repo(&cmdopts.repo)?;
let tx = repo.auto_transaction(cancellable)?;
let signed_commit = crate::ima::ima_sign(&repo, cmdopts.src_rev.as_str(), &signopts)?;
repo.transaction_set_ref(
None,
cmdopts.target_ref.as_str(),
Some(signed_commit.as_str()),
);
let _stats = tx.commit(cancellable)?;
println!("{} => {}", cmdopts.target_ref, signed_commit);
Ok(())
}
#[cfg(feature = "internal-testing-api")]
async fn testing(opts: &TestingOpts) -> Result<()> {
match opts {
TestingOpts::DetectEnv => {
println!("{}", crate::integrationtest::detectenv()?);
Ok(())
}
TestingOpts::CreateFixture => crate::integrationtest::create_fixture().await,
TestingOpts::Run => crate::integrationtest::run_tests(),
TestingOpts::RunIMA => crate::integrationtest::test_ima(),
TestingOpts::FilterTar => {
crate::tar::filter_tar(std::io::stdin(), std::io::stdout()).map(|_| {})
}
}
}
#[context("Remounting sysroot writable")]
fn container_remount_sysroot(sysroot: &Utf8Path) -> Result<()> {
if !Utf8Path::new("/run/.containerenv").exists() {
return Ok(());
}
println!("Running in container, assuming we can remount {sysroot} writable");
let st = Command::new("mount")
.args(["-o", "remount,rw", sysroot.as_str()])
.status()?;
if !st.success() {
anyhow::bail!("Failed to remount {sysroot}: {st:?}");
}
Ok(())
}
#[context("Serializing to output file")]
fn handle_serialize_to_file<T: serde::Serialize>(path: Option<&Utf8Path>, obj: T) -> Result<()> {
if let Some(path) = path {
let mut out = std::fs::File::create(path)
.map(BufWriter::new)
.with_context(|| anyhow::anyhow!("Opening {path} for writing"))?;
serde_json::to_writer(&mut out, &obj).context("Serializing output")?;
}
Ok(())
}
pub async fn run_from_iter<I>(args: I) -> Result<()>
where
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
let opt = Opt::parse_from(args);
match opt {
Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await,
Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt),
Opt::Container(o) => match o {
ContainerOpts::Info { imgref } => container_info(&imgref).await,
ContainerOpts::Commit {} => container_commit().await,
ContainerOpts::Unencapsulate {
repo,
imgref,
write_ref,
quiet,
} => {
let repo = parse_repo(&repo)?;
container_import(&repo, &imgref, write_ref.as_deref(), quiet).await
}
ContainerOpts::Encapsulate {
repo,
rev,
imgref,
labels,
copy_meta_keys,
copy_meta_opt_keys,
cmd,
compression_fast,
} => {
let labels: Result<BTreeMap<_, _>> = labels
.into_iter()
.map(|l| {
let (k, v) = l
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("Missing '=' in label {}", l))?;
Ok((k.to_string(), v.to_string()))
})
.collect();
let repo = parse_repo(&repo)?;
container_export(
&repo,
&rev,
&imgref,
labels?,
copy_meta_keys,
copy_meta_opt_keys,
cmd,
compression_fast,
)
.await
}
ContainerOpts::Image(opts) => match opts {
ContainerImageOpts::List { repo } => {
let repo = parse_repo(&repo)?;
for image in crate::container::store::list_images(&repo)? {
println!("{}", image);
}
Ok(())
}
ContainerImageOpts::Pull {
repo,
imgref,
proxyopts,
quiet,
} => {
let repo = parse_repo(&repo)?;
container_store(&repo, &imgref, proxyopts, quiet).await
}
ContainerImageOpts::History { repo, imgref } => {
let repo = parse_repo(&repo)?;
container_history(&repo, &imgref).await
}
ContainerImageOpts::Remove {
repo,
imgrefs,
skip_gc,
} => {
let nimgs = imgrefs.len();
let repo = parse_repo(&repo)?;
crate::container::store::remove_images(&repo, imgrefs.iter())?;
if !skip_gc {
let nlayers = crate::container::store::gc_image_layers(&repo)?;
println!("Removed images: {nimgs} layers: {nlayers}");
} else {
println!("Removed images: {nimgs}");
}
Ok(())
}
ContainerImageOpts::PruneLayers { repo } => {
let repo = parse_repo(&repo)?;
let nlayers = crate::container::store::gc_image_layers(&repo)?;
println!("Removed layers: {nlayers}");
Ok(())
}
ContainerImageOpts::PruneImages {
sysroot,
and_layers,
} => {
let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
sysroot.load(gio::Cancellable::NONE)?;
let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
let removed = crate::container::deploy::remove_undeployed_images(sysroot)?;
match removed.as_slice() {
[] => {
println!("No unreferenced images.");
return Ok(());
}
o => {
for imgref in o {
println!("Removed: {imgref}");
}
}
}
if and_layers {
let nlayers = crate::container::store::gc_image_layers(&sysroot.repo())?;
println!("Removed layers: {nlayers}");
}
Ok(())
}
ContainerImageOpts::Copy {
src_repo,
dest_repo,
imgref,
} => {
let src_repo = parse_repo(&src_repo)?;
let dest_repo = parse_repo(&dest_repo)?;
let imgref = &imgref.imgref;
crate::container::store::copy_as(&src_repo, imgref, &dest_repo, imgref).await
}
ContainerImageOpts::ReplaceDetachedMetadata {
src,
dest,
contents,
} => {
let contents = contents.map(std::fs::read).transpose()?;
let digest = crate::container::update_detached_metadata(
&src,
&dest,
contents.as_deref(),
)
.await?;
println!("Pushed: {}", digest);
Ok(())
}
ContainerImageOpts::Deploy {
sysroot,
stateroot,
imgref,
image,
transport,
no_signature_verification,
ostree_remote,
target_imgref,
no_imgref,
karg,
proxyopts,
write_commitid_to,
} => {
let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
sysroot.load(gio::Cancellable::NONE)?;
let repo = &sysroot.repo();
let kargs = karg.as_deref();
let kargs = kargs.map(|v| {
let r: Vec<_> = v.iter().map(|s| s.as_str()).collect();
r
});
let imgref = if let Some(image) = image {
let transport = transport.as_deref().unwrap_or("registry");
let transport = ostree_container::Transport::try_from(transport)?;
let imgref = ostree_container::ImageReference {
transport,
name: image,
};
let sigverify = if no_signature_verification {
ostree_container::SignatureSource::ContainerPolicyAllowInsecure
} else if let Some(remote) = ostree_remote.as_ref() {
ostree_container::SignatureSource::OstreeRemote(remote.to_string())
} else {
ostree_container::SignatureSource::ContainerPolicy
};
ostree_container::OstreeImageReference { sigverify, imgref }
} else {
let imgref = imgref.expect("imgref option should be set");
imgref.as_str().try_into()?
};
#[allow(clippy::needless_update)]
let options = crate::container::deploy::DeployOpts {
kargs: kargs.as_deref(),
target_imgref: target_imgref.as_ref(),
proxy_cfg: Some(proxyopts.into()),
no_imgref,
..Default::default()
};
let state = crate::container::deploy::deploy(
sysroot,
&stateroot,
&imgref,
Some(options),
)
.await?;
let wrote_imgref = target_imgref.as_ref().unwrap_or(&imgref);
if let Some(msg) = ostree_container::store::image_filtered_content_warning(
repo,
&wrote_imgref.imgref,
)? {
eprintln!("{msg}")
}
if let Some(p) = write_commitid_to {
std::fs::write(&p, state.merge_commit.as_bytes())
.with_context(|| format!("Failed to write commitid to {}", p))?;
}
Ok(())
}
},
ContainerOpts::Compare {
imgref_old,
imgref_new,
} => {
let (manifest_old, _) = crate::container::fetch_manifest(&imgref_old).await?;
let (manifest_new, _) = crate::container::fetch_manifest(&imgref_new).await?;
let manifest_diff =
crate::container::ManifestDiff::new(&manifest_old, &manifest_new);
manifest_diff.print();
Ok(())
}
},
Opt::ImaSign(ref opts) => ima_sign(opts),
#[cfg(feature = "internal-testing-api")]
Opt::InternalOnlyForTesting(ref opts) => testing(opts).await,
#[cfg(feature = "docgen")]
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
Opt::ProvisionalRepair(opts) => match opts {
ProvisionalRepairOpts::AnalyzeInodes {
repo,
verbose,
write_result_to,
} => {
let repo = parse_repo(&repo)?;
let check_res = crate::repair::check_inode_collision(&repo, verbose)?;
handle_serialize_to_file(write_result_to.as_deref(), &check_res)?;
if check_res.collisions.is_empty() {
println!("OK: No colliding objects found.");
} else {
eprintln!(
"warning: {} potentially colliding inodes found",
check_res.collisions.len()
);
}
Ok(())
}
ProvisionalRepairOpts::Repair {
sysroot,
verbose,
dry_run,
write_result_to,
} => {
container_remount_sysroot(&sysroot)?;
let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
sysroot.load(gio::Cancellable::NONE)?;
let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
let result = crate::repair::analyze_for_repair(sysroot, verbose)?;
handle_serialize_to_file(write_result_to.as_deref(), &result)?;
if dry_run {
result.check()
} else {
result.repair(sysroot)
}
}
},
}
}