Skip to main content

hmm_rs/
lib.rs

1/// `println!` for step-by-step narration ("Checking out …", "✓ Cloned …");
2/// silent unless `-v`/`--verbose` was passed.
3macro_rules! vprintln {
4    ($($arg:tt)*) => {
5        if $crate::verbose() {
6            println!($($arg)*);
7        }
8    };
9}
10
11pub mod commands;
12pub mod hmm;
13
14use std::path::PathBuf;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17use anyhow::{Ok, Result};
18
19use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
20use shadow_rs::shadow;
21
22use crate::commands::add_command;
23
24shadow!(build);
25
26#[derive(Parser, Debug)]
27#[command(author, version, about, long_about = None)]
28#[command(version = build::CLAP_LONG_VERSION)]
29struct Cli {
30    #[command(subcommand)]
31    cmd: Commands,
32
33    #[command(flatten)]
34    global_opts: GlobalOpts,
35}
36
37/// Optional list of library names to scope a command to a subset of `hmm.json` deps.
38/// Empty means: apply to all dependencies. Names not in `hmm.json` print a warning and are skipped.
39#[derive(Debug, Args, Clone)]
40pub struct LibraryFilter {
41    /// Library names to operate on. If omitted, applies to all dependencies in hmm.json.
42    #[arg(value_name = "LIBS")]
43    pub lib: Vec<String>,
44}
45
46#[derive(Subcommand, Debug, Clone)]
47enum Commands {
48    /// Lists the dependencies in the hmm.json file (or a file of your choice with --path)
49    /// use `hmm-rs check` to see if the dependencies are installed at the correct versions
50    #[command(visible_alias = "ls")]
51    List {
52        #[command(flatten)]
53        filter: LibraryFilter,
54    },
55    /// Creates a .haxelib/ folder and an empty hmm.json file, leaving either alone if it already exists
56    Init,
57    /// Removes local .haxelib directory, useful for full clean reinstalls
58    #[command(visible_alias = "cl")]
59    Clean,
60    /// dumps the dependencies in hmm.json, either to a .hxml file or stdout
61    ToHxml {
62        /// The path to the hxml file you want to write to
63        #[arg(value_name = "HXML")]
64        hxml: Option<PathBuf>,
65    },
66    /// Checks if the dependencies are installed at their correct hmm.json versions.
67    /// Optionally specify library names to check only those.
68    #[command(visible_alias = "ch")]
69    Check {
70        #[command(flatten)]
71        filter: LibraryFilter,
72    },
73    /// Installs the dependencies from hmm.json, if they aren't already installed.
74    /// Optionally specify library names to install only those.
75    #[command(visible_alias = "i")]
76    Install {
77        #[command(flatten)]
78        filter: LibraryFilter,
79    },
80    Add(AddArgs),
81    /// Installs one or more haxelibs from lib.haxe.org. Each name may be `lib` or `lib@version`.
82    Haxelib {
83        /// Library specs to install. Each is `name` or `name@version` (e.g. `lime` or `lime@5.0.0`).
84        #[arg(required = true, num_args = 1..)]
85        names: Vec<String>,
86    },
87    /// Installs a library from a git repository
88    Git {
89        /// The name of the library
90        name: String,
91        /// The git repository URL (e.g., https://github.com/user/repo)
92        url: String,
93        /// Optional git ref (branch, tag, or commit SHA). If not specified, uses default branch
94        #[arg(value_name = "REF")]
95        git_ref: Option<String>,
96        /// Optional subdirectory in the repo where the library (haxelib.json) lives
97        #[arg(value_name = "DIR")]
98        dir: Option<String>,
99    },
100    /// Removes one or more library dependencies from `hmm.json` and the `.haxelib/` folder
101    #[command(visible_alias = "rm")]
102    Remove {
103        #[command(flatten)]
104        filter: LibraryFilter,
105    },
106    /// Adds a local development dependency to hmm.json
107    Dev {
108        /// The name of the haxelib
109        name: String,
110        /// The file system path (absolute or relative)
111        path: String,
112    },
113    /// Check for and install updates to hmm-rs itself
114    #[command(visible_alias = "self-update")]
115    Upgrade {
116        /// Only check for updates without installing
117        #[arg(long)]
118        check: bool,
119    },
120    /// Locks dependencies to their currently installed versions
121    Lock {
122        #[command(subcommand)]
123        subcommand: Option<LockCommands>,
124
125        /// Use shortened commit IDs instead of full ones for git repositories
126        #[arg(short = 's', long = "short-id")]
127        short_id: bool,
128
129        #[command(flatten)]
130        filter: LibraryFilter,
131    },
132    /// Prints a shell completion script to stdout
133    Completions {
134        /// The shell to generate the script for
135        shell: clap_complete::Shell,
136    },
137}
138
139#[derive(Debug, Args, Clone)]
140pub struct AddArgs {
141    /// One or more library names. With `--git URL`, exactly one name is required.
142    #[arg(required = true, num_args = 1..)]
143    pub names: Vec<String>,
144    #[arg(long, value_name = "URL")]
145    pub git: Option<String>,
146    /// Optional git ref (branch, tag, or commit SHA), used with `--git`. If not specified, uses default branch.
147    #[arg(long = "ref", value_name = "REF")]
148    pub git_ref: Option<String>,
149    /// Optional subdirectory in the repo where the library lives, used with `--git`.
150    #[arg(long = "dir", value_name = "DIR")]
151    pub dir: Option<String>,
152}
153
154#[derive(Debug, Args)]
155struct GlobalOpts {
156    /// Color
157    #[arg(long, value_enum, global = true, default_value_t = Color::Auto)]
158    color: Color,
159    /// Sets a custom hmm.json file to use
160    #[arg(short, long, value_name = "JSON", default_value = "hmm.json")]
161    json: Option<PathBuf>,
162    /// Verbosity level (can be specified multiple times, -v or -vvvv)
163    #[arg(long, short, global = true, action = clap::ArgAction::Count)]
164    verbose: u8,
165    /// Separator used in git remote names derived from URLs (default: ".").
166    /// Falls back to $HMM_REMOTE_SEPARATOR if unset.
167    #[arg(long, global = true, value_name = "SEP")]
168    remote_separator: Option<String>,
169    //... other global options
170}
171
172#[derive(Clone, Debug, ValueEnum)]
173enum Color {
174    Always,
175    Auto,
176    Never,
177}
178
179#[derive(Subcommand, Debug, Clone)]
180enum LockCommands {
181    /// Check if all dependencies are locked to specific versions
182    Check,
183}
184
185static VERBOSE: AtomicBool = AtomicBool::new(false);
186
187/// True when `-v`/`--verbose` was passed; gates step-by-step narration.
188pub fn verbose() -> bool {
189    VERBOSE.load(Ordering::Relaxed)
190}
191
192pub fn run() -> Result<()> {
193    let args = Cli::parse();
194    VERBOSE.store(args.global_opts.verbose > 0, Ordering::Relaxed);
195
196    let path = args.global_opts.json.clone().unwrap();
197    let load_deps = || hmm::json::read_json(&path);
198    let remote_separator = commands::install_command::resolve_remote_separator(
199        args.global_opts.remote_separator.as_deref(),
200    );
201
202    match args.cmd {
203        Commands::Add(add_args) => {
204            add_command::add_dependency(add_args, load_deps()?, path, &remote_separator)?
205        }
206        Commands::List { filter } => hmm::json::read_json(&path)?.print_string_list(&filter.lib)?,
207        Commands::Init => commands::init_command::init_hmm()?,
208        Commands::Clean => commands::clean_command::remove_haxelib_folder()?,
209        Commands::ToHxml { hxml } => commands::tohxml_command::dump_to_hxml(&load_deps()?, hxml)?,
210        Commands::Check { filter } => commands::check_command::check(
211            &load_deps()?,
212            &filter.lib,
213            args.global_opts.verbose > 0,
214        )?,
215        Commands::Install { filter } => commands::install_command::install_from_hmm(
216            &load_deps()?,
217            &filter.lib,
218            &remote_separator,
219            &path,
220        )?,
221        Commands::Haxelib { names } => commands::haxelib_command::install_haxelibs(&names, path)?,
222        Commands::Git {
223            name,
224            url,
225            git_ref,
226            dir,
227        } => commands::git_command::install_git(
228            &name,
229            &url,
230            &git_ref,
231            &dir,
232            load_deps()?,
233            path,
234            &remote_separator,
235        )?,
236        Commands::Remove { filter } => {
237            commands::remove_command::remove_haxelibs(load_deps()?, &filter.lib, path)?
238        }
239        Commands::Upgrade { check } => commands::upgrade_command::upgrade(check)?,
240        Commands::Dev { name, path } => commands::dev_command::add_dev_dependency(
241            &name,
242            &path,
243            args.global_opts.json.clone().unwrap(),
244        )?,
245        Commands::Lock {
246            subcommand,
247            short_id,
248            filter,
249        } => match subcommand {
250            Some(LockCommands::Check) => commands::lock_command::check_locked(&load_deps()?)?,
251            None => commands::lock_command::lock_dependencies(
252                &load_deps()?,
253                &filter.lib,
254                args.global_opts.json.unwrap(),
255                short_id,
256            )?,
257        },
258        Commands::Completions { shell } => {
259            let mut cmd = Cli::command();
260            let name = cmd.get_name().to_string();
261            clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
262        }
263    }
264    Ok(())
265}
266
267#[test]
268fn verify_cli() {
269    Cli::command().debug_assert();
270}