[pve-devel] [PATCH installer 2/5] chroot: replace clap with pico-args for command argument parsing

Christoph Heiss c.heiss at proxmox.com
Fri May 9 14:09:14 CEST 2025


Drops clap for pico-args, as the former is pretty heavy-weight and
completely overhauls its API with ~every major release.

As for the resulting binary, there is a ~18% decrease in binary size,
from 2.9M to 2.4MiB (after stripping).

   text    data     bss     dec     hex filename
2743595  274576     424 3018595  2e0f63 proxmox-chroot-clap
2208631  261224     408 2470263  25b177 proxmox-chroot-pico

Further, the dependency tree goes from 40 to 29 dependencies, as well as
the package compile time on my machine (on hot caches), roughly measured
with

`cargo clean && time cargo build --package proxmox-chroot --release`

from ~20s to ~16s, so a nice little change w.r.t. to that aspect too.

No functional changes intended.

Signed-off-by: Christoph Heiss <c.heiss at proxmox.com>
---
 proxmox-chroot/Cargo.toml  |   6 +-
 proxmox-chroot/src/main.rs | 159 +++++++++++++++++++++++++++----------
 2 files changed, 120 insertions(+), 45 deletions(-)

diff --git a/proxmox-chroot/Cargo.toml b/proxmox-chroot/Cargo.toml
index 7a1390a..c043938 100644
--- a/proxmox-chroot/Cargo.toml
+++ b/proxmox-chroot/Cargo.toml
@@ -9,8 +9,6 @@ homepage = "https://www.proxmox.com"
 
 [dependencies]
 anyhow.workspace = true
-proxmox-installer-common.workspace = true
-serde_json.workspace = true
 regex.workspace = true
-
-clap = { version = "4.0", features = ["derive"] }
+proxmox-installer-common = { workspace = true, features = [ "cli" ] }
+serde_json.workspace = true
diff --git a/proxmox-chroot/src/main.rs b/proxmox-chroot/src/main.rs
index ef339ed..037f079 100644
--- a/proxmox-chroot/src/main.rs
+++ b/proxmox-chroot/src/main.rs
@@ -1,13 +1,20 @@
+//! Tool to help prepare a successfully installed target environment for
+//! chroot'ing.
+//!
+//! Can also be used in the rescue environment for mounting an existing system.
+
+#![forbid(unsafe_code)]
+
 use std::{
-    fs, io,
+    env, fs, io,
     path::{self, Path, PathBuf},
-    process::Command,
+    process::{self, Command},
+    str::FromStr,
 };
 
 use anyhow::{Result, bail};
-use clap::{Args, Parser, Subcommand, ValueEnum};
 use proxmox_installer_common::{
-    RUNTIME_DIR,
+    RUNTIME_DIR, cli,
     options::FsType,
     setup::{InstallConfig, SetupInfo},
 };
@@ -18,46 +25,87 @@ static BINDMOUNTS: [&str; 4] = ["dev", "proc", "run", "sys"];
 const TARGET_DIR: &str = "/target";
 const ZPOOL_NAME: &str = "rpool";
 
-/// Helper tool to prepare everything to `chroot` into an installation
-#[derive(Parser, Debug)]
-#[command(author, version, about, long_about = None)]
-struct Cli {
-    #[command(subcommand)]
-    command: Commands,
-}
-
-#[derive(Subcommand, Debug)]
-enum Commands {
-    Prepare(CommandPrepare),
-    Cleanup(CommandCleanup),
-}
-
-/// Mount the root file system and bind mounts in preparation to chroot into the installation
-#[derive(Args, Debug)]
-struct CommandPrepare {
+/// Arguments for the `prepare` command.
+struct CommandPrepareArgs {
     /// Filesystem used for the installation. Will try to automatically detect it after a
     /// successful installation.
-    #[arg(short, long, value_enum)]
     filesystem: Option<Filesystems>,
 
-    /// Numerical ID of `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` are present.
-    #[arg(long)]
+    /// Numerical ID of the `rpool` ZFS pool to import. Needed if multiple pools of name `rpool`
+    /// are present.
     rpool_id: Option<u64>,
 
     /// UUID of the BTRFS file system to mount. Needed if multiple BTRFS file systems are present.
-    #[arg(long)]
     btrfs_uuid: Option<String>,
 }
 
-/// Unmount everything. Use once done with chroot.
-#[derive(Args, Debug)]
-struct CommandCleanup {
+impl cli::Subcommand for CommandPrepareArgs {
+    fn parse(args: &mut cli::Arguments) -> Result<Self> {
+        Ok(Self {
+            filesystem: args.opt_value_from_str(["-f", "--filesystem"])?,
+            rpool_id: args.opt_value_from_fn("--rpool-id", str::parse)?,
+            btrfs_uuid: args.opt_value_from_str("--btrfs-uuid")?,
+        })
+    }
+
+    fn print_usage() {
+        eprintln!(
+            r#"Mount the root file system and bind mounts in preparation to chroot into the installation.
+
+USAGE:
+  {} prepare <OPTIONS>
+
+OPTIONS:
+  -f, --filesystem <FILESYSTEM>  Filesystem used for the installation. Will try to automatically detect it after a successful installation. [possible values: zfs, ext4, xfs, btrfs]
+      --rpool-id <RPOOL_ID>      Numerical ID of `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` are present.
+      --btrfs-uuid <BTRFS_UUID>  UUID of the BTRFS file system to mount. Needed if multiple BTRFS file systems are present.
+  -h, --help                       Print this help
+  -V, --version                    Print version
+"#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        prepare(self)
+    }
+}
+
+/// Arguments for the `cleanup` command.
+struct CommandCleanupArgs {
     /// Filesystem used for the installation. Will try to automatically detect it by default.
-    #[arg(short, long, value_enum)]
     filesystem: Option<Filesystems>,
 }
 
-#[derive(Copy, Clone, Debug, ValueEnum)]
+impl cli::Subcommand for CommandCleanupArgs {
+    fn parse(args: &mut cli::Arguments) -> Result<Self> {
+        Ok(Self {
+            filesystem: args.opt_value_from_str(["-f", "--filesystem"])?,
+        })
+    }
+
+    fn print_usage() {
+        eprintln!(
+            r#"Unmount everything. Use once done with the chroot.
+
+USAGE:
+  {} cleanup <OPTIONS>
+
+OPTIONS:
+  -f, --filesystem <FILESYSTEM>    Filesystem used for the installation. Will try to automatically detect it after a successful installation. [possible values: zfs, ext4, xfs, btrfs]
+  -h, --help                       Print this help
+  -V, --version                    Print version
+"#,
+            env!("CARGO_PKG_NAME")
+        );
+    }
+
+    fn run(&self) -> Result<()> {
+        cleanup(self)
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
 enum Filesystems {
     Zfs,
     Ext4,
@@ -76,19 +124,48 @@ impl From<FsType> for Filesystems {
     }
 }
 
-fn main() {
-    let args = Cli::parse();
-    let res = match &args.command {
-        Commands::Prepare(args) => prepare(args),
-        Commands::Cleanup(args) => cleanup(args),
-    };
-    if let Err(err) = res {
-        eprintln!("{err:#}");
-        std::process::exit(1);
+impl FromStr for Filesystems {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        match s {
+            "ext4" => Ok(Filesystems::Ext4),
+            "xfs" => Ok(Filesystems::Xfs),
+            _ if s.starts_with("zfs") => Ok(Filesystems::Zfs),
+            _ if s.starts_with("btrfs") => Ok(Filesystems::Btrfs),
+            _ => bail!("unknown filesystem"),
+        }
     }
 }
 
-fn prepare(args: &CommandPrepare) -> Result<()> {
+fn main() -> process::ExitCode {
+    cli::run(cli::AppInfo {
+        global_help: &format!(
+            r#"Helper tool to set up a `chroot` into an existing installation.
+
+USAGE:
+  {} <COMMAND> <OPTIONS>
+
+COMMANDS:
+  prepare            Mount the root file system and bind mounts in preparation to chroot into the installation.
+  cleanup            Unmount everything. Use once done with the chroot.
+
+GLOBAL OPTIONS:
+  -h, --help         Print help
+  -V, --version      Print version information
+"#,
+            env!("CARGO_PKG_NAME")
+        ),
+        on_command: |s, args| match s {
+            Some("prepare") => cli::handle_command::<CommandPrepareArgs>(args),
+            Some("cleanup") => cli::handle_command::<CommandCleanupArgs>(args),
+            Some(s) => bail!("unknown subcommand '{s}'"),
+            None => bail!("subcommand required"),
+        },
+    })
+}
+
+fn prepare(args: &CommandPrepareArgs) -> Result<()> {
     let fs = get_fs(args.filesystem)?;
 
     fs::create_dir_all(TARGET_DIR)?;
@@ -108,7 +185,7 @@ fn prepare(args: &CommandPrepare) -> Result<()> {
     Ok(())
 }
 
-fn cleanup(args: &CommandCleanup) -> Result<()> {
+fn cleanup(args: &CommandCleanupArgs) -> Result<()> {
     let fs = get_fs(args.filesystem)?;
 
     if let Err(e) = bind_umount() {
-- 
2.49.0





More information about the pve-devel mailing list