Skip to main content

lux_lib/config/
mod.rs

1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4
5use serde::{Deserialize, Serialize, Serializer};
6use std::{collections::HashMap, env, io, path::PathBuf, time::Duration};
7use thiserror::Error;
8use tree::RockLayoutConfig;
9use url::Url;
10
11use crate::lua_version::LuaVersion;
12use crate::tree::{Tree, TreeError};
13use crate::variables::GetVariableError;
14use crate::{build::utils, variables::HasVariables};
15
16pub mod external_deps;
17pub mod tree;
18
19const DEV_PATH: &str = "dev/";
20const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
21
22#[derive(Error, Debug)]
23#[error("could not find a valid home directory")]
24pub struct NoValidHomeDirectory;
25
26/// The resolved configuration for a Lux session.
27/// Can be constructed via [`ConfigBuilder`], which supports layering multiple
28/// configuration sources (config file, CLI flags, environment variables).
29#[derive(Debug, Clone)]
30pub struct Config {
31    enable_development_packages: bool,
32    server: Url,
33    extra_servers: Vec<Url>,
34    namespace: Option<String>,
35    lua_dir: Option<PathBuf>,
36    lua_version: Option<LuaVersion>,
37    user_tree: PathBuf,
38    verbose: bool,
39    /// Don't display progress bars
40    no_progress: bool,
41    /// Skip prompts (choosing the default choice)
42    no_prompt: bool,
43    timeout: Duration,
44    max_jobs: usize,
45    variables: HashMap<String, String>,
46    external_deps: ExternalDependencySearchConfig,
47    entrypoint_layout: RockLayoutConfig,
48
49    cache_dir: PathBuf,
50    data_dir: PathBuf,
51    vendor_dir: Option<PathBuf>,
52
53    user_agent: String,
54
55    generate_luarc: bool,
56    wrap_bin_scripts: bool,
57}
58
59impl Config {
60    /// Lux application directories
61    fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
62        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
63    }
64
65    /// Lux cache directory
66    fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
67        let project_dirs = Config::project_dirs()?;
68        Ok(project_dirs.cache_dir().to_path_buf())
69    }
70
71    /// Lux data directory
72    fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
73        let project_dirs = Config::project_dirs()?;
74        Ok(project_dirs.data_local_dir().to_path_buf())
75    }
76
77    /// Create a copy of this config for the specified Lua version
78    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
79        Self {
80            lua_version: Some(lua_version),
81            ..self
82        }
83    }
84
85    /// Create a copy of this config with the specified install tree
86    pub fn with_tree(self, tree: PathBuf) -> Self {
87        Self {
88            user_tree: tree,
89            ..self
90        }
91    }
92
93    /// The luarocks repository server
94    pub fn server(&self) -> &Url {
95        &self.server
96    }
97
98    /// Additional luarocks repository servers
99    pub fn extra_servers(&self) -> &Vec<Url> {
100        self.extra_servers.as_ref()
101    }
102
103    /// Enabled luarocks repository servers that provide dev/scm rocks
104    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
105        let mut enabled_dev_servers = Vec::new();
106        if self.enable_development_packages {
107            enabled_dev_servers.push(self.server().join(DEV_PATH)?);
108            for server in self.extra_servers() {
109                enabled_dev_servers.push(server.join(DEV_PATH)?);
110            }
111        }
112        Ok(enabled_dev_servers)
113    }
114
115    /// The luarocks server namespace to use
116    pub fn namespace(&self) -> Option<&String> {
117        self.namespace.as_ref()
118    }
119
120    /// The directory in which to install Lua{n} if not found
121    pub fn lua_dir(&self) -> Option<&PathBuf> {
122        self.lua_dir.as_ref()
123    }
124
125    // TODO(vhyrro): Remove `LuaVersion::from(&config)` and keep this only.
126    pub fn lua_version(&self) -> Option<&LuaVersion> {
127        self.lua_version.as_ref()
128    }
129
130    /// The tree in which to install rocks.
131    /// If installing packages for a project, use `Project::tree` instead.
132    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
133        Tree::new(self.user_tree.clone(), version, self)
134    }
135
136    /// Whether to display verbose output of commands executed
137    pub fn verbose(&self) -> bool {
138        self.verbose
139    }
140
141    /// Whether to disable printing progress bars and spinners
142    pub fn no_progress(&self) -> bool {
143        self.no_progress
144    }
145
146    /// Whether to skip prompts, selecting the default option
147    pub fn no_prompt(&self) -> bool {
148        self.no_prompt
149    }
150
151    /// Timeout on network operations, in seconds.
152    /// 0 means no timeout (wait forever).
153    pub fn timeout(&self) -> &Duration {
154        &self.timeout
155    }
156
157    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
158    /// 0 means no limit.
159    pub fn max_jobs(&self) -> usize {
160        self.max_jobs
161    }
162
163    /// Command to use for running `make` builds
164    pub fn make_cmd(&self) -> String {
165        match self.variables.get("MAKE") {
166            Some(make) => make.clone(),
167            None => "make".into(),
168        }
169    }
170
171    /// Command to use for running `cmake` builds
172    pub fn cmake_cmd(&self) -> String {
173        match self.variables.get("CMAKE") {
174            Some(cmake) => cmake.clone(),
175            None => "cmake".into(),
176        }
177    }
178
179    /// Variable names, mapped to their values.
180    /// Lux populates variables in the `lux.toml` and in RockSpecs
181    /// with these before building.
182    pub fn variables(&self) -> &HashMap<String, String> {
183        &self.variables
184    }
185
186    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
187        &self.external_deps
188    }
189
190    /// The rock layout for entrypoints of new install trees.
191    /// Does not affect existing install trees or dependency rock layouts.
192    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
193        &self.entrypoint_layout
194    }
195
196    /// The Lux cache directory
197    pub fn cache_dir(&self) -> &PathBuf {
198        &self.cache_dir
199    }
200
201    /// The Lux data directory
202    pub fn data_dir(&self) -> &PathBuf {
203        &self.data_dir
204    }
205
206    /// Specifies a directory with locally vendored sources and RockSpecs.
207    /// When building or installing a package with this flag,
208    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
209    pub fn vendor_dir(&self) -> Option<&PathBuf> {
210        self.vendor_dir.as_ref()
211    }
212
213    /// The user agent to use when making web requests.
214    pub fn user_agent(&self) -> &str {
215        &self.user_agent
216    }
217
218    /// Whether to generate a `.luarc.json` on build.
219    pub fn generate_luarc(&self) -> bool {
220        self.generate_luarc
221    }
222
223    /// Whether to wrap installed Lua bin scripts to be executed with
224    /// the detected or configured Lua installation.
225    /// If `true`, individual rocks can still disable wrapping of their own bin scripts.
226    pub fn wrap_bin_scripts(&self) -> bool {
227        self.wrap_bin_scripts
228    }
229}
230
231impl HasVariables for Config {
232    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
233        Ok(self.variables.get(input).cloned())
234    }
235}
236
237#[derive(Error, Debug)]
238pub enum ConfigError {
239    #[error(transparent)]
240    Io(#[from] io::Error),
241    #[error(transparent)]
242    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
243    #[error("error deserializing lux config: {0}")]
244    Deserialize(#[from] toml::de::Error),
245    #[error("error parsing URL: {0}")]
246    UrlParseError(#[from] url::ParseError),
247    #[error("error initializing compiler toolchain: {0}")]
248    CompilerToolchain(#[from] cc::Error),
249}
250
251/// Incrementally builds a [`Config`] by layering configuration sources.
252///
253/// - Call [`ConfigBuilder::default`] to start with a blank slate,
254///   or call [`ConfigBuilder::new`] to start from a deserialised configuration file.
255/// - Populate the fields from overriding sources (e.g. CLI arguments).
256/// - Finish with [`ConfigBuilder::build`].
257#[derive(Clone, Default, Deserialize, Serialize)]
258pub struct ConfigBuilder {
259    #[serde(
260        default,
261        deserialize_with = "deserialize_url",
262        serialize_with = "serialize_url"
263    )]
264    server: Option<Url>,
265    #[serde(
266        default,
267        deserialize_with = "deserialize_url_vec",
268        serialize_with = "serialize_url_vec"
269    )]
270    extra_servers: Option<Vec<Url>>,
271    namespace: Option<String>,
272    lua_version: Option<LuaVersion>,
273    user_tree: Option<PathBuf>,
274    lua_dir: Option<PathBuf>,
275    cache_dir: Option<PathBuf>,
276    data_dir: Option<PathBuf>,
277    vendor_dir: Option<PathBuf>,
278    enable_development_packages: Option<bool>,
279    verbose: Option<bool>,
280    no_progress: Option<bool>,
281    no_prompt: Option<bool>,
282    timeout: Option<Duration>,
283    max_jobs: Option<usize>,
284    variables: Option<HashMap<String, String>>,
285    #[serde(default)]
286    external_deps: ExternalDependencySearchConfig,
287    #[serde(default)]
288    entrypoint_layout: RockLayoutConfig,
289    user_agent: Option<String>,
290    generate_luarc: Option<bool>,
291    wrap_bin_scripts: Option<bool>,
292}
293
294/// A builder for the lux `Config`.
295impl ConfigBuilder {
296    /// Create a new `ConfigBuilder` by deserializing from a config file
297    /// if present, or otherwise by instantiating the default config.
298    pub fn new() -> Result<Self, ConfigError> {
299        let config_file = Self::config_file()?;
300        if config_file.is_file() {
301            Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
302        } else {
303            Ok(Self::default())
304        }
305    }
306
307    /// Get the path to the lux config file.
308    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
309        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
310            .ok_or(NoValidHomeDirectory)?;
311        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
312    }
313
314    /// Whether to enable development packages
315    /// Default: `false`
316    pub fn dev(self, dev: Option<bool>) -> Self {
317        Self {
318            enable_development_packages: dev.or(self.enable_development_packages),
319            ..self
320        }
321    }
322
323    /// Fetch rocks/rockspecs from this luarocks server
324    /// Default: `"https://luarocks.org/"`
325    pub fn server(self, server: Option<Url>) -> Self {
326        Self {
327            server: server.or(self.server),
328            ..self
329        }
330    }
331
332    /// Fetch rocks/rockspecs from these servers in addition to the main server
333    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
334        Self {
335            extra_servers: extra_servers.or(self.extra_servers),
336            ..self
337        }
338    }
339
340    /// The luarocks server namespace to use
341    pub fn namespace(self, namespace: Option<String>) -> Self {
342        Self {
343            namespace: namespace.or(self.namespace),
344            ..self
345        }
346    }
347
348    /// The directory in which to install Lua if not found
349    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
350        Self {
351            lua_dir: lua_dir.or(self.lua_dir),
352            ..self
353        }
354    }
355
356    /// Which Lua version to use.
357    /// Default: The installed Lua version, if detected
358    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
359        Self {
360            lua_version: lua_version.or(self.lua_version),
361            ..self
362        }
363    }
364
365    /// Which tree to operate on
366    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
367        Self {
368            user_tree: tree.or(self.user_tree),
369            ..self
370        }
371    }
372
373    /// Variable names, mapped to their values.
374    /// Lux populates variables in the `lux.toml` and in RockSpecs
375    /// with these before building.
376    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
377        Self {
378            variables: variables.or(self.variables),
379            ..self
380        }
381    }
382
383    /// Whether to display verbose output of commands executed.
384    /// Default: `false`
385    pub fn verbose(self, verbose: Option<bool>) -> Self {
386        Self {
387            verbose: verbose.or(self.verbose),
388            ..self
389        }
390    }
391
392    /// Whether to disable printing progress bars and spinners
393    /// Default: `false`
394    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
395        Self {
396            no_progress: no_progress.or(self.no_progress),
397            ..self
398        }
399    }
400
401    /// Whether to disable user prompts
402    /// Default: `false`
403    pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
404        Self {
405            no_prompt: no_prompt.or(self.no_prompt),
406            ..self
407        }
408    }
409
410    /// Timeout on network operations, in seconds.
411    /// 0 means no timeout (wait forever).
412    /// Default: 30 s
413    pub fn timeout(self, timeout: Option<Duration>) -> Self {
414        Self {
415            timeout: timeout.or(self.timeout),
416            ..self
417        }
418    }
419
420    /// Maximum buffer size for parallel jobs, such as downloading rockspecs and installing rocks.
421    /// 0 means no limit.
422    /// Default: 0
423    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
424        Self {
425            max_jobs: max_jobs.or(self.max_jobs),
426            ..self
427        }
428    }
429
430    /// The cache directory, e.g. for luarocks manifests.
431    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
432        Self {
433            cache_dir: cache_dir.or(self.cache_dir),
434            ..self
435        }
436    }
437
438    /// The data directory, in which the default user install tree resides.
439    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
440        Self {
441            data_dir: data_dir.or(self.data_dir),
442            ..self
443        }
444    }
445
446    /// Specifies a directory with locally vendored sources and RockSpecs.
447    /// When building or installing a package with this flag,
448    /// Lux will fetch sources from the <vendor-dir> instead of from a remote server.
449    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
450        Self {
451            vendor_dir: vendor_dir.or(self.vendor_dir),
452            ..self
453        }
454    }
455
456    /// The rock layout for entrypoints of new install trees.
457    /// Does not affect existing install trees or dependency rock layouts.
458    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
459        Self {
460            entrypoint_layout: rock_layout,
461            ..self
462        }
463    }
464
465    /// The user agent to set when making web requests.
466    /// Default: "lux-lib/<version>".
467    pub fn user_agent(self, user_agent: Option<String>) -> Self {
468        Self {
469            user_agent: user_agent.or(self.user_agent),
470            ..self
471        }
472    }
473
474    /// Whether to generate a `.luarc.json` on build.
475    /// Default: `true`
476    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
477        Self {
478            generate_luarc: generate.or(self.generate_luarc),
479            ..self
480        }
481    }
482
483    /// Whether to wrap installed Lua bin scripts to be executed with
484    /// the detected or configured Lua installation.
485    /// Setting this to `false` disables wrapping globally.
486    /// If set to `true`, individual rocks can still disable wrapping of their own bin scripts.
487    /// Default: `true`.
488    pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
489        Self {
490            wrap_bin_scripts: generate.or(self.generate_luarc),
491            ..self
492        }
493    }
494
495    pub fn build(self) -> Result<Config, ConfigError> {
496        let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
497        let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
498        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
499
500        let lua_version = self
501            .lua_version
502            .or(crate::lua_installation::detect_installed_lua_version());
503
504        Ok(Config {
505            enable_development_packages: self.enable_development_packages.unwrap_or(false),
506            server: self.server.unwrap_or_else(|| unsafe {
507                Url::parse("https://luarocks.org/").unwrap_unchecked()
508            }),
509            extra_servers: self.extra_servers.unwrap_or_default(),
510            namespace: self.namespace,
511            lua_dir: self.lua_dir,
512            lua_version,
513            user_tree,
514            verbose: self.verbose.unwrap_or(false),
515            no_progress: self.no_progress.unwrap_or(false),
516            no_prompt: self.no_prompt.unwrap_or(false),
517            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
518            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
519                0 => usize::MAX,
520                max_jobs => max_jobs,
521            },
522            variables: default_variables()
523                .chain(self.variables.unwrap_or_default())
524                .collect(),
525            external_deps: self.external_deps,
526            entrypoint_layout: self.entrypoint_layout,
527            cache_dir,
528            data_dir,
529            vendor_dir: self.vendor_dir,
530            user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
531            generate_luarc: self.generate_luarc.unwrap_or(true),
532            wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
533        })
534    }
535}
536
537/// Useful for printing the current config
538impl From<Config> for ConfigBuilder {
539    fn from(value: Config) -> Self {
540        ConfigBuilder {
541            enable_development_packages: Some(value.enable_development_packages),
542            server: Some(value.server),
543            extra_servers: Some(value.extra_servers),
544            namespace: value.namespace,
545            lua_dir: value.lua_dir,
546            lua_version: value.lua_version,
547            user_tree: Some(value.user_tree),
548            verbose: Some(value.verbose),
549            no_progress: Some(value.no_progress),
550            no_prompt: Some(value.no_prompt),
551            timeout: Some(value.timeout),
552            max_jobs: if value.max_jobs == usize::MAX {
553                None
554            } else {
555                Some(value.max_jobs)
556            },
557            variables: Some(value.variables),
558            cache_dir: Some(value.cache_dir),
559            data_dir: Some(value.data_dir),
560            vendor_dir: value.vendor_dir,
561            external_deps: value.external_deps,
562            entrypoint_layout: value.entrypoint_layout,
563            user_agent: Some(value.user_agent),
564            generate_luarc: Some(value.generate_luarc),
565            wrap_bin_scripts: Some(value.wrap_bin_scripts),
566        }
567    }
568}
569
570fn default_variables() -> impl Iterator<Item = (String, String)> {
571    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
572    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
573    vec![
574        ("MAKE".into(), "make".into()),
575        ("CMAKE".into(), "cmake".into()),
576        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
577        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
578        ("CFLAGS".into(), cflags),
579        ("LDFLAGS".into(), ldflags),
580        ("LIBFLAG".into(), utils::default_libflag().into()),
581    ]
582    .into_iter()
583}
584
585fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
586where
587    D: serde::Deserializer<'de>,
588{
589    let s = Option::<String>::deserialize(deserializer)?;
590    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
591        .transpose()
592}
593
594fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
595where
596    S: Serializer,
597{
598    match url {
599        Some(url) => serializer.serialize_some(url.as_str()),
600        None => serializer.serialize_none(),
601    }
602}
603
604fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
605where
606    D: serde::Deserializer<'de>,
607{
608    let s = Option::<Vec<String>>::deserialize(deserializer)?;
609    s.map(|v| {
610        v.into_iter()
611            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
612            .try_collect()
613    })
614    .transpose()
615}
616
617fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
618where
619    S: Serializer,
620{
621    match urls {
622        Some(urls) => {
623            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
624            serializer.serialize_some(&url_strings)
625        }
626        None => serializer.serialize_none(),
627    }
628}