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