aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/pkg_mgr
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_cli/src/pkg_mgr')
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_install.rs180
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_uninstall.rs149
2 files changed, 324 insertions, 5 deletions
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
+}