Skip to main content

lux_lib/config/
mod.rs

1use build::BuildConfig;
2use directories::ProjectDirs;
3use external_deps::ExternalDependencySearchConfig;
4use itertools::Itertools;
5
6use miette::Diagnostic;
7use serde::{Deserialize, Serialize, Serializer};
8use std::ffi::OsStr;
9use std::path::Path;
10use std::{collections::HashMap, env, path::PathBuf, time::Duration};
11use thiserror::Error;
12use tokio::process::Command;
13use tree::RockLayoutConfig;
14use url::Url;
15
16use crate::fs;
17use crate::lua_version::LuaVersion;
18use crate::package::RemotePackageTypeFilterSpec;
19use crate::project::TomlDeError;
20use crate::tree::{Tree, TreeError};
21use crate::variables::GetVariableError;
22use crate::{build::utils, variables::HasVariables};
23
24pub mod build;
25pub mod external_deps;
26pub mod tree;
27
28const DEV_PATH: &str = "dev/";
29const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
30
31#[derive(Error, Debug, Diagnostic)]
32#[error("could not find a valid home directory")]
33#[diagnostic(
34    code(lux_lib::no_home_directory),
35    help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
36)]
37pub struct NoValidHomeDirectory;
38
39/// The resolved configuration for a Lux session.
40/// Can be constructed via [`ConfigBuilder`], which supports layering multiple
41/// configuration sources (config file, CLI flags, environment variables).
42#[derive(Debug, Clone)]
43pub struct Config {
44    enable_development_packages: bool,
45    server: Url,
46    extra_servers: Vec<Url>,
47    namespace: Option<String>,
48    lua_dir: Option<PathBuf>,
49    lua_version: Option<LuaVersion>,
50    user_tree: PathBuf,
51    workspace_tree: Option<PathBuf>,
52    verbose: bool,
53    /// Don't display progress bars
54    no_progress: bool,
55    /// Skip prompts (choosing the default choice)
56    no_prompt: bool,
57    timeout: Duration,
58    max_jobs: usize,
59    variables: HashMap<String, String>,
60    external_deps: ExternalDependencySearchConfig,
61
62    build: BuildConfig,
63    entrypoint_layout: RockLayoutConfig,
64
65    cache_dir: PathBuf,
66    data_dir: PathBuf,
67    vendor_dir: Option<PathBuf>,
68
69    user_agent: String,
70
71    generate_luarc: bool,
72    luarc_file_name: String,
73    wrap_bin_scripts: bool,
74    package_types: RemotePackageTypeFilterSpec,
75    no_tfa: bool,
76}
77
78impl Config {
79    /// Lux application directories
80    pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
81        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
82    }
83
84    /// Lux cache directory
85    fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
86        let project_dirs = Config::project_dirs()?;
87        Ok(project_dirs.cache_dir().to_path_buf())
88    }
89
90    /// Lux data directory
91    fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
92        let project_dirs = Config::project_dirs()?;
93        Ok(project_dirs.data_local_dir().to_path_buf())
94    }
95
96    /// Create a copy of this config for the specified Lua version
97    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
98        Self {
99            lua_version: Some(lua_version),
100            ..self
101        }
102    }
103
104    /// Create a copy of this config with the specified install tree
105    pub fn with_tree(self, tree: PathBuf) -> Self {
106        Self {
107            user_tree: tree,
108            ..self
109        }
110    }
111
112    /// Create a copy of this config with the specified workspace tree root
113    pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
114        Self {
115            workspace_tree: tree,
116            ..self
117        }
118    }
119
120    /// The luarocks repository server
121    pub fn server(&self) -> &Url {
122        &self.server
123    }
124
125    /// Additional luarocks repository servers
126    pub fn extra_servers(&self) -> &Vec<Url> {
127        self.extra_servers.as_ref()
128    }
129
130    /// Enabled luarocks repository servers that provide dev/scm rocks
131    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
132        let mut enabled_dev_servers = Vec::new();
133        if self.enable_development_packages {
134            let config_file = ConfigBuilder::config_file()
135                .map(|p| p.to_string_lossy().to_string())
136                .unwrap_or_default();
137            enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
138                ConfigError::UrlParseError {
139                    source,
140                    help: Some(format!("check the `server` URL in {config_file}")),
141                }
142            })?);
143            for server in self.extra_servers() {
144                enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
145                    ConfigError::UrlParseError {
146                        source,
147                        help: Some(format!("check the `extra_servers` URLs in {config_file}")),
148                    }
149                })?);
150            }
151        }
152        Ok(enabled_dev_servers)
153    }
154
155    /// The luarocks server namespace to use
156    pub fn namespace(&self) -> Option<&String> {
157        self.namespace.as_ref()
158    }
159
160    /// The directory in which to install Lua{n} if not found
161    pub fn lua_dir(&self) -> Option<&PathBuf> {
162        self.lua_dir.as_ref()
163    }
164
165    // TODO(vhyrro): Remove `LuaVersion::from(&config)` and keep this only.
166    pub fn lua_version(&self) -> Option<&LuaVersion> {
167        self.lua_version.as_ref()
168    }
169
170    /// The tree in which to install rocks.
171    /// If installing packages for a project, use `Project::tree` instead.
172    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
173        Tree::new(self.user_tree.clone(), version, self)
174    }
175
176    /// The detached workspace tree root, if set.
177    pub fn workspace_tree(&self) -> Option<&PathBuf> {
178        self.workspace_tree.as_ref()
179    }
180
181    /// Whether to display verbose output of commands executed
182    pub fn verbose(&self) -> bool {
183        self.verbose
184    }
185
186    /// Whether to disable printing progress bars and spinners
187    pub fn no_progress(&self) -> bool {
188        self.no_progress
189    }
190
191    /// Whether to skip prompts, selecting the default option
192    pub fn no_prompt(&self) -> bool {
193        self.no_prompt
194    }
195
196    /// Timeout on network operations, in seconds.
197    /// 0 means no timeout (wait forever).
198    pub fn timeout(&self) -> &Duration {
199        &self.timeout
200    }
201
202    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
203    /// 0 means no limit.
204    pub fn max_jobs(&self) -> usize {
205        self.max_jobs
206    }
207
208    /// Command to use for running `make` builds
209    pub fn make_cmd(&self) -> String {
210        match self.variables.get("MAKE") {
211            Some(make) => make.clone(),
212            None => "make".into(),
213        }
214    }
215
216    /// Command to use for running `cmake` builds
217    pub fn cmake_cmd(&self) -> String {
218        match self.variables.get("CMAKE") {
219            Some(cmake) => cmake.clone(),
220            None => "cmake".into(),
221        }
222    }
223
224    /// Construct a [`Command`] for the given program and arguments,
225    /// wrapped in the configured [`BuildConfig::runner`], if any.
226    ///
227    /// If no runner is configured, this is equivalent to
228    /// `Command::new(program).args(args)`.
229    pub(crate) fn wrapped_command<P, A>(&self, program: P, args: A) -> Command
230    where
231        P: AsRef<OsStr>,
232        A: IntoIterator,
233        A::Item: AsRef<OsStr>,
234    {
235        if self.build.runner.is_empty() {
236            let mut cmd = Command::new(program);
237            cmd.args(args);
238            cmd
239        } else {
240            let runner = &self.build.runner;
241            let mut cmd = Command::new(&runner[0]);
242            cmd.args(&runner[1..]).arg(program).args(args);
243            cmd
244        }
245    }
246
247    /// The build profile to use when compiling packages.
248    pub(crate) fn build_profile(&self) -> build::Profile {
249        self.build
250            .profile
251            .as_ref()
252            .cloned()
253            .unwrap_or(build::Profile::Release)
254    }
255
256    /// Variable names, mapped to their values.
257    /// Lux populates variables in the `lux.toml` and in RockSpecs
258    /// with these before building.
259    pub fn variables(&self) -> &HashMap<String, String> {
260        &self.variables
261    }
262
263    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
264        &self.external_deps
265    }
266
267    /// The rock layout for entrypoints of new install trees.
268    /// Does not affect existing install trees or dependency rock layouts.
269    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
270        &self.entrypoint_layout
271    }
272
273    /// The Lux cache directory
274    pub fn cache_dir(&self) -> &PathBuf {
275        &self.cache_dir
276    }
277
278    /// The Lux data directory
279    pub fn data_dir(&self) -> &PathBuf {
280        &self.data_dir
281    }
282
283    /// Specifies a directory with locally vendored sources and RockSpecs.
284    /// When building or installing a package with this flag,
285    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
286    pub fn vendor_dir(&self) -> Option<&PathBuf> {
287        self.vendor_dir.as_ref()
288    }
289
290    /// The user agent to use when making web requests.
291    pub fn user_agent(&self) -> &str {
292        &self.user_agent
293    }
294
295    /// Whether to generate a `.luarc.json` on build.
296    pub fn generate_luarc(&self) -> bool {
297        self.generate_luarc
298    }
299
300    // Lua runtime configuration file name
301    pub fn luarc_file_name(&self) -> &str {
302        &self.luarc_file_name
303    }
304
305    /// Whether to wrap installed Lua bin scripts to be executed with
306    /// the detected or configured Lua installation.
307    /// If `true`, individual rocks can still disable wrapping of their own bin scripts.
308    pub fn wrap_bin_scripts(&self) -> bool {
309        self.wrap_bin_scripts
310    }
311
312    /// Filter specification for package types to include in searches.
313    pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
314        &self.package_types
315    }
316    /// Whether to disable prompts for two-factor authentication (2FA) codes.
317    pub fn no_tfa(&self) -> bool {
318        self.no_tfa
319    }
320}
321
322impl HasVariables for Config {
323    #[tracing::instrument(level = "trace")]
324    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
325        Ok(self.variables.get(input).cloned())
326    }
327}
328
329#[derive(Error, Debug, Diagnostic)]
330pub enum ConfigError {
331    #[error(transparent)]
332    #[diagnostic(transparent)]
333    Fs(#[from] fs::FsError),
334    #[error(transparent)]
335    #[diagnostic(transparent)]
336    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
337    #[error("error parsing {config_file}")]
338    Deserialize {
339        config_file: String,
340        #[diagnostic_source]
341        source: TomlDeError,
342    },
343    #[error("error parsing URL: {source}")]
344    UrlParseError {
345        source: url::ParseError,
346        #[help]
347        help: Option<String>,
348    },
349}
350
351/// Incrementally builds a [`Config`] by layering configuration sources.
352///
353/// - Call [`ConfigBuilder::default`] to start with a blank slate,
354///   or call [`ConfigBuilder::new`] to start from a deserialised configuration file.
355/// - Populate the fields from overriding sources (e.g. CLI arguments).
356/// - Finish with [`ConfigBuilder::build`].
357#[derive(Debug, Clone, Default, Deserialize, Serialize)]
358pub struct ConfigBuilder {
359    #[serde(
360        default,
361        deserialize_with = "deserialize_url",
362        serialize_with = "serialize_url"
363    )]
364    server: Option<Url>,
365    #[serde(
366        default,
367        deserialize_with = "deserialize_url_vec",
368        serialize_with = "serialize_url_vec"
369    )]
370    extra_servers: Option<Vec<Url>>,
371    namespace: Option<String>,
372    lua_version: Option<LuaVersion>,
373    user_tree: Option<PathBuf>,
374    workspace_tree: Option<PathBuf>,
375    lua_dir: Option<PathBuf>,
376    cache_dir: Option<PathBuf>,
377    data_dir: Option<PathBuf>,
378    vendor_dir: Option<PathBuf>,
379    enable_development_packages: Option<bool>,
380    verbose: Option<bool>,
381    no_progress: Option<bool>,
382    no_prompt: Option<bool>,
383    timeout: Option<Duration>,
384    max_jobs: Option<usize>,
385    variables: Option<HashMap<String, String>>,
386    #[serde(default)]
387    external_deps: ExternalDependencySearchConfig,
388    #[serde(default)]
389    build: BuildConfig,
390
391    #[serde(default)]
392    entrypoint_layout: RockLayoutConfig,
393    user_agent: Option<String>,
394    generate_luarc: Option<bool>,
395    luarc_file_name: Option<String>,
396    wrap_bin_scripts: Option<bool>,
397    package_types: Option<RemotePackageTypeFilterSpec>,
398    no_tfa: Option<bool>,
399}
400
401/// A builder for the lux `Config`.
402impl ConfigBuilder {
403    /// Create a new `ConfigBuilder` by deserializing from a config file
404    /// if present, or otherwise by instantiating the default config.
405    pub fn new() -> Result<Self, ConfigError> {
406        let config_file = Self::config_file()?;
407        if config_file.is_file() {
408            Self::from_file(&config_file)
409        } else {
410            Ok(Self::default())
411        }
412    }
413
414    pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
415        let config_file_name = config_file.to_string_lossy().to_string();
416        let content = fs::sync::read_to_string(config_file)?;
417        crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
418            ConfigError::Deserialize {
419                config_file: config_file_name,
420                source,
421            }
422        })
423    }
424
425    /// Get the path to the lux config file.
426    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
427        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
428            .ok_or(NoValidHomeDirectory)?;
429        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
430    }
431
432    /// Whether to enable development packages
433    /// Default: `false`
434    pub fn dev(self, dev: Option<bool>) -> Self {
435        Self {
436            enable_development_packages: dev.or(self.enable_development_packages),
437            ..self
438        }
439    }
440
441    /// Fetch rocks/rockspecs from this luarocks server
442    /// Default: `"https://luarocks.org/"`
443    pub fn server(self, server: Option<Url>) -> Self {
444        Self {
445            server: server.or(self.server),
446            ..self
447        }
448    }
449
450    /// Fetch rocks/rockspecs from these servers in addition to the main server
451    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
452        Self {
453            extra_servers: extra_servers.or(self.extra_servers),
454            ..self
455        }
456    }
457
458    /// The luarocks server namespace to use
459    pub fn namespace(self, namespace: Option<String>) -> Self {
460        Self {
461            namespace: namespace.or(self.namespace),
462            ..self
463        }
464    }
465
466    /// The directory in which to install Lua if not found
467    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
468        Self {
469            lua_dir: lua_dir.or(self.lua_dir),
470            ..self
471        }
472    }
473
474    /// Which Lua version to use.
475    /// Default: The installed Lua version, if detected
476    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
477        Self {
478            lua_version: lua_version.or(self.lua_version),
479            ..self
480        }
481    }
482
483    /// Which tree to operate on
484    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
485        Self {
486            user_tree: tree.or(self.user_tree),
487            ..self
488        }
489    }
490
491    /// Which tree to operate on when in a workspace
492    /// Default: A `.lux` directory in the workspace root.
493    pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
494        Self {
495            workspace_tree: tree.or(self.workspace_tree),
496            ..self
497        }
498    }
499
500    /// Variable names, mapped to their values.
501    /// Lux populates variables in the `lux.toml` and in RockSpecs
502    /// with these before building.
503    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
504        Self {
505            variables: variables.or(self.variables),
506            ..self
507        }
508    }
509
510    /// Whether to display verbose output of commands executed.
511    /// Default: `false`
512    pub fn verbose(self, verbose: Option<bool>) -> Self {
513        Self {
514            verbose: verbose.or(self.verbose),
515            ..self
516        }
517    }
518
519    /// Whether to disable printing progress bars and spinners
520    /// Default: `false`
521    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
522        Self {
523            no_progress: no_progress.or(self.no_progress),
524            ..self
525        }
526    }
527
528    /// Whether to disable user prompts
529    /// Default: `false`
530    pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
531        Self {
532            no_prompt: no_prompt.or(self.no_prompt),
533            ..self
534        }
535    }
536
537    /// Timeout on network operations, in seconds.
538    /// 0 means no timeout (wait forever).
539    /// Default: 30 s
540    pub fn timeout(self, timeout: Option<Duration>) -> Self {
541        Self {
542            timeout: timeout.or(self.timeout),
543            ..self
544        }
545    }
546
547    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
548    /// 0 means no limit.
549    /// Default: 0
550    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
551        Self {
552            max_jobs: max_jobs.or(self.max_jobs),
553            ..self
554        }
555    }
556
557    /// The cache directory, e.g. for luarocks manifests.
558    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
559        Self {
560            cache_dir: cache_dir.or(self.cache_dir),
561            ..self
562        }
563    }
564
565    /// The data directory, in which the default user install tree resides.
566    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
567        Self {
568            data_dir: data_dir.or(self.data_dir),
569            ..self
570        }
571    }
572
573    /// Specifies a directory with locally vendored sources and RockSpecs.
574    /// When building or installing a package with this flag,
575    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
576    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
577        Self {
578            vendor_dir: vendor_dir.or(self.vendor_dir),
579            ..self
580        }
581    }
582
583    /// The rock layout for entrypoints of new install trees.
584    /// Does not affect existing install trees or dependency rock layouts.
585    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
586        Self {
587            entrypoint_layout: rock_layout,
588            ..self
589        }
590    }
591
592    /// The user agent to set when making web requests.
593    /// Default: "lux-lib/<version>".
594    pub fn user_agent(self, user_agent: Option<String>) -> Self {
595        Self {
596            user_agent: user_agent.or(self.user_agent),
597            ..self
598        }
599    }
600
601    /// Whether to generate a `.luarc.json` on build.
602    /// Default: `true`
603    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
604        Self {
605            generate_luarc: generate.or(self.generate_luarc),
606            ..self
607        }
608    }
609
610    /// Lua runtime configuration file name
611    /// Default: `.luarc.json`
612    pub fn luarc_file_name(self, file: Option<String>) -> Self {
613        Self {
614            luarc_file_name: file.or(self.luarc_file_name),
615            ..self
616        }
617    }
618
619    /// Whether to wrap installed Lua bin scripts to be executed with
620    /// the detected or configured Lua installation.
621    /// Setting this to `false` disables wrapping globally.
622    /// If set to `true`, individual rocks can still disable wrapping of their own bin scripts.
623    /// Default: `true`.
624    pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
625        Self {
626            wrap_bin_scripts: generate.or(self.generate_luarc),
627            ..self
628        }
629    }
630    /// Whether to disable prompts for two-factor authentication (2FA) codes.
631    /// Default: `false`.
632    pub fn no_tfa(self, tfa: Option<bool>) -> Self {
633        Self {
634            no_tfa: tfa.or(self.no_tfa),
635            ..self
636        }
637    }
638
639    /// The command prefix with which to wrap all build commands.
640    ///
641    /// If set, every command spawned by the build backends (`make`, `cmake`,
642    /// `rust-mlua`, `command`, and `luarocks`) is invoked as
643    /// `runner + [command, arguments...]`.
644    ///
645    /// If unset, no wrapping is performed.
646    ///
647    /// # Examples
648    ///
649    /// Use [`bubblewrap`](https://github.com/containers/bubblewrap) to run
650    /// builds in a sandbox with read-only access to the rest of the
651    /// filesystem (Linux):
652    ///
653    /// ```toml
654    /// [build]
655    /// runner = [
656    ///     "bwrap",
657    ///     "--ro-bind", "/", "/",
658    ///     "--dev-bind", "/dev", "/dev",
659    ///     "--proc", "/proc",
660    ///     "--bind", "/tmp", "/tmp",
661    ///     "--bind", "/home/user/.cache/lux", "/home/user/.cache/lux",
662    ///     "--bind", "/home/user/.local/share/lux", "/home/user/.local/share/lux",
663    ///     "--unshare-net",
664    ///     "--new-session",
665    /// ]
666    /// ```
667    ///
668    /// Or use `sandbox-exec` with a seatbelt profile (macOS):
669    ///
670    /// ```text
671    /// (version 1)
672    /// (deny default)
673    /// (allow file-read*)
674    /// (allow process*)
675    /// (allow sysctl-read)
676    /// (allow file-write* (subpath "/tmp") (subpath "/Users/user/Library/Caches/lux") (subpath "/Users/user/.local/share/lux"))
677    /// ```
678    ///
679    /// ```toml
680    /// [build]
681    /// runner = ["sandbox-exec", "-f", "/Users/user/sandbox.sb"]
682    /// ```
683    ///
684    /// The writable directories must cover the places lux writes to during a
685    /// build: the temporary build directory (a tempfile, usually under
686    /// `/tmp`), the install tree (under the data directory), and cache
687    /// directories used by the various build backends.
688    pub fn build_runner(self, runner: Option<Vec<String>>) -> Self {
689        Self {
690            build: BuildConfig {
691                runner: runner.unwrap_or(self.build.runner),
692                ..self.build
693            },
694            ..self
695        }
696    }
697
698    /// The build profile to use when compiling packages.
699    /// Default: [`BuildProfile::Release`], if not set by [`Self::default_build_profile`].
700    pub fn build_profile(self, profile: Option<build::Profile>) -> Self {
701        Self {
702            build: BuildConfig {
703                profile: profile.or(self.build.profile),
704                ..self.build
705            },
706            ..self
707        }
708    }
709
710    /// set the default build profile to use when compiling packages.
711    pub fn default_build_profile(self, profile: build::Profile) -> Self {
712        Self {
713            build: BuildConfig {
714                profile: self.build.profile.or(Some(profile)),
715                ..self.build
716            },
717            ..self
718        }
719    }
720
721    /// Merge with another [`ConfigBuilder`]. The other one takes precedence.
722    pub fn merge(self, other: Self) -> Self {
723        Self {
724            server: other.server.or(self.server),
725            extra_servers: other.extra_servers.or(self.extra_servers),
726            namespace: other.namespace.or(self.namespace),
727            lua_version: other.lua_version.or(self.lua_version),
728            user_tree: other.user_tree.or(self.user_tree),
729            workspace_tree: other.workspace_tree.or(self.workspace_tree),
730            lua_dir: other.lua_dir.or(self.lua_dir),
731            cache_dir: other.cache_dir.or(self.cache_dir),
732            data_dir: other.data_dir.or(self.data_dir),
733            vendor_dir: other.vendor_dir.or(self.vendor_dir),
734            enable_development_packages: other
735                .enable_development_packages
736                .or(self.enable_development_packages),
737            verbose: other.verbose.or(self.verbose),
738            no_progress: other.no_progress.or(self.no_progress),
739            no_prompt: other.no_prompt.or(self.no_prompt),
740            timeout: other.timeout.or(self.timeout),
741            max_jobs: other.max_jobs.or(self.max_jobs),
742            variables: other.variables.or(self.variables),
743            external_deps: other.external_deps,
744            build: BuildConfig {
745                profile: other.build.profile.or(self.build.profile),
746                ..other.build
747            },
748            entrypoint_layout: other.entrypoint_layout,
749            user_agent: other.user_agent.or(self.user_agent),
750            generate_luarc: other.generate_luarc.or(self.generate_luarc),
751            luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
752            wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
753            package_types: other.package_types.or(self.package_types),
754            no_tfa: other.no_tfa.or(self.no_tfa),
755        }
756    }
757
758    #[tracing::instrument(level = "trace")]
759    pub fn build(self) -> Result<Config, ConfigError> {
760        let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
761        let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
762        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
763
764        let lua_version = self
765            .lua_version
766            .or(crate::lua_installation::detect_installed_lua_version());
767
768        Ok(Config {
769            enable_development_packages: self.enable_development_packages.unwrap_or(false),
770            server: self.server.unwrap_or_else(|| unsafe {
771                Url::parse("https://luarocks.org/").unwrap_unchecked()
772            }),
773            extra_servers: self.extra_servers.unwrap_or_default(),
774            namespace: self.namespace,
775            lua_dir: self.lua_dir,
776            lua_version,
777            user_tree,
778            workspace_tree: self.workspace_tree,
779            verbose: self.verbose.unwrap_or(false),
780            no_progress: self.no_progress.unwrap_or(false),
781            no_prompt: self.no_prompt.unwrap_or(false),
782            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
783            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
784                0 => usize::MAX,
785                max_jobs => max_jobs,
786            },
787            variables: default_variables()
788                .chain(self.variables.unwrap_or_default())
789                .collect(),
790            external_deps: self.external_deps,
791            build: self.build,
792            entrypoint_layout: self.entrypoint_layout,
793            cache_dir,
794            data_dir,
795            vendor_dir: self.vendor_dir,
796            user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
797            generate_luarc: self.generate_luarc.unwrap_or(true),
798            luarc_file_name: self
799                .luarc_file_name
800                .unwrap_or_else(|| ".luarc.json".to_string()),
801            wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
802            package_types: self.package_types.unwrap_or_default(),
803            no_tfa: self.no_tfa.unwrap_or(false),
804        })
805    }
806}
807
808/// Useful for printing the current config
809impl From<Config> for ConfigBuilder {
810    fn from(value: Config) -> Self {
811        ConfigBuilder {
812            enable_development_packages: Some(value.enable_development_packages),
813            server: Some(value.server),
814            extra_servers: Some(value.extra_servers),
815            namespace: value.namespace,
816            lua_dir: value.lua_dir,
817            lua_version: value.lua_version,
818            user_tree: Some(value.user_tree),
819            workspace_tree: value.workspace_tree,
820            verbose: Some(value.verbose),
821            no_progress: Some(value.no_progress),
822            no_prompt: Some(value.no_prompt),
823            timeout: Some(value.timeout),
824            max_jobs: if value.max_jobs == usize::MAX {
825                None
826            } else {
827                Some(value.max_jobs)
828            },
829            variables: Some(value.variables),
830            cache_dir: Some(value.cache_dir),
831            data_dir: Some(value.data_dir),
832            vendor_dir: value.vendor_dir,
833            external_deps: value.external_deps,
834            build: value.build,
835            entrypoint_layout: value.entrypoint_layout,
836            user_agent: Some(value.user_agent),
837            generate_luarc: Some(value.generate_luarc),
838            luarc_file_name: Some(value.luarc_file_name),
839            wrap_bin_scripts: Some(value.wrap_bin_scripts),
840            package_types: Some(value.package_types),
841            no_tfa: Some(value.no_tfa),
842        }
843    }
844}
845
846fn default_variables() -> impl Iterator<Item = (String, String)> {
847    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
848    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
849    vec![
850        ("MAKE".into(), "make".into()),
851        ("CMAKE".into(), "cmake".into()),
852        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
853        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
854        ("CFLAGS".into(), cflags),
855        ("LDFLAGS".into(), ldflags),
856        ("LIBFLAG".into(), utils::default_libflag().into()),
857    ]
858    .into_iter()
859}
860
861fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
862where
863    D: serde::Deserializer<'de>,
864{
865    let s = Option::<String>::deserialize(deserializer)?;
866    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
867        .transpose()
868}
869
870fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
871where
872    S: Serializer,
873{
874    match url {
875        Some(url) => serializer.serialize_some(url.as_str()),
876        None => serializer.serialize_none(),
877    }
878}
879
880fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
881where
882    D: serde::Deserializer<'de>,
883{
884    let s = Option::<Vec<String>>::deserialize(deserializer)?;
885    s.map(|v| {
886        v.into_iter()
887            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
888            .try_collect()
889    })
890    .transpose()
891}
892
893fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
894where
895    S: Serializer,
896{
897    match urls {
898        Some(urls) => {
899            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
900            serializer.serialize_some(&url_strings)
901        }
902        None => serializer.serialize_none(),
903    }
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909
910    #[tokio::test]
911    async fn wrapped_command_without_runner() {
912        let echo = which::which("echo").unwrap();
913        let config = ConfigBuilder::default().build().unwrap();
914        let output = config
915            .wrapped_command(&echo, ["hello", "world"])
916            .output()
917            .await
918            .unwrap();
919        assert!(output.status.success());
920        assert_eq!(
921            String::from_utf8_lossy(&output.stdout).trim(),
922            "hello world"
923        );
924    }
925
926    #[tokio::test]
927    async fn wrapped_command_prepends_runner_argv() {
928        let echo = which::which("echo").unwrap();
929        let config = ConfigBuilder::default()
930            .build_runner(Some(vec![echo.to_string_lossy().into(), "--prefix".into()]))
931            .build()
932            .unwrap();
933        let output = config
934            .wrapped_command("world", Vec::<String>::new())
935            .output()
936            .await
937            .unwrap();
938        assert!(output.status.success());
939        assert_eq!(
940            String::from_utf8_lossy(&output.stdout).trim(),
941            "--prefix world"
942        );
943    }
944}