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