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