Skip to main content

lux_lib/config/
mod.rs

1use directories::ProjectDirs;
2use external_deps::ExternalDependencySearchConfig;
3use itertools::Itertools;
4use mlua::{ExternalError, ExternalResult, FromLua, IntoLua, UserData};
5use serde::{Deserialize, Serialize, Serializer};
6use std::env::current_exe;
7use std::path::Path;
8use std::{
9    collections::HashMap, env, fmt::Display, io, path::PathBuf, str::FromStr, time::Duration,
10};
11use thiserror::Error;
12use tree::RockLayoutConfig;
13use url::Url;
14
15use crate::tree::{Tree, TreeError};
16use crate::variables::GetVariableError;
17use crate::{
18    build::utils,
19    package::{PackageVersion, PackageVersionReq},
20    variables::HasVariables,
21};
22
23pub mod external_deps;
24pub mod tree;
25
26const DEV_PATH: &str = "dev/";
27
28#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
29pub enum LuaVersion {
30    #[serde(rename = "5.1")]
31    Lua51,
32    #[serde(rename = "5.2")]
33    Lua52,
34    #[serde(rename = "5.3")]
35    Lua53,
36    #[serde(rename = "5.4")]
37    Lua54,
38    #[serde(rename = "5.5")]
39    Lua55,
40    #[serde(rename = "jit")]
41    LuaJIT,
42    #[serde(rename = "jit5.2")]
43    LuaJIT52,
44    // TODO(vhyrro): Support luau?
45    // LuaU,
46}
47
48impl FromLua for LuaVersion {
49    fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
50        let version_str: String = FromLua::from_lua(value, lua)?;
51        LuaVersion::from_str(&version_str).into_lua_err()
52    }
53}
54
55impl IntoLua for LuaVersion {
56    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
57        self.to_string().into_lua(lua)
58    }
59}
60
61#[derive(Debug, Error)]
62pub enum LuaVersionError {
63    #[error("unsupported Lua version: {0}")]
64    UnsupportedLuaVersion(PackageVersion),
65}
66
67impl LuaVersion {
68    pub fn as_version(&self) -> PackageVersion {
69        unsafe {
70            match self {
71                LuaVersion::Lua51 => "5.1.0".parse().unwrap_unchecked(),
72                LuaVersion::Lua52 => "5.2.0".parse().unwrap_unchecked(),
73                LuaVersion::Lua53 => "5.3.0".parse().unwrap_unchecked(),
74                LuaVersion::Lua54 => "5.4.0".parse().unwrap_unchecked(),
75                LuaVersion::Lua55 => "5.5.0".parse().unwrap_unchecked(),
76                LuaVersion::LuaJIT => "5.1.0".parse().unwrap_unchecked(),
77                LuaVersion::LuaJIT52 => "5.2.0".parse().unwrap_unchecked(),
78            }
79        }
80    }
81    pub fn version_compatibility_str(&self) -> String {
82        match self {
83            LuaVersion::Lua51 | LuaVersion::LuaJIT => "5.1".into(),
84            LuaVersion::Lua52 | LuaVersion::LuaJIT52 => "5.2".into(),
85            LuaVersion::Lua53 => "5.3".into(),
86            LuaVersion::Lua54 => "5.4".into(),
87            LuaVersion::Lua55 => "5.5".into(),
88        }
89    }
90    pub fn as_version_req(&self) -> PackageVersionReq {
91        unsafe {
92            format!("~> {}", self.version_compatibility_str())
93                .parse()
94                .unwrap_unchecked()
95        }
96    }
97
98    /// Get the LuaVersion from a version that has been parsed from the `lua -v` output
99    pub fn from_version(version: PackageVersion) -> Result<LuaVersion, LuaVersionError> {
100        // NOTE: Special case. luajit -v outputs 2.x.y as a version
101        let luajit_version_req: PackageVersionReq = unsafe { "~> 2".parse().unwrap_unchecked() };
102        if luajit_version_req.matches(&version) {
103            Ok(LuaVersion::LuaJIT)
104        } else if LuaVersion::Lua51.as_version_req().matches(&version) {
105            Ok(LuaVersion::Lua51)
106        } else if LuaVersion::Lua52.as_version_req().matches(&version) {
107            Ok(LuaVersion::Lua52)
108        } else if LuaVersion::Lua53.as_version_req().matches(&version) {
109            Ok(LuaVersion::Lua53)
110        } else if LuaVersion::Lua54.as_version_req().matches(&version) {
111            Ok(LuaVersion::Lua54)
112        } else if LuaVersion::Lua55.as_version_req().matches(&version) {
113            Ok(LuaVersion::Lua55)
114        } else {
115            Err(LuaVersionError::UnsupportedLuaVersion(version))
116        }
117    }
118
119    pub(crate) fn is_luajit(&self) -> bool {
120        matches!(self, Self::LuaJIT | Self::LuaJIT52)
121    }
122
123    /// Searches for the path to the lux-lua library for this version
124    pub fn lux_lib_dir(&self) -> Option<PathBuf> {
125        option_env!("LUX_LIB_DIR")
126            .map(PathBuf::from)
127            .map(|path| path.join(self.to_string()))
128            .or_else(|| {
129                let lib_name = format!("lux-lua{self}");
130                pkg_config::Config::new()
131                    .print_system_libs(false)
132                    .cargo_metadata(false)
133                    .env_metadata(false)
134                    .probe(&lib_name)
135                    .ok()
136                    .and_then(|library| library.link_paths.first().cloned())
137            })
138            .or_else(|| lux_lib_resource_dir().map(|path| path.join(self.to_string())))
139    }
140}
141
142/// Searches for the lux-lib directory in a binary distribution's resources
143fn lux_lib_resource_dir() -> Option<PathBuf> {
144    if cfg!(target_env = "msvc") {
145        // The msvc .exe and .msi binary installers install lux-lua to the executable's directory.
146        current_exe()
147            .ok()
148            .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
149            .and_then(|exe_dir| {
150                let lib_dir = exe_dir.join("lux-lua");
151                if lib_dir.is_dir() {
152                    Some(lib_dir)
153                } else {
154                    None
155                }
156            })
157    } else if cfg!(target_os = "macos") {
158        // Currently, we only bundle resources with an .app ApplicationBundle
159        current_exe()
160            .ok()
161            .and_then(|exe_path| exe_path.parent().map(Path::to_path_buf))
162            .and_then(|macos_dir| macos_dir.parent().map(Path::to_path_buf))
163            .and_then(|contents_dir| {
164                let lib_dir = contents_dir.join("Resources").join("lux-lua");
165                if lib_dir.is_dir() {
166                    Some(lib_dir)
167                } else {
168                    None
169                }
170            })
171    } else {
172        // .deb and AppImage packages
173        let lib_dir = PathBuf::from("/usr/share/lux-lua");
174        if lib_dir.is_dir() {
175            Some(lib_dir)
176        } else {
177            None
178        }
179    }
180}
181
182#[derive(Error, Debug)]
183#[error(
184    r#"lua version not set.
185Please provide a version through `lx --lua-version <ver> <cmd>`
186Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit' and 'jit52'.
187"#
188)]
189pub struct LuaVersionUnset;
190
191impl LuaVersion {
192    pub fn from(config: &Config) -> Result<&Self, LuaVersionUnset> {
193        config.lua_version.as_ref().ok_or(LuaVersionUnset)
194    }
195}
196
197impl FromStr for LuaVersion {
198    type Err = String;
199
200    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
201        match s {
202            "5.1" | "51" => Ok(LuaVersion::Lua51),
203            "5.2" | "52" => Ok(LuaVersion::Lua52),
204            "5.3" | "53" => Ok(LuaVersion::Lua53),
205            "5.4" | "54" => Ok(LuaVersion::Lua54),
206            "5.5" | "55" => Ok(LuaVersion::Lua55),
207            "jit" | "luajit" => Ok(LuaVersion::LuaJIT),
208            "jit52" | "luajit52" => Ok(LuaVersion::LuaJIT52),
209            _ => Err(r#"unrecognized Lua version.
210                 Supported versions: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit', 'jit52'.
211                 "#
212            .into()),
213        }
214    }
215}
216
217impl Display for LuaVersion {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        f.write_str(match self {
220            LuaVersion::Lua51 => "5.1",
221            LuaVersion::Lua52 => "5.2",
222            LuaVersion::Lua53 => "5.3",
223            LuaVersion::Lua54 => "5.4",
224            LuaVersion::Lua55 => "5.5",
225            LuaVersion::LuaJIT => "jit",
226            LuaVersion::LuaJIT52 => "jit52",
227        })
228    }
229}
230
231#[derive(Error, Debug)]
232#[error("could not find a valid home directory")]
233pub struct NoValidHomeDirectory;
234
235#[derive(Debug, Clone, FromLua)]
236pub struct Config {
237    enable_development_packages: bool,
238    server: Url,
239    extra_servers: Vec<Url>,
240    only_sources: Option<String>,
241    namespace: Option<String>,
242    lua_dir: Option<PathBuf>,
243    lua_version: Option<LuaVersion>,
244    user_tree: PathBuf,
245    verbose: bool,
246    /// Don't display progress bars
247    no_progress: bool,
248    timeout: Duration,
249    max_jobs: usize,
250    variables: HashMap<String, String>,
251    external_deps: ExternalDependencySearchConfig,
252    /// The rock layout for entrypoints of new install trees.
253    /// Does not affect existing install trees or dependency rock layouts.
254    entrypoint_layout: RockLayoutConfig,
255
256    cache_dir: PathBuf,
257    data_dir: PathBuf,
258    vendor_dir: Option<PathBuf>,
259
260    generate_luarc: bool,
261}
262
263impl Config {
264    pub fn get_project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
265        directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
266    }
267
268    pub fn get_default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
269        let project_dirs = Config::get_project_dirs()?;
270        Ok(project_dirs.cache_dir().to_path_buf())
271    }
272
273    pub fn get_default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
274        let project_dirs = Config::get_project_dirs()?;
275        Ok(project_dirs.data_local_dir().to_path_buf())
276    }
277
278    pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
279        Self {
280            lua_version: Some(lua_version),
281            ..self
282        }
283    }
284
285    pub fn with_tree(self, tree: PathBuf) -> Self {
286        Self {
287            user_tree: tree,
288            ..self
289        }
290    }
291
292    pub fn server(&self) -> &Url {
293        &self.server
294    }
295
296    pub fn extra_servers(&self) -> &Vec<Url> {
297        self.extra_servers.as_ref()
298    }
299
300    pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
301        let mut enabled_dev_servers = Vec::new();
302        if self.enable_development_packages {
303            enabled_dev_servers.push(self.server().join(DEV_PATH)?);
304            for server in self.extra_servers() {
305                enabled_dev_servers.push(server.join(DEV_PATH)?);
306            }
307        }
308        Ok(enabled_dev_servers)
309    }
310
311    pub fn only_sources(&self) -> Option<&String> {
312        self.only_sources.as_ref()
313    }
314
315    pub fn namespace(&self) -> Option<&String> {
316        self.namespace.as_ref()
317    }
318
319    pub fn lua_dir(&self) -> Option<&PathBuf> {
320        self.lua_dir.as_ref()
321    }
322
323    #[cfg(test)]
324    pub(crate) fn lua_version(&self) -> Option<&LuaVersion> {
325        self.lua_version.as_ref()
326    }
327
328    /// The tree in which to install rocks.
329    /// If installing packges for a project, use `Project::tree` instead.
330    pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
331        Tree::new(self.user_tree.clone(), version, self)
332    }
333
334    pub fn verbose(&self) -> bool {
335        self.verbose
336    }
337
338    pub fn no_progress(&self) -> bool {
339        self.no_progress
340    }
341
342    pub fn timeout(&self) -> &Duration {
343        &self.timeout
344    }
345
346    pub fn max_jobs(&self) -> usize {
347        self.max_jobs
348    }
349
350    pub fn make_cmd(&self) -> String {
351        match self.variables.get("MAKE") {
352            Some(make) => make.clone(),
353            None => "make".into(),
354        }
355    }
356
357    pub fn cmake_cmd(&self) -> String {
358        match self.variables.get("CMAKE") {
359            Some(cmake) => cmake.clone(),
360            None => "cmake".into(),
361        }
362    }
363
364    pub fn variables(&self) -> &HashMap<String, String> {
365        &self.variables
366    }
367
368    pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
369        &self.external_deps
370    }
371
372    pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
373        &self.entrypoint_layout
374    }
375
376    pub fn cache_dir(&self) -> &PathBuf {
377        &self.cache_dir
378    }
379
380    pub fn data_dir(&self) -> &PathBuf {
381        &self.data_dir
382    }
383
384    pub fn vendor_dir(&self) -> Option<&PathBuf> {
385        self.vendor_dir.as_ref()
386    }
387
388    pub fn generate_luarc(&self) -> bool {
389        self.generate_luarc
390    }
391}
392
393impl HasVariables for Config {
394    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
395        Ok(self.variables.get(input).cloned())
396    }
397}
398
399#[derive(Error, Debug)]
400pub enum ConfigError {
401    #[error(transparent)]
402    Io(#[from] io::Error),
403    #[error(transparent)]
404    NoValidHomeDirectory(#[from] NoValidHomeDirectory),
405    #[error("error deserializing lux config: {0}")]
406    Deserialize(#[from] toml::de::Error),
407    #[error("error parsing URL: {0}")]
408    UrlParseError(#[from] url::ParseError),
409    #[error("error initializing compiler toolchain: {0}")]
410    CompilerToolchain(#[from] cc::Error),
411}
412
413#[derive(Clone, Default, Deserialize, Serialize)]
414pub struct ConfigBuilder {
415    #[serde(
416        default,
417        deserialize_with = "deserialize_url",
418        serialize_with = "serialize_url"
419    )]
420    server: Option<Url>,
421    #[serde(
422        default,
423        deserialize_with = "deserialize_url_vec",
424        serialize_with = "serialize_url_vec"
425    )]
426    extra_servers: Option<Vec<Url>>,
427    only_sources: Option<String>,
428    namespace: Option<String>,
429    lua_version: Option<LuaVersion>,
430    user_tree: Option<PathBuf>,
431    lua_dir: Option<PathBuf>,
432    cache_dir: Option<PathBuf>,
433    data_dir: Option<PathBuf>,
434    vendor_dir: Option<PathBuf>,
435    enable_development_packages: Option<bool>,
436    verbose: Option<bool>,
437    no_progress: Option<bool>,
438    timeout: Option<Duration>,
439    max_jobs: Option<usize>,
440    variables: Option<HashMap<String, String>>,
441    #[serde(default)]
442    external_deps: ExternalDependencySearchConfig,
443    /// The rock layout for new install trees.
444    /// Does not affect existing install trees.
445    #[serde(default)]
446    entrypoint_layout: RockLayoutConfig,
447    generate_luarc: Option<bool>,
448}
449
450/// A builder for the lux `Config`.
451impl ConfigBuilder {
452    /// Create a new `ConfigBuilder` from a config file by deserializing from a config file
453    /// if present, or otherwise by instantiating the default config.
454    pub fn new() -> Result<Self, ConfigError> {
455        let config_file = Self::config_file()?;
456        if config_file.is_file() {
457            Ok(toml::from_str(&std::fs::read_to_string(&config_file)?)?)
458        } else {
459            Ok(Self::default())
460        }
461    }
462
463    /// Get the path to the lux config file.
464    pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
465        let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
466            .ok_or(NoValidHomeDirectory)?;
467        Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
468    }
469
470    pub fn dev(self, dev: Option<bool>) -> Self {
471        Self {
472            enable_development_packages: dev.or(self.enable_development_packages),
473            ..self
474        }
475    }
476
477    pub fn server(self, server: Option<Url>) -> Self {
478        Self {
479            server: server.or(self.server),
480            ..self
481        }
482    }
483
484    pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
485        Self {
486            extra_servers: extra_servers.or(self.extra_servers),
487            ..self
488        }
489    }
490
491    pub fn only_sources(self, sources: Option<String>) -> Self {
492        Self {
493            only_sources: sources.or(self.only_sources),
494            ..self
495        }
496    }
497
498    pub fn namespace(self, namespace: Option<String>) -> Self {
499        Self {
500            namespace: namespace.or(self.namespace),
501            ..self
502        }
503    }
504
505    pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
506        Self {
507            lua_dir: lua_dir.or(self.lua_dir),
508            ..self
509        }
510    }
511
512    pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
513        Self {
514            lua_version: lua_version.or(self.lua_version),
515            ..self
516        }
517    }
518
519    pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
520        Self {
521            user_tree: tree.or(self.user_tree),
522            ..self
523        }
524    }
525
526    pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
527        Self {
528            variables: variables.or(self.variables),
529            ..self
530        }
531    }
532
533    pub fn verbose(self, verbose: Option<bool>) -> Self {
534        Self {
535            verbose: verbose.or(self.verbose),
536            ..self
537        }
538    }
539
540    pub fn no_progress(self, no_progress: Option<bool>) -> Self {
541        Self {
542            no_progress: no_progress.or(self.no_progress),
543            ..self
544        }
545    }
546
547    pub fn timeout(self, timeout: Option<Duration>) -> Self {
548        Self {
549            timeout: timeout.or(self.timeout),
550            ..self
551        }
552    }
553
554    pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
555        Self {
556            max_jobs: max_jobs.or(self.max_jobs),
557            ..self
558        }
559    }
560
561    pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
562        Self {
563            cache_dir: cache_dir.or(self.cache_dir),
564            ..self
565        }
566    }
567
568    pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
569        Self {
570            data_dir: data_dir.or(self.data_dir),
571            ..self
572        }
573    }
574
575    pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
576        Self {
577            vendor_dir: vendor_dir.or(self.vendor_dir),
578            ..self
579        }
580    }
581
582    pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
583        Self {
584            entrypoint_layout: rock_layout,
585            ..self
586        }
587    }
588
589    pub fn generate_luarc(self, generate: Option<bool>) -> Self {
590        Self {
591            generate_luarc: generate.or(self.generate_luarc),
592            ..self
593        }
594    }
595
596    pub fn build(self) -> Result<Config, ConfigError> {
597        let data_dir = self.data_dir.unwrap_or(Config::get_default_data_path()?);
598        let cache_dir = self.cache_dir.unwrap_or(Config::get_default_cache_path()?);
599        let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
600
601        let lua_version = self
602            .lua_version
603            .or(crate::lua_installation::detect_installed_lua_version());
604
605        Ok(Config {
606            enable_development_packages: self.enable_development_packages.unwrap_or(false),
607            server: self.server.unwrap_or_else(|| unsafe {
608                Url::parse("https://luarocks.org/").unwrap_unchecked()
609            }),
610            extra_servers: self.extra_servers.unwrap_or_default(),
611            only_sources: self.only_sources,
612            namespace: self.namespace,
613            lua_dir: self.lua_dir,
614            lua_version,
615            user_tree,
616            verbose: self.verbose.unwrap_or(false),
617            no_progress: self.no_progress.unwrap_or(false),
618            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
619            max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
620                0 => usize::MAX,
621                max_jobs => max_jobs,
622            },
623            variables: default_variables()
624                .chain(self.variables.unwrap_or_default())
625                .collect(),
626            external_deps: self.external_deps,
627            entrypoint_layout: self.entrypoint_layout,
628            cache_dir,
629            data_dir,
630            vendor_dir: self.vendor_dir,
631            generate_luarc: self.generate_luarc.unwrap_or(true),
632        })
633    }
634}
635
636/// Useful for printing the current config
637impl From<Config> for ConfigBuilder {
638    fn from(value: Config) -> Self {
639        ConfigBuilder {
640            enable_development_packages: Some(value.enable_development_packages),
641            server: Some(value.server),
642            extra_servers: Some(value.extra_servers),
643            only_sources: value.only_sources,
644            namespace: value.namespace,
645            lua_dir: value.lua_dir,
646            lua_version: value.lua_version,
647            user_tree: Some(value.user_tree),
648            verbose: Some(value.verbose),
649            no_progress: Some(value.no_progress),
650            timeout: Some(value.timeout),
651            max_jobs: if value.max_jobs == usize::MAX {
652                None
653            } else {
654                Some(value.max_jobs)
655            },
656            variables: Some(value.variables),
657            cache_dir: Some(value.cache_dir),
658            data_dir: Some(value.data_dir),
659            vendor_dir: value.vendor_dir,
660            external_deps: value.external_deps,
661            entrypoint_layout: value.entrypoint_layout,
662            generate_luarc: Some(value.generate_luarc),
663        }
664    }
665}
666
667fn default_variables() -> impl Iterator<Item = (String, String)> {
668    let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
669    let ldflags = env::var("LDFLAGS").unwrap_or("".into());
670    vec![
671        ("MAKE".into(), "make".into()),
672        ("CMAKE".into(), "cmake".into()),
673        ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
674        ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
675        ("CFLAGS".into(), cflags),
676        ("LDFLAGS".into(), ldflags),
677        ("LIBFLAG".into(), utils::default_libflag().into()),
678    ]
679    .into_iter()
680}
681
682fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
683where
684    D: serde::Deserializer<'de>,
685{
686    let s = Option::<String>::deserialize(deserializer)?;
687    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
688        .transpose()
689}
690
691fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
692where
693    S: Serializer,
694{
695    match url {
696        Some(url) => serializer.serialize_some(url.as_str()),
697        None => serializer.serialize_none(),
698    }
699}
700
701fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
702where
703    D: serde::Deserializer<'de>,
704{
705    let s = Option::<Vec<String>>::deserialize(deserializer)?;
706    s.map(|v| {
707        v.into_iter()
708            .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
709            .try_collect()
710    })
711    .transpose()
712}
713
714fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
715where
716    S: Serializer,
717{
718    match urls {
719        Some(urls) => {
720            let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
721            serializer.serialize_some(&url_strings)
722        }
723        None => serializer.serialize_none(),
724    }
725}
726
727struct LuaUrl(Url);
728
729impl FromLua for LuaUrl {
730    fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
731        let url_str: String = FromLua::from_lua(value, lua)?;
732
733        Url::parse(&url_str).map(LuaUrl).into_lua_err()
734    }
735}
736
737impl UserData for Config {
738    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
739        methods.add_function("default", |_, _: ()| {
740            ConfigBuilder::default()
741                .build()
742                .map_err(|err| err.into_lua_err())
743        });
744
745        methods.add_function("builder", |_, ()| ConfigBuilder::new().into_lua_err());
746
747        methods.add_method("server", |_, this, ()| Ok(this.server().to_string()));
748        methods.add_method("extra_servers", |_, this, ()| {
749            Ok(this
750                .extra_servers()
751                .iter()
752                .map(|url| url.to_string())
753                .collect_vec())
754        });
755        methods.add_method("only_sources", |_, this, ()| {
756            Ok(this.only_sources().cloned())
757        });
758        methods.add_method("namespace", |_, this, ()| Ok(this.namespace().cloned()));
759        methods.add_method("lua_dir", |_, this, ()| Ok(this.lua_dir().cloned()));
760        methods.add_method("user_tree", |_, this, lua_version: LuaVersion| {
761            this.user_tree(lua_version).into_lua_err()
762        });
763        methods.add_method("verbose", |_, this, ()| Ok(this.verbose()));
764        methods.add_method("no_progress", |_, this, ()| Ok(this.no_progress()));
765        methods.add_method("timeout", |_, this, ()| Ok(this.timeout().as_secs()));
766        methods.add_method("cache_dir", |_, this, ()| Ok(this.cache_dir().clone()));
767        methods.add_method("data_dir", |_, this, ()| Ok(this.data_dir().clone()));
768        methods.add_method("entrypoint_layout", |_, this, ()| {
769            Ok(this.entrypoint_layout().clone())
770        });
771        methods.add_method("variables", |_, this, ()| Ok(this.variables().clone()));
772        // FIXME: This is a temporary workaround to get the external_deps hooked up to Lua
773        // methods.add_method("external_deps", |_, this, ()| {
774        //     Ok(this.external_deps().clone())
775        // });
776        methods.add_method("make_cmd", |_, this, ()| Ok(this.make_cmd()));
777        methods.add_method("cmake_cmd", |_, this, ()| Ok(this.cmake_cmd()));
778        methods.add_method("enabled_dev_servers", |_, this, ()| {
779            Ok(this
780                .enabled_dev_servers()
781                .into_lua_err()?
782                .into_iter()
783                .map(|url| url.to_string())
784                .collect_vec())
785        });
786    }
787}
788
789impl UserData for ConfigBuilder {
790    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
791        methods.add_method("dev", |_, this, dev: Option<bool>| {
792            Ok(this.clone().dev(dev))
793        });
794        methods.add_method("server", |_, this, server: Option<LuaUrl>| {
795            Ok(this.clone().server(server.map(|url| url.0)))
796        });
797        methods.add_method("extra_servers", |_, this, servers: Option<Vec<LuaUrl>>| {
798            Ok(this
799                .clone()
800                .extra_servers(servers.map(|urls| urls.into_iter().map(|url| url.0).collect())))
801        });
802        methods.add_method("only_sources", |_, this, sources: Option<String>| {
803            Ok(this.clone().only_sources(sources))
804        });
805        methods.add_method("namespace", |_, this, namespace: Option<String>| {
806            Ok(this.clone().namespace(namespace))
807        });
808        methods.add_method("lua_dir", |_, this, lua_dir: Option<PathBuf>| {
809            Ok(this.clone().lua_dir(lua_dir))
810        });
811        methods.add_method("lua_version", |_, this, lua_version: Option<LuaVersion>| {
812            Ok(this.clone().lua_version(lua_version))
813        });
814        methods.add_method("user_tree", |_, this, tree: Option<PathBuf>| {
815            Ok(this.clone().user_tree(tree))
816        });
817        methods.add_method("verbose", |_, this, verbose: Option<bool>| {
818            Ok(this.clone().verbose(verbose))
819        });
820        methods.add_method("no_progress", |_, this, no_progress: Option<bool>| {
821            Ok(this.clone().no_progress(no_progress))
822        });
823        methods.add_method("timeout", |_, this, timeout: Option<u64>| {
824            Ok(this.clone().timeout(timeout.map(Duration::from_secs)))
825        });
826        methods.add_method("cache_dir", |_, this, cache_dir: Option<PathBuf>| {
827            Ok(this.clone().cache_dir(cache_dir))
828        });
829        methods.add_method("data_dir", |_, this, data_dir: Option<PathBuf>| {
830            Ok(this.clone().data_dir(data_dir))
831        });
832        methods.add_method(
833            "entrypoint_layout",
834            |_, this, entrypoint_layout: Option<RockLayoutConfig>| {
835                Ok(this
836                    .clone()
837                    .entrypoint_layout(entrypoint_layout.unwrap_or_default()))
838            },
839        );
840        methods.add_method("generate_luarc", |_, this, generate: Option<bool>| {
841            Ok(this.clone().generate_luarc(generate))
842        });
843        methods.add_method("build", |_, this, ()| this.clone().build().into_lua_err());
844    }
845}