diff options
| -rw-r--r-- | mling/src/cargo_style.rs | 60 | ||||
| -rw-r--r-- | mling/src/cli.rs | 69 | ||||
| -rw-r--r-- | mling/src/helps/mling_help.txt | 2 | ||||
| -rw-r--r-- | mling/src/lib.rs | 43 | ||||
| -rw-r--r-- | mling/src/pkg_mgr/mod.rs | 16 | ||||
| -rw-r--r-- | mling/src/proj_mgr/metadata.rs | 142 | ||||
| -rw-r--r-- | mling/src/proj_mgr/mod.rs | 22 | ||||
| -rw-r--r-- | mling/src/res/manifest_path.rs | 13 | ||||
| -rw-r--r-- | mling/src/res/mod.rs | 3 |
9 files changed, 345 insertions, 25 deletions
diff --git a/mling/src/cargo_style.rs b/mling/src/cargo_style.rs index f127cc9..c212094 100644 --- a/mling/src/cargo_style.rs +++ b/mling/src/cargo_style.rs @@ -54,6 +54,29 @@ macro_rules! eformat_cargo { }; } +/// Formats a help message in cargo-style format with a bright white "help" prefix. +/// +/// # Macros +/// +/// - `hformat_cargo!("prefix: {}", arg)` — format-style invocation +/// - `hformat_cargo!(expr)` — direct expression invocation +/// +/// # Examples +/// +/// ```ignore +/// hformat_cargo!("use --verbose for more info"); +/// // Output: "help: use --verbose for more info" (bright white "help") +/// ``` +#[macro_export] +macro_rules! hformat_cargo { + ($fmt:literal, $($arg:tt)*) => { + $crate::get_cargo_help_format(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::get_cargo_help_format($cmd) + }; +} + /// Print a message in cargo-style format with a bold green prefix. /// /// # Macros @@ -98,6 +121,28 @@ macro_rules! eprintln_cargo { }; } +/// Print a help message in cargo-style format with a bright white "help" prefix. +/// +/// # Macros +/// +/// - `hprintln_cargo!("prefix: {}", arg)` — format-style invocation +/// - `hprintln_cargo!(expr)` — direct expression invocation +/// +/// # Examples +/// +/// ```ignore +/// hprintln_cargo!("use --verbose for more info"); +/// ``` +#[macro_export] +macro_rules! hprintln_cargo { + ($fmt:literal, $($arg:tt)*) => { + println!("{}", $crate::get_cargo_help_format(format!($fmt, $($arg)*))) + }; + ($cmd:expr) => { + println!("{}", $crate::get_cargo_help_format($cmd)) + }; +} + /// Format a message in cargo style format, with bold green prefix. /// /// The input string is split at the first `:`. The part before the colon becomes @@ -159,3 +204,18 @@ pub fn get_cargo_info_format(str: impl Into<String>) -> String { pub fn get_cargo_error_format(str: impl Into<String>) -> String { format!("{}: {}", "error".bold().bright_red(), str.into()) } + +/// Format a help message in cargo style format, with bright white "help" prefix. +/// +/// The input string is printed as the help content, prefixed by a bright white +/// `help:` label (not bold). +/// +/// # Examples +/// +/// ```ignore +/// get_cargo_help_format("use --verbose for more info"); +/// // returns "help: use --verbose for more info" +/// ``` +pub fn get_cargo_help_format(str: impl Into<String>) -> String { + format!("{}: {}", "help".bright_white(), str.into()) +} diff --git a/mling/src/cli.rs b/mling/src/cli.rs index 57f562f..0bae4a4 100644 --- a/mling/src/cli.rs +++ b/mling/src/cli.rs @@ -1,15 +1,16 @@ -use std::{env::current_dir, process::exit}; +use std::{env::current_dir, path::PathBuf, process::exit, str::FromStr}; use crate::{ - CMDCompletion, ThisProgram, + CMDCompletion, PackageManagerSetup, ProjectManagerSetup, ThisProgram, display::markdown, - proj_mgr::{CMDInstall, CMDListNamespace, CMDRemoveNamespace}, - res::ResCurrentDir, + eprintln_cargo, + pkg_mgr::{CMDInstall, CMDListNamespace, CMDRemoveNamespace}, + res::{ResCurrentDir, ResManifestPath}, }; use mingling::{ Program, hook::ProgramHook, - macros::program_setup, + macros::{help, program_setup}, setup::{ExitCodeSetup, GeneralRendererSetup, HelpFlagSetup, QuietFlagSetup}, }; @@ -48,22 +49,40 @@ pub fn run() { _ => {} })); + // Commands + program.with_dispatcher(CMDCompletion); + // Setups program.with_setup(HelpFlagSetup::new(["-h", "--help"])); program.with_setup(GeneralRendererSetup); - program.with_setup(StandardOutputSetup); program.with_setup(ExitCodeSetup::default()); + program.with_setup(StandardOutputSetup); + program.with_setup(PackageManagerSetup); + program.with_setup(ProjectManagerSetup); + // Resources program.with_resource(ResCurrentDir { path: current_dir().unwrap(), }); - // Commands - program.with_dispatcher(CMDCompletion); - program.with_dispatcher(CMDInstall); - program.with_dispatcher(CMDListNamespace); - program.with_dispatcher(CMDRemoveNamespace); + let manifest_path = program.pick_global_argument(["-P", "--manifest-path"]); + program.with_resource(ResManifestPath { + raw: manifest_path, + resolved: None, + }); + + // Manifest Path Check + program.with_hook(ProgramHook::empty().on_post_dispatch(|c| match c { + // Skip completion (bypass completion) + ThisProgram::CompletionContext => {} + _ => { + let p = ThisProgram::this(); + p.modify_res(|manifest_path: &mut ResManifestPath| { + manifest_path.resolved = Some(resolve_manifest_path(manifest_path.raw.clone())); + }); + } + })); // Execute let quiet = program.stdout_setting.quiet; @@ -93,3 +112,31 @@ fn standard_output_setup(program: &mut Program<ThisProgram>) { program.stdout_setting.quiet = true; }); } + +fn resolve_manifest_path(provided: Option<String>) -> PathBuf { + if let Some(path) = provided { + let p = PathBuf::from_str(&path).unwrap(); + if p.is_dir() { + let candidate = p.join("Cargo.toml"); + if candidate.exists() { + return candidate; + } + eprintln_cargo!("`{}` is not a crate root", p.display()); + exit(1); + } + return p; + } + // Walk up from current directory to find nearest Cargo.toml + let mut dir = current_dir().unwrap(); + loop { + let candidate = dir.join("Cargo.toml"); + if candidate.exists() { + return candidate; + } + if !dir.pop() { + // Reached filesystem root without finding Cargo.toml + eprintln_cargo!("`{}` is not a crate root", current_dir().unwrap().display()); + exit(1); + } + } +} diff --git a/mling/src/helps/mling_help.txt b/mling/src/helps/mling_help.txt index 4bf1ca6..73bfd4d 100644 --- a/mling/src/helps/mling_help.txt +++ b/mling/src/helps/mling_help.txt @@ -15,6 +15,6 @@ __ [[b_cyan]]**--no-result**[[/]] Suppress result output [[b_green]]**Commands:**[[/]] __ [[b_cyan]]**install**[[/]] Install current project into __ the **mling** package manager -__ [[b_cyan]]**ls**[[/]], [[b_cyan]]**add**[[/]], [[b_cyan]]**rm**[[/]] List, add, or remove something +__ [[b_cyan]]**ls**[[/]], [[b_cyan]]**show**[[/]], [[b_cyan]]**add**[[/]], [[b_cyan]]**rm**[[/]] List, show, add, or remove something Run \`[[b_cyan]]**cargo help mling**[[/]]\` for more detailed information. diff --git a/mling/src/lib.rs b/mling/src/lib.rs index d4ce697..add20ac 100644 --- a/mling/src/lib.rs +++ b/mling/src/lib.rs @@ -1,32 +1,57 @@ #![allow(unused_imports)] +use colored::Colorize; use mingling::{ - macros::{gen_program, r_println, renderer}, + macros::{chain, gen_program, pack, r_println, renderer}, res::ResExitCode, }; pub mod cli; +pub use cli::*; mod cargo_style; pub use cargo_style::*; pub mod display; pub mod res; +mod pkg_mgr; +pub use pkg_mgr::*; + mod proj_mgr; -use proj_mgr::*; +pub use proj_mgr::*; use crate::display::markdown; -#[renderer] -pub fn render_error_dispatch_not_found(err: ErrorDispatcherNotFound, ec: &mut ResExitCode) { +pack!(ResultMlingHelp = ()); +pack!(ResultUnknownCommand = String); + +#[chain] +fn handle_error_dispatcher_not_found(err: ErrorDispatcherNotFound) -> Next { if err.is_empty() { - r_println!("{}", markdown(include_str!("helps/mling_help.txt"))); - ec.exit_code = 0; + ResultMlingHelp::default().to_render() } else { - let s = eformat_cargo!("Cannot find subcommand \"{}\"", err.join(" ")); - r_println!("{}", s); - ec.exit_code = 1; + ResultUnknownCommand::new(err.join(" ")).to_render() } } +#[renderer] +fn render_mling_help(_prev: ResultMlingHelp, ec: &mut ResExitCode) { + r_println!("{}", markdown(include_str!("helps/mling_help.txt"))); + ec.exit_code = 0; +} + +#[renderer] +fn render_unknown_command(prev: ResultUnknownCommand, ec: &mut ResExitCode) { + r_println!( + "{}", + eformat_cargo!("no such command: `{}`", prev.bright_yellow().bold()) + ); + r_println!(); + r_println!( + "{}", + hformat_cargo!("view all commands with `cargo help mling`") + ); + ec.exit_code = 101; +} + gen_program!(); diff --git a/mling/src/pkg_mgr/mod.rs b/mling/src/pkg_mgr/mod.rs new file mode 100644 index 0000000..d03e1d8 --- /dev/null +++ b/mling/src/pkg_mgr/mod.rs @@ -0,0 +1,16 @@ +use crate::ThisProgram; +use mingling::{ + Program, + macros::{dispatcher, program_setup}, +}; + +dispatcher!("install"); +dispatcher!("ls.namespace", CMDListNamespace => EntryListNamespace); +dispatcher!("rm.namespace", CMDRemoveNamespace => EntryRemoveNamespace); + +#[program_setup] +pub fn package_manager_setup(p: &mut Program<ThisProgram>) { + p.with_dispatcher(CMDInstall); + p.with_dispatcher(CMDListNamespace); + p.with_dispatcher(CMDRemoveNamespace); +} diff --git a/mling/src/proj_mgr/metadata.rs b/mling/src/proj_mgr/metadata.rs new file mode 100644 index 0000000..c44ed46 --- /dev/null +++ b/mling/src/proj_mgr/metadata.rs @@ -0,0 +1,142 @@ +use serde::Deserialize; +use serde::Serialize; + +use std::path::PathBuf; +use std::process::Command; + +/// Read cargo metadata by running `cargo metadata` with the given manifest path. +pub fn read_metadata(cargo_toml: PathBuf) -> Result<CargoLockFile, std::io::Error> { + let output = Command::new("cargo") + .arg("metadata") + .arg("--format-version") + .arg("1") + .arg("--manifest-path") + .arg(cargo_toml) + .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 lock_file: CargoLockFile = serde_json::from_slice(&output.stdout) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + Ok(lock_file) +} + +/// A cargo metadata lock file that serde can serialize and deserialize. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CargoLockFile { + pub packages: Vec<Package>, + #[serde(rename = "workspace_members")] + pub workspace_members: Vec<String>, + #[serde(rename = "workspace_default_members")] + pub workspace_default_members: Vec<String>, + pub resolve: Resolve, + #[serde(rename = "target_directory")] + pub target_directory: String, + #[serde(rename = "build_directory")] + pub build_directory: String, + pub version: u64, + #[serde(rename = "workspace_root")] + pub workspace_root: String, + pub metadata: Option<serde_json::Value>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Package { + pub name: String, + pub version: String, + pub id: String, + pub license: Option<String>, + #[serde(rename = "license_file")] + pub license_file: Option<String>, + pub description: Option<String>, + pub source: Option<String>, + pub dependencies: Vec<Dependency>, + pub targets: Vec<Target>, + pub features: std::collections::BTreeMap<String, Vec<String>>, + #[serde(rename = "manifest_path")] + pub manifest_path: String, + pub metadata: Option<serde_json::Value>, + pub publish: Option<serde_json::Value>, + pub authors: Vec<String>, + pub categories: Vec<String>, + pub keywords: Vec<String>, + pub readme: Option<String>, + pub repository: Option<String>, + pub homepage: Option<String>, + pub documentation: Option<String>, + pub edition: String, + pub links: Option<String>, + #[serde(rename = "default_run")] + pub default_run: Option<String>, + #[serde(rename = "rust_version")] + pub rust_version: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Dependency { + pub name: String, + pub source: Option<String>, + pub req: String, + pub kind: Option<String>, + pub rename: Option<String>, + pub optional: bool, + #[serde(rename = "uses_default_features")] + pub uses_default_features: bool, + pub features: Vec<String>, + pub target: Option<String>, + pub registry: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Target { + pub kind: Vec<String>, + #[serde(rename = "crate_types")] + pub crate_types: Vec<String>, + pub name: String, + #[serde(rename = "src_path")] + pub src_path: String, + pub edition: String, + pub doc: bool, + pub doctest: bool, + pub test: bool, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "required-features")] + pub required_features: Option<Vec<String>>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Resolve { + pub nodes: Vec<ResolveNode>, + pub root: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolveNode { + pub id: String, + pub dependencies: Vec<String>, + pub deps: Vec<DepInfo>, + pub features: Vec<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DepInfo { + pub name: String, + pub pkg: String, + #[serde(rename = "dep_kinds")] + pub dep_kinds: Vec<DepKind>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DepKind { + pub kind: Option<String>, + pub target: Option<String>, +} diff --git a/mling/src/proj_mgr/mod.rs b/mling/src/proj_mgr/mod.rs index 381784c..07a9910 100644 --- a/mling/src/proj_mgr/mod.rs +++ b/mling/src/proj_mgr/mod.rs @@ -1,6 +1,20 @@ -use mingling::macros::dispatcher; +use crate::ThisProgram; +use mingling::{ + Program, + macros::{dispatcher, program_setup}, +}; -dispatcher!("install"); +pub mod metadata; -dispatcher!("ls.namespace", CMDListNamespace => EntryListNamespace); -dispatcher!("rm.namespace", CMDRemoveNamespace => EntryRemoveNamespace); +dispatcher!("show.binaries"); +dispatcher!("show.workspace"); +dispatcher!("show.target-dir", + CMDShowTargetDirectories => EntryShowTargetDirectories +); + +#[program_setup] +pub fn project_manager_setup(p: &mut Program<ThisProgram>) { + p.with_dispatcher(CMDShowBinaries); + p.with_dispatcher(CMDShowWorkspace); + p.with_dispatcher(CMDShowTargetDirectories); +} diff --git a/mling/src/res/manifest_path.rs b/mling/src/res/manifest_path.rs new file mode 100644 index 0000000..7f1686e --- /dev/null +++ b/mling/src/res/manifest_path.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +#[derive(Default, Clone)] +pub struct ResManifestPath { + pub raw: Option<String>, + pub resolved: Option<PathBuf>, +} + +impl ResManifestPath { + pub fn resolved(&self) -> &PathBuf { + self.resolved.as_ref().unwrap() + } +} diff --git a/mling/src/res/mod.rs b/mling/src/res/mod.rs index b6ea60d..caa843c 100644 --- a/mling/src/res/mod.rs +++ b/mling/src/res/mod.rs @@ -1,2 +1,5 @@ mod current_dir; pub use current_dir::*; + +mod manifest_path; +pub use manifest_path::*; |
