1use build::BuildConfig;
2use directories::ProjectDirs;
3use external_deps::ExternalDependencySearchConfig;
4use itertools::Itertools;
5
6use miette::Diagnostic;
7use serde::{Deserialize, Serialize, Serializer};
8use std::ffi::OsStr;
9use std::path::Path;
10use std::{collections::HashMap, env, path::PathBuf, sync::Arc, time::Duration};
11use thiserror::Error;
12use tokio::process::Command;
13use url::Url;
14
15use crate::config::access_tokens::AccessToken;
16use crate::fs;
17use crate::lua_version::LuaVersion;
18use crate::package::RemotePackageTypeFilterSpec;
19use crate::project::TomlDeError;
20use crate::tree::{CustomRockLayout, Tree, TreeError};
21use crate::variables::GetVariableError;
22use crate::{build::utils, variables::HasVariables};
23
24pub mod access_tokens;
25pub mod build;
26pub mod external_deps;
27
28const DEV_PATH: &str = "dev/";
29const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
30
31#[derive(Error, Debug, Diagnostic)]
32#[error("could not find a valid home directory")]
33#[diagnostic(
34 code(lux_lib::no_home_directory),
35 help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
36)]
37pub struct NoValidHomeDirectory;
38
39#[derive(Debug, Clone)]
43pub struct Config {
44 enable_development_packages: bool,
45 server: Url,
46 extra_servers: Vec<Url>,
47 manifest_cache_url: Option<Url>,
48 extra_wally_registries: Vec<Url>,
49 namespace: Option<String>,
50 lua_dir: Option<PathBuf>,
51 lua_version: Option<LuaVersion>,
52 user_tree: PathBuf,
53 workspace_tree: Option<PathBuf>,
54 verbose: bool,
55 no_progress: bool,
57 no_prompt: bool,
59 timeout: Duration,
60 max_jobs: usize,
61 variables: HashMap<String, String>,
62 access_tokens: HashMap<String, AccessToken>,
63 external_deps: ExternalDependencySearchConfig,
64
65 build: BuildConfig,
66 entrypoint_layout: Option<Arc<dyn CustomRockLayout>>,
67
68 cache_dir: PathBuf,
69 data_dir: PathBuf,
70 vendor_dir: Option<PathBuf>,
71
72 user_agent: String,
73
74 generate_luarc: bool,
75 luarc_file_name: String,
76 wrap_bin_scripts: bool,
77 package_types: RemotePackageTypeFilterSpec,
78 no_tfa: bool,
79}
80
81impl Config {
82 pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
84 directories::ProjectDirs::from("org", "lumenlabs", "lux")
85 .filter(|_| Self::home_dir_exists())
86 .ok_or(NoValidHomeDirectory)
87 }
88
89 fn home_dir_exists() -> bool {
90 directories::BaseDirs::new().is_some_and(|base| base.home_dir().is_dir())
91 }
92
93 fn default_cache_path() -> PathBuf {
95 match Config::project_dirs() {
96 Ok(project_dirs) => project_dirs.cache_dir().to_path_buf(),
97 Err(_) => Self::fallback_data_dir("cache"),
98 }
99 }
100
101 fn default_data_path() -> PathBuf {
103 match Config::project_dirs() {
104 Ok(project_dirs) => project_dirs.data_local_dir().to_path_buf(),
105 Err(_) => Self::fallback_data_dir("data"),
106 }
107 }
108
109 fn fallback_data_dir(subdir: &str) -> PathBuf {
111 let path = std::env::temp_dir().join("lux").join(subdir);
112 tracing::warn!(
113 "no valid home directory found; falling back to `{}`",
114 path.display()
115 );
116 path
117 }
118
119 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
121 Self {
122 lua_version: Some(lua_version),
123 ..self
124 }
125 }
126
127 pub fn with_tree(self, tree: PathBuf) -> Self {
129 Self {
130 user_tree: tree,
131 ..self
132 }
133 }
134
135 pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
137 Self {
138 workspace_tree: tree,
139 ..self
140 }
141 }
142
143 pub fn server(&self) -> &Url {
145 &self.server
146 }
147
148 pub fn extra_servers(&self) -> &Vec<Url> {
150 self.extra_servers.as_ref()
151 }
152
153 pub fn manifest_cache_url(&self) -> Option<&Url> {
155 self.manifest_cache_url.as_ref()
156 }
157
158 pub fn extra_wally_registries(&self) -> &Vec<Url> {
161 self.extra_wally_registries.as_ref()
162 }
163
164 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
166 let mut enabled_dev_servers = Vec::new();
167 if self.enable_development_packages {
168 let config_file = ConfigBuilder::config_file()
169 .map(|p| p.to_string_lossy().to_string())
170 .unwrap_or_default();
171 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
172 ConfigError::UrlParseError {
173 source,
174 help: Some(format!("check the `server` URL in {config_file}")),
175 }
176 })?);
177 for server in self.extra_servers() {
178 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
179 ConfigError::UrlParseError {
180 source,
181 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
182 }
183 })?);
184 }
185 }
186 Ok(enabled_dev_servers)
187 }
188
189 pub fn namespace(&self) -> Option<&String> {
191 self.namespace.as_ref()
192 }
193
194 pub fn lua_dir(&self) -> Option<&PathBuf> {
196 self.lua_dir.as_ref()
197 }
198
199 pub fn lua_version(&self) -> Option<&LuaVersion> {
201 self.lua_version.as_ref()
202 }
203
204 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
207 Tree::new(self.user_tree.clone(), version, self)
208 }
209
210 pub fn workspace_tree(&self) -> Option<&PathBuf> {
212 self.workspace_tree.as_ref()
213 }
214
215 pub fn verbose(&self) -> bool {
217 self.verbose
218 }
219
220 pub fn no_progress(&self) -> bool {
222 self.no_progress
223 }
224
225 pub fn no_prompt(&self) -> bool {
227 self.no_prompt
228 }
229
230 pub fn timeout(&self) -> &Duration {
233 &self.timeout
234 }
235
236 pub fn max_jobs(&self) -> usize {
239 self.max_jobs
240 }
241
242 pub fn make_cmd(&self) -> String {
244 match self.variables.get("MAKE") {
245 Some(make) => make.clone(),
246 None => "make".into(),
247 }
248 }
249
250 pub fn cmake_cmd(&self) -> String {
252 match self.variables.get("CMAKE") {
253 Some(cmake) => cmake.clone(),
254 None => "cmake".into(),
255 }
256 }
257
258 pub(crate) fn wrapped_command<P, A>(&self, program: P, args: A) -> Command
264 where
265 P: AsRef<OsStr>,
266 A: IntoIterator,
267 A::Item: AsRef<OsStr>,
268 {
269 if self.build.runner.is_empty() {
270 let mut cmd = Command::new(program);
271 cmd.args(args);
272 cmd
273 } else {
274 let runner = &self.build.runner;
275 let mut cmd = Command::new(&runner[0]);
276 cmd.args(&runner[1..]).arg(program).args(args);
277 cmd
278 }
279 }
280
281 pub(crate) fn build_profile(&self) -> build::Profile {
283 self.build
284 .profile
285 .as_ref()
286 .cloned()
287 .unwrap_or(build::Profile::Release)
288 }
289
290 pub fn variables(&self) -> &HashMap<String, String> {
294 &self.variables
295 }
296
297 pub fn access_token(&self, host: &str) -> Option<AccessToken> {
302 access_tokens::env_access_tokens()
303 .get(host)
304 .cloned()
305 .or_else(|| self.access_tokens.get(host).cloned())
306 .or_else(|| {
307 (host == "github.com")
308 .then(|| env::var("GITHUB_TOKEN").ok())
309 .flatten()
310 .map(|raw| AccessToken::from(raw.as_str()))
311 })
312 }
313
314 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
315 &self.external_deps
316 }
317
318 pub fn entrypoint_layout(&self) -> Option<&Arc<dyn CustomRockLayout>> {
320 self.entrypoint_layout.as_ref()
321 }
322
323 pub fn cache_dir(&self) -> &PathBuf {
325 &self.cache_dir
326 }
327
328 pub fn data_dir(&self) -> &PathBuf {
330 &self.data_dir
331 }
332
333 pub fn vendor_dir(&self) -> Option<&PathBuf> {
337 self.vendor_dir.as_ref()
338 }
339
340 pub fn user_agent(&self) -> &str {
342 &self.user_agent
343 }
344
345 pub fn generate_luarc(&self) -> bool {
347 self.generate_luarc
348 }
349
350 pub fn luarc_file_name(&self) -> &str {
352 &self.luarc_file_name
353 }
354
355 pub fn wrap_bin_scripts(&self) -> bool {
359 self.wrap_bin_scripts
360 }
361
362 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
364 &self.package_types
365 }
366 pub fn no_tfa(&self) -> bool {
368 self.no_tfa
369 }
370}
371
372impl HasVariables for Config {
373 #[tracing::instrument(level = "trace")]
374 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
375 Ok(self.variables.get(input).cloned())
376 }
377}
378
379#[derive(Error, Debug, Diagnostic)]
380pub enum ConfigError {
381 #[error(transparent)]
382 #[diagnostic(transparent)]
383 Fs(#[from] fs::FsError),
384 #[error(transparent)]
385 #[diagnostic(transparent)]
386 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
387 #[error("error parsing {config_file}")]
388 Deserialize {
389 config_file: String,
390 #[diagnostic_source]
391 source: TomlDeError,
392 },
393 #[error("error parsing URL: {source}")]
394 UrlParseError {
395 source: url::ParseError,
396 #[help]
397 help: Option<String>,
398 },
399}
400
401#[derive(Debug, Clone, Default, Deserialize, Serialize)]
408#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
409pub struct ConfigBuilder {
410 #[serde(
413 default,
414 deserialize_with = "deserialize_url",
415 serialize_with = "serialize_url"
416 )]
417 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
418 server: Option<Url>,
419 #[serde(
421 default,
422 deserialize_with = "deserialize_url_vec",
423 serialize_with = "serialize_url_vec"
424 )]
425 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<String>>"))]
426 extra_servers: Option<Vec<Url>>,
427 #[serde(
428 default,
429 deserialize_with = "deserialize_url",
430 serialize_with = "serialize_url"
431 )]
432 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
433 manifest_cache_url: Option<Url>,
434 no_manifest_cache: Option<bool>,
435 #[serde(
438 default,
439 deserialize_with = "deserialize_url_vec",
440 serialize_with = "serialize_url_vec"
441 )]
442 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<String>>"))]
443 extra_wally_registries: Option<Vec<Url>>,
444 namespace: Option<String>,
446 lua_version: Option<LuaVersion>,
448 user_tree: Option<PathBuf>,
450 workspace_tree: Option<PathBuf>,
453 lua_dir: Option<PathBuf>,
455 cache_dir: Option<PathBuf>,
457 data_dir: Option<PathBuf>,
459 vendor_dir: Option<PathBuf>,
462 enable_development_packages: Option<bool>,
464 verbose: Option<bool>,
466 no_progress: Option<bool>,
468 no_prompt: Option<bool>,
470 #[cfg_attr(feature = "schema", schemars(with = "Option<u64>"))]
473 timeout: Option<Duration>,
474 max_jobs: Option<usize>,
477 variables: Option<HashMap<String, String>>,
480 #[serde(default, skip_serializing)]
483 #[cfg_attr(feature = "schema", schemars(with = "Option<HashMap<String, String>>"))]
484 access_tokens: Option<HashMap<String, AccessToken>>,
485 #[serde(default)]
486 external_deps: ExternalDependencySearchConfig,
487 #[serde(default)]
488 build: BuildConfig,
489
490 #[serde(skip)]
491 entrypoint_layout: Option<Arc<dyn CustomRockLayout>>,
492 user_agent: Option<String>,
495 generate_luarc: Option<bool>,
497 luarc_file_name: Option<String>,
499 wrap_bin_scripts: Option<bool>,
502 package_types: Option<RemotePackageTypeFilterSpec>,
504 no_tfa: Option<bool>,
507}
508
509impl ConfigBuilder {
511 pub fn new() -> Result<Self, ConfigError> {
514 let config_file = Self::config_file()?;
515 if config_file.is_file() {
516 Self::from_file(&config_file)
517 } else {
518 Ok(Self::default())
519 }
520 }
521
522 pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
523 let config_file_name = config_file.to_string_lossy().to_string();
524 let content = fs::sync::read_to_string(config_file)?;
525 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
526 ConfigError::Deserialize {
527 config_file: config_file_name,
528 source,
529 }
530 })
531 }
532
533 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
535 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
536 .ok_or(NoValidHomeDirectory)?;
537 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
538 }
539
540 pub fn dev(self, dev: Option<bool>) -> Self {
543 Self {
544 enable_development_packages: dev.or(self.enable_development_packages),
545 ..self
546 }
547 }
548
549 pub fn server(self, server: Option<Url>) -> Self {
552 Self {
553 server: server.or(self.server),
554 ..self
555 }
556 }
557
558 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
560 Self {
561 extra_servers: extra_servers.or(self.extra_servers),
562 ..self
563 }
564 }
565
566 pub fn manifest_cache_url(self, manifest_cache_url: Option<Url>) -> Self {
569 Self {
570 manifest_cache_url: manifest_cache_url.or(self.manifest_cache_url),
571 ..self
572 }
573 }
574
575 pub fn no_manifest_cache(self, no_manifest_cache: Option<bool>) -> Self {
578 Self {
579 no_manifest_cache: no_manifest_cache.or(self.no_manifest_cache),
580 ..self
581 }
582 }
583
584 pub fn extra_wally_registries(self, extra_wally_registries: Option<Vec<Url>>) -> Self {
586 Self {
587 extra_wally_registries: extra_wally_registries.or(self.extra_wally_registries),
588 ..self
589 }
590 }
591
592 pub fn namespace(self, namespace: Option<String>) -> Self {
594 Self {
595 namespace: namespace.or(self.namespace),
596 ..self
597 }
598 }
599
600 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
602 Self {
603 lua_dir: lua_dir.or(self.lua_dir),
604 ..self
605 }
606 }
607
608 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
611 Self {
612 lua_version: lua_version.or(self.lua_version),
613 ..self
614 }
615 }
616
617 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
619 Self {
620 user_tree: tree.or(self.user_tree),
621 ..self
622 }
623 }
624
625 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
628 Self {
629 workspace_tree: tree.or(self.workspace_tree),
630 ..self
631 }
632 }
633
634 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
638 Self {
639 variables: variables.or(self.variables),
640 ..self
641 }
642 }
643
644 pub fn verbose(self, verbose: Option<bool>) -> Self {
647 Self {
648 verbose: verbose.or(self.verbose),
649 ..self
650 }
651 }
652
653 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
656 Self {
657 no_progress: no_progress.or(self.no_progress),
658 ..self
659 }
660 }
661
662 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
665 Self {
666 no_prompt: no_prompt.or(self.no_prompt),
667 ..self
668 }
669 }
670
671 pub fn timeout(self, timeout: Option<Duration>) -> Self {
675 Self {
676 timeout: timeout.or(self.timeout),
677 ..self
678 }
679 }
680
681 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
685 Self {
686 max_jobs: max_jobs.or(self.max_jobs),
687 ..self
688 }
689 }
690
691 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
693 Self {
694 cache_dir: cache_dir.or(self.cache_dir),
695 ..self
696 }
697 }
698
699 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
701 Self {
702 data_dir: data_dir.or(self.data_dir),
703 ..self
704 }
705 }
706
707 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
711 Self {
712 vendor_dir: vendor_dir.or(self.vendor_dir),
713 ..self
714 }
715 }
716
717 pub fn entrypoint_layout(self, layout: impl CustomRockLayout + 'static) -> Self {
719 Self {
720 entrypoint_layout: Some(Arc::new(layout)),
721 ..self
722 }
723 }
724
725 pub fn user_agent(self, user_agent: Option<String>) -> Self {
728 Self {
729 user_agent: user_agent.or(self.user_agent),
730 ..self
731 }
732 }
733
734 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
737 Self {
738 generate_luarc: generate.or(self.generate_luarc),
739 ..self
740 }
741 }
742
743 pub fn luarc_file_name(self, file: Option<String>) -> Self {
746 Self {
747 luarc_file_name: file.or(self.luarc_file_name),
748 ..self
749 }
750 }
751
752 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
758 Self {
759 wrap_bin_scripts: generate.or(self.generate_luarc),
760 ..self
761 }
762 }
763 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
766 Self {
767 no_tfa: tfa.or(self.no_tfa),
768 ..self
769 }
770 }
771
772 pub fn build_runner(self, runner: Option<Vec<String>>) -> Self {
822 Self {
823 build: BuildConfig {
824 runner: runner.unwrap_or(self.build.runner),
825 ..self.build
826 },
827 ..self
828 }
829 }
830
831 pub fn build_profile(self, profile: Option<build::Profile>) -> Self {
834 Self {
835 build: BuildConfig {
836 profile: profile.or(self.build.profile),
837 ..self.build
838 },
839 ..self
840 }
841 }
842
843 pub fn default_build_profile(self, profile: build::Profile) -> Self {
845 Self {
846 build: BuildConfig {
847 profile: self.build.profile.or(Some(profile)),
848 ..self.build
849 },
850 ..self
851 }
852 }
853
854 pub fn merge(self, other: Self) -> Self {
856 Self {
857 server: other.server.or(self.server),
858 extra_servers: other.extra_servers.or(self.extra_servers),
859 manifest_cache_url: other.manifest_cache_url.or(self.manifest_cache_url),
860 no_manifest_cache: other.no_manifest_cache.or(self.no_manifest_cache),
861 extra_wally_registries: other.extra_wally_registries.or(self.extra_wally_registries),
862 namespace: other.namespace.or(self.namespace),
863 lua_version: other.lua_version.or(self.lua_version),
864 user_tree: other.user_tree.or(self.user_tree),
865 workspace_tree: other.workspace_tree.or(self.workspace_tree),
866 lua_dir: other.lua_dir.or(self.lua_dir),
867 cache_dir: other.cache_dir.or(self.cache_dir),
868 data_dir: other.data_dir.or(self.data_dir),
869 vendor_dir: other.vendor_dir.or(self.vendor_dir),
870 enable_development_packages: other
871 .enable_development_packages
872 .or(self.enable_development_packages),
873 verbose: other.verbose.or(self.verbose),
874 no_progress: other.no_progress.or(self.no_progress),
875 no_prompt: other.no_prompt.or(self.no_prompt),
876 timeout: other.timeout.or(self.timeout),
877 max_jobs: other.max_jobs.or(self.max_jobs),
878 variables: other.variables.or(self.variables),
879 access_tokens: other.access_tokens.or(self.access_tokens),
880 external_deps: other.external_deps,
881 build: BuildConfig {
882 profile: other.build.profile.or(self.build.profile),
883 ..other.build
884 },
885 entrypoint_layout: other.entrypoint_layout.or(self.entrypoint_layout),
886 user_agent: other.user_agent.or(self.user_agent),
887 generate_luarc: other.generate_luarc.or(self.generate_luarc),
888 luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
889 wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
890 package_types: other.package_types.or(self.package_types),
891 no_tfa: other.no_tfa.or(self.no_tfa),
892 }
893 }
894
895 #[tracing::instrument(level = "trace")]
896 pub fn build(self) -> Result<Config, ConfigError> {
897 let data_dir = self.data_dir.unwrap_or(Config::default_data_path());
898 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path());
899 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
900
901 let lua_version = self
902 .lua_version
903 .or(crate::lua_installation::detect_installed_lua_version());
904
905 Ok(normalise_base_urls(Config {
906 enable_development_packages: self.enable_development_packages.unwrap_or(false),
907 server: self.server.unwrap_or_else(|| unsafe {
908 Url::parse("https://luarocks.org/").unwrap_unchecked()
909 }),
910 extra_servers: self.extra_servers.unwrap_or_default(),
911 manifest_cache_url: if self.no_manifest_cache.unwrap_or(false) {
912 None
913 } else {
914 self.manifest_cache_url.or_else(|| unsafe {
915 Some(Url::parse("https://cache.luanox.org/").unwrap_unchecked())
916 })
917 },
918 extra_wally_registries: self.extra_wally_registries.unwrap_or_default(),
919 namespace: self.namespace,
920 lua_dir: self.lua_dir,
921 lua_version,
922 user_tree,
923 workspace_tree: self.workspace_tree,
924 verbose: self.verbose.unwrap_or(false),
925 no_progress: self.no_progress.unwrap_or(false),
926 no_prompt: self.no_prompt.unwrap_or(false),
927 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
928 max_jobs: match self.max_jobs.unwrap_or_else(|| {
929 std::thread::available_parallelism().map_or(1, |available| available.get())
930 }) {
931 0 => usize::MAX,
932 max_jobs => max_jobs,
933 },
934 variables: default_variables()
935 .chain(self.variables.unwrap_or_default())
936 .collect(),
937 access_tokens: self.access_tokens.unwrap_or_default(),
938 external_deps: self.external_deps,
939 build: self.build,
940 entrypoint_layout: self.entrypoint_layout,
941 cache_dir,
942 data_dir,
943 vendor_dir: self.vendor_dir,
944 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
945 generate_luarc: self.generate_luarc.unwrap_or(true),
946 luarc_file_name: self
947 .luarc_file_name
948 .unwrap_or_else(|| ".luarc.json".to_string()),
949 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
950 package_types: self.package_types.unwrap_or_default(),
951 no_tfa: self.no_tfa.unwrap_or(false),
952 }))
953 }
954}
955
956impl From<Config> for ConfigBuilder {
958 fn from(value: Config) -> Self {
959 let no_manifest_cache = Some(value.manifest_cache_url.is_none());
960 ConfigBuilder {
961 enable_development_packages: Some(value.enable_development_packages),
962 server: Some(value.server),
963 extra_servers: Some(value.extra_servers),
964 manifest_cache_url: value.manifest_cache_url,
965 no_manifest_cache,
966 extra_wally_registries: Some(value.extra_wally_registries),
967 namespace: value.namespace,
968 lua_dir: value.lua_dir,
969 lua_version: value.lua_version,
970 user_tree: Some(value.user_tree),
971 workspace_tree: value.workspace_tree,
972 verbose: Some(value.verbose),
973 no_progress: Some(value.no_progress),
974 no_prompt: Some(value.no_prompt),
975 timeout: Some(value.timeout),
976 max_jobs: if value.max_jobs == usize::MAX {
977 None
978 } else {
979 Some(value.max_jobs)
980 },
981 variables: Some(value.variables),
982 access_tokens: Some(value.access_tokens),
983 cache_dir: Some(value.cache_dir),
984 data_dir: Some(value.data_dir),
985 vendor_dir: value.vendor_dir,
986 external_deps: value.external_deps,
987 build: value.build,
988 entrypoint_layout: value.entrypoint_layout,
989 user_agent: Some(value.user_agent),
990 generate_luarc: Some(value.generate_luarc),
991 luarc_file_name: Some(value.luarc_file_name),
992 wrap_bin_scripts: Some(value.wrap_bin_scripts),
993 package_types: Some(value.package_types),
994 no_tfa: Some(value.no_tfa),
995 }
996 }
997}
998
999fn default_variables() -> impl Iterator<Item = (String, String)> {
1000 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
1001 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
1002 vec![
1003 ("MAKE".into(), "make".into()),
1004 ("CMAKE".into(), "cmake".into()),
1005 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
1006 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
1007 ("CFLAGS".into(), cflags),
1008 ("LDFLAGS".into(), ldflags),
1009 ("LIBFLAG".into(), utils::default_libflag().into()),
1010 ]
1011 .into_iter()
1012}
1013
1014fn normalise_base_url(mut url: Url) -> Url {
1015 if !url.path().ends_with('/') {
1016 url.set_path(&format!("{}/", url.path()));
1017 }
1018 url
1019}
1020
1021fn normalise_base_urls(mut config: Config) -> Config {
1022 config.server = normalise_base_url(config.server);
1023 config.extra_servers = config
1024 .extra_servers
1025 .into_iter()
1026 .map(normalise_base_url)
1027 .collect();
1028 config.manifest_cache_url = config.manifest_cache_url.map(normalise_base_url);
1029 config
1030}
1031
1032fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
1033where
1034 D: serde::Deserializer<'de>,
1035{
1036 let s = Option::<String>::deserialize(deserializer)?;
1037 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
1038 .transpose()
1039}
1040
1041fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
1042where
1043 S: Serializer,
1044{
1045 match url {
1046 Some(url) => serializer.serialize_some(url.as_str()),
1047 None => serializer.serialize_none(),
1048 }
1049}
1050
1051fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
1052where
1053 D: serde::Deserializer<'de>,
1054{
1055 let s = Option::<Vec<String>>::deserialize(deserializer)?;
1056 s.map(|v| {
1057 v.into_iter()
1058 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
1059 .try_collect()
1060 })
1061 .transpose()
1062}
1063
1064fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
1065where
1066 S: Serializer,
1067{
1068 match urls {
1069 Some(urls) => {
1070 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
1071 serializer.serialize_some(&url_strings)
1072 }
1073 None => serializer.serialize_none(),
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080
1081 #[tokio::test]
1082 async fn wrapped_command_without_runner() {
1083 let echo = which::which("echo").unwrap();
1084 let config = ConfigBuilder::default().build().unwrap();
1085 let output = config
1086 .wrapped_command(&echo, ["hello", "world"])
1087 .output()
1088 .await
1089 .unwrap();
1090 assert!(output.status.success());
1091 assert_eq!(
1092 String::from_utf8_lossy(&output.stdout).trim(),
1093 "hello world"
1094 );
1095 }
1096
1097 #[tokio::test]
1098 async fn wrapped_command_prepends_runner_argv() {
1099 let echo = which::which("echo").unwrap();
1100 let config = ConfigBuilder::default()
1101 .build_runner(Some(vec![echo.to_string_lossy().into(), "--prefix".into()]))
1102 .build()
1103 .unwrap();
1104 let output = config
1105 .wrapped_command("world", Vec::<String>::new())
1106 .output()
1107 .await
1108 .unwrap();
1109 assert!(output.status.success());
1110 assert_eq!(
1111 String::from_utf8_lossy(&output.stdout).trim(),
1112 "--prefix world"
1113 );
1114 }
1115
1116 #[test]
1117 fn base_urls_resolve_relative_paths() {
1118 let config = ConfigBuilder::default()
1119 .server(Some(Url::parse("https://example.com/texrocks").unwrap()))
1120 .extra_servers(Some(vec![
1121 Url::parse("https://example.com/texrocks").unwrap()
1122 ]))
1123 .build()
1124 .unwrap();
1125
1126 assert_eq!(
1127 config.server().join("manifest-5.1.zip").unwrap().as_str(),
1128 "https://example.com/texrocks/manifest-5.1.zip"
1129 );
1130 assert_eq!(
1131 config.extra_servers()[0]
1132 .join("manifest-5.1.zip")
1133 .unwrap()
1134 .as_str(),
1135 "https://example.com/texrocks/manifest-5.1.zip"
1136 );
1137
1138 let rebuilt = ConfigBuilder::from(config.clone()).build().unwrap();
1139 assert_eq!(rebuilt.server(), config.server());
1140 assert_eq!(rebuilt.extra_servers(), config.extra_servers());
1141 }
1142
1143 #[test]
1144 fn debug_redacts_access_tokens() {
1145 let config: Config =
1146 toml::from_str::<ConfigBuilder>("[access_tokens]\n\"github.com\" = \"supersecret\"\n")
1147 .unwrap()
1148 .build()
1149 .unwrap();
1150 let debug = format!("{config:?}");
1151 assert!(!debug.contains("supersecret"));
1152 assert!(debug.contains("access_tokens"));
1153 }
1154}