Skip to main content

lux_lib/config/
mod.rs

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