Skip to main content

lux_cli/
lib.rs

1use crate::{
2    args::PackageOrRockspec,
3    dist::{Bin, Dist, FlatArchive},
4    format::Fmt,
5    project::NewProject,
6    util::Util,
7};
8use miette::Result;
9use std::error::Error;
10use std::path::PathBuf;
11
12use add::Add;
13use build::Build;
14use check::Check;
15use clap::{Parser, Subcommand};
16use config::ConfigCmd;
17use debug::Debug;
18use doc::Doc;
19use download::Download;
20use exec::Exec;
21use generate_rockspec::GenerateRockspec;
22use info::Info;
23use install::Install;
24use install_rockspec::InstallRockspec;
25use lint::Lint;
26use list::ListCmd;
27use lux_lib::{
28    config::ConfigBuilder, lua_version::LuaVersion, package::PackageName, workspace::Workspace,
29};
30use miette::IntoDiagnostic;
31use outdated::Outdated;
32use pack::Pack;
33use path::Path;
34use pin::ChangePin;
35use remove::Remove;
36use run::Run;
37use run_lua::RunLua;
38use search::Search;
39use shell::Shell;
40use strum::IntoEnumIterator;
41use sync::SyncProject;
42use test::Test;
43use uninstall::Uninstall;
44use update::Update;
45use upload::Upload;
46use url::Url;
47use vendor::Vendor;
48use which::Which;
49
50pub mod add;
51pub mod args;
52pub mod build;
53pub mod check;
54pub mod config;
55pub mod debug;
56pub mod dist;
57pub mod doc;
58pub mod download;
59pub mod exec;
60pub mod fetch;
61pub mod format;
62pub mod generate_rockspec;
63pub mod info;
64pub mod install;
65pub mod install_lua;
66pub mod install_rockspec;
67pub mod lint;
68pub mod list;
69pub mod outdated;
70pub mod pack;
71pub mod path;
72pub mod pin;
73pub mod progress;
74pub mod project;
75pub mod purge;
76pub mod remove;
77pub mod run;
78pub mod run_lua;
79pub mod search;
80pub mod shell;
81pub mod sync;
82pub mod test;
83pub mod uninstall;
84pub mod unpack;
85pub mod update;
86pub mod upload;
87pub mod util;
88pub mod utils;
89pub mod vendor;
90pub mod which;
91pub mod workspace;
92
93/// A luxurious package manager for Lua.
94#[derive(Parser)]
95#[command(author, version, about, long_about = None, arg_required_else_help = true)]
96pub struct Cli {
97    /// Enable the sub-repositories in luarocks servers for rockspecs of in-development versions.
98    #[arg(long)]
99    pub dev: bool,
100
101    /// Fetch rocks/rockspecs from this server (takes priority over config file).
102    #[arg(long, value_name = "server")]
103    pub server: Option<Url>,
104
105    /// Fetch rocks/rockspecs from these servers in addition to the main server{n}
106    /// (overrides any entries in the config file).
107    #[arg(long, value_name = "extra-server")]
108    pub extra_servers: Option<Vec<Url>>,
109
110    /// Specify the luarocks server namespace to use.
111    #[arg(long, value_name = "namespace")]
112    pub namespace: Option<String>,
113
114    /// Specify the directory in which to install Lua if not found.
115    #[arg(long, value_name = "prefix")]
116    pub lua_dir: Option<PathBuf>,
117
118    /// Which Lua installation to use.{n}
119    /// Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit' and 'jit52'.{n}
120    /// If not set, Lux will detempt to detect the Lua version:{n}
121    ///   - From the current project, if it has an exact Lua version requirement.{n}
122    ///   - From the Lua installation that is available on the PATH.
123    #[arg(long, value_name = "ver")]
124    pub lua_version: Option<LuaVersion>,
125
126    /// Which tree to operate on.{n}
127    /// In a workspace, this can be used to specify a detached workspace tree.
128    #[arg(long, value_name = "tree")]
129    pub tree: Option<PathBuf>,
130
131    /// Specifies the cache directory, e.g. for luarocks manifests.
132    #[arg(long, value_name = "cache-dir")]
133    pub cache_dir: Option<PathBuf>,
134
135    /// Specifies the data directory,{n}
136    /// in which the default user install tree resides{n}
137    /// (e.g. ~/.local/share/lux).
138    #[arg(long, value_name = "data-dir")]
139    pub data_dir: Option<PathBuf>,
140
141    /// Specifies a directory with locally vendored sources and RockSpecs.{n}
142    /// When building or installing a package with this flag,{n}
143    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
144    #[arg(long, value_name = "vendor-dir")]
145    pub vendor_dir: Option<PathBuf>,
146
147    /// Override config variables.{n}
148    /// Example: `lx -v "LUA=/path/to/lua" ...`
149    #[arg(long, value_name = "variable", visible_short_alias = 'v', value_parser = parse_key_val::<String, String>)]
150    pub variables: Option<Vec<(String, String)>>,
151
152    /// The build profile to use when compiling packages.{n}
153    /// Default: `release`.
154    #[arg(long, value_enum, value_name = "profile")]
155    pub profile: Option<lux_lib::config::build::Profile>,
156
157    /// Display verbose output of commands executed, enabling DEBUG logs.{n}
158    /// To enable TRACE logs, set RUST_LOG=trace.
159    #[arg(long)]
160    pub verbose: bool,
161
162    /// Don't print any progress bars or spinners.
163    #[arg(long)]
164    pub no_progress: bool,
165
166    /// Skip prompts, selecting the default option.{n}
167    /// Prompting is enabled by default in terminal/TTY environments.
168    #[arg(long)]
169    pub no_prompt: Option<bool>,
170
171    /// Configure lux for installing Neovim packages.
172    #[arg(long)]
173    pub nvim: bool,
174    /// Disable prompts for two-factor authentication (2FA) codes.{n}
175    /// It is strongly recommended to enable 2FA instead
176    /// see (https://luarocks.org/settings/two-factor-auth).
177    #[arg(long)]
178    pub no_tfa: bool,
179
180    /// Timeout on network operations, in seconds.{n}
181    /// 0 means no timeout (wait forever). Default is 30.
182    #[arg(long, value_name = "seconds")]
183    pub timeout: Option<usize>,
184
185    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
186    /// 0 means no limit. Default is 0.
187    #[arg(long, visible_short_alias = 'j')]
188    pub max_jobs: Option<usize>,
189
190    /// Do not generate or update a `.luarc.json` file when building{n}
191    /// a project.
192    #[arg(long)]
193    pub no_luarc: bool,
194
195    /// Do not wrap Lua `bin` scripts.
196    #[arg(long)]
197    pub no_wrap_bin: bool,
198
199    /// The user agent to set when making web requests.
200    /// Default is "lux/<version>"
201    #[arg(long)]
202    pub user_agent: Option<String>,
203
204    #[command(subcommand)]
205    pub command: Commands,
206}
207
208#[derive(Subcommand)]
209pub enum Commands {
210    /// Add a dependency to the current project.
211    Add(Add),
212    /// Build/compile a project.
213    Build(Build),
214    /// [EXPERIMENTAL]{n}
215    /// Type check the current project based on EmmyLua/LuaCATS annotations.{n}
216    /// Respects `.emmyrc.json` and `.luarc.json` files in the project directory.
217    Check(Check),
218    /// Interact with the lux configuration.
219    #[command(subcommand, arg_required_else_help = true)]
220    Config(ConfigCmd),
221    /// Internal commands for debugging Lux itself.
222    #[command(subcommand, arg_required_else_help = true)]
223    Debug(Debug),
224    /// Distribute a Lux project.
225    #[command(subcommand, arg_required_else_help = true)]
226    Dist(Dist),
227    /// Show documentation for an installed rock.
228    Doc(Doc),
229    /// Download a specific rock file from a luarocks server.
230    #[command(arg_required_else_help = true)]
231    Download(Download),
232    /// Formats the codebase with stylua.
233    Fmt(Fmt),
234    /// Generate a rockspec file from a project.
235    GenerateRockspec(GenerateRockspec),
236    /// Show metadata for any rock.
237    Info(Info),
238    /// Install a rock for use on the system.
239    #[command(arg_required_else_help = true)]
240    Install(Install),
241    /// Install a local rockspec for use on the system.
242    #[command(arg_required_else_help = true)]
243    InstallRockspec(InstallRockspec),
244    /// Manually install and manage Lua headers for various Lua versions.
245    InstallLua,
246    /// Lint the current project using `luacheck`.
247    Lint(Lint),
248    /// List currently installed rocks.
249    List(ListCmd),
250    /// Run lua, with the `LUA_PATH` and `LUA_CPATH` set to the specified lux tree.
251    Lua(RunLua),
252    /// Create a new Lua project.
253    New(NewProject),
254    /// List outdated rocks.
255    Outdated(Outdated),
256    /// Create a packed rock for distribution, packing sources or binaries.
257    Pack(Pack),
258    /// Return the currently configured package path.
259    Path(Path),
260    /// Pin an existing rock, preventing any updates to the package.
261    Pin(ChangePin),
262    /// Remove all installed rocks from a tree.
263    Purge,
264    /// Remove a rock from the current project's lux.toml dependencies.
265    Remove(Remove),
266    /// Run the current project with the provided arguments.
267    Run(Run),
268    /// Execute a command that has been installed with lux.
269    /// If the command is not found, a package named after the command
270    /// will be installed.
271    Exec(Exec),
272    /// Query the luarocks servers.
273    #[command(arg_required_else_help = true)]
274    Search(Search),
275    /// Run the test suite in the current project directory.{n}
276    /// Lux supports the following test backends, specified by the `[test]` table in the lux.toml:{n}
277    /// {n}
278    ///   - busted:{n}
279    ///     {n}
280    ///     https://lunarmodules.github.io/busted/{n}
281    ///     {n}
282    ///     Example:{n}
283    ///     {n}
284    ///     ```toml{n}
285    ///     [test]{n}
286    ///     type = "busted"{n}
287    ///     flags = [ ] # Optional CLI flags to pass to busted{n}
288    ///     ```{n}
289    ///     {n}
290    ///     `lx test` will default to using `busted` if no test backend is specified and:{n}
291    ///         * there is a `.busted` file in the project root{n}
292    ///         * or `busted` is one of the `test_dependencies`).{n}
293    /// {n}
294    ///   - busted-nlua:{n}:
295    ///     {n}
296    ///     [currently broken on Windows]{n}
297    ///     A build backend for running busted tests with Neovim as the Lua interpreter.
298    ///     Used for testing Neovim plugins.
299    ///     {n}
300    ///     Example:{n}
301    ///     {n}
302    ///     ```toml{n}
303    ///     [test]{n}
304    ///     type = "busted-nlua"{n}
305    ///     flags = [ ] # Optional CLI flags to pass to busted{n}
306    ///     ```{n}
307    ///     {n}
308    ///     `lx test` will default to using `busted-nlua` if no test backend is specified and:{n}
309    ///         * there is a `.busted` file in the project root{n}
310    ///         * or `busted` and `nlua` are `test_dependencies`.{n}
311    /// {n}
312    ///   - command:{n}
313    ///     {n}
314    ///     Name/file name of a shell command that will run the test suite.{n}
315    ///     Example:{n}
316    ///     {n}
317    ///     ```toml{n}
318    ///     [test]{n}
319    ///     type = "command"{n}
320    ///     command = "make"{n}
321    ///     flags = [ "test" ]{n}
322    ///     ```{n}
323    ///     {n}
324    ///   - script:{n}
325    ///     {n}
326    ///     Relative path to a Lua script that will run the test suite.{n}
327    ///     Example:{n}
328    ///     {n}
329    ///     ```toml{n}
330    ///     [test]{n}
331    ///     type = "script"{n}
332    ///     script = "tests.lua" # Expects a tests.lua file in the project root{n}
333    ///     flags = [ ] # Optional arguments passed to the test script{n}
334    ///     ```{n}
335    Test(Test),
336    /// Uninstall a rock from the system.
337    Uninstall(Uninstall),
338    /// Unpins an existing rock, allowing updates to alter the package.
339    Unpin(ChangePin),
340    /// Updates all rocks in a project.
341    Update(Update),
342    /// Generate a Lua rockspec for a Lux project and upload it to the public luarocks repository.{n}
343    /// You can specify a source template for release and dev packages in the lux.toml.{n}
344    /// {n}
345    /// Example:{n}
346    /// {n}
347    /// ```toml{n}
348    /// [source]{n}
349    /// url = "https://host.com/owner/$(PACKAGE)/refs/tags/$(REF).zip"{n}
350    /// dev = "git+https://host.com/owner/$(PACKAGE).git"{n}
351    /// ```{n}
352    /// {n}
353    /// You can use the following variables in the source template:{n}
354    /// {n}
355    ///  - $(PACKAGE): The package name.{n}
356    ///  - $(VERSION): The package version.{n}
357    ///  - $(REF): The git tag or revision (if in a git repository).{n}
358    ///  - You may also specify environment variables with `$(<VAR_NAME>)`.{n}
359    /// {n}
360    /// If the `version` is not set in the lux.toml, lux will search the current
361    /// commit for SemVer tags and if found, will use it to generate the package version.
362    Upload(Upload),
363    /// Infrequently used commands such as for generating shell completions and man pages.
364    #[command(subcommand, arg_required_else_help = true)]
365    Util(Util),
366    /// Vendor the dependencies of a project or RockSpec locally.
367    /// When building or installing a package with the `--vendor-dir` option{n}
368    /// or the `[vendor_dir]` config option, Lux will fetch sources from the <vendor-dir>{n}
369    /// instead of from a remote server.
370    Vendor(Vendor),
371    /// Tell which file corresponds to a given module name.
372    Which(Which),
373    /// Spawns an interactive shell with PATH, LUA_PATH, LUA_CPATH and LUA_INIT set.
374    Shell(Shell),
375    /// Synchronize the project tree with the current lux.toml,{n}
376    /// ensuring all packages are installed correctly.
377    Sync(SyncProject),
378}
379
380impl Commands {
381    /// For workspace commands, try to determine the project's Lua version.
382    ///
383    /// Returns [`None`]:
384    /// - if the project does not have an exact Lua version
385    /// - if there is more than one project an no `--package` has been specified
386    /// - if the command is not a project command
387    /// - if the workspace cannot be loaded
388    pub fn lua_version(&self) -> Option<LuaVersion> {
389        match self {
390            Self::Add(Add { package, .. })
391            | Self::Build(Build { package, .. })
392            | Self::Fmt(Fmt { package, .. })
393            | Self::Upload(Upload { package, .. })
394            | Self::GenerateRockspec(GenerateRockspec { package, .. })
395            | Self::Pin(ChangePin { package, .. })
396            | Self::Unpin(ChangePin { package, .. })
397            | Self::Remove(Remove { package, .. })
398            | Self::Test(Test { package, .. })
399            | Self::Update(Update { package, .. })
400            | Self::Run(Run {
401                build: Build { package, .. },
402                ..
403            }) => project_lua_version(package),
404            Self::Dist(d) => match d {
405                Dist::Bin(Bin { package, .. }) => project_lua_version(package),
406                Dist::FlatArchive(FlatArchive {
407                    package_or_rockspec,
408                    ..
409                }) => match package_or_rockspec {
410                    Some(PackageOrRockspec::Package(p)) => {
411                        project_lua_version(&Some(p.name().clone()))
412                    }
413                    Some(PackageOrRockspec::RockSpec(_)) => None,
414                    None => project_lua_version(&None),
415                },
416            },
417            Self::Pack(Pack {
418                package_or_rockspec,
419            }) => match package_or_rockspec {
420                Some(PackageOrRockspec::Package(p)) => project_lua_version(&Some(p.name().clone())),
421                Some(PackageOrRockspec::RockSpec(_)) => None,
422                None => project_lua_version(&None),
423            },
424            | Self::Debug(Debug::Project(_)) => project_lua_version(&None),
425            // workspace commands without a --package flag
426            Self::Check(_)
427            | Self::Exec(_)
428            | Self::Info(_)
429            | Self::Lua(_)
430            | Self::Lint(_)
431            | Self::Outdated(_)
432            | Self::Path(_)
433            | Self::Shell(_)
434            | Self::Sync(_)
435            | Self::Vendor(_) => {
436                project_lua_version(&None)
437            },
438            | Self::New(_)
439            // non-project commands
440            | Self::Config(_)
441            | Self::Util(_)
442            | Self::Debug(Debug::Unpack(_))
443            | Self::Debug(Debug::FetchRemote(_))
444            | Self::Debug(Debug::UnpackRemote(_))
445            | Self::Debug(Debug::Toolchains(_))
446            | Self::Doc(_)
447            | Self::Download(_)
448            | Self::Install(_)
449            | Self::InstallRockspec(_)
450            | Self::InstallLua
451            | Self::List(_)
452            | Self::Purge
453            | Self::Search(_)
454            | Self::Uninstall(_)
455            | Self::Which(_) => None,
456        }
457    }
458
459    /// Load the user [`ConfigBuilder`], merged with the workspace-local [`ConfigBuilder`],
460    /// if present and running a workspace command.
461    pub fn config(&self) -> Result<ConfigBuilder> {
462        let config = ConfigBuilder::new()?;
463        if let Some(workspace) = self.workspace()? {
464            let config = if let Some(workspace_config) = workspace.config()? {
465                config.merge(workspace_config)
466            } else {
467                config
468            };
469            if let Self::Dist(_) = self {
470                Ok(config)
471            } else {
472                Ok(config.default_build_profile(lux_lib::config::build::Profile::Dev))
473            }
474        } else {
475            Ok(config)
476        }
477    }
478
479    /// For commands that can operate on a workspace, load the current workspace, if present.
480    fn workspace(&self) -> Result<Option<Workspace>> {
481        match self {
482            Self::Add(_)
483            | Self::Build(_)
484            | Self::Fmt(_)
485            | Self::Upload(_)
486            | Self::GenerateRockspec(_)
487            | Self::Pin(_)
488            | Self::Unpin(_)
489            | Self::Remove(_)
490            | Self::Test(_)
491            | Self::Update(_)
492            | Self::Check(_)
493            | Self::Exec(_)
494            | Self::Info(_)
495            | Self::Lua(_)
496            | Self::Lint(_)
497            | Self::Outdated(_)
498            | Self::Path(_)
499            | Self::Shell(_)
500            | Self::Sync(_)
501            | Self::Config(_)
502            | Self::Vendor(_)
503            | Self::New(_)
504            | Self::Run(_)
505            | Self::Dist(Dist::Bin(_))
506            | Self::Dist(Dist::FlatArchive(FlatArchive {
507                package_or_rockspec: Some(PackageOrRockspec::Package(_)),
508                ..
509            }))
510            | Self::Dist(Dist::FlatArchive(FlatArchive {
511                package_or_rockspec: None,
512                ..
513            }))
514            | Self::Pack(Pack {
515                package_or_rockspec: Some(PackageOrRockspec::Package(_)),
516            })
517            | Self::Pack(Pack {
518                package_or_rockspec: None,
519            })
520            | Self::Debug(Debug::Project(_)) => Workspace::current().into_diagnostic(),
521            // non-project commands
522            Self::Debug(Debug::Unpack(_))
523            | Self::Debug(Debug::FetchRemote(_))
524            | Self::Debug(Debug::UnpackRemote(_))
525            | Self::Debug(Debug::Toolchains(_))
526            | Self::Dist(Dist::FlatArchive(FlatArchive {
527                package_or_rockspec: Some(PackageOrRockspec::RockSpec(_)),
528                ..
529            }))
530            | Self::Pack(Pack {
531                package_or_rockspec: Some(PackageOrRockspec::RockSpec(_)),
532            })
533            | Self::Util(_)
534            | Self::Doc(_)
535            | Self::Download(_)
536            | Self::Install(_)
537            | Self::InstallRockspec(_)
538            | Self::InstallLua
539            | Self::List(_)
540            | Self::Purge
541            | Self::Search(_)
542            | Self::Uninstall(_)
543            | Self::Which(_) => Ok(None),
544        }
545    }
546}
547
548/// Parse a key=value pair.
549fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
550where
551    T: std::str::FromStr,
552    T::Err: Error + Send + Sync + 'static,
553    U: std::str::FromStr,
554    U::Err: Error + Send + Sync + 'static,
555{
556    let pos = s
557        .find('=')
558        .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
559    Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
560}
561
562fn project_lua_version(pkg: &Option<PackageName>) -> Option<LuaVersion> {
563    let current_workspace = Workspace::current().ok().flatten()?;
564    let project = current_workspace.single_member_or_select(pkg).ok()?;
565    let lua = project.toml().lua()?;
566    let mut matches = LuaVersion::iter().filter(|v| {
567        !matches!(v, LuaVersion::LuaJIT | LuaVersion::LuaJIT52) && lua.matches(&v.as_version())
568    });
569    let version = matches.next()?;
570    if matches.next().is_none() {
571        Some(version)
572    } else {
573        None
574    }
575}