Feature: Shell-Completion wird automatisch mitpaketiert statt über eigenen Befehl
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
+12
@@ -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" = "*" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: '<name>.bash', Zsh: '_<name>', Fish: '<name>.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()))
|
||||
|
||||
|
||||
@@ -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 <output-dir>");
|
||||
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());
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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()?;
|
||||
|
||||
+8
-10
@@ -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<String> {
|
||||
|
||||
/// 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 {
|
||||
|
||||
+2
-16
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<DrivePair>> {
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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<String>, 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))
|
||||
|
||||
+3
-4
@@ -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")?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
mod cli;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use smart_mount::cli;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
|
||||
Reference in New Issue
Block a user