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 /// Display verbose output of commands executed, enabling DEBUG logs.{n}
153 /// To enable TRACE logs, set RUST_LOG=trace.
154 #[arg(long)]
155 pub verbose: bool,
156
157 /// Don't print any progress bars or spinners.
158 #[arg(long)]
159 pub no_progress: bool,
160
161 /// Skip prompts, selecting the default option.
162 #[arg(long)]
163 pub no_prompt: bool,
164
165 /// Configure lux for installing Neovim packages.
166 #[arg(long)]
167 pub nvim: bool,
168 /// Disable prompts for two-factor authentication (2FA) codes.{n}
169 /// It is strongly recommended to enable 2FA instead
170 /// see (https://luarocks.org/settings/two-factor-auth).
171 #[arg(long)]
172 pub no_tfa: bool,
173
174 /// Timeout on network operations, in seconds.{n}
175 /// 0 means no timeout (wait forever). Default is 30.
176 #[arg(long, value_name = "seconds")]
177 pub timeout: Option<usize>,
178
179 /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
180 /// 0 means no limit. Default is 0.
181 #[arg(long, visible_short_alias = 'j')]
182 pub max_jobs: Option<usize>,
183
184 /// Do not generate or update a `.luarc.json` file when building{n}
185 /// a project.
186 #[arg(long)]
187 pub no_luarc: bool,
188
189 /// Do not wrap Lua `bin` scripts.
190 #[arg(long)]
191 pub no_wrap_bin: bool,
192
193 /// The user agent to set when making web requests.
194 /// Default is "lux/<version>"
195 #[arg(long)]
196 pub user_agent: Option<String>,
197
198 #[command(subcommand)]
199 pub command: Commands,
200}
201
202#[derive(Subcommand)]
203pub enum Commands {
204 /// Add a dependency to the current project.
205 Add(Add),
206 /// Build/compile a project.
207 Build(Build),
208 /// [EXPERIMENTAL]{n}
209 /// Type check the current project based on EmmyLua/LuaCATS annotations.{n}
210 /// Respects `.emmyrc.json` and `.luarc.json` files in the project directory.
211 Check(Check),
212 /// Interact with the lux configuration.
213 #[command(subcommand, arg_required_else_help = true)]
214 Config(ConfigCmd),
215 /// Internal commands for debugging Lux itself.
216 #[command(subcommand, arg_required_else_help = true)]
217 Debug(Debug),
218 /// Distribute a Lux project.
219 #[command(subcommand, arg_required_else_help = true)]
220 Dist(Dist),
221 /// Show documentation for an installed rock.
222 Doc(Doc),
223 /// Download a specific rock file from a luarocks server.
224 #[command(arg_required_else_help = true)]
225 Download(Download),
226 /// Formats the codebase with stylua.
227 Fmt(Fmt),
228 /// Generate a rockspec file from a project.
229 GenerateRockspec(GenerateRockspec),
230 /// Show metadata for any rock.
231 Info(Info),
232 /// Install a rock for use on the system.
233 #[command(arg_required_else_help = true)]
234 Install(Install),
235 /// Install a local rockspec for use on the system.
236 #[command(arg_required_else_help = true)]
237 InstallRockspec(InstallRockspec),
238 /// Manually install and manage Lua headers for various Lua versions.
239 InstallLua,
240 /// Lint the current project using `luacheck`.
241 Lint(Lint),
242 /// List currently installed rocks.
243 List(ListCmd),
244 /// Run lua, with the `LUA_PATH` and `LUA_CPATH` set to the specified lux tree.
245 Lua(RunLua),
246 /// Create a new Lua project.
247 New(NewProject),
248 /// List outdated rocks.
249 Outdated(Outdated),
250 /// Create a packed rock for distribution, packing sources or binaries.
251 Pack(Pack),
252 /// Return the currently configured package path.
253 Path(Path),
254 /// Pin an existing rock, preventing any updates to the package.
255 Pin(ChangePin),
256 /// Remove all installed rocks from a tree.
257 Purge,
258 /// Remove a rock from the current project's lux.toml dependencies.
259 Remove(Remove),
260 /// Run the current project with the provided arguments.
261 Run(Run),
262 /// Execute a command that has been installed with lux.
263 /// If the command is not found, a package named after the command
264 /// will be installed.
265 Exec(Exec),
266 /// Query the luarocks servers.
267 #[command(arg_required_else_help = true)]
268 Search(Search),
269 /// Run the test suite in the current project directory.{n}
270 /// Lux supports the following test backends, specified by the `[test]` table in the lux.toml:{n}
271 /// {n}
272 /// - busted:{n}
273 /// {n}
274 /// https://lunarmodules.github.io/busted/{n}
275 /// {n}
276 /// Example:{n}
277 /// {n}
278 /// ```toml{n}
279 /// [test]{n}
280 /// type = "busted"{n}
281 /// flags = [ ] # Optional CLI flags to pass to busted{n}
282 /// ```{n}
283 /// {n}
284 /// `lx test` will default to using `busted` if no test backend is specified and:{n}
285 /// * there is a `.busted` file in the project root{n}
286 /// * or `busted` is one of the `test_dependencies`).{n}
287 /// {n}
288 /// - busted-nlua:{n}:
289 /// {n}
290 /// [currently broken on Windows]{n}
291 /// A build backend for running busted tests with Neovim as the Lua interpreter.
292 /// Used for testing Neovim plugins.
293 /// {n}
294 /// Example:{n}
295 /// {n}
296 /// ```toml{n}
297 /// [test]{n}
298 /// type = "busted-nlua"{n}
299 /// flags = [ ] # Optional CLI flags to pass to busted{n}
300 /// ```{n}
301 /// {n}
302 /// `lx test` will default to using `busted-nlua` if no test backend is specified and:{n}
303 /// * there is a `.busted` file in the project root{n}
304 /// * or `busted` and `nlua` are `test_dependencies`.{n}
305 /// {n}
306 /// - command:{n}
307 /// {n}
308 /// Name/file name of a shell command that will run the test suite.{n}
309 /// Example:{n}
310 /// {n}
311 /// ```toml{n}
312 /// [test]{n}
313 /// type = "command"{n}
314 /// command = "make"{n}
315 /// flags = [ "test" ]{n}
316 /// ```{n}
317 /// {n}
318 /// - script:{n}
319 /// {n}
320 /// Relative path to a Lua script that will run the test suite.{n}
321 /// Example:{n}
322 /// {n}
323 /// ```toml{n}
324 /// [test]{n}
325 /// type = "script"{n}
326 /// script = "tests.lua" # Expects a tests.lua file in the project root{n}
327 /// flags = [ ] # Optional arguments passed to the test script{n}
328 /// ```{n}
329 Test(Test),
330 /// Uninstall a rock from the system.
331 Uninstall(Uninstall),
332 /// Unpins an existing rock, allowing updates to alter the package.
333 Unpin(ChangePin),
334 /// Updates all rocks in a project.
335 Update(Update),
336 /// Generate a Lua rockspec for a Lux project and upload it to the public luarocks repository.{n}
337 /// You can specify a source template for release and dev packages in the lux.toml.{n}
338 /// {n}
339 /// Example:{n}
340 /// {n}
341 /// ```toml{n}
342 /// [source]{n}
343 /// url = "https://host.com/owner/$(PACKAGE)/refs/tags/$(REF).zip"{n}
344 /// dev = "git+https://host.com/owner/$(PACKAGE).git"{n}
345 /// ```{n}
346 /// {n}
347 /// You can use the following variables in the source template:{n}
348 /// {n}
349 /// - $(PACKAGE): The package name.{n}
350 /// - $(VERSION): The package version.{n}
351 /// - $(REF): The git tag or revision (if in a git repository).{n}
352 /// - You may also specify environment variables with `$(<VAR_NAME>)`.{n}
353 /// {n}
354 /// If the `version` is not set in the lux.toml, lux will search the current
355 /// commit for SemVer tags and if found, will use it to generate the package version.
356 Upload(Upload),
357 /// Infrequently used commands such as for generating shell completions and man pages.
358 #[command(subcommand, arg_required_else_help = true)]
359 Util(Util),
360 /// Vendor the dependencies of a project or RockSpec locally.
361 /// When building or installing a package with the `--vendor-dir` option{n}
362 /// or the `[vendor_dir]` config option, Lux will fetch sources from the <vendor-dir>{n}
363 /// instead of from a remote server.
364 Vendor(Vendor),
365 /// Tell which file corresponds to a given module name.
366 Which(Which),
367 /// Spawns an interactive shell with PATH, LUA_PATH, LUA_CPATH and LUA_INIT set.
368 Shell(Shell),
369 /// Synchronize the project tree with the current lux.toml,{n}
370 /// ensuring all packages are installed correctly.
371 Sync(SyncProject),
372}
373
374impl Commands {
375 /// For workspace commands, try to determine the project's Lua version.
376 ///
377 /// Returns [`None`]:
378 /// - if the project does not have an exact Lua version
379 /// - if there is more than one project an no `--package` has been specified
380 /// - if the command is not a project command
381 /// - if the workspace cannot be loaded
382 pub fn lua_version(&self) -> Option<LuaVersion> {
383 match self {
384 Self::Add(Add { package, .. })
385 | Self::Build(Build { package, .. })
386 | Self::Fmt(Fmt { package, .. })
387 | Self::Upload(Upload { package, .. })
388 | Self::GenerateRockspec(GenerateRockspec { package, .. })
389 | Self::Pin(ChangePin { package, .. })
390 | Self::Unpin(ChangePin { package, .. })
391 | Self::Remove(Remove { package, .. })
392 | Self::Test(Test { package, .. })
393 | Self::Update(Update { package, .. })
394 | Self::Run(Run {
395 build: Build { package, .. },
396 ..
397 }) => project_lua_version(package),
398 Self::Dist(d) => match d {
399 Dist::Bin(Bin { package, .. }) => project_lua_version(package),
400 Dist::FlatArchive(FlatArchive {
401 package_or_rockspec,
402 ..
403 }) => match package_or_rockspec {
404 Some(PackageOrRockspec::Package(p)) => {
405 project_lua_version(&Some(p.name().clone()))
406 }
407 Some(PackageOrRockspec::RockSpec(_)) => None,
408 None => project_lua_version(&None),
409 },
410 },
411 Self::Pack(Pack {
412 package_or_rockspec,
413 }) => match package_or_rockspec {
414 Some(PackageOrRockspec::Package(p)) => project_lua_version(&Some(p.name().clone())),
415 Some(PackageOrRockspec::RockSpec(_)) => None,
416 None => project_lua_version(&None),
417 },
418 | Self::Debug(Debug::Project(_)) => project_lua_version(&None),
419 // workspace commands without a --package flag
420 Self::Check(_)
421 | Self::Exec(_)
422 | Self::Info(_)
423 | Self::Lua(_)
424 | Self::Lint(_)
425 | Self::Outdated(_)
426 | Self::Path(_)
427 | Self::Shell(_)
428 | Self::Sync(_)
429 | Self::Vendor(_) => {
430 project_lua_version(&None)
431 },
432 | Self::New(_)
433 // non-project commands
434 | Self::Config(_)
435 | Self::Util(_)
436 | Self::Debug(Debug::Unpack(_))
437 | Self::Debug(Debug::FetchRemote(_))
438 | Self::Debug(Debug::UnpackRemote(_))
439 | Self::Debug(Debug::Toolchains(_))
440 | Self::Doc(_)
441 | Self::Download(_)
442 | Self::Install(_)
443 | Self::InstallRockspec(_)
444 | Self::InstallLua
445 | Self::List(_)
446 | Self::Purge
447 | Self::Search(_)
448 | Self::Uninstall(_)
449 | Self::Which(_) => None,
450 }
451 }
452
453 /// Load the user [`ConfigBuilder`], merged with the workspace-local [`ConfigBuilder`],
454 /// if present and running a workspace command.
455 pub fn config(&self) -> Result<ConfigBuilder> {
456 let config = ConfigBuilder::new()?;
457 if let Some(workspace_config) = self
458 .workspace()?
459 .map(|ws| ws.config())
460 .transpose()
461 .into_diagnostic()?
462 .flatten()
463 {
464 Ok(config.merge(workspace_config))
465 } else {
466 Ok(config)
467 }
468 }
469
470 /// For commands that can operate on a workspace, load the current workspace, if present.
471 fn workspace(&self) -> Result<Option<Workspace>> {
472 match self {
473 Self::Add(_)
474 | Self::Build(_)
475 | Self::Fmt(_)
476 | Self::Upload(_)
477 | Self::GenerateRockspec(_)
478 | Self::Pin(_)
479 | Self::Unpin(_)
480 | Self::Remove(_)
481 | Self::Test(_)
482 | Self::Update(_)
483 | Self::Check(_)
484 | Self::Exec(_)
485 | Self::Info(_)
486 | Self::Lua(_)
487 | Self::Lint(_)
488 | Self::Outdated(_)
489 | Self::Path(_)
490 | Self::Shell(_)
491 | Self::Sync(_)
492 | Self::Config(_)
493 | Self::Vendor(_)
494 | Self::New(_)
495 | Self::Run(_)
496 | Self::Dist(Dist::Bin(_))
497 | Self::Dist(Dist::FlatArchive(FlatArchive {
498 package_or_rockspec: Some(PackageOrRockspec::Package(_)),
499 ..
500 }))
501 | Self::Dist(Dist::FlatArchive(FlatArchive {
502 package_or_rockspec: None,
503 ..
504 }))
505 | Self::Pack(Pack {
506 package_or_rockspec: Some(PackageOrRockspec::Package(_)),
507 })
508 | Self::Pack(Pack {
509 package_or_rockspec: None,
510 })
511 | Self::Debug(Debug::Project(_)) => Workspace::current().into_diagnostic(),
512 // non-project commands
513 Self::Debug(Debug::Unpack(_))
514 | Self::Debug(Debug::FetchRemote(_))
515 | Self::Debug(Debug::UnpackRemote(_))
516 | Self::Debug(Debug::Toolchains(_))
517 | Self::Dist(Dist::FlatArchive(FlatArchive {
518 package_or_rockspec: Some(PackageOrRockspec::RockSpec(_)),
519 ..
520 }))
521 | Self::Pack(Pack {
522 package_or_rockspec: Some(PackageOrRockspec::RockSpec(_)),
523 })
524 | Self::Util(_)
525 | Self::Doc(_)
526 | Self::Download(_)
527 | Self::Install(_)
528 | Self::InstallRockspec(_)
529 | Self::InstallLua
530 | Self::List(_)
531 | Self::Purge
532 | Self::Search(_)
533 | Self::Uninstall(_)
534 | Self::Which(_) => Ok(None),
535 }
536 }
537}
538
539/// Parse a key=value pair.
540fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
541where
542 T: std::str::FromStr,
543 T::Err: Error + Send + Sync + 'static,
544 U: std::str::FromStr,
545 U::Err: Error + Send + Sync + 'static,
546{
547 let pos = s
548 .find('=')
549 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
550 Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
551}
552
553fn project_lua_version(pkg: &Option<PackageName>) -> Option<LuaVersion> {
554 let current_workspace = Workspace::current().ok().flatten()?;
555 let project = current_workspace.single_member_or_select(pkg).ok()?;
556 let lua = project.toml().lua()?;
557 let mut matches = LuaVersion::iter().filter(|v| {
558 !matches!(v, LuaVersion::LuaJIT | LuaVersion::LuaJIT52) && lua.matches(&v.as_version())
559 });
560 let version = matches.next()?;
561 if matches.next().is_none() {
562 Some(version)
563 } else {
564 None
565 }
566}