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