From 881e7399b2417c32fa996d94c6b389c1e06d8eb1 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 28 Apr 2026 16:18:12 +0800 Subject: Add scaffolding CLI tool `mling` --- mling/src/cli.rs | 45 ++++++++++++ mling/src/cli/list.rs | 117 +++++++++++++++++++++++++++++++ mling/src/cli/namespace_mgr.rs | 128 ++++++++++++++++++++++++++++++++++ mling/src/cli/read.rs | 77 +++++++++++++++++++++ mling/src/cli/refresh.rs | 32 +++++++++ mling/src/main.rs | 14 ++++ mling/src/namespace_manager.rs | 113 ++++++++++++++++++++++++++++++ mling/src/project_installer.rs | 153 +++++++++++++++++++++++++++++++++++++++++ mling/src/project_solver.rs | 105 ++++++++++++++++++++++++++++ 9 files changed, 784 insertions(+) create mode 100644 mling/src/cli.rs create mode 100644 mling/src/cli/list.rs create mode 100644 mling/src/cli/namespace_mgr.rs create mode 100644 mling/src/cli/read.rs create mode 100644 mling/src/cli/refresh.rs create mode 100644 mling/src/main.rs create mode 100644 mling/src/namespace_manager.rs create mode 100644 mling/src/project_installer.rs create mode 100644 mling/src/project_solver.rs (limited to 'mling/src') diff --git a/mling/src/cli.rs b/mling/src/cli.rs new file mode 100644 index 0000000..e0dfbe6 --- /dev/null +++ b/mling/src/cli.rs @@ -0,0 +1,45 @@ +use mingling::{ + macros::renderer, + setup::{BasicProgramSetup, GeneralRendererSetup}, +}; + +use crate::{__completion_gen::CompletionDispatcher, DispatcherNotFound, ThisProgram}; + +pub mod list; +pub use list::*; + +pub mod namespace_mgr; +pub use namespace_mgr::*; + +pub mod read; +pub use read::*; + +pub mod refresh; +pub use refresh::*; + +pub fn cli_entry() { + let mut program = ThisProgram::new(); + + program.with_setup(BasicProgramSetup); + program.with_setup(GeneralRendererSetup); + program.with_dispatcher(CompletionDispatcher); + + program.with_dispatcher(ListInstalledCommand); + program.with_dispatchers(( + TrustNamespaceCommand, + UntrustNamespaceCommand, + SetTrustNamespaceCommand, + RemoveNamespaceCommand, + )); + program.with_dispatcher(RefreshCommand); + program.with_dispatchers(( + ReadTargetDirCommand, + ReadWorkspaceRootCommand, + ReadBinariesCommand, + )); + + program.exec(); +} + +#[renderer] +pub(crate) fn render_help(_prev: DispatcherNotFound) {} diff --git a/mling/src/cli/list.rs b/mling/src/cli/list.rs new file mode 100644 index 0000000..9aff22b --- /dev/null +++ b/mling/src/cli/list.rs @@ -0,0 +1,117 @@ +use colored::Colorize; +use mingling::{ + Groupped, RenderResult, ShellContext, Suggest, + macros::{chain, completion, dispatcher, pack, r_println, renderer, suggest}, + parser::Picker, +}; +use serde::Serialize; + +use crate::{ThisProgram, namespace_manager::list_namespaces}; + +dispatcher!("ls-namespace", ListInstalledCommand => ListInstalledEntry); + +#[completion(ListInstalledEntry)] +pub(crate) fn comp_list_installed(ctx: &ShellContext) -> Suggest { + if ctx.typing_argument() { + return suggest! { + "--trusted": "Show only trusted namespaces", + "--untrusted": "Show only untrusted namespaces", + }; + } + return suggest!(); +} + +#[derive(Debug, Serialize, Default, Groupped)] +pub(crate) enum StateListInstalledOptions { + #[default] + All, + OnlyTrusted, + OnlyUntrusted, +} + +pack!(MutexErrorListInstalled = ()); + +#[chain] +pub(crate) fn handle_list_installed_entry(prev: ListInstalledEntry) -> NextProcess { + let picker = Picker::new(prev.inner); + let r = picker + .pick::("--trusted") + .pick::("--untrusted") + .unpack(); + + let option: StateListInstalledOptions = match r { + // (show_trusted, show_untrusted) + (true, false) => StateListInstalledOptions::OnlyTrusted, + (false, true) => StateListInstalledOptions::OnlyUntrusted, + (false, false) => StateListInstalledOptions::All, + (true, true) => return MutexErrorListInstalled::default().to_render(), + }; + + option.to_chain() +} + +#[renderer] +pub(crate) fn render_list_installed_mutex_error(_prev: MutexErrorListInstalled) { + r_println!("Error: cannot use both --trusted and --untrusted options at the same time") +} + +#[derive(Debug, Groupped, Serialize)] +pub(crate) struct ResultInstalledNamespaces { + trusted: Vec, + untrusted: Vec, + untagged: Vec, + option: StateListInstalledOptions, +} + +#[chain] +pub(crate) fn handle_state_list_installed_option(prev: StateListInstalledOptions) -> NextProcess { + ResultInstalledNamespaces { + trusted: list_namespaces(true, false, false), + untrusted: list_namespaces(false, true, false), + untagged: list_namespaces(false, false, true), + option: prev, + } +} + +#[renderer] +pub(crate) fn render_installed(prev: ResultInstalledNamespaces) { + match prev.option { + StateListInstalledOptions::All => { + print_list("Trusted".bright_green().bold().to_string(), prev.trusted, r); + print_list( + "Unrusted".bright_red().bold().to_string(), + prev.untrusted, + r, + ); + print_list( + "Untagged".bright_black().bold().to_string(), + prev.untagged, + r, + ); + } + StateListInstalledOptions::OnlyTrusted => { + print_list("Trusted".bright_green().bold().to_string(), prev.trusted, r); + } + StateListInstalledOptions::OnlyUntrusted => { + print_list( + "Unrusted".bright_red().bold().to_string(), + prev.untrusted, + r, + ); + } + } +} + +fn print_list(title: String, list: Vec, r: &mut RenderResult) { + if list.is_empty() { + return; + } + + r_println!("{}", title); + + let mut i = 1; + for namespace in list.iter() { + r_println!(" {}. {}\n", i.to_string(), namespace.bold()); + i += 1; + } +} diff --git a/mling/src/cli/namespace_mgr.rs b/mling/src/cli/namespace_mgr.rs new file mode 100644 index 0000000..9781040 --- /dev/null +++ b/mling/src/cli/namespace_mgr.rs @@ -0,0 +1,128 @@ +use mingling::{ + ShellContext, Suggest, SuggestItem, + macros::{chain, completion, dispatcher, pack, r_println, renderer, route, suggest}, + parser::{Picker, Yes}, +}; + +use crate::{ + ThisProgram, + namespace_manager::{list_namespaces, remove_namespace, set_namespace_trusted}, +}; + +dispatcher!("trust", TrustNamespaceCommand => TrustNamespaceEntry); +dispatcher!("untrust", UntrustNamespaceCommand => UntrustNamespaceEntry); + +dispatcher!("set-trust", SetTrustNamespaceCommand => SetTrustNamespaceEntry); + +dispatcher!("rm-namespace", RemoveNamespaceCommand => RemoveNamespaceEntry); + +pack!(ErrorNamespaceNotProvided = ()); +pack!(ResultNamespaceTrustChanged = ()); +pack!(ResultNamespaceRemoved = ()); + +#[completion(TrustNamespaceEntry)] +pub(crate) fn comp_trust(ctx: &ShellContext) -> Suggest { + if ctx.previous_word == "trust" { + return Suggest::Suggest( + list_namespaces(false, true, true) + .into_iter() + .map(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + return suggest!(); +} + +#[completion(UntrustNamespaceEntry)] +pub(crate) fn comp_untrust(ctx: &ShellContext) -> Suggest { + if ctx.previous_word == "untrust" { + return Suggest::Suggest( + list_namespaces(true, false, true) + .into_iter() + .map(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + return suggest!(); +} + +#[completion(SetTrustNamespaceEntry)] +pub(crate) fn comp_set_trust(ctx: &ShellContext) -> Suggest { + if ctx.typing_argument() { + return suggest!( + "-t": "Whether to trust this namespace", + "--trusted": "Whether to trust this namespace", + ); + } + if ctx.filling_argument_first(["-t", "--trusted"]) { + return suggest!("yes", "no"); + } + if ctx.previous_word == "set-trust" { + return Suggest::Suggest( + list_namespaces(true, true, true) + .into_iter() + .map(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + return suggest!(); +} + +#[completion(RemoveNamespaceEntry)] +pub(crate) fn comp_remove_namespace(ctx: &ShellContext) -> Suggest { + if ctx.previous_word == "rm-namespace" { + return Suggest::Suggest( + list_namespaces(true, true, true) + .into_iter() + .map(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + return suggest!(); +} + +#[chain] +pub(crate) fn handle_set_trust(p: SetTrustNamespaceEntry) -> NextProcess { + let (trusted, namespace) = route!( + Picker::new(p.inner) + .pick::(["-t", "--trusted"]) + .pick_or_route((), ErrorNamespaceNotProvided::default().to_render()) + .unpack() + ); + set_namespace_trusted(namespace, trusted.is_yes()); + ResultNamespaceTrustChanged::default().to_render() +} + +#[chain] +pub(crate) fn handle_trust(p: TrustNamespaceEntry) -> NextProcess { + SetTrustNamespaceEntry::new({ + let mut args = p.inner.clone(); + args.extend(vec!["-t".to_string(), "yes".to_string()]); + args + }) +} + +#[chain] +pub(crate) fn handle_untrust(p: UntrustNamespaceEntry) -> NextProcess { + SetTrustNamespaceEntry::new({ + let mut args = p.inner.clone(); + args.extend(vec!["-t".to_string(), "no".to_string()]); + args + }) +} + +#[chain] +pub(crate) fn handle_remove_namespace(p: RemoveNamespaceEntry) -> NextProcess { + let namespace = route!( + Picker::new(p.inner) + .pick_or_route((), ErrorNamespaceNotProvided::default().to_render()) + .unpack() + ); + remove_namespace(namespace); + ResultNamespaceRemoved::default().to_render() +} + +#[renderer] +pub(crate) fn render_error_namespace_not_provided(_prev: ErrorNamespaceNotProvided) { + r_println!("Error: no namespace was provided!") +} diff --git a/mling/src/cli/read.rs b/mling/src/cli/read.rs new file mode 100644 index 0000000..8717932 --- /dev/null +++ b/mling/src/cli/read.rs @@ -0,0 +1,77 @@ +use colored::Colorize; +use std::path::PathBuf; + +use mingling::{ + Groupped, + macros::{chain, dispatcher, pack, r_println, renderer}, +}; +use serde::Serialize; + +use crate::{ + ThisProgram, + project_solver::{BinaryItem, solve_current_dir}, +}; + +dispatcher!("show-target-dir", ReadTargetDirCommand => ReadTargetDirEntry); +dispatcher!("show-workspace-root", ReadWorkspaceRootCommand => ReadWorkspaceRootEntry); +dispatcher!("show-binaries", ReadBinariesCommand => ReadBinariesEntry); + +pack!(ResultDir = PathBuf); +pack!(ResultTargetDirNotFound = ()); + +#[derive(Debug, Serialize, Default, Groupped)] +pub(crate) struct ResultBinaries { + bin: Vec, +} + +#[chain] +pub(crate) fn handle_target_dir_entry(_prev: ReadTargetDirEntry) -> NextProcess { + match solve_current_dir() { + Ok(solved) => { + let dir = solved.target_dir; + ResultDir::new(dir).to_render() + } + Err(_) => ResultTargetDirNotFound::new(()).to_render(), + } +} + +#[chain] +pub(crate) fn handle_workspace_root_entry(_prev: ReadWorkspaceRootEntry) -> NextProcess { + match solve_current_dir() { + Ok(solved) => { + let dir = solved.workspace_root; + ResultDir::new(dir).to_render() + } + Err(_) => ResultTargetDirNotFound::new(()).to_render(), + } +} + +#[chain] +pub(crate) fn handle_binaries_entry(_prev: ReadBinariesEntry) -> NextProcess { + match solve_current_dir() { + Ok(solved) => { + let binaries = solved.binaries; + ResultBinaries { bin: binaries }.to_render() + } + Err(_) => ResultTargetDirNotFound::new(()).to_render(), + } +} + +#[renderer] +pub(crate) fn render_dir(prev: ResultDir) { + r_println!("{}", prev.inner.display()) +} + +#[renderer] +pub(crate) fn render_binaries(prev: ResultBinaries) { + let mut i = 1; + for item in prev.bin.iter() { + r_println!( + "{}. {} ({})", + i.to_string(), + item.name.bold(), + item.path.to_string_lossy().underline().bright_cyan() + ); + i += 1; + } +} diff --git a/mling/src/cli/refresh.rs b/mling/src/cli/refresh.rs new file mode 100644 index 0000000..368670e --- /dev/null +++ b/mling/src/cli/refresh.rs @@ -0,0 +1,32 @@ +use mingling::{ + ShellContext, Suggest, + macros::{chain, completion, dispatcher, pack, suggest}, + parser::Picker, +}; + +use crate::{ThisProgram, project_installer::install_all}; + +dispatcher!("refresh", RefreshCommand => RefreshEntry); + +pack!(ResultRefreshCompleted = ()); + +#[completion(RefreshEntry)] +pub(crate) fn comp_refresh(ctx: &ShellContext) -> Suggest { + if ctx.typing_argument() { + return suggest! { + "--clean": "Clean build artifacts before installation", + "-c": "Clean build artifacts before installation", + }; + } + return suggest!(); +} + +#[chain] +pub(crate) fn handle_refresh_entry(prev: RefreshEntry) -> NextProcess { + let is_clean_before_build = Picker::new(prev.inner) + .pick::(["--clean", "-c"]) + .unpack(); + let _ = install_all(is_clean_before_build); + + ResultRefreshCompleted::new(()) +} diff --git a/mling/src/main.rs b/mling/src/main.rs new file mode 100644 index 0000000..7b72dc1 --- /dev/null +++ b/mling/src/main.rs @@ -0,0 +1,14 @@ +use mingling::macros::gen_program; + +pub mod cli; +pub mod namespace_manager; +pub mod project_installer; +pub mod project_solver; + +use crate::cli::*; + +fn main() { + cli_entry(); +} + +gen_program!(); diff --git a/mling/src/namespace_manager.rs b/mling/src/namespace_manager.rs new file mode 100644 index 0000000..4d36136 --- /dev/null +++ b/mling/src/namespace_manager.rs @@ -0,0 +1,113 @@ +use std::path::PathBuf; + +use just_fmt::kebab_case; + +pub fn list_namespaces( + show_trusted: bool, + show_untrusted: bool, + show_untagged: bool, +) -> Vec { + let wdir = working_dir(); + if !wdir.exists() { + return Vec::new(); + } + + let mut namespaces = Vec::new(); + let entries = match std::fs::read_dir(&wdir) { + Ok(entries) => entries, + Err(_) => return Vec::new(), + }; + for entry in entries { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let path = entry.path(); + if path.is_dir() { + if let Some(name) = path.file_name() { + if let Some(name_str) = name.to_str() { + // Skip directories starting with a dot + if name_str.starts_with('.') { + continue; + } + let namespace = name_str.to_string(); + let is_trusted = is_trusted_namespace(namespace.clone()); + let is_untrusted = is_untrusted_namespace(namespace.clone()); + let is_untagged = is_untagged_namespace(namespace.clone()); + + if (show_trusted && is_trusted) + || (show_untrusted && is_untrusted) + || (show_untagged && is_untagged) + { + namespaces.push(namespace); + } + } + } + } + } + + namespaces +} + +pub fn set_namespace_trusted(namespace: String, trusted: bool) { + let ndir = namespace_dir(namespace); + let trusted_file = ndir.join("TRUSTED"); + let untrusted_file = ndir.join("UNTRUSTED"); + + if trusted { + // Create TRUSTED file and remove UNTRUSTED if it exists + let _ = std::fs::write(&trusted_file, ""); + let _ = std::fs::remove_file(&untrusted_file); + } else { + // Remove TRUSTED file + let _ = std::fs::remove_file(&trusted_file); + } +} + +pub fn remove_namespace(namespace: String) { + let ndir = namespace_dir(namespace); + if ndir.exists() { + let _ = std::fs::remove_dir_all(&ndir); + } +} + +pub fn working_dir() -> PathBuf { + dirs::data_dir().unwrap().join("mingling") +} + +pub fn namespace_dir(namespace: String) -> PathBuf { + working_dir().join(kebab_case!(namespace)) +} + +pub fn is_untrusted_namespace(namespace: String) -> bool { + let untrusted_file = namespace_dir(namespace).join("UNTRUSTED"); + untrusted_file.exists() +} + +pub fn is_trusted_namespace(namespace: String) -> bool { + let trusted = namespace_dir(namespace).join("TRUSTED"); + trusted.exists() +} + +pub fn is_untagged_namespace(namespace: String) -> bool { + let ndir = namespace_dir(namespace); + let trusted = ndir.join("TRUSTED"); + let untrusted = ndir.join("UNTRUSTED"); + !trusted.exists() && !untrusted.exists() +} + +pub fn bin_dir(namespace: String) -> PathBuf { + namespace_dir(namespace).join("bin") +} + +pub fn comp_dir(namespace: String) -> PathBuf { + namespace_dir(namespace).join("comp") +} + +pub fn exe_path(namespace: String, bin_name_without_ext: String) -> PathBuf { + if cfg!(target_os = "windows") { + bin_dir(namespace).join(bin_name_without_ext + ".exe") + } else { + bin_dir(namespace).join(bin_name_without_ext) + } +} diff --git a/mling/src/project_installer.rs b/mling/src/project_installer.rs new file mode 100644 index 0000000..d004e40 --- /dev/null +++ b/mling/src/project_installer.rs @@ -0,0 +1,153 @@ +use std::path::PathBuf; + +use mingling::{ShellFlag, build::build_comp_script_to}; + +use crate::{ + namespace_manager::{bin_dir, comp_dir, exe_path, working_dir}, + project_solver::solve, +}; + +const SCRIPT_LOAD_BASH: &str = include_str!("../tmpl/load.sh"); +const SCRIPT_LOAD_FISH: &str = include_str!("../tmpl/load.fish"); +const SCRIPT_LOAD_PWSH: &str = include_str!("../tmpl/load.ps1"); + +#[derive(serde::Deserialize)] +struct CargoToml { + package: Package, +} + +#[derive(serde::Deserialize)] +struct Package { + name: String, +} + +pub fn install_all(clean_before_build: bool) -> Result<(), std::io::Error> { + let current = std::env::current_dir()?; + install_this_project(current, clean_before_build)?; + install_shell_scripts()?; + Ok(()) +} + +pub fn install_this_project( + current: PathBuf, + clean_before_build: bool, +) -> Result<(), std::io::Error> { + // Obtain context data + let solved = solve(current)?; + + let workspace_root = &solved.workspace_root; + + // If clean_before_build, execute cargo clean in workspace_root first + if clean_before_build { + let status = std::process::Command::new("cargo") + .arg("clean") + .current_dir(workspace_root) + .status()?; + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "exec `cargo clean` failed", + )); + } + } + + // Execute cargo build --release in workspace_root + let status = std::process::Command::new("cargo") + .args(["build", "--release"]) + .current_dir(workspace_root) + .status()?; + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "cargo build --release failed", + )); + } + + // Parse package.name from workspace_root's Cargo.toml as namespace + let cargo_toml_content = std::fs::read_to_string(workspace_root.join("Cargo.toml"))?; + let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("failed to parse Cargo.toml: {e}"), + ) + })?; + let namespace = cargo_toml.package.name; + + // Ensure destination directories exist + std::fs::create_dir_all(bin_dir(namespace.clone()))?; + std::fs::create_dir_all(comp_dir(namespace.clone()))?; + + // Copy binaries to corresponding exe_path + for bin in &solved.binaries { + let dst = exe_path(namespace.clone(), bin.name.clone()); + std::fs::copy(&bin.path, &dst)?; + } + + // Copy all completion scripts containing _comp from target/release to comp_dir + let target_dir = &solved.target_dir; + let release_dir = target_dir.join("release"); + if release_dir.exists() { + for entry in std::fs::read_dir(&release_dir)? { + let entry = entry?; + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + if file_name_str.contains("_comp") { + let dest = comp_dir(namespace.clone()).join(file_name.as_os_str()); + std::fs::copy(entry.path(), &dest)?; + } + } + } + + Ok(()) +} + +pub fn install_shell_scripts() -> Result<(), std::io::Error> { + // Get the working directory (mingling data dir) + let wdir = working_dir(); + std::fs::create_dir_all(&wdir)?; + + // Build shell completion scripts for the "mling" command based on the current OS + let mling_comp = if cfg!(target_os = "windows") { + vec![ShellFlag::Powershell] + } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") { + vec![ShellFlag::Bash, ShellFlag::Zsh, ShellFlag::Fish] + } else { + vec![ShellFlag::Bash] + }; + + for flag in mling_comp { + build_comp_script_to( + &flag, + "mling", + wdir.join(".comp").display().to_string().as_str(), + )?; + } + + // Determine which scripts to write based on platform + let scripts: Vec<(&str, &str)> = if cfg!(target_os = "windows") { + vec![("load.ps1", SCRIPT_LOAD_PWSH)] + } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") { + vec![ + ("load.sh", SCRIPT_LOAD_BASH), + ("load.fish", SCRIPT_LOAD_FISH), + ] + } else { + // Fallback: write bash script + vec![("load.sh", SCRIPT_LOAD_BASH)] + }; + + for (filename, content) in scripts { + let dest = wdir.join(filename); + std::fs::write(&dest, content)?; + if cfg!(target_os = "linux") { + let status = std::process::Command::new("chmod") + .args(["+x", &dest.to_string_lossy()]) + .status()?; + if !status.success() { + eprintln!("Failed to chmod {}", filename); + } + } + } + + Ok(()) +} diff --git a/mling/src/project_solver.rs b/mling/src/project_solver.rs new file mode 100644 index 0000000..381bba2 --- /dev/null +++ b/mling/src/project_solver.rs @@ -0,0 +1,105 @@ +use std::path::PathBuf; + +use serde::Serialize; + +pub type BinaryName = String; +pub type BinaryTargetPath = PathBuf; + +pub struct ProjectSolveResult { + pub target_dir: PathBuf, + pub workspace_root: PathBuf, + pub binaries: Vec, +} + +#[derive(Debug, Serialize)] +pub struct BinaryItem { + pub name: String, + pub path: PathBuf, +} + +pub fn solve_current_dir() -> Result { + let current = std::env::current_dir()?; + solve(current) +} + +pub fn solve(current: PathBuf) -> Result { + let (target_dir, workspace_root, binaries) = solve_inner(¤t)?; + Ok(ProjectSolveResult { + target_dir, + workspace_root, + binaries, + }) +} + +fn solve_inner(current: &PathBuf) -> Result<(PathBuf, PathBuf, Vec), std::io::Error> { + let output = std::process::Command::new("cargo") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(current) + .output()?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("cargo metadata failed: {}", stderr), + )); + } + let metadata: serde_json::Value = serde_json::from_slice(&output.stdout) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let workspace_root_str = metadata["workspace_root"].as_str().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "missing workspace_root") + })?; + let workspace_root = PathBuf::from(workspace_root_str); + + let target_dir_str = metadata["target_directory"].as_str().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "missing target_directory") + })?; + let target_dir = PathBuf::from(target_dir_str); + + let packages = metadata["packages"].as_array().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "missing packages array") + })?; + + let mut binaries = Vec::new(); + let cargo_toml_path = workspace_root.join("Cargo.toml"); + + // Find the package whose manifest_path matches workspace_root/Cargo.toml + for pkg in packages { + let manifest_path = pkg["manifest_path"].as_str().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "missing manifest_path") + })?; + let manifest_path_buf = PathBuf::from(manifest_path); + if manifest_path_buf == cargo_toml_path { + // Found the workspace root package + if let Some(targets) = pkg["targets"].as_array() { + for target in targets { + let kind = target["kind"].as_array(); + let is_bin = kind + .map(|k| k.iter().any(|v| v.as_str() == Some("bin"))) + .unwrap_or(false); + if is_bin { + let name = target["name"].as_str().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "missing target name", + ) + })?; + let mut binary_path = target_dir.join("release").join(name); + if cfg!(target_os = "windows") { + binary_path.set_extension("exe"); + } + binaries.push(BinaryItem { + name: name.to_string(), + path: binary_path, + }); + } + } + } + break; + } + } + + Ok((target_dir, workspace_root, binaries)) +} -- cgit