diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-07 15:48:37 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-07 16:57:24 +0800 |
| commit | 180f72d307d75351bc8baa19b8ac662ef58fc35f (patch) | |
| tree | 95e20641afff1b0ac9c2c45e0cfa03ad1dbf2d63 /mingling_cli | |
| parent | a4c49f54420b640a1379e2a84042bdc64be2f798 (diff) | |
feat(pkg_mgr): add uninstall command and improve install
Implement `mling uninstall` to remove installed packages by name or
name@version, with shell completion support. Rework install to build
release binaries, copy them plus completion scripts to a per-version
directory under the user data dir.
Diffstat (limited to 'mingling_cli')
| -rw-r--r-- | mingling_cli/Cargo.lock | 59 | ||||
| -rw-r--r-- | mingling_cli/Cargo.toml | 1 | ||||
| -rw-r--r-- | mingling_cli/help/help.txt | 3 | ||||
| -rw-r--r-- | mingling_cli/src/bin/cli.rs | 2 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr.rs | 38 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_install.rs | 180 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_uninstall.rs | 149 |
7 files changed, 426 insertions, 6 deletions
diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock index 5953c15..9a8a226 100644 --- a/mingling_cli/Cargo.lock +++ b/mingling_cli/Cargo.lock @@ -164,6 +164,27 @@ dependencies = [ ] [[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -186,6 +207,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -253,6 +285,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7743783ea728ef5c31194c6590797eed286449b4a4e87d626d8a51f0a94e732" [[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -295,6 +336,7 @@ dependencies = [ "annotate-snippets", "cargo_metadata", "colored", + "dirs", "just_template", "mingling", "proc-macro2", @@ -347,6 +389,12 @@ dependencies = [ ] [[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -403,6 +451,17 @@ dependencies = [ ] [[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml index 94b0c69..945258b 100644 --- a/mingling_cli/Cargo.toml +++ b/mingling_cli/Cargo.toml @@ -58,6 +58,7 @@ serde_json = "1.0.151" tokio = { version = "1.53.1", features = ["full"] } toml_edit = "0.25.13" colored = "3.1.1" +dirs = "6.0.0" [build-dependencies] # Configure & Serialization diff --git a/mingling_cli/help/help.txt b/mingling_cli/help/help.txt index f9e64f6..4137cb5 100644 --- a/mingling_cli/help/help.txt +++ b/mingling_cli/help/help.txt @@ -25,7 +25,8 @@ __ proj-new <NAME> **[NOT_IMPL]** *Create a project* __ proj-init **[NOT_IMPL]** *Initialize a project* __ **PACKAGE MANAGE:** -__ install **[NOT_IMPL]** *Install the project to the Mingling package list* +__ install *Install the project to the Mingling package list* +__ uninstall [PACKAGE] *Uninstall the project from the Mingling package list* __ pkg-show **[NOT_IMPL]** *Show locally installed packages* __ pkg-enable <NAME> **[NOT_IMPL]** *Enable the specified package* __ pkg-disable <NAME> **[NOT_IMPL]** *Disable the specified package* diff --git a/mingling_cli/src/bin/cli.rs b/mingling_cli/src/bin/cli.rs index fe2e60a..6f7556e 100644 --- a/mingling_cli/src/bin/cli.rs +++ b/mingling_cli/src/bin/cli.rs @@ -1,6 +1,7 @@ use mingling::setup::{ExitCodeSetup, picker::HelpFlagSetup}; use mingling_cli::{ ThisProgram, linter::registry::LintRegistrySetup, metadata::MinglingMetadataSetup, + pkg_mgr::PackageManagerSetup, }; #[tokio::main] @@ -13,6 +14,7 @@ async fn main() { program.with_setup(MinglingMetadataSetup); program.with_setup(LintRegistrySetup); + program.with_setup(PackageManagerSetup); // Exec program.exec_and_exit().await; diff --git a/mingling_cli/src/pkg_mgr.rs b/mingling_cli/src/pkg_mgr.rs index b77c3b3..7aea59d 100644 --- a/mingling_cli/src/pkg_mgr.rs +++ b/mingling_cli/src/pkg_mgr.rs @@ -1,2 +1,40 @@ pub mod cmd_install; +pub mod cmd_uninstall; +use std::path::PathBuf; + +use mingling::{ + Program, + macros::{buffer, pack_err, program_setup, r_println, renderer}, +}; + +use crate::ThisProgram; + +pack_err!(ErrorRootPackageNotFound); +pack_err!(ErrorNoDataDirectory); + +/// The `.mingling` packages directory under the user's data directory. +#[derive(Debug, Default, Clone)] +pub struct ResPackagesDir { + pub path: PathBuf, +} + +#[program_setup] +pub fn package_manager_setup(program: &mut Program<ThisProgram>) { + let path = dirs::data_dir() + .map(|data_dir| data_dir.join(".mingling")) + .unwrap_or_default(); + program.with_resource(ResPackagesDir { path }); +} + +#[renderer(buffer)] +pub fn render_error_root_package_not_found(_: ErrorRootPackageNotFound) { + r_println!("error: failed to determine the root package"); + r_println!(""); + r_println!("Run `mling install` / `mling uninstall` inside a Cargo workspace"); +} + +#[renderer(buffer)] +pub fn render_error_no_data_directory(_: ErrorNoDataDirectory) { + r_println!("error: failed to determine the data directory"); +} diff --git a/mingling_cli/src/pkg_mgr/cmd_install.rs b/mingling_cli/src/pkg_mgr/cmd_install.rs index ad45f04..a399e83 100644 --- a/mingling_cli/src/pkg_mgr/cmd_install.rs +++ b/mingling_cli/src/pkg_mgr/cmd_install.rs @@ -1,8 +1,178 @@ -use mingling::{LazyRes, macros::command}; +use std::{env, fs, io, path::PathBuf, process::Command}; -use crate::metadata::setup::ResMetadata; +use cargo_metadata::TargetKind; +use mingling::{ + Grouped, LazyRes, Routable, + macros::{buffer, chain, command, metadata, pack_err, r_println, renderer, routeify}, + metadata::Description, +}; -#[command] -pub fn install(_metadata: &mut LazyRes<ResMetadata>) { - // let metadata = metadata.get_ref().data(); +use crate::{ + Next, + metadata::setup::ResMetadata, + pkg_mgr::{ErrorNoDataDirectory, ErrorRootPackageNotFound, ResPackagesDir}, +}; + +pack_err!(ErrorBuildFailed = String); +pack_err!(ErrorBinaryNotFound = String); + +/// Resolved install paths, used by the build step. +#[derive(Debug, Default, Grouped)] +pub struct StateInstallBuild { + pub workspace_root: PathBuf, + pub install_dir: PathBuf, + pub release_dir: PathBuf, + pub exe_suffix: &'static str, +} + +/// State after `cargo build --release`, used by the copy step. +#[derive(Debug, Default, Grouped)] +pub struct StateInstallCopy { + pub install_dir: PathBuf, + pub release_dir: PathBuf, + pub exe_suffix: &'static str, + pub installed: Vec<PathBuf>, +} + +#[derive(Debug, Default, Grouped)] +pub struct ResultInstall { + pub install_dir: PathBuf, + pub installed: Vec<PathBuf>, +} + +/// Parse arguments and resolve the install directory: +/// {data_dir}/.mingling/{PACKAGE_NAME}@{PACKAGE_VERSION} +#[metadata(EntryInstall)] +pub fn desc_install() -> Description { + "Install the project to the Mingling package list".into() +} + +#[command(routeify)] +pub fn install(packages_dir: &ResPackagesDir, metadata: &mut LazyRes<ResMetadata>) -> Next { + let metadata = metadata.get_ref().data(); + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + + let root_package = metadata + .root_package() + .or_else(|| metadata.workspace_packages().first().copied()) + .ok_or(ErrorRootPackageNotFound::default())?; + + StateInstallBuild { + workspace_root: metadata.workspace_root.clone().into_std_path_buf(), + install_dir: packages_dir.join(format!("{}@{}", root_package.name, root_package.version)), + release_dir: metadata + .target_directory + .join("release") + .into_std_path_buf(), + exe_suffix: env::consts::EXE_SUFFIX, + } + .to_chain() +} + +/// Step 1: build release binaries. +#[chain(routeify)] +pub fn handle_state_install_build(state: StateInstallBuild) -> Next { + let status = Command::new("cargo") + .args(["build", "--release"]) + .current_dir(&state.workspace_root) + .status() + .map_err(|e| { + ErrorBuildFailed::new(format!("failed to run `cargo build --release`: {e}")) + })?; + if !status.success() { + return ErrorBuildFailed::new(format!("`cargo build --release` failed with {status}")) + .to_chain(); + } + + StateInstallCopy { + install_dir: state.install_dir, + release_dir: state.release_dir, + exe_suffix: state.exe_suffix, + installed: vec![], + } + .to_chain() +} + +/// Step 2: copy binaries and completion scripts. +#[chain(routeify)] +pub fn handle_state_install_copy( + mut state: StateInstallCopy, + metadata: &mut LazyRes<ResMetadata>, +) -> Next { + let metadata = metadata.get_ref().data(); + fs::create_dir_all(&state.install_dir).map_err(|e| { + io::Error::new( + e.kind(), + format!("failed to create {}: {e}", state.install_dir.display()), + ) + })?; + + for package in metadata.workspace_packages() { + let bin_targets: Vec<_> = package + .targets + .iter() + .filter(|target| target.kind.contains(&TargetKind::Bin)) + .collect(); + + for target in bin_targets { + let bin_file = format!("{}{}", target.name, state.exe_suffix); + let src = state.release_dir.join(&bin_file); + if !src.is_file() { + return ErrorBinaryNotFound::new(bin_file).to_chain(); + } + let dst = state.install_dir.join(&bin_file); + fs::copy(&src, &dst).map_err(|e| { + io::Error::new(e.kind(), format!("failed to copy {}: {e}", src.display())) + })?; + state.installed.push(dst); + } + } + + // Completion scripts are generated into the build profile directory + // (OUT_DIR/../../../), copy every one whose name contains `_comp`, + // regardless of its suffix + for entry in fs::read_dir(&state.release_dir).map_err(|e| { + io::Error::new( + e.kind(), + format!("failed to read {}: {e}", state.release_dir.display()), + ) + })? { + let entry = entry.map_err(|e| { + io::Error::new(e.kind(), format!("failed to read directory entry: {e}")) + })?; + let name = entry.file_name().to_string_lossy().into_owned(); + if name.contains("_comp") { + let dst = state.install_dir.join(&name); + fs::copy(entry.path(), &dst) + .map_err(|e| io::Error::new(e.kind(), format!("failed to copy {name}: {e}")))?; + state.installed.push(dst); + } + } + + ResultInstall { + install_dir: state.install_dir, + installed: state.installed, + } + .to_chain() +} + +#[renderer(buffer)] +pub fn render_result_install(r: ResultInstall) { + r_println!("Installed to {}", r.install_dir.display()); + for file in r.installed { + r_println!(" {}", file.display()); + } +} + +#[renderer(buffer)] +pub fn render_error_build_failed(err: ErrorBuildFailed) { + r_println!("error: {}", err.info); +} + +#[renderer(buffer)] +pub fn render_error_binary_not_found(err: ErrorBinaryNotFound) { + r_println!("error: binary not found: {}", err.info); } diff --git a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs new file mode 100644 index 0000000..d9a3004 --- /dev/null +++ b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs @@ -0,0 +1,149 @@ +use std::{fs, io, path::PathBuf}; + +use mingling::{ + Grouped, LazyRes, Routable, ShellContext, Suggest, SuggestItem, + macros::{ + arg, buffer, chain, command, completion, metadata, pack, pack_err, r_println, renderer, + routeify, + }, + metadata::Description, + picker::{EntryPicker, PickerArg}, +}; + +use crate::{ + Next, + metadata::setup::ResMetadata, + pkg_mgr::{ErrorNoDataDirectory, ErrorRootPackageNotFound, ResPackagesDir}, +}; + +/// Optional positional argument: package spec (`name` or `name@version`) +pub static ARG_PACKAGE: PickerArg<Option<String>> = arg![Option<String>]; + +pack_err!(ErrorPackageSpecInvalid = String); +pack!(StateUninstallPackages = Vec<String>); + +#[derive(Debug, Default, Grouped)] +pub struct ResultUninstall { + pub removed: Vec<PathBuf>, + pub not_installed: Vec<PathBuf>, +} + +/// `{data_dir}/.mingling` +#[metadata(EntryUninstall)] +pub fn desc_uninstall() -> Description { + "Uninstall the project from the Mingling package list".into() +} + +#[command(routeify)] +pub fn uninstall( + args: EntryUninstall, + packages_dir: &ResPackagesDir, + metadata: &mut LazyRes<ResMetadata>, +) -> Next { + let spec = args.pick(&ARG_PACKAGE).to_result()?; + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + + let targets = match spec { + // No spec: uninstall the version installed by the current project + None => { + let metadata = metadata.get_ref().data(); + let root_package = metadata + .root_package() + .or_else(|| metadata.workspace_packages().first().copied()) + .ok_or(ErrorRootPackageNotFound::default())?; + vec![format!("{}@{}", root_package.name, root_package.version)] + } + // `name` matches every installed version, `name@version` matches exactly + Some(spec) => { + if spec.contains('/') || spec.contains('\\') || spec.contains("..") { + return ErrorPackageSpecInvalid::new(spec).to_chain(); + } + if spec.contains('@') { + vec![spec] + } else { + let prefix = format!("{spec}@"); + if !packages_dir.is_dir() { + Vec::new() + } else { + fs::read_dir(packages_dir) + .map_err(|e| { + io::Error::new( + e.kind(), + format!("failed to read {}: {e}", packages_dir.display()), + ) + })? + .filter_map(|e| e.ok()) + .filter_map(|e| e.file_name().into_string().ok()) + .filter(|name| name.starts_with(&prefix)) + .collect() + } + } + } + }; + + StateUninstallPackages::new(targets).to_chain() +} + +#[chain(routeify)] +pub fn handle_state_uninstall_packages( + p: StateUninstallPackages, + packages_dir: &ResPackagesDir, +) -> Next { + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + let mut removed = Vec::new(); + let mut not_installed = Vec::new(); + + for name in p.inner { + let dir = packages_dir.join(&name); + if !dir.exists() { + not_installed.push(dir); + continue; + } + fs::remove_dir_all(&dir).map_err(|e| { + io::Error::new(e.kind(), format!("failed to remove {}: {e}", dir.display())) + })?; + removed.push(dir); + } + + ResultUninstall { + removed, + not_installed, + } + .to_chain() +} + +#[renderer(buffer)] +pub fn render_result_uninstall(r: ResultUninstall) { + if r.removed.is_empty() && r.not_installed.is_empty() { + r_println!("No matching packages installed"); + } else { + for dir in r.removed { + r_println!("Uninstalled: {}", dir.display()); + } + for dir in r.not_installed { + r_println!("Not installed: {}", dir.display()); + } + } +} + +#[completion(EntryUninstall)] +pub fn complete_uninstall(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest { + if ctx.previous_word != "uninstall" { + return Suggest::FileCompletion; + } + let mut suggest = Suggest::new(); + if let Ok(entries) = fs::read_dir(&packages_dir.path) { + for entry in entries.flatten() { + if let Some(name) = entry.file_name().to_str() { + suggest.insert(SuggestItem::Simple(name.to_string())); + } + } + } + suggest +} |
