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