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