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