ostree_ext/
cli.rs

1//! # Commandline parsing
2//!
3//! While there is a separate `ostree-ext-cli` crate that
4//! can be installed and used directly, the CLI code is
5//! also exported as a library too, so that projects
6//! such as `rpm-ostree` can directly reuse it.
7
8use anyhow::{Context, Result};
9use camino::{Utf8Path, Utf8PathBuf};
10use cap_std::fs::Dir;
11use cap_std_ext::cap_std;
12use cap_std_ext::prelude::CapStdExtDirExt;
13use clap::{Parser, Subcommand};
14use fn_error_context::context;
15use indexmap::IndexMap;
16use io_lifetimes::AsFd;
17use ostree::{gio, glib};
18use std::borrow::Cow;
19use std::collections::BTreeMap;
20use std::ffi::OsString;
21use std::fs::File;
22use std::io::{BufReader, BufWriter, Write};
23use std::num::NonZeroU32;
24use std::path::PathBuf;
25use std::process::Command;
26use tokio::sync::mpsc::Receiver;
27
28use crate::chunking::{ObjectMetaSized, ObjectSourceMetaSized};
29use crate::commit::container_commit;
30use crate::container::store::{ExportToOCIOpts, ImportProgress, LayerProgress, PreparedImport};
31use crate::container::{self as ostree_container, ManifestDiff};
32use crate::container::{Config, ImageReference, OstreeImageReference};
33use crate::objectsource::ObjectSourceMeta;
34use crate::sysroot::SysrootLock;
35use ostree_container::store::{ImageImporter, PrepareResult};
36use serde::{Deserialize, Serialize};
37
38/// Parse an [`OstreeImageReference`] from a CLI arguemnt.
39pub fn parse_imgref(s: &str) -> Result<OstreeImageReference> {
40    OstreeImageReference::try_from(s)
41}
42
43/// Parse a base [`ImageReference`] from a CLI arguemnt.
44pub fn parse_base_imgref(s: &str) -> Result<ImageReference> {
45    ImageReference::try_from(s)
46}
47
48/// Parse an [`ostree::Repo`] from a CLI arguemnt.
49pub fn parse_repo(s: &Utf8Path) -> Result<ostree::Repo> {
50    let repofd = cap_std::fs::Dir::open_ambient_dir(s, cap_std::ambient_authority())
51        .with_context(|| format!("Opening directory at '{s}'"))?;
52    ostree::Repo::open_at_dir(repofd.as_fd(), ".")
53        .with_context(|| format!("Opening ostree repository at '{s}'"))
54}
55
56/// Options for importing a tar archive.
57#[derive(Debug, Parser)]
58pub(crate) struct ImportOpts {
59    /// Path to the repository
60    #[clap(long, value_parser)]
61    repo: Utf8PathBuf,
62
63    /// Path to a tar archive; if unspecified, will be stdin.  Currently the tar archive must not be compressed.
64    path: Option<String>,
65}
66
67/// Options for exporting a tar archive.
68#[derive(Debug, Parser)]
69pub(crate) struct ExportOpts {
70    /// Path to the repository
71    #[clap(long, value_parser)]
72    repo: Utf8PathBuf,
73
74    /// The format version.  Must be 1.
75    #[clap(long, hide(true))]
76    format_version: u32,
77
78    /// The ostree ref or commit to export
79    rev: String,
80}
81
82/// Options for import/export to tar archives.
83#[derive(Debug, Subcommand)]
84pub(crate) enum TarOpts {
85    /// Import a tar archive (currently, must not be compressed)
86    Import(ImportOpts),
87
88    /// Write a tar archive to stdout
89    Export(ExportOpts),
90}
91
92/// Options for container import/export.
93#[derive(Debug, Subcommand)]
94pub(crate) enum ContainerOpts {
95    #[clap(alias = "import")]
96    /// Import an ostree commit embedded in a remote container image
97    Unencapsulate {
98        /// Path to the repository
99        #[clap(long, value_parser)]
100        repo: Utf8PathBuf,
101
102        #[clap(flatten)]
103        proxyopts: ContainerProxyOpts,
104
105        /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest
106        #[clap(value_parser = parse_imgref)]
107        imgref: OstreeImageReference,
108
109        /// Create an ostree ref pointing to the imported commit
110        #[clap(long)]
111        write_ref: Option<String>,
112
113        /// Don't display progress
114        #[clap(long)]
115        quiet: bool,
116    },
117
118    /// Print information about an exported ostree-container image.
119    Info {
120        /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest
121        #[clap(value_parser = parse_imgref)]
122        imgref: OstreeImageReference,
123    },
124
125    /// Wrap an ostree commit into a container image.
126    ///
127    /// The resulting container image will have a single layer, which is
128    /// very often not what's desired. To handle things more intelligently,
129    /// you will need to use (or create) a higher level tool that splits
130    /// content into distinct "chunks"; functionality for this is
131    /// exposed by the API but not CLI currently.
132    #[clap(alias = "export")]
133    Encapsulate {
134        /// Path to the repository
135        #[clap(long, value_parser)]
136        repo: Utf8PathBuf,
137
138        /// The ostree ref or commit to export
139        rev: String,
140
141        /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest
142        #[clap(value_parser = parse_base_imgref)]
143        imgref: ImageReference,
144
145        /// Additional labels for the container
146        #[clap(name = "label", long, short)]
147        labels: Vec<String>,
148
149        #[clap(long)]
150        /// Path to Docker-formatted authentication file.
151        authfile: Option<PathBuf>,
152
153        /// Path to a JSON-formatted serialized container configuration; this is the
154        /// `config` property of https://github.com/opencontainers/image-spec/blob/main/config.md
155        #[clap(long)]
156        config: Option<Utf8PathBuf>,
157
158        /// Propagate an OSTree commit metadata key to container label
159        #[clap(name = "copymeta", long)]
160        copy_meta_keys: Vec<String>,
161
162        /// Propagate an optionally-present OSTree commit metadata key to container label
163        #[clap(name = "copymeta-opt", long)]
164        copy_meta_opt_keys: Vec<String>,
165
166        /// Corresponds to the Dockerfile `CMD` instruction.
167        #[clap(long)]
168        cmd: Option<Vec<String>>,
169
170        /// Compress at the fastest level (e.g. gzip level 1)
171        #[clap(long)]
172        compression_fast: bool,
173
174        /// Path to a JSON-formatted content meta object.
175        #[clap(long)]
176        contentmeta: Option<Utf8PathBuf>,
177    },
178
179    /// Perform build-time checking and canonicalization.
180    /// This is presently an optional command, but may become required in the future.
181    Commit,
182
183    /// Commands for working with (possibly layered, non-encapsulated) container images.
184    #[clap(subcommand)]
185    Image(ContainerImageOpts),
186
187    /// Compare the contents of two OCI compliant images.
188    Compare {
189        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
190        #[clap(value_parser = parse_imgref)]
191        imgref_old: OstreeImageReference,
192
193        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
194        #[clap(value_parser = parse_imgref)]
195        imgref_new: OstreeImageReference,
196    },
197}
198
199/// Options for container image fetching.
200#[derive(Debug, Parser)]
201pub(crate) struct ContainerProxyOpts {
202    #[clap(long)]
203    /// Do not use default authentication files.
204    auth_anonymous: bool,
205
206    #[clap(long)]
207    /// Path to Docker-formatted authentication file.
208    authfile: Option<PathBuf>,
209
210    #[clap(long)]
211    /// Directory with certificates (*.crt, *.cert, *.key) used to connect to registry
212    /// Equivalent to `skopeo --cert-dir`
213    cert_dir: Option<PathBuf>,
214
215    #[clap(long)]
216    /// Skip TLS verification.
217    insecure_skip_tls_verification: bool,
218}
219
220/// Options for import/export to tar archives.
221#[derive(Debug, Subcommand)]
222pub(crate) enum ContainerImageOpts {
223    /// List container images
224    List {
225        /// Path to the repository
226        #[clap(long, value_parser)]
227        repo: Utf8PathBuf,
228    },
229
230    /// Pull (or update) a container image.
231    Pull {
232        /// Path to the repository
233        #[clap(value_parser)]
234        repo: Utf8PathBuf,
235
236        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
237        #[clap(value_parser = parse_imgref)]
238        imgref: OstreeImageReference,
239
240        #[clap(flatten)]
241        proxyopts: ContainerProxyOpts,
242
243        /// Don't display progress
244        #[clap(long)]
245        quiet: bool,
246
247        /// Just check for an updated manifest, but do not download associated container layers.
248        /// If an updated manifest is found, a file at the provided path will be created and contain
249        /// the new manifest.
250        #[clap(long)]
251        check: Option<Utf8PathBuf>,
252    },
253
254    /// Output metadata about an already stored container image.
255    History {
256        /// Path to the repository
257        #[clap(long, value_parser)]
258        repo: Utf8PathBuf,
259
260        /// Container image reference, e.g. registry:quay.io/exampleos/exampleos:latest
261        #[clap(value_parser = parse_base_imgref)]
262        imgref: ImageReference,
263    },
264
265    /// Output manifest or configuration for an already stored container image.
266    Metadata {
267        /// Path to the repository
268        #[clap(long, value_parser)]
269        repo: Utf8PathBuf,
270
271        /// Container image reference, e.g. registry:quay.io/exampleos/exampleos:latest
272        #[clap(value_parser = parse_base_imgref)]
273        imgref: ImageReference,
274
275        /// Output the config, not the manifest
276        #[clap(long)]
277        config: bool,
278    },
279
280    /// Copy a pulled container image from one repo to another.
281    Copy {
282        /// Path to the source repository
283        #[clap(long, value_parser)]
284        src_repo: Utf8PathBuf,
285
286        /// Path to the destination repository
287        #[clap(long, value_parser)]
288        dest_repo: Utf8PathBuf,
289
290        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
291        #[clap(value_parser = parse_imgref)]
292        imgref: OstreeImageReference,
293    },
294
295    /// Re-export a fetched image.
296    ///
297    /// Unlike `encapsulate`, this verb handles layered images, and will
298    /// also automatically preserve chunked structure from the fetched image.
299    Reexport {
300        /// Path to the repository
301        #[clap(long, value_parser)]
302        repo: Utf8PathBuf,
303
304        /// Source image reference, e.g. registry:quay.io/exampleos/exampleos:latest
305        #[clap(value_parser = parse_base_imgref)]
306        src_imgref: ImageReference,
307
308        /// Destination image reference, e.g. registry:quay.io/exampleos/exampleos:latest
309        #[clap(value_parser = parse_base_imgref)]
310        dest_imgref: ImageReference,
311
312        #[clap(long)]
313        /// Path to Docker-formatted authentication file.
314        authfile: Option<PathBuf>,
315
316        /// Compress at the fastest level (e.g. gzip level 1)
317        #[clap(long)]
318        compression_fast: bool,
319    },
320
321    /// Replace the detached metadata (e.g. to add a signature)
322    ReplaceDetachedMetadata {
323        /// Path to the source repository
324        #[clap(long)]
325        #[clap(value_parser = parse_base_imgref)]
326        src: ImageReference,
327
328        /// Target image
329        #[clap(long)]
330        #[clap(value_parser = parse_base_imgref)]
331        dest: ImageReference,
332
333        /// Path to file containing new detached metadata; if not provided,
334        /// any existing detached metadata will be deleted.
335        contents: Option<Utf8PathBuf>,
336    },
337
338    /// Unreference one or more pulled container images and perform a garbage collection.
339    Remove {
340        /// Path to the repository
341        #[clap(long, value_parser)]
342        repo: Utf8PathBuf,
343
344        /// Image reference, e.g. quay.io/exampleos/exampleos:latest
345        #[clap(value_parser = parse_base_imgref)]
346        imgrefs: Vec<ImageReference>,
347
348        /// Do not garbage collect unused layers
349        #[clap(long)]
350        skip_gc: bool,
351    },
352
353    /// Garbage collect unreferenced image layer references.
354    PruneLayers {
355        /// Path to the repository
356        #[clap(long, value_parser)]
357        repo: Utf8PathBuf,
358    },
359
360    /// Garbage collect unreferenced image layer references.
361    PruneImages {
362        /// Path to the system root
363        #[clap(long)]
364        sysroot: Utf8PathBuf,
365
366        #[clap(long)]
367        /// Also prune layers
368        and_layers: bool,
369
370        #[clap(long, conflicts_with = "and_layers")]
371        /// Also prune layers and OSTree objects
372        full: bool,
373    },
374
375    /// Perform initial deployment for a container image
376    Deploy {
377        /// Path to the system root
378        #[clap(long)]
379        sysroot: Option<String>,
380
381        /// Name for the state directory, also known as "osname".
382        /// If the current system is booted via ostree, then this will default to the booted stateroot.
383        /// Otherwise, the default is `default`.
384        #[clap(long)]
385        stateroot: Option<String>,
386
387        /// Source image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos@sha256:abcd...
388        /// This conflicts with `--image`.
389        /// This conflicts with `--image`. Supports `registry:`, `docker://`, `oci:`, `oci-archive:`, `containers-storage:`, and `dir:`
390        #[clap(long, required_unless_present = "image")]
391        imgref: Option<String>,
392
393        /// Name of the container image; for the `registry` transport this would be e.g. `quay.io/exampleos/foo:latest`.
394        /// This conflicts with `--imgref`.
395        #[clap(long, required_unless_present = "imgref")]
396        image: Option<String>,
397
398        /// The transport; e.g. registry, oci, oci-archive.  The default is `registry`.
399        #[clap(long)]
400        transport: Option<String>,
401
402        /// This option does nothing and is now deprecated.  Signature verification enforcement
403        /// proved to not be viable.
404        ///
405        /// If you want to still enforce it, use `--enforce-container-sigpolicy`.
406        #[clap(long, conflicts_with = "enforce_container_sigpolicy")]
407        no_signature_verification: bool,
408
409        /// Require that the containers-storage stack
410        #[clap(long)]
411        enforce_container_sigpolicy: bool,
412
413        /// Enable verification via an ostree remote
414        #[clap(long)]
415        ostree_remote: Option<String>,
416
417        #[clap(flatten)]
418        proxyopts: ContainerProxyOpts,
419
420        /// Target image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
421        ///
422        /// If specified, `--imgref` will be used as a source, but this reference will be emitted into the origin
423        /// so that later OS updates pull from it.
424        #[clap(long)]
425        #[clap(value_parser = parse_imgref)]
426        target_imgref: Option<OstreeImageReference>,
427
428        /// If set, only write the layer refs, but not the final container image reference.  This
429        /// allows generating a disk image that when booted uses "native ostree", but has layer
430        /// references "pre-cached" such that a container image fetch will avoid redownloading
431        /// everything.
432        #[clap(long)]
433        no_imgref: bool,
434
435        #[clap(long)]
436        /// Add a kernel argument
437        karg: Option<Vec<String>>,
438
439        /// Write the deployed checksum to this file
440        #[clap(long)]
441        write_commitid_to: Option<Utf8PathBuf>,
442    },
443}
444
445/// Options for deployment repair.
446#[derive(Debug, Parser)]
447pub(crate) enum ProvisionalRepairOpts {
448    AnalyzeInodes {
449        /// Path to the repository
450        #[clap(long, value_parser)]
451        repo: Utf8PathBuf,
452
453        /// Print additional information
454        #[clap(long)]
455        verbose: bool,
456
457        /// Serialize the repair result to this file as JSON
458        #[clap(long)]
459        write_result_to: Option<Utf8PathBuf>,
460    },
461
462    Repair {
463        /// Path to the sysroot
464        #[clap(long, value_parser)]
465        sysroot: Utf8PathBuf,
466
467        /// Do not mutate any system state
468        #[clap(long)]
469        dry_run: bool,
470
471        /// Serialize the repair result to this file as JSON
472        #[clap(long)]
473        write_result_to: Option<Utf8PathBuf>,
474
475        /// Print additional information
476        #[clap(long)]
477        verbose: bool,
478    },
479}
480
481/// Options for the Integrity Measurement Architecture (IMA).
482#[derive(Debug, Parser)]
483pub(crate) struct ImaSignOpts {
484    /// Path to the repository
485    #[clap(long, value_parser)]
486    repo: Utf8PathBuf,
487
488    /// The ostree ref or commit to use as a base
489    src_rev: String,
490    /// The ostree ref to use for writing the signed commit
491    target_ref: String,
492
493    /// Digest algorithm
494    algorithm: String,
495    /// Path to IMA key
496    key: Utf8PathBuf,
497
498    #[clap(long)]
499    /// Overwrite any existing signatures
500    overwrite: bool,
501}
502
503/// Options for internal testing
504#[derive(Debug, Subcommand)]
505pub(crate) enum TestingOpts {
506    /// Detect the current environment
507    DetectEnv,
508    /// Generate a test fixture
509    CreateFixture,
510    /// Execute integration tests, assuming mutable environment
511    Run,
512    /// Execute IMA tests
513    RunIMA,
514    FilterTar,
515}
516
517/// Options for man page generation
518#[derive(Debug, Parser)]
519pub(crate) struct ManOpts {
520    #[clap(long)]
521    /// Output to this directory
522    directory: Utf8PathBuf,
523}
524
525/// Toplevel options for extended ostree functionality.
526#[derive(Debug, Parser)]
527#[clap(name = "ostree-ext")]
528#[clap(rename_all = "kebab-case")]
529#[allow(clippy::large_enum_variant)]
530pub(crate) enum Opt {
531    /// Import and export to tar
532    #[clap(subcommand)]
533    Tar(TarOpts),
534    /// Import and export to a container image
535    #[clap(subcommand)]
536    Container(ContainerOpts),
537    /// IMA signatures
538    ImaSign(ImaSignOpts),
539    /// Internal integration testing helpers.
540    #[clap(hide(true), subcommand)]
541    #[cfg(feature = "internal-testing-api")]
542    InternalOnlyForTesting(TestingOpts),
543    #[clap(hide(true))]
544    #[cfg(feature = "docgen")]
545    Man(ManOpts),
546    #[clap(hide = true, subcommand)]
547    ProvisionalRepair(ProvisionalRepairOpts),
548}
549
550#[allow(clippy::from_over_into)]
551impl Into<ostree_container::store::ImageProxyConfig> for ContainerProxyOpts {
552    fn into(self) -> ostree_container::store::ImageProxyConfig {
553        ostree_container::store::ImageProxyConfig {
554            auth_anonymous: self.auth_anonymous,
555            authfile: self.authfile,
556            certificate_directory: self.cert_dir,
557            insecure_skip_tls_verification: Some(self.insecure_skip_tls_verification),
558            ..Default::default()
559        }
560    }
561}
562
563/// Import a tar archive containing an ostree commit.
564async fn tar_import(opts: &ImportOpts) -> Result<()> {
565    let repo = parse_repo(&opts.repo)?;
566    let imported = if let Some(path) = opts.path.as_ref() {
567        let instream = tokio::fs::File::open(path).await?;
568        crate::tar::import_tar(&repo, instream, None).await?
569    } else {
570        let stdin = tokio::io::stdin();
571        crate::tar::import_tar(&repo, stdin, None).await?
572    };
573    println!("Imported: {}", imported);
574    Ok(())
575}
576
577/// Export a tar archive containing an ostree commit.
578fn tar_export(opts: &ExportOpts) -> Result<()> {
579    let repo = parse_repo(&opts.repo)?;
580    #[allow(clippy::needless_update)]
581    let subopts = crate::tar::ExportOptions {
582        ..Default::default()
583    };
584    crate::tar::export_commit(&repo, opts.rev.as_str(), std::io::stdout(), Some(subopts))?;
585    Ok(())
586}
587
588/// Render an import progress notification as a string.
589pub fn layer_progress_format(p: &ImportProgress) -> String {
590    let (starting, s, layer) = match p {
591        ImportProgress::OstreeChunkStarted(v) => (true, "ostree chunk", v),
592        ImportProgress::OstreeChunkCompleted(v) => (false, "ostree chunk", v),
593        ImportProgress::DerivedLayerStarted(v) => (true, "layer", v),
594        ImportProgress::DerivedLayerCompleted(v) => (false, "layer", v),
595    };
596    // podman outputs 12 characters of digest, let's add 7 for `sha256:`.
597    let short_digest = layer
598        .digest()
599        .digest()
600        .chars()
601        .take(12 + 7)
602        .collect::<String>();
603    if starting {
604        let size = glib::format_size(layer.size());
605        format!("Fetching {s} {short_digest} ({size})")
606    } else {
607        format!("Fetched {s} {short_digest}")
608    }
609}
610
611/// Write container fetch progress to standard output.
612pub async fn handle_layer_progress_print(
613    mut layers: Receiver<ImportProgress>,
614    mut layer_bytes: tokio::sync::watch::Receiver<Option<LayerProgress>>,
615) {
616    let style = indicatif::ProgressStyle::default_bar();
617    let pb = indicatif::ProgressBar::new(100);
618    pb.set_style(
619        style
620            .template("{prefix} {bytes} [{bar:20}] ({eta}) {msg}")
621            .unwrap(),
622    );
623    loop {
624        tokio::select! {
625            // Always handle layer changes first.
626            biased;
627            layer = layers.recv() => {
628                if let Some(l) = layer {
629                    if l.is_starting() {
630                        pb.set_position(0);
631                    } else {
632                        pb.finish();
633                    }
634                    pb.set_message(layer_progress_format(&l));
635                } else {
636                    // If the receiver is disconnected, then we're done
637                    break
638                };
639            },
640            r = layer_bytes.changed() => {
641                if r.is_err() {
642                    // If the receiver is disconnected, then we're done
643                    break
644                }
645                let bytes = layer_bytes.borrow();
646                if let Some(bytes) = &*bytes {
647                    pb.set_length(bytes.total);
648                    pb.set_position(bytes.fetched);
649                }
650            }
651
652        }
653    }
654}
655
656/// Write the status of layers to download.
657pub fn print_layer_status(prep: &PreparedImport) {
658    if let Some(status) = prep.format_layer_status() {
659        println!("{status}");
660    }
661}
662
663/// Write a deprecation notice, and sleep for 3 seconds.
664pub async fn print_deprecated_warning(msg: &str) {
665    eprintln!("warning: {msg}");
666    tokio::time::sleep(std::time::Duration::from_secs(3)).await
667}
668
669/// Import a container image with an encapsulated ostree commit.
670async fn container_import(
671    repo: &ostree::Repo,
672    imgref: &OstreeImageReference,
673    proxyopts: ContainerProxyOpts,
674    write_ref: Option<&str>,
675    quiet: bool,
676) -> Result<()> {
677    let target = indicatif::ProgressDrawTarget::stdout();
678    let style = indicatif::ProgressStyle::default_bar();
679    let pb = (!quiet).then(|| {
680        let pb = indicatif::ProgressBar::new_spinner();
681        pb.set_draw_target(target);
682        pb.set_style(style.template("{spinner} {prefix} {msg}").unwrap());
683        pb.enable_steady_tick(std::time::Duration::from_millis(200));
684        pb.set_message("Downloading...");
685        pb
686    });
687    let importer = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
688    let import = importer.unencapsulate().await;
689    // Ensure we finish the progress bar before potentially propagating an error
690    if let Some(pb) = pb.as_ref() {
691        pb.finish();
692    }
693    let import = import?;
694    if let Some(warning) = import.deprecated_warning.as_deref() {
695        print_deprecated_warning(warning).await;
696    }
697    if let Some(write_ref) = write_ref {
698        repo.set_ref_immediate(
699            None,
700            write_ref,
701            Some(import.ostree_commit.as_str()),
702            gio::Cancellable::NONE,
703        )?;
704        println!(
705            "Imported: {} => {}",
706            write_ref,
707            import.ostree_commit.as_str()
708        );
709    } else {
710        println!("Imported: {}", import.ostree_commit);
711    }
712
713    Ok(())
714}
715
716/// Grouping of metadata about an object.
717#[derive(Debug, Default, Serialize, Deserialize)]
718pub struct RawMeta {
719    /// The metadata format version. Should be set to 1.
720    pub version: u32,
721    /// The image creation timestamp. Format is YYYY-MM-DDTHH:MM:SSZ.
722    /// Should be synced with the label io.container.image.created.
723    pub created: Option<String>,
724    /// Top level labels, to be prefixed to the ones with --label
725    /// Applied to both the outer config annotations and the inner config labels.
726    pub labels: Option<BTreeMap<String, String>>,
727    /// The output layers ordered. Provided as an ordered mapping of a unique
728    /// machine readable strings to a human readable name (e.g., the layer contents).
729    /// The human-readable name is placed in a layer annotation.
730    pub layers: IndexMap<String, String>,
731    /// The layer contents. The key is an ostree hash and the value is the
732    /// machine readable string of the layer the hash belongs to.
733    /// WARNING: needs to contain all ostree hashes in the input commit.
734    pub mapping: IndexMap<String, String>,
735    /// Whether the mapping is ordered. If true, the output tar stream of the
736    /// layers will reflect the order of the hashes in the mapping.
737    /// Otherwise, a deterministic ordering will be used regardless of mapping
738    /// order. Potentially useful for optimizing zstd:chunked compression.
739    /// WARNING: not currently supported.
740    pub ordered: Option<bool>,
741}
742
743/// Export a container image with an encapsulated ostree commit.
744#[allow(clippy::too_many_arguments)]
745async fn container_export(
746    repo: &ostree::Repo,
747    rev: &str,
748    imgref: &ImageReference,
749    labels: BTreeMap<String, String>,
750    authfile: Option<PathBuf>,
751    copy_meta_keys: Vec<String>,
752    copy_meta_opt_keys: Vec<String>,
753    container_config: Option<Utf8PathBuf>,
754    cmd: Option<Vec<String>>,
755    compression_fast: bool,
756    contentmeta: Option<Utf8PathBuf>,
757) -> Result<()> {
758    let container_config = if let Some(container_config) = container_config {
759        serde_json::from_reader(File::open(container_config).map(BufReader::new)?)?
760    } else {
761        None
762    };
763
764    let mut contentmeta_data = None;
765    let mut created = None;
766    let mut labels = labels.clone();
767    if let Some(contentmeta) = contentmeta {
768        let buf = File::open(contentmeta).map(BufReader::new);
769        let raw: RawMeta = serde_json::from_reader(buf?)?;
770
771        // Check future variables are set correctly
772        let supported_version = 1;
773        if raw.version != supported_version {
774            return Err(anyhow::anyhow!(
775                "Unsupported metadata version: {}. Currently supported: {}",
776                raw.version,
777                supported_version
778            ));
779        }
780        if let Some(ordered) = raw.ordered {
781            if ordered {
782                return Err(anyhow::anyhow!("Ordered mapping not currently supported."));
783            }
784        }
785
786        created = raw.created;
787        contentmeta_data = Some(ObjectMetaSized {
788            map: raw
789                .mapping
790                .into_iter()
791                .map(|(k, v)| (k, v.into()))
792                .collect(),
793            sizes: raw
794                .layers
795                .into_iter()
796                .map(|(k, v)| ObjectSourceMetaSized {
797                    meta: ObjectSourceMeta {
798                        identifier: k.clone().into(),
799                        name: v.into(),
800                        srcid: k.clone().into(),
801                        change_frequency: if k == "unpackaged" { u32::MAX } else { 1 },
802                        change_time_offset: 1,
803                    },
804                    size: 1,
805                })
806                .collect(),
807        });
808
809        // Merge --label args to the labels from the metadata
810        labels.extend(raw.labels.into_iter().flatten());
811    }
812
813    // Use enough layers so that each package ends in its own layer
814    // while respecting the layer ordering.
815    let max_layers = if let Some(contentmeta_data) = &contentmeta_data {
816        NonZeroU32::new((contentmeta_data.sizes.len() + 1).try_into().unwrap())
817    } else {
818        None
819    };
820
821    let config = Config {
822        labels: Some(labels),
823        cmd,
824    };
825
826    let opts = crate::container::ExportOpts {
827        copy_meta_keys,
828        copy_meta_opt_keys,
829        container_config,
830        authfile,
831        skip_compression: compression_fast, // TODO rename this in the struct at the next semver break
832        contentmeta: contentmeta_data.as_ref(),
833        max_layers,
834        created,
835        ..Default::default()
836    };
837    let pushed = crate::container::encapsulate(repo, rev, &config, Some(opts), imgref).await?;
838    println!("{}", pushed);
839    Ok(())
840}
841
842/// Load metadata for a container image with an encapsulated ostree commit.
843async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
844    let (_, digest) = crate::container::fetch_manifest(imgref).await?;
845    println!("{} digest: {}", imgref, digest);
846    Ok(())
847}
848
849/// Write a layered container image into an OSTree commit.
850async fn container_store(
851    repo: &ostree::Repo,
852    imgref: &OstreeImageReference,
853    proxyopts: ContainerProxyOpts,
854    quiet: bool,
855    check: Option<Utf8PathBuf>,
856) -> Result<()> {
857    let mut imp = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
858    let prep = match imp.prepare().await? {
859        PrepareResult::AlreadyPresent(c) => {
860            println!("No changes in {} => {}", imgref, c.merge_commit);
861            return Ok(());
862        }
863        PrepareResult::Ready(r) => r,
864    };
865    if let Some(warning) = prep.deprecated_warning() {
866        print_deprecated_warning(warning).await;
867    }
868    if let Some(check) = check.as_deref() {
869        let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
870        rootfs.atomic_replace_with(check.as_str().trim_start_matches('/'), |w| {
871            serde_json::to_writer(w, &prep.manifest).context("Serializing manifest")
872        })?;
873        // In check mode, we're done
874        return Ok(());
875    }
876    if let Some(previous_state) = prep.previous_state.as_ref() {
877        let diff = ManifestDiff::new(&previous_state.manifest, &prep.manifest);
878        diff.print();
879    }
880    print_layer_status(&prep);
881    let printer = (!quiet).then(|| {
882        let layer_progress = imp.request_progress();
883        let layer_byte_progress = imp.request_layer_progress();
884        tokio::task::spawn(async move {
885            handle_layer_progress_print(layer_progress, layer_byte_progress).await
886        })
887    });
888    let import = imp.import(prep).await;
889    if let Some(printer) = printer {
890        let _ = printer.await;
891    }
892    let import = import?;
893    if let Some(msg) =
894        ostree_container::store::image_filtered_content_warning(repo, &imgref.imgref)?
895    {
896        eprintln!("{msg}")
897    }
898    println!("Wrote: {} => {}", imgref, import.merge_commit);
899    Ok(())
900}
901
902fn print_column(s: &str, clen: u16, remaining: &mut terminal_size::Width) {
903    let l: u16 = s.len().try_into().unwrap();
904    let l = l.min(remaining.0);
905    print!("{}", &s[0..l as usize]);
906    if clen > 0 {
907        // We always want two trailing spaces
908        let pad = clen.saturating_sub(l) + 2;
909        for _ in 0..pad {
910            print!(" ");
911        }
912        remaining.0 = remaining.0.checked_sub(l + pad).unwrap();
913    }
914}
915
916/// Output the container image history
917async fn container_history(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
918    let img = crate::container::store::query_image(repo, imgref)?
919        .ok_or_else(|| anyhow::anyhow!("No such image: {}", imgref))?;
920    let columns = [("ID", 20u16), ("SIZE", 10), ("CREATED BY", 0)];
921    let width = terminal_size::terminal_size()
922        .map(|x| x.0)
923        .unwrap_or(terminal_size::Width(80));
924    {
925        let mut remaining = width;
926        for (name, width) in columns.iter() {
927            print_column(name, *width, &mut remaining);
928        }
929        println!();
930    }
931
932    let mut history = img.configuration.history().iter();
933    let layers = img.manifest.layers().iter();
934    for layer in layers {
935        let histent = history.next();
936        let created_by = histent
937            .and_then(|s| s.created_by().as_deref())
938            .unwrap_or("");
939
940        let mut remaining = width;
941
942        let digest = layer.digest().digest();
943        // Verify it's OK to slice, this should all be ASCII
944        assert!(digest.is_ascii());
945        let digest_max = columns[0].1;
946        let digest = &digest[0..digest_max as usize];
947        print_column(digest, digest_max, &mut remaining);
948        let size = glib::format_size(layer.size());
949        print_column(size.as_str(), columns[1].1, &mut remaining);
950        print_column(created_by, columns[2].1, &mut remaining);
951        println!();
952    }
953    Ok(())
954}
955
956/// Add IMA signatures to an ostree commit, generating a new commit.
957fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> {
958    let cancellable = gio::Cancellable::NONE;
959    let signopts = crate::ima::ImaOpts {
960        algorithm: cmdopts.algorithm.clone(),
961        key: cmdopts.key.clone(),
962        overwrite: cmdopts.overwrite,
963    };
964    let repo = parse_repo(&cmdopts.repo)?;
965    let tx = repo.auto_transaction(cancellable)?;
966    let signed_commit = crate::ima::ima_sign(&repo, cmdopts.src_rev.as_str(), &signopts)?;
967    repo.transaction_set_ref(
968        None,
969        cmdopts.target_ref.as_str(),
970        Some(signed_commit.as_str()),
971    );
972    let _stats = tx.commit(cancellable)?;
973    println!("{} => {}", cmdopts.target_ref, signed_commit);
974    Ok(())
975}
976
977#[cfg(feature = "internal-testing-api")]
978async fn testing(opts: &TestingOpts) -> Result<()> {
979    match opts {
980        TestingOpts::DetectEnv => {
981            println!("{}", crate::integrationtest::detectenv()?);
982            Ok(())
983        }
984        TestingOpts::CreateFixture => crate::integrationtest::create_fixture().await,
985        TestingOpts::Run => crate::integrationtest::run_tests(),
986        TestingOpts::RunIMA => crate::integrationtest::test_ima(),
987        TestingOpts::FilterTar => {
988            let tmpdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
989            crate::tar::filter_tar(
990                std::io::stdin(),
991                std::io::stdout(),
992                &Default::default(),
993                &tmpdir,
994            )
995            .map(|_| {})
996        }
997    }
998}
999
1000// Quick hack; TODO dedup this with the code in bootc or lower here
1001#[context("Remounting sysroot writable")]
1002fn container_remount_sysroot(sysroot: &Utf8Path) -> Result<()> {
1003    if !Utf8Path::new("/run/.containerenv").exists() {
1004        return Ok(());
1005    }
1006    println!("Running in container, assuming we can remount {sysroot} writable");
1007    let st = Command::new("mount")
1008        .args(["-o", "remount,rw", sysroot.as_str()])
1009        .status()?;
1010    if !st.success() {
1011        anyhow::bail!("Failed to remount {sysroot}: {st:?}");
1012    }
1013    Ok(())
1014}
1015
1016#[context("Serializing to output file")]
1017fn handle_serialize_to_file<T: serde::Serialize>(path: Option<&Utf8Path>, obj: T) -> Result<()> {
1018    if let Some(path) = path {
1019        let mut out = std::fs::File::create(path)
1020            .map(BufWriter::new)
1021            .with_context(|| anyhow::anyhow!("Opening {path} for writing"))?;
1022        serde_json::to_writer(&mut out, &obj).context("Serializing output")?;
1023    }
1024    Ok(())
1025}
1026
1027/// Parse the provided arguments and execute.
1028/// Calls [`clap::Error::exit`] on failure, printing the error message and aborting the program.
1029pub async fn run_from_iter<I>(args: I) -> Result<()>
1030where
1031    I: IntoIterator,
1032    I::Item: Into<OsString> + Clone,
1033{
1034    run_from_opt(Opt::parse_from(args)).await
1035}
1036
1037async fn run_from_opt(opt: Opt) -> Result<()> {
1038    match opt {
1039        Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await,
1040        Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt),
1041        Opt::Container(o) => match o {
1042            ContainerOpts::Info { imgref } => container_info(&imgref).await,
1043            ContainerOpts::Commit {} => container_commit().await,
1044            ContainerOpts::Unencapsulate {
1045                repo,
1046                imgref,
1047                proxyopts,
1048                write_ref,
1049                quiet,
1050            } => {
1051                let repo = parse_repo(&repo)?;
1052                container_import(&repo, &imgref, proxyopts, write_ref.as_deref(), quiet).await
1053            }
1054            ContainerOpts::Encapsulate {
1055                repo,
1056                rev,
1057                imgref,
1058                labels,
1059                authfile,
1060                copy_meta_keys,
1061                copy_meta_opt_keys,
1062                config,
1063                cmd,
1064                compression_fast,
1065                contentmeta,
1066            } => {
1067                let labels: Result<BTreeMap<_, _>> = labels
1068                    .into_iter()
1069                    .map(|l| {
1070                        let (k, v) = l
1071                            .split_once('=')
1072                            .ok_or_else(|| anyhow::anyhow!("Missing '=' in label {}", l))?;
1073                        Ok((k.to_string(), v.to_string()))
1074                    })
1075                    .collect();
1076                let repo = parse_repo(&repo)?;
1077                container_export(
1078                    &repo,
1079                    &rev,
1080                    &imgref,
1081                    labels?,
1082                    authfile,
1083                    copy_meta_keys,
1084                    copy_meta_opt_keys,
1085                    config,
1086                    cmd,
1087                    compression_fast,
1088                    contentmeta,
1089                )
1090                .await
1091            }
1092            ContainerOpts::Image(opts) => match opts {
1093                ContainerImageOpts::List { repo } => {
1094                    let repo = parse_repo(&repo)?;
1095                    for image in crate::container::store::list_images(&repo)? {
1096                        println!("{}", image);
1097                    }
1098                    Ok(())
1099                }
1100                ContainerImageOpts::Pull {
1101                    repo,
1102                    imgref,
1103                    proxyopts,
1104                    quiet,
1105                    check,
1106                } => {
1107                    let repo = parse_repo(&repo)?;
1108                    container_store(&repo, &imgref, proxyopts, quiet, check).await
1109                }
1110                ContainerImageOpts::Reexport {
1111                    repo,
1112                    src_imgref,
1113                    dest_imgref,
1114                    authfile,
1115                    compression_fast,
1116                } => {
1117                    let repo = &parse_repo(&repo)?;
1118                    let opts = ExportToOCIOpts {
1119                        authfile,
1120                        skip_compression: compression_fast,
1121                        ..Default::default()
1122                    };
1123                    let digest = ostree_container::store::export(
1124                        repo,
1125                        &src_imgref,
1126                        &dest_imgref,
1127                        Some(opts),
1128                    )
1129                    .await?;
1130                    println!("Exported: {digest}");
1131                    Ok(())
1132                }
1133                ContainerImageOpts::History { repo, imgref } => {
1134                    let repo = parse_repo(&repo)?;
1135                    container_history(&repo, &imgref).await
1136                }
1137                ContainerImageOpts::Metadata {
1138                    repo,
1139                    imgref,
1140                    config,
1141                } => {
1142                    let repo = parse_repo(&repo)?;
1143                    let image = crate::container::store::query_image(&repo, &imgref)?
1144                        .ok_or_else(|| anyhow::anyhow!("No such image"))?;
1145                    let stdout = std::io::stdout().lock();
1146                    let mut stdout = std::io::BufWriter::new(stdout);
1147                    if config {
1148                        serde_json::to_writer(&mut stdout, &image.configuration)?;
1149                    } else {
1150                        serde_json::to_writer(&mut stdout, &image.manifest)?;
1151                    }
1152                    stdout.flush()?;
1153                    Ok(())
1154                }
1155                ContainerImageOpts::Remove {
1156                    repo,
1157                    imgrefs,
1158                    skip_gc,
1159                } => {
1160                    let nimgs = imgrefs.len();
1161                    let repo = parse_repo(&repo)?;
1162                    crate::container::store::remove_images(&repo, imgrefs.iter())?;
1163                    if !skip_gc {
1164                        let nlayers = crate::container::store::gc_image_layers(&repo)?;
1165                        println!("Removed images: {nimgs} layers: {nlayers}");
1166                    } else {
1167                        println!("Removed images: {nimgs}");
1168                    }
1169                    Ok(())
1170                }
1171                ContainerImageOpts::PruneLayers { repo } => {
1172                    let repo = parse_repo(&repo)?;
1173                    let nlayers = crate::container::store::gc_image_layers(&repo)?;
1174                    println!("Removed layers: {nlayers}");
1175                    Ok(())
1176                }
1177                ContainerImageOpts::PruneImages {
1178                    sysroot,
1179                    and_layers,
1180                    full,
1181                } => {
1182                    let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1183                    sysroot.load(gio::Cancellable::NONE)?;
1184                    let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1185                    if full {
1186                        let res = crate::container::deploy::prune(sysroot)?;
1187                        if res.is_empty() {
1188                            println!("No content was pruned.");
1189                        } else {
1190                            println!("Removed images: {}", res.n_images);
1191                            println!("Removed layers: {}", res.n_layers);
1192                            println!("Removed objects: {}", res.n_objects_pruned);
1193                            let objsize = glib::format_size(res.objsize);
1194                            println!("Freed: {objsize}");
1195                        }
1196                    } else {
1197                        let removed = crate::container::deploy::remove_undeployed_images(sysroot)?;
1198                        match removed.as_slice() {
1199                            [] => {
1200                                println!("No unreferenced images.");
1201                                return Ok(());
1202                            }
1203                            o => {
1204                                for imgref in o {
1205                                    println!("Removed: {imgref}");
1206                                }
1207                            }
1208                        }
1209                        if and_layers {
1210                            let nlayers =
1211                                crate::container::store::gc_image_layers(&sysroot.repo())?;
1212                            println!("Removed layers: {nlayers}");
1213                        }
1214                    }
1215                    Ok(())
1216                }
1217                ContainerImageOpts::Copy {
1218                    src_repo,
1219                    dest_repo,
1220                    imgref,
1221                } => {
1222                    let src_repo = parse_repo(&src_repo)?;
1223                    let dest_repo = parse_repo(&dest_repo)?;
1224                    let imgref = &imgref.imgref;
1225                    crate::container::store::copy(&src_repo, imgref, &dest_repo, imgref).await
1226                }
1227                ContainerImageOpts::ReplaceDetachedMetadata {
1228                    src,
1229                    dest,
1230                    contents,
1231                } => {
1232                    let contents = contents.map(std::fs::read).transpose()?;
1233                    let digest = crate::container::update_detached_metadata(
1234                        &src,
1235                        &dest,
1236                        contents.as_deref(),
1237                    )
1238                    .await?;
1239                    println!("Pushed: {}", digest);
1240                    Ok(())
1241                }
1242                ContainerImageOpts::Deploy {
1243                    sysroot,
1244                    stateroot,
1245                    imgref,
1246                    image,
1247                    transport,
1248                    mut no_signature_verification,
1249                    enforce_container_sigpolicy,
1250                    ostree_remote,
1251                    target_imgref,
1252                    no_imgref,
1253                    karg,
1254                    proxyopts,
1255                    write_commitid_to,
1256                } => {
1257                    // As of recent releases, signature verification enforcement is
1258                    // off by default, and must be explicitly enabled.
1259                    no_signature_verification = !enforce_container_sigpolicy;
1260                    let sysroot = &if let Some(sysroot) = sysroot {
1261                        ostree::Sysroot::new(Some(&gio::File::for_path(sysroot)))
1262                    } else {
1263                        ostree::Sysroot::new_default()
1264                    };
1265                    sysroot.load(gio::Cancellable::NONE)?;
1266                    let repo = &sysroot.repo();
1267                    let kargs = karg.as_deref();
1268                    let kargs = kargs.map(|v| {
1269                        let r: Vec<_> = v.iter().map(|s| s.as_str()).collect();
1270                        r
1271                    });
1272
1273                    // If the user specified a stateroot, we always use that.
1274                    let stateroot = if let Some(stateroot) = stateroot.as_deref() {
1275                        Cow::Borrowed(stateroot)
1276                    } else {
1277                        // Otherwise, if we're booted via ostree, use the booted.
1278                        // If that doesn't hold, then use `default`.
1279                        let booted_stateroot = sysroot
1280                            .booted_deployment()
1281                            .map(|d| Cow::Owned(d.osname().to_string()));
1282                        booted_stateroot.unwrap_or({
1283                            Cow::Borrowed(crate::container::deploy::STATEROOT_DEFAULT)
1284                        })
1285                    };
1286
1287                    let imgref = if let Some(image) = image {
1288                        let transport = transport.as_deref().unwrap_or("registry");
1289                        let transport = ostree_container::Transport::try_from(transport)?;
1290                        let imgref = ostree_container::ImageReference {
1291                            transport,
1292                            name: image,
1293                        };
1294                        let sigverify = if no_signature_verification {
1295                            ostree_container::SignatureSource::ContainerPolicyAllowInsecure
1296                        } else if let Some(remote) = ostree_remote.as_ref() {
1297                            ostree_container::SignatureSource::OstreeRemote(remote.to_string())
1298                        } else {
1299                            ostree_container::SignatureSource::ContainerPolicy
1300                        };
1301                        ostree_container::OstreeImageReference { sigverify, imgref }
1302                    } else {
1303                        // SAFETY: We use the clap required_unless_present flag, so this must be set
1304                        // because --image is not.
1305                        let imgref = imgref.expect("imgref option should be set");
1306                        imgref.as_str().try_into()?
1307                    };
1308
1309                    #[allow(clippy::needless_update)]
1310                    let options = crate::container::deploy::DeployOpts {
1311                        kargs: kargs.as_deref(),
1312                        target_imgref: target_imgref.as_ref(),
1313                        proxy_cfg: Some(proxyopts.into()),
1314                        no_imgref,
1315                        ..Default::default()
1316                    };
1317                    let state = crate::container::deploy::deploy(
1318                        sysroot,
1319                        &stateroot,
1320                        &imgref,
1321                        Some(options),
1322                    )
1323                    .await?;
1324                    let wrote_imgref = target_imgref.as_ref().unwrap_or(&imgref);
1325                    if let Some(msg) = ostree_container::store::image_filtered_content_warning(
1326                        repo,
1327                        &wrote_imgref.imgref,
1328                    )? {
1329                        eprintln!("{msg}")
1330                    }
1331                    if let Some(p) = write_commitid_to {
1332                        std::fs::write(&p, state.merge_commit.as_bytes())
1333                            .with_context(|| format!("Failed to write commitid to {}", p))?;
1334                    }
1335                    Ok(())
1336                }
1337            },
1338            ContainerOpts::Compare {
1339                imgref_old,
1340                imgref_new,
1341            } => {
1342                let (manifest_old, _) = crate::container::fetch_manifest(&imgref_old).await?;
1343                let (manifest_new, _) = crate::container::fetch_manifest(&imgref_new).await?;
1344                let manifest_diff =
1345                    crate::container::ManifestDiff::new(&manifest_old, &manifest_new);
1346                manifest_diff.print();
1347                Ok(())
1348            }
1349        },
1350        Opt::ImaSign(ref opts) => ima_sign(opts),
1351        #[cfg(feature = "internal-testing-api")]
1352        Opt::InternalOnlyForTesting(ref opts) => testing(opts).await,
1353        #[cfg(feature = "docgen")]
1354        Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
1355        Opt::ProvisionalRepair(opts) => match opts {
1356            ProvisionalRepairOpts::AnalyzeInodes {
1357                repo,
1358                verbose,
1359                write_result_to,
1360            } => {
1361                let repo = parse_repo(&repo)?;
1362                let check_res = crate::repair::check_inode_collision(&repo, verbose)?;
1363                handle_serialize_to_file(write_result_to.as_deref(), &check_res)?;
1364                if check_res.collisions.is_empty() {
1365                    println!("OK: No colliding objects found.");
1366                } else {
1367                    eprintln!(
1368                        "warning: {} potentially colliding inodes found",
1369                        check_res.collisions.len()
1370                    );
1371                }
1372                Ok(())
1373            }
1374            ProvisionalRepairOpts::Repair {
1375                sysroot,
1376                verbose,
1377                dry_run,
1378                write_result_to,
1379            } => {
1380                container_remount_sysroot(&sysroot)?;
1381                let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1382                sysroot.load(gio::Cancellable::NONE)?;
1383                let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1384                let result = crate::repair::analyze_for_repair(sysroot, verbose)?;
1385                handle_serialize_to_file(write_result_to.as_deref(), &result)?;
1386                if dry_run {
1387                    result.check()
1388                } else {
1389                    result.repair(sysroot)
1390                }
1391            }
1392        },
1393    }
1394}