From abcc11f07861bcbb386e0ba025da749b39b8ee70 Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Sun, 20 Sep 2026 17:07:11 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Shell-Completion=20wird=20automatisc?= =?UTF-8?q?h=20mitpaketiert=20statt=20=C3=BCber=20eigenen=20Befehl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entfernt den 'smart-mount completions'-Befehl komplett - Shell-Completion (bash/zsh/fish) wird stattdessen beim Bauen des .deb/.rpm/.pkg.tar.zst-Pakets generiert und als normales Paket-Asset ausgeliefert, sodass sie beim Installieren automatisch mit installiert und beim Entfernen automatisch mit entfernt wird, ganz ohne Zutun des Nutzers. Das 'cli'-Modul ist dafür Teil der Library geworden (pub mod cli), damit ein neues, separates Build-Hilfsprogramm (src/bin/generate-completions.rs) die bestehende Cli-Definition als einzige Quelle wiederverwenden kann, statt sie zu duplizieren. Die Gitea-Workflows rufen es nach dem Release-Build auf; Cargo.toml und scripts/package-arch.py bündeln die erzeugten Dateien für alle drei Paketformate an den jeweils distributionsüblichen Pfaden. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/main.yaml | 7 ++++++- .gitea/workflows/testing.yaml | 7 ++++++- Cargo.toml | 12 ++++++++++++ README.md | 8 ++++---- scripts/package-arch.py | 29 ++++++++++++++++++++++++++++ src/bin/generate-completions.rs | 34 +++++++++++++++++++++++++++++++++ src/cli/doctor.rs | 4 ++-- src/cli/drive.rs | 18 ++++++++--------- src/cli/mod.rs | 18 ++--------------- src/cli/mount_cmd.rs | 8 ++++---- src/cli/service.rs | 5 +++-- src/cli/status.rs | 10 +++++----- src/cli/watch.rs | 7 +++---- src/lib.rs | 1 + src/main.rs | 3 +-- 15 files changed, 120 insertions(+), 51 deletions(-) create mode 100644 src/bin/generate-completions.rs diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml index b9fac9e..7769ea2 100644 --- a/.gitea/workflows/main.yaml +++ b/.gitea/workflows/main.yaml @@ -59,7 +59,7 @@ jobs: cargo-${{ runner.os }}- - name: Alte Paketierungs-Ausgaben aus dem Cache entfernen - run: rm -rf target/debian target/generate-rpm target/arch + run: rm -rf target/debian target/generate-rpm target/arch target/completions - name: Install Cross-Compilation Toolchains (apt) run: | @@ -111,6 +111,11 @@ jobs: cargo build --release --target x86_64-unknown-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu + - name: Generate Shell Completions + run: | + cargo build --release --bin generate-completions + ./target/release/generate-completions target/completions + - name: Determine Build Number id: build_num env: diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 42cf728..0d49f7f 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -59,7 +59,7 @@ jobs: cargo-${{ runner.os }}- - name: Alte Paketierungs-Ausgaben aus dem Cache entfernen - run: rm -rf target/debian target/generate-rpm target/arch + run: rm -rf target/debian target/generate-rpm target/arch target/completions - name: Install Cross-Compilation Toolchains (apt) run: | @@ -111,6 +111,11 @@ jobs: cargo build --release --target x86_64-unknown-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu + - name: Generate Shell Completions + run: | + cargo build --release --bin generate-completions + ./target/release/generate-completions target/completions + - name: Determine Build Number id: build_num env: diff --git a/Cargo.toml b/Cargo.toml index d44ca91..cd952df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,13 @@ assets = [ ["packaging/systemd/smart-mount-mount.service", "usr/lib/systemd/system/smart-mount-mount.service", "644"], ["packaging/systemd/smart-mount-watch.service", "usr/lib/systemd/system/smart-mount-watch.service", "644"], ["packaging/systemd/smart-mount-watch.timer", "usr/lib/systemd/system/smart-mount-watch.timer", "644"], + # Von 'cargo build --bin generate-completions' erzeugt (siehe .gitea/workflows/main.yaml) - + # kein 'smart-mount completions'-Subcommand mehr, Shell-Completion wird stattdessen wie jedes + # andere Paket-Asset automatisch mit installiert/entfernt. Debian/Ubuntu laden Zsh-Completions + # aus 'vendor-completions' (anders als Fedora/Arch, siehe [package.metadata.generate-rpm]). + ["target/completions/smart-mount.bash", "usr/share/bash-completion/completions/smart-mount", "644"], + ["target/completions/_smart-mount", "usr/share/zsh/vendor-completions/_smart-mount", "644"], + ["target/completions/smart-mount.fish", "usr/share/fish/vendor_completions.d/smart-mount.fish", "644"], ] [package.metadata.generate-rpm] @@ -78,6 +85,11 @@ assets = [ { source = "packaging/systemd/smart-mount-mount.service", dest = "/usr/lib/systemd/system/smart-mount-mount.service", mode = "644" }, { source = "packaging/systemd/smart-mount-watch.service", dest = "/usr/lib/systemd/system/smart-mount-watch.service", mode = "644" }, { source = "packaging/systemd/smart-mount-watch.timer", dest = "/usr/lib/systemd/system/smart-mount-watch.timer", mode = "644" }, + # Fedora/RHEL (wie Arch, siehe scripts/package-arch.py) laden Zsh-Completions aus + # 'site-functions' statt Debians 'vendor-completions' (siehe [package.metadata.deb]). + { source = "target/completions/smart-mount.bash", dest = "/usr/share/bash-completion/completions/smart-mount", mode = "644" }, + { source = "target/completions/_smart-mount", dest = "/usr/share/zsh/site-functions/_smart-mount", mode = "644" }, + { source = "target/completions/smart-mount.fish", dest = "/usr/share/fish/vendor_completions.d/smart-mount.fish", mode = "644" }, ] requires = { "mac2ip" = "*" } suggests = { "davfs2" = "*", "cifs-utils" = "*", "nfs-utils" = "*" } diff --git a/README.md b/README.md index 32130a7..57e5226 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ sudo smart-mount service crontab --remove # einzeln beim Mount-Fehlschlag entdecken würde smart-mount doctor smart-mount doctor --json - -# Shell-Completion-Skript ausgeben (bash/zsh/fish/elvish/powershell) -smart-mount completions bash > /etc/bash_completion.d/smart-mount -smart-mount completions zsh > "${fpath[1]}/_smart-mount" ``` +Shell-Completion (bash/zsh/fish) wird beim Installieren des .deb/.rpm/.pkg.tar.zst-Pakets +automatisch mit installiert (und beim Entfernen des Pakets automatisch mit entfernt) - dafür ist +kein eigenes `smart-mount`-Subcommand nötig. + Die Konfiguration liegt immer unter `/etc/smart-mount/config.toml`, unabhängig davon, ob `smart-mount` selbst mit oder ohne Root-Rechte aufgerufen wird (siehe oben). Die verschlüsselte Zugangsdaten-Datenbank (`smart-mount.db`) liegt im selben Verzeichnis. diff --git a/scripts/package-arch.py b/scripts/package-arch.py index 80a6f69..fcfbb36 100755 --- a/scripts/package-arch.py +++ b/scripts/package-arch.py @@ -61,6 +61,27 @@ def collect_systemd_units(systemd_src_dir="packaging/systemd"): return unit_files, installable +def collect_completions(completions_dir="target/completions"): + """Findet die von 'cargo build --bin generate-completions' erzeugten Shell-Completion- + Skripte (siehe src/bin/generate-completions.rs), falls vorhanden, und ordnet sie generisch - + ohne den Anwendungsnamen zu kennen - allein anhand ihrer clap_complete-Namenskonvention + (Bash: '.bash', Zsh: '_', Fish: '.fish') dem jeweiligen + System-Verzeichnis zu. Gibt eine Liste aus (Quellpfad, Zielverzeichnis, Zieldateiname) zurück.""" + if not os.path.isdir(completions_dir): + return [] + + mapping = [] + for f in sorted(os.listdir(completions_dir)): + src = os.path.join(completions_dir, f) + if f.endswith(".bash"): + mapping.append((src, "usr/share/bash-completion/completions", f[: -len(".bash")])) + elif f.endswith(".fish"): + mapping.append((src, "usr/share/fish/vendor_completions.d", f)) + elif f.startswith("_"): + mapping.append((src, "usr/share/zsh/site-functions", f)) + return mapping + + def build_install_scriptlet(installable, all_units): """Erzeugt den Inhalt einer Arch-'.INSTALL'-Datei (siehe `man PKGBUILD`, Abschnitt 'install'), die den paketierten systemd-Dienst beim Installieren aktiviert/startet und beim @@ -155,6 +176,14 @@ def build_package(target_triple=None, target_arch=None, pkgrel=None): check=True, ) + for src, dest_dir, dest_name in collect_completions(): + target_dir = os.path.join(build_dir, dest_dir) + os.makedirs(target_dir, exist_ok=True) + subprocess.run( + ["install", "-m", "644", src, os.path.join(target_dir, dest_name)], + check=True, + ) + installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0] builddate = str(int(time.time())) diff --git a/src/bin/generate-completions.rs b/src/bin/generate-completions.rs new file mode 100644 index 0000000..fc7dc52 --- /dev/null +++ b/src/bin/generate-completions.rs @@ -0,0 +1,34 @@ +//! Erzeugt die Shell-Completion-Skripte (bash/zsh/fish) für `smart-mount` als Dateien in einem +//! Ausgabeverzeichnis - kein `smart-mount`-Subcommand mehr (siehe `smart_mount::cli::Commands`), +//! sondern ein separates Build-Hilfsprogramm, das ausschließlich während der CI-Paketierung +//! aufgerufen wird (siehe `.gitea/workflows/main.yaml`/`testing.yaml`): die erzeugten Dateien +//! werden dort als normale Paket-Assets gebündelt, sodass sie beim Installieren des .deb/.rpm/ +//! .pkg.tar.zst-Pakets automatisch mit installiert und beim Entfernen automatisch mit entfernt +//! werden - ganz ohne Zutun des Nutzers. +//! +//! Nutzt `smart_mount::cli::Cli` als einzige Quelle der CLI-Definition (kein separat gepflegtes +//! Duplikat), da `cli` als `pub mod` Teil der Library ist und so auch von diesem zusätzlichen +//! Binary-Target aus der Cargo-eigenen `src/bin/`-Autodiscovery erreichbar ist. + +use std::path::PathBuf; + +use clap::CommandFactory; +use clap_complete::Shell; +use smart_mount::cli::Cli; + +fn main() { + let Some(out_dir) = std::env::args_os().nth(1).map(PathBuf::from) else { + eprintln!("usage: generate-completions "); + std::process::exit(1); + }; + std::fs::create_dir_all(&out_dir) + .unwrap_or_else(|e| panic!("could not create '{}': {e}", out_dir.display())); + + let mut cmd = Cli::command(); + let bin_name = cmd.get_name().to_string(); + for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] { + let path = clap_complete::generate_to(shell, &mut cmd, &bin_name, &out_dir) + .unwrap_or_else(|e| panic!("could not generate {shell} completions: {e}")); + println!("generated: {}", path.display()); + } +} diff --git a/src/cli/doctor.rs b/src/cli/doctor.rs index 952d570..daa5167 100644 --- a/src/cli/doctor.rs +++ b/src/cli/doctor.rs @@ -1,8 +1,8 @@ //! `smart-mount doctor` - prüft die im Laufe der Entwicklung angesammelten Voraussetzungen //! (Binaries, Gruppenmitgliedschaft, fstab-Setup, Scheduler) gebündelt an einer Stelle. -use smart_mount::config; -use smart_mount::doctor::{self, CheckStatus}; +use crate::config; +use crate::doctor::{self, CheckStatus}; pub async fn run(json: bool) -> anyhow::Result<()> { let cfg = config::pairs::load()?; diff --git a/src/cli/drive.rs b/src/cli/drive.rs index e8cea97..c81c070 100644 --- a/src/cli/drive.rs +++ b/src/cli/drive.rs @@ -12,10 +12,8 @@ use std::str::FromStr; use clap::{Args, Subcommand}; use dialoguer::{Confirm, Input, Password, Select}; -use smart_mount::config::{ - self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind, -}; -use smart_mount::db::credentials::{Credential, CredentialStore, Side}; +use crate::config::{self, AppConfig, CloudSide, DrivePair, LocalAddress, LocalSide, MountKind}; +use crate::db::credentials::{Credential, CredentialStore, Side}; #[derive(Subcommand)] pub enum DriveAction { @@ -145,7 +143,7 @@ async fn remove(id: &str) -> anyhow::Result<()> { // Vor dem Entfernen aus der Config nachschlagen, damit wir hinterher noch wissen, welche // Mount-Typen/Adressen betroffen sind - nötig, um die passenden Klartext-Zugangsdaten - // (davfs2 secrets, .cred-Datei) aufzuräumen, siehe smart_mount::mount::cleanup_credentials. + // (davfs2 secrets, .cred-Datei) aufzuräumen, siehe crate::mount::cleanup_credentials. let cfg = config::pairs::load()?; let pair = config::pairs::find_pair(&cfg, id)?; @@ -154,7 +152,7 @@ async fn remove(id: &str) -> anyhow::Result<()> { // darf das Entfernen aus der Konfiguration nicht blockieren - sonst käme der Nutzer nicht // mehr an sein eigenes `drive remove` heran, ohne vorher erst als root/mit den richtigen // Rechten manuell auszuhängen. - if let Err(e) = smart_mount::reconcile::unmount_pair(&pair, &cfg.settings).await { + if let Err(e) = crate::reconcile::unmount_pair(&pair, &cfg.settings).await { logger_ctdra::warn( "drive", &format!("could not unmount drive pair '{id}' before removal: {e}"), @@ -163,7 +161,7 @@ async fn remove(id: &str) -> anyhow::Result<()> { config::pairs::remove_pair(id)?; let creds = CredentialStore::open().await?; creds.delete(id, None).await?; - smart_mount::mount::cleanup_credentials(&pair, &cfg.settings); + crate::mount::cleanup_credentials(&pair, &cfg.settings); logger_ctdra::info("drive", &format!("drive pair '{id}' removed")); println!("Drive pair '{id}' removed."); @@ -356,7 +354,7 @@ async fn edit(id: &str, args: DriveArgs) -> anyhow::Result<()> { // würde ein aktiver Mount verwaisen. Nur eine explizite '--mount-point'-Angabe ändert ihn. let mount_point = match args.mount_point { Some(p) => { - if smart_mount::mount::target::active_side(&existing).is_some() { + if crate::mount::target::active_side(&existing).is_some() { println!( "Warning: pair '{id}' appears to be actively mounted - the old backing \ directories will be orphaned. Run 'sudo smart-mount unmount --name {id}' \ @@ -515,7 +513,7 @@ fn invoking_user() -> Option { /// Optional: falls gesetzt, bekommt dieser Nutzer bei CIFS/WebDAV vollen Zugriff /// (uid=/gid=/file_mode=0700/dir_mode=0700) statt der sonst üblichen root-Ownership - der -/// Mount selbst läuft immer als root (siehe [`smart_mount::systemd`]). `default_owner` ist der +/// Mount selbst läuft immer als root (siehe [`crate::systemd`]). `default_owner` ist der /// vorbelegte/vorgeschlagene Wert (bei `add`: der einrichtende Nutzer, siehe `invoking_user`; /// bei `edit`: der bisherige `owner_user`) - immer überschreibbar per Flag, im interaktiven /// Prompt auch durch Ablehnen oder einen anderen Namen. @@ -693,7 +691,7 @@ fn resolve_credentials( #[cfg(test)] mod tests { use super::*; - use smart_mount::config::LocalAddress; + use crate::config::LocalAddress; use std::net::Ipv4Addr; fn sample_pair(mount_point: &str) -> DrivePair { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f8cd2e3..42516c1 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,12 +7,11 @@ pub mod service; pub mod status; pub mod watch; -use clap::{CommandFactory, Parser, Subcommand}; -use clap_complete::Shell; +use clap::{Parser, Subcommand}; /// Startet den Prozess bei Bedarf transparent über `sudo` neu, falls nicht bereits als root /// aufgerufen. Mounten läuft ausschließlich als root/System-Dienst (siehe -/// [`smart_mount::systemd`]) - es gibt seit dem Wegfall des Nutzerkontexts keine +/// [`crate::systemd`]) - es gibt seit dem Wegfall des Nutzerkontexts keine /// unprivilegierte Mount-Variante mehr, für die ein Rechte-Check hier zu früh käme. /// /// `run_as_root()` ersetzt den aktuellen Prozess per `execve` und kehrt bei Erfolg nie zurück @@ -88,9 +87,6 @@ pub enum Commands { #[arg(long)] json: bool, }, - /// Prints a shell completion script, e.g.: - /// `smart-mount completions bash > /etc/bash_completion.d/smart-mount`. - Completions { shell: Shell }, } /// Kurzname eines Subcommands fürs Logging (siehe [`dispatch`]) - kein `Debug`-Derive auf @@ -104,7 +100,6 @@ fn command_label(cmd: &Commands) -> &'static str { Commands::Watch => "watch", Commands::Service { .. } => "service", Commands::Doctor { .. } => "doctor", - Commands::Completions { .. } => "completions", } } @@ -122,14 +117,5 @@ pub async fn dispatch(cli: Cli) -> anyhow::Result<()> { Commands::Watch => watch::run().await, Commands::Service { action } => service::run(action), Commands::Doctor { json } => doctor::run(json).await, - Commands::Completions { shell } => { - clap_complete::generate( - shell, - &mut Cli::command(), - "smart-mount", - &mut std::io::stdout(), - ); - Ok(()) - } } } diff --git a/src/cli/mount_cmd.rs b/src/cli/mount_cmd.rs index 82e0d77..d1f36ba 100644 --- a/src/cli/mount_cmd.rs +++ b/src/cli/mount_cmd.rs @@ -1,11 +1,11 @@ //! `smart-mount mount` / `smart-mount unmount`. -use smart_mount::config::{self, DrivePair}; -use smart_mount::db::credentials::CredentialStore; -use smart_mount::reconcile; +use crate::config::{self, DrivePair}; +use crate::db::credentials::CredentialStore; +use crate::reconcile; fn select_pairs( - cfg: &smart_mount::config::AppConfig, + cfg: &crate::config::AppConfig, name: Option<&str>, all: bool, ) -> anyhow::Result> { diff --git a/src/cli/service.rs b/src/cli/service.rs index 8c36fe1..68890e4 100644 --- a/src/cli/service.rs +++ b/src/cli/service.rs @@ -1,8 +1,9 @@ //! `smart-mount service crontab [--remove]`. use clap::Subcommand; -use smart_mount::config; -use smart_mount::systemd; + +use crate::config; +use crate::systemd; #[derive(Subcommand)] pub enum ServiceAction { diff --git a/src/cli/status.rs b/src/cli/status.rs index 524bc3b..2a90703 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -2,10 +2,10 @@ use serde::Serialize; -use smart_mount::config; -use smart_mount::db::credentials::Side; -use smart_mount::mount::target; -use smart_mount::network; +use crate::config; +use crate::db::credentials::Side; +use crate::mount::target; +use crate::network; #[derive(Serialize)] struct PairStatus { @@ -48,7 +48,7 @@ pub async fn run(name: Option, json: bool) -> anyhow::Result<()> { // wiederverwendet, statt sie für beide Zwecke unabhängig voneinander ein zweites // Mal aufzulösen. let resolved_local_ip = - smart_mount::network::address::resolve_ip(&pair.local.address, &cfg.settings); + crate::network::address::resolve_ip(&pair.local.address, &cfg.settings); let local_source = resolved_local_ip .as_ref() .map(|ip| target::format_local_source(&pair.local, *ip)) diff --git a/src/cli/watch.rs b/src/cli/watch.rs index 8352c87..bf6bbd0 100644 --- a/src/cli/watch.rs +++ b/src/cli/watch.rs @@ -1,10 +1,9 @@ //! `smart-mount watch` - ein einzelner Reconcile-Durchlauf, gedacht für systemd-Timer/Cron. -use smart_mount::config; -use smart_mount::db::credentials::CredentialStore; -use smart_mount::reconcile::{self, Action}; - use crate::cli::mount_cmd::print_outcome; +use crate::config; +use crate::db::credentials::CredentialStore; +use crate::reconcile::{self, Action}; pub async fn run() -> anyhow::Result<()> { crate::cli::require_root("watch")?; diff --git a/src/lib.rs b/src/lib.rs index 3757831..b2e1695 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ //! smart-mount: bindet lokale/Cloud-Laufwerkspaare (WebDAV/SMB/NFS) dynamisch ein und //! schaltet automatisch zwischen LAN und Cloud um. +pub mod cli; pub mod config; pub mod crypto; pub mod db; diff --git a/src/main.rs b/src/main.rs index e7c55b4..323f24a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ -mod cli; - use std::process::ExitCode; use clap::Parser; +use smart_mount::cli; #[tokio::main] async fn main() -> ExitCode {