Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ platforms = ["*-unknown-linux-gnu"]

[dependencies]
# Internal crates
bootc-lib = { version = "1.11", path = "../lib" }
bootc-lib = { version = "1.12", path = "../lib" }
bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.1.0" }

# Workspace dependencies
Expand Down
3 changes: 2 additions & 1 deletion crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name = "bootc-lib"
repository = "https://github.com/bootc-dev/bootc"
# The intention is we'll follow semver here, even though this
# project isn't actually published as a crate.
version = "1.11.0"
version = "1.12.0"
# In general we try to keep this pinned to what's in the latest RHEL9.
rust-version = "1.84.0"

Expand All @@ -33,6 +33,7 @@ cap-std-ext = { workspace = true, features = ["fs_utf8"] }
cfg-if = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive","cargo"] }
clap_complete = "4"
clap_mangen = { workspace = true, optional = true }
composefs = { workspace = true }
composefs-boot = { workspace = true }
Expand Down
58 changes: 58 additions & 0 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use anyhow::{anyhow, ensure, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::Dir;
use clap::CommandFactory;
use clap::Parser;
use clap::ValueEnum;
use composefs::dumpfile;
Expand Down Expand Up @@ -417,6 +418,15 @@ pub(crate) enum ImageCmdOpts {
},
}

/// Supported completion shells
#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
#[clap(rename_all = "lowercase")]
pub(crate) enum CompletionShell {
Bash,
Zsh,
Fish,
}

#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ImageListType {
Expand Down Expand Up @@ -744,6 +754,15 @@ pub(crate) enum Opt {
/// Diff current /etc configuration versus default
#[clap(hide = true)]
ConfigDiff,
/// Generate shell completion script for supported shells.
///
/// Example: `bootc completion bash` prints a bash completion script to stdout.
#[clap(hide = true)]
Completion {
/// Shell type to generate (bash, zsh, fish)
#[clap(value_enum)]
shell: CompletionShell,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just directly use https://docs.rs/clap_complete/latest/clap_complete/aot/enum.Shell.html and avoid a custom enum?

},
#[clap(hide = true)]
DeleteDeployment {
depl_id: String,
Expand Down Expand Up @@ -1581,6 +1600,20 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
Ok(())
}
},
Opt::Completion { shell } => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a totally different option is to generate this data at compile time, per https://docs.rs/clap_complete/latest/clap_complete/aot/fn.generate_to.html

I don't have a strong sense of the tradeoffs - if this doesn't notably increase the binary size, then doing it dynamically seems completely fine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept the runtime generation approach primarily for two reasons:

  1. Flexibility: Users can generate completions on-demand via bootc completion <shell>, which is convenient for both end users and developers who want to quickly set up completions without needing to locate pre-generated files.

  2. Distribution-friendly: This approach allows distributions (Fedora, RHEL, etc.) to generate completion scripts during RPM builds by simply running bootc completion <shell> commands. This gives distributions full control over when and how completions are generated, without requiring changes to the build script or build dependencies.

If you think compile-time generation would be better for this use case, I can refactor it. What are your thoughts?

use clap_complete::aot::{generate, Shell};

let mut cmd = Opt::command();
let mut stdout = std::io::stdout();
let bin_name = "bootc";
let shell_generator = match shell {
CompletionShell::Bash => Shell::Bash,
CompletionShell::Zsh => Shell::Zsh,
CompletionShell::Fish => Shell::Fish,
};
generate(shell_generator, &mut cmd, bin_name, &mut stdout);
Ok(())
}
Opt::Image(opts) => match opts {
ImageOpts::List {
list_type,
Expand Down Expand Up @@ -1986,4 +2019,29 @@ mod tests {
]));
assert_eq!(args.as_slice(), ["container", "image", "pull"]);
}

#[test]
fn test_generate_completion_scripts_contain_commands() {
use clap_complete::aot::{generate, Shell};

// For each supported shell, generate the completion script and
// ensure obvious subcommands appear in the output. This mirrors
// the style of completion checks used in other projects (e.g.
// podman) where the generated script is examined for expected
// tokens.

// `completion` is intentionally hidden from --help / suggestions;
// ensure other visible subcommands are present instead.
let want = ["install", "upgrade"];

for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
let mut cmd = Opt::command();
let mut buf = Vec::new();
generate(shell, &mut cmd, "bootc", &mut buf);
let s = String::from_utf8(buf).expect("completion should be utf8");
for w in &want {
assert!(s.contains(w), "{shell:?} completion missing {w}");
}
}
}
}
6 changes: 5 additions & 1 deletion crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,11 @@ fn container_inspect_print_human(
rows.push(("Kargs", kargs));

// Find the max label width for right-alignment
let max_label_len = rows.iter().map(|(label, _)| label.len()).max().unwrap_or(0);
let max_label_len = rows
.iter()
.map(|(label, _)| label.as_bytes().len())
.max()
.unwrap_or(0);

for (label, value) in rows {
write_row_name(&mut out, label, max_label_len)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/tests-integration/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ pub(crate) fn test_compute_composefs_digest() -> Result<()> {

// Verify it's a valid hex string of expected length (SHA-512 = 128 hex chars)
assert_eq!(
digest.len(),
digest.as_bytes().len(),
128,
"Expected 512-bit hex digest, got length {}",
digest.len()
digest.as_bytes().len()
);
assert!(
digest.chars().all(|c| c.is_ascii_hexdigit()),
Expand Down
Loading