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