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 namespace: Option<String>,
49 lua_dir: Option<PathBuf>,
50 lua_version: Option<LuaVersion>,
51 user_tree: PathBuf,
52 workspace_tree: Option<PathBuf>,
53 verbose: bool,
54 no_progress: bool,
56 no_prompt: bool,
58 timeout: Duration,
59 max_jobs: usize,
60 variables: HashMap<String, String>,
61 access_tokens: HashMap<String, AccessToken>,
62 external_deps: ExternalDependencySearchConfig,
63
64 build: BuildConfig,
65 entrypoint_layout: Option<Arc<dyn CustomRockLayout>>,
66
67 cache_dir: PathBuf,
68 data_dir: PathBuf,
69 vendor_dir: Option<PathBuf>,
70
71 user_agent: String,
72
73 generate_luarc: bool,
74 luarc_file_name: String,
75 wrap_bin_scripts: bool,
76 package_types: RemotePackageTypeFilterSpec,
77 no_tfa: bool,
78}
79
80impl Config {
81 pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
83 directories::ProjectDirs::from("org", "lumenlabs", "lux")
84 .filter(|_| Self::home_dir_exists())
85 .ok_or(NoValidHomeDirectory)
86 }
87
88 fn home_dir_exists() -> bool {
89 directories::BaseDirs::new().is_some_and(|base| base.home_dir().is_dir())
90 }
91
92 fn default_cache_path() -> PathBuf {
94 match Config::project_dirs() {
95 Ok(project_dirs) => project_dirs.cache_dir().to_path_buf(),
96 Err(_) => Self::fallback_data_dir("cache"),
97 }
98 }
99
100 fn default_data_path() -> PathBuf {
102 match Config::project_dirs() {
103 Ok(project_dirs) => project_dirs.data_local_dir().to_path_buf(),
104 Err(_) => Self::fallback_data_dir("data"),
105 }
106 }
107
108 fn fallback_data_dir(subdir: &str) -> PathBuf {
110 let path = std::env::temp_dir().join("lux").join(subdir);
111 tracing::warn!(
112 "no valid home directory found; falling back to `{}`",
113 path.display()
114 );
115 path
116 }
117
118 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
120 Self {
121 lua_version: Some(lua_version),
122 ..self
123 }
124 }
125
126 pub fn with_tree(self, tree: PathBuf) -> Self {
128 Self {
129 user_tree: tree,
130 ..self
131 }
132 }
133
134 pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
136 Self {
137 workspace_tree: tree,
138 ..self
139 }
140 }
141
142 pub fn server(&self) -> &Url {
144 &self.server
145 }
146
147 pub fn extra_servers(&self) -> &Vec<Url> {
149 self.extra_servers.as_ref()
150 }
151
152 pub fn manifest_cache_url(&self) -> Option<&Url> {
154 self.manifest_cache_url.as_ref()
155 }
156
157 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
159 let mut enabled_dev_servers = Vec::new();
160 if self.enable_development_packages {
161 let config_file = ConfigBuilder::config_file()
162 .map(|p| p.to_string_lossy().to_string())
163 .unwrap_or_default();
164 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
165 ConfigError::UrlParseError {
166 source,
167 help: Some(format!("check the `server` URL in {config_file}")),
168 }
169 })?);
170 for server in self.extra_servers() {
171 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
172 ConfigError::UrlParseError {
173 source,
174 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
175 }
176 })?);
177 }
178 }
179 Ok(enabled_dev_servers)
180 }
181
182 pub fn namespace(&self) -> Option<&String> {
184 self.namespace.as_ref()
185 }
186
187 pub fn lua_dir(&self) -> Option<&PathBuf> {
189 self.lua_dir.as_ref()
190 }
191
192 pub fn lua_version(&self) -> Option<&LuaVersion> {
194 self.lua_version.as_ref()
195 }
196
197 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
200 Tree::new(self.user_tree.clone(), version, self)
201 }
202
203 pub fn workspace_tree(&self) -> Option<&PathBuf> {
205 self.workspace_tree.as_ref()
206 }
207
208 pub fn verbose(&self) -> bool {
210 self.verbose
211 }
212
213 pub fn no_progress(&self) -> bool {
215 self.no_progress
216 }
217
218 pub fn no_prompt(&self) -> bool {
220 self.no_prompt
221 }
222
223 pub fn timeout(&self) -> &Duration {
226 &self.timeout
227 }
228
229 pub fn max_jobs(&self) -> usize {
232 self.max_jobs
233 }
234
235 pub fn make_cmd(&self) -> String {
237 match self.variables.get("MAKE") {
238 Some(make) => make.clone(),
239 None => "make".into(),
240 }
241 }
242
243 pub fn cmake_cmd(&self) -> String {
245 match self.variables.get("CMAKE") {
246 Some(cmake) => cmake.clone(),
247 None => "cmake".into(),
248 }
249 }
250
251 pub(crate) fn wrapped_command<P, A>(&self, program: P, args: A) -> Command
257 where
258 P: AsRef<OsStr>,
259 A: IntoIterator,
260 A::Item: AsRef<OsStr>,
261 {
262 if self.build.runner.is_empty() {
263 let mut cmd = Command::new(program);
264 cmd.args(args);
265 cmd
266 } else {
267 let runner = &self.build.runner;
268 let mut cmd = Command::new(&runner[0]);
269 cmd.args(&runner[1..]).arg(program).args(args);
270 cmd
271 }
272 }
273
274 pub(crate) fn build_profile(&self) -> build::Profile {
276 self.build
277 .profile
278 .as_ref()
279 .cloned()
280 .unwrap_or(build::Profile::Release)
281 }
282
283 pub fn variables(&self) -> &HashMap<String, String> {
287 &self.variables
288 }
289
290 pub fn access_token(&self, host: &str) -> Option<AccessToken> {
295 access_tokens::env_access_tokens()
296 .get(host)
297 .cloned()
298 .or_else(|| self.access_tokens.get(host).cloned())
299 .or_else(|| {
300 (host == "github.com")
301 .then(|| env::var("GITHUB_TOKEN").ok())
302 .flatten()
303 .map(|raw| AccessToken::from(raw.as_str()))
304 })
305 }
306
307 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
308 &self.external_deps
309 }
310
311 pub fn entrypoint_layout(&self) -> Option<&Arc<dyn CustomRockLayout>> {
313 self.entrypoint_layout.as_ref()
314 }
315
316 pub fn cache_dir(&self) -> &PathBuf {
318 &self.cache_dir
319 }
320
321 pub fn data_dir(&self) -> &PathBuf {
323 &self.data_dir
324 }
325
326 pub fn vendor_dir(&self) -> Option<&PathBuf> {
330 self.vendor_dir.as_ref()
331 }
332
333 pub fn user_agent(&self) -> &str {
335 &self.user_agent
336 }
337
338 pub fn generate_luarc(&self) -> bool {
340 self.generate_luarc
341 }
342
343 pub fn luarc_file_name(&self) -> &str {
345 &self.luarc_file_name
346 }
347
348 pub fn wrap_bin_scripts(&self) -> bool {
352 self.wrap_bin_scripts
353 }
354
355 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
357 &self.package_types
358 }
359 pub fn no_tfa(&self) -> bool {
361 self.no_tfa
362 }
363}
364
365impl HasVariables for Config {
366 #[tracing::instrument(level = "trace")]
367 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
368 Ok(self.variables.get(input).cloned())
369 }
370}
371
372#[derive(Error, Debug, Diagnostic)]
373pub enum ConfigError {
374 #[error(transparent)]
375 #[diagnostic(transparent)]
376 Fs(#[from] fs::FsError),
377 #[error(transparent)]
378 #[diagnostic(transparent)]
379 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
380 #[error("error parsing {config_file}")]
381 Deserialize {
382 config_file: String,
383 #[diagnostic_source]
384 source: TomlDeError,
385 },
386 #[error("error parsing URL: {source}")]
387 UrlParseError {
388 source: url::ParseError,
389 #[help]
390 help: Option<String>,
391 },
392}
393
394#[derive(Debug, Clone, Default, Deserialize, Serialize)]
401pub struct ConfigBuilder {
402 #[serde(
403 default,
404 deserialize_with = "deserialize_url",
405 serialize_with = "serialize_url"
406 )]
407 server: Option<Url>,
408 #[serde(
409 default,
410 deserialize_with = "deserialize_url_vec",
411 serialize_with = "serialize_url_vec"
412 )]
413 extra_servers: Option<Vec<Url>>,
414 #[serde(
415 default,
416 deserialize_with = "deserialize_url",
417 serialize_with = "serialize_url"
418 )]
419 manifest_cache_url: Option<Url>,
420 no_manifest_cache: Option<bool>,
421 namespace: Option<String>,
422 lua_version: Option<LuaVersion>,
423 user_tree: Option<PathBuf>,
424 workspace_tree: Option<PathBuf>,
425 lua_dir: Option<PathBuf>,
426 cache_dir: Option<PathBuf>,
427 data_dir: Option<PathBuf>,
428 vendor_dir: Option<PathBuf>,
429 enable_development_packages: Option<bool>,
430 verbose: Option<bool>,
431 no_progress: Option<bool>,
432 no_prompt: Option<bool>,
433 timeout: Option<Duration>,
434 max_jobs: Option<usize>,
435 variables: Option<HashMap<String, String>>,
436 #[serde(default, skip_serializing)]
439 access_tokens: Option<HashMap<String, AccessToken>>,
440 #[serde(default)]
441 external_deps: ExternalDependencySearchConfig,
442 #[serde(default)]
443 build: BuildConfig,
444
445 #[serde(skip)]
446 entrypoint_layout: Option<Arc<dyn CustomRockLayout>>,
447 user_agent: Option<String>,
448 generate_luarc: Option<bool>,
449 luarc_file_name: Option<String>,
450 wrap_bin_scripts: Option<bool>,
451 package_types: Option<RemotePackageTypeFilterSpec>,
452 no_tfa: Option<bool>,
453}
454
455impl ConfigBuilder {
457 pub fn new() -> Result<Self, ConfigError> {
460 let config_file = Self::config_file()?;
461 if config_file.is_file() {
462 Self::from_file(&config_file)
463 } else {
464 Ok(Self::default())
465 }
466 }
467
468 pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
469 let config_file_name = config_file.to_string_lossy().to_string();
470 let content = fs::sync::read_to_string(config_file)?;
471 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
472 ConfigError::Deserialize {
473 config_file: config_file_name,
474 source,
475 }
476 })
477 }
478
479 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
481 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
482 .ok_or(NoValidHomeDirectory)?;
483 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
484 }
485
486 pub fn dev(self, dev: Option<bool>) -> Self {
489 Self {
490 enable_development_packages: dev.or(self.enable_development_packages),
491 ..self
492 }
493 }
494
495 pub fn server(self, server: Option<Url>) -> Self {
498 Self {
499 server: server.or(self.server),
500 ..self
501 }
502 }
503
504 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
506 Self {
507 extra_servers: extra_servers.or(self.extra_servers),
508 ..self
509 }
510 }
511
512 pub fn manifest_cache_url(self, manifest_cache_url: Option<Url>) -> Self {
515 Self {
516 manifest_cache_url: manifest_cache_url.or(self.manifest_cache_url),
517 ..self
518 }
519 }
520
521 pub fn no_manifest_cache(self, no_manifest_cache: Option<bool>) -> Self {
524 Self {
525 no_manifest_cache: no_manifest_cache.or(self.no_manifest_cache),
526 ..self
527 }
528 }
529
530 pub fn namespace(self, namespace: Option<String>) -> Self {
532 Self {
533 namespace: namespace.or(self.namespace),
534 ..self
535 }
536 }
537
538 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
540 Self {
541 lua_dir: lua_dir.or(self.lua_dir),
542 ..self
543 }
544 }
545
546 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
549 Self {
550 lua_version: lua_version.or(self.lua_version),
551 ..self
552 }
553 }
554
555 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
557 Self {
558 user_tree: tree.or(self.user_tree),
559 ..self
560 }
561 }
562
563 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
566 Self {
567 workspace_tree: tree.or(self.workspace_tree),
568 ..self
569 }
570 }
571
572 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
576 Self {
577 variables: variables.or(self.variables),
578 ..self
579 }
580 }
581
582 pub fn verbose(self, verbose: Option<bool>) -> Self {
585 Self {
586 verbose: verbose.or(self.verbose),
587 ..self
588 }
589 }
590
591 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
594 Self {
595 no_progress: no_progress.or(self.no_progress),
596 ..self
597 }
598 }
599
600 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
603 Self {
604 no_prompt: no_prompt.or(self.no_prompt),
605 ..self
606 }
607 }
608
609 pub fn timeout(self, timeout: Option<Duration>) -> Self {
613 Self {
614 timeout: timeout.or(self.timeout),
615 ..self
616 }
617 }
618
619 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
623 Self {
624 max_jobs: max_jobs.or(self.max_jobs),
625 ..self
626 }
627 }
628
629 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
631 Self {
632 cache_dir: cache_dir.or(self.cache_dir),
633 ..self
634 }
635 }
636
637 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
639 Self {
640 data_dir: data_dir.or(self.data_dir),
641 ..self
642 }
643 }
644
645 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
649 Self {
650 vendor_dir: vendor_dir.or(self.vendor_dir),
651 ..self
652 }
653 }
654
655 pub fn entrypoint_layout(self, layout: impl CustomRockLayout + 'static) -> Self {
657 Self {
658 entrypoint_layout: Some(Arc::new(layout)),
659 ..self
660 }
661 }
662
663 pub fn user_agent(self, user_agent: Option<String>) -> Self {
666 Self {
667 user_agent: user_agent.or(self.user_agent),
668 ..self
669 }
670 }
671
672 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
675 Self {
676 generate_luarc: generate.or(self.generate_luarc),
677 ..self
678 }
679 }
680
681 pub fn luarc_file_name(self, file: Option<String>) -> Self {
684 Self {
685 luarc_file_name: file.or(self.luarc_file_name),
686 ..self
687 }
688 }
689
690 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
696 Self {
697 wrap_bin_scripts: generate.or(self.generate_luarc),
698 ..self
699 }
700 }
701 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
704 Self {
705 no_tfa: tfa.or(self.no_tfa),
706 ..self
707 }
708 }
709
710 pub fn build_runner(self, runner: Option<Vec<String>>) -> Self {
760 Self {
761 build: BuildConfig {
762 runner: runner.unwrap_or(self.build.runner),
763 ..self.build
764 },
765 ..self
766 }
767 }
768
769 pub fn build_profile(self, profile: Option<build::Profile>) -> Self {
772 Self {
773 build: BuildConfig {
774 profile: profile.or(self.build.profile),
775 ..self.build
776 },
777 ..self
778 }
779 }
780
781 pub fn default_build_profile(self, profile: build::Profile) -> Self {
783 Self {
784 build: BuildConfig {
785 profile: self.build.profile.or(Some(profile)),
786 ..self.build
787 },
788 ..self
789 }
790 }
791
792 pub fn merge(self, other: Self) -> Self {
794 Self {
795 server: other.server.or(self.server),
796 extra_servers: other.extra_servers.or(self.extra_servers),
797 manifest_cache_url: other.manifest_cache_url.or(self.manifest_cache_url),
798 no_manifest_cache: other.no_manifest_cache.or(self.no_manifest_cache),
799 namespace: other.namespace.or(self.namespace),
800 lua_version: other.lua_version.or(self.lua_version),
801 user_tree: other.user_tree.or(self.user_tree),
802 workspace_tree: other.workspace_tree.or(self.workspace_tree),
803 lua_dir: other.lua_dir.or(self.lua_dir),
804 cache_dir: other.cache_dir.or(self.cache_dir),
805 data_dir: other.data_dir.or(self.data_dir),
806 vendor_dir: other.vendor_dir.or(self.vendor_dir),
807 enable_development_packages: other
808 .enable_development_packages
809 .or(self.enable_development_packages),
810 verbose: other.verbose.or(self.verbose),
811 no_progress: other.no_progress.or(self.no_progress),
812 no_prompt: other.no_prompt.or(self.no_prompt),
813 timeout: other.timeout.or(self.timeout),
814 max_jobs: other.max_jobs.or(self.max_jobs),
815 variables: other.variables.or(self.variables),
816 access_tokens: other.access_tokens.or(self.access_tokens),
817 external_deps: other.external_deps,
818 build: BuildConfig {
819 profile: other.build.profile.or(self.build.profile),
820 ..other.build
821 },
822 entrypoint_layout: other.entrypoint_layout.or(self.entrypoint_layout),
823 user_agent: other.user_agent.or(self.user_agent),
824 generate_luarc: other.generate_luarc.or(self.generate_luarc),
825 luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
826 wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
827 package_types: other.package_types.or(self.package_types),
828 no_tfa: other.no_tfa.or(self.no_tfa),
829 }
830 }
831
832 #[tracing::instrument(level = "trace")]
833 pub fn build(self) -> Result<Config, ConfigError> {
834 let data_dir = self.data_dir.unwrap_or(Config::default_data_path());
835 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path());
836 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
837
838 let lua_version = self
839 .lua_version
840 .or(crate::lua_installation::detect_installed_lua_version());
841
842 Ok(Config {
843 enable_development_packages: self.enable_development_packages.unwrap_or(false),
844 server: self.server.unwrap_or_else(|| unsafe {
845 Url::parse("https://luarocks.org/").unwrap_unchecked()
846 }),
847 extra_servers: self.extra_servers.unwrap_or_default(),
848 manifest_cache_url: if self.no_manifest_cache.unwrap_or(false) {
849 None
850 } else {
851 self.manifest_cache_url.or_else(|| unsafe {
852 Some(Url::parse("https://cache.luanox.org/").unwrap_unchecked())
853 })
854 },
855 namespace: self.namespace,
856 lua_dir: self.lua_dir,
857 lua_version,
858 user_tree,
859 workspace_tree: self.workspace_tree,
860 verbose: self.verbose.unwrap_or(false),
861 no_progress: self.no_progress.unwrap_or(false),
862 no_prompt: self.no_prompt.unwrap_or(false),
863 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
864 max_jobs: match self.max_jobs.unwrap_or_else(|| {
865 std::thread::available_parallelism().map_or(1, |available| available.get())
866 }) {
867 0 => usize::MAX,
868 max_jobs => max_jobs,
869 },
870 variables: default_variables()
871 .chain(self.variables.unwrap_or_default())
872 .collect(),
873 access_tokens: self.access_tokens.unwrap_or_default(),
874 external_deps: self.external_deps,
875 build: self.build,
876 entrypoint_layout: self.entrypoint_layout,
877 cache_dir,
878 data_dir,
879 vendor_dir: self.vendor_dir,
880 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
881 generate_luarc: self.generate_luarc.unwrap_or(true),
882 luarc_file_name: self
883 .luarc_file_name
884 .unwrap_or_else(|| ".luarc.json".to_string()),
885 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
886 package_types: self.package_types.unwrap_or_default(),
887 no_tfa: self.no_tfa.unwrap_or(false),
888 })
889 }
890}
891
892impl From<Config> for ConfigBuilder {
894 fn from(value: Config) -> Self {
895 let no_manifest_cache = Some(value.manifest_cache_url.is_none());
896 ConfigBuilder {
897 enable_development_packages: Some(value.enable_development_packages),
898 server: Some(value.server),
899 extra_servers: Some(value.extra_servers),
900 manifest_cache_url: value.manifest_cache_url,
901 no_manifest_cache,
902 namespace: value.namespace,
903 lua_dir: value.lua_dir,
904 lua_version: value.lua_version,
905 user_tree: Some(value.user_tree),
906 workspace_tree: value.workspace_tree,
907 verbose: Some(value.verbose),
908 no_progress: Some(value.no_progress),
909 no_prompt: Some(value.no_prompt),
910 timeout: Some(value.timeout),
911 max_jobs: if value.max_jobs == usize::MAX {
912 None
913 } else {
914 Some(value.max_jobs)
915 },
916 variables: Some(value.variables),
917 access_tokens: Some(value.access_tokens),
918 cache_dir: Some(value.cache_dir),
919 data_dir: Some(value.data_dir),
920 vendor_dir: value.vendor_dir,
921 external_deps: value.external_deps,
922 build: value.build,
923 entrypoint_layout: value.entrypoint_layout,
924 user_agent: Some(value.user_agent),
925 generate_luarc: Some(value.generate_luarc),
926 luarc_file_name: Some(value.luarc_file_name),
927 wrap_bin_scripts: Some(value.wrap_bin_scripts),
928 package_types: Some(value.package_types),
929 no_tfa: Some(value.no_tfa),
930 }
931 }
932}
933
934fn default_variables() -> impl Iterator<Item = (String, String)> {
935 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
936 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
937 vec![
938 ("MAKE".into(), "make".into()),
939 ("CMAKE".into(), "cmake".into()),
940 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
941 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
942 ("CFLAGS".into(), cflags),
943 ("LDFLAGS".into(), ldflags),
944 ("LIBFLAG".into(), utils::default_libflag().into()),
945 ]
946 .into_iter()
947}
948
949fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
950where
951 D: serde::Deserializer<'de>,
952{
953 let s = Option::<String>::deserialize(deserializer)?;
954 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
955 .transpose()
956}
957
958fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
959where
960 S: Serializer,
961{
962 match url {
963 Some(url) => serializer.serialize_some(url.as_str()),
964 None => serializer.serialize_none(),
965 }
966}
967
968fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
969where
970 D: serde::Deserializer<'de>,
971{
972 let s = Option::<Vec<String>>::deserialize(deserializer)?;
973 s.map(|v| {
974 v.into_iter()
975 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
976 .try_collect()
977 })
978 .transpose()
979}
980
981fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
982where
983 S: Serializer,
984{
985 match urls {
986 Some(urls) => {
987 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
988 serializer.serialize_some(&url_strings)
989 }
990 None => serializer.serialize_none(),
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997
998 #[tokio::test]
999 async fn wrapped_command_without_runner() {
1000 let echo = which::which("echo").unwrap();
1001 let config = ConfigBuilder::default().build().unwrap();
1002 let output = config
1003 .wrapped_command(&echo, ["hello", "world"])
1004 .output()
1005 .await
1006 .unwrap();
1007 assert!(output.status.success());
1008 assert_eq!(
1009 String::from_utf8_lossy(&output.stdout).trim(),
1010 "hello world"
1011 );
1012 }
1013
1014 #[tokio::test]
1015 async fn wrapped_command_prepends_runner_argv() {
1016 let echo = which::which("echo").unwrap();
1017 let config = ConfigBuilder::default()
1018 .build_runner(Some(vec![echo.to_string_lossy().into(), "--prefix".into()]))
1019 .build()
1020 .unwrap();
1021 let output = config
1022 .wrapped_command("world", Vec::<String>::new())
1023 .output()
1024 .await
1025 .unwrap();
1026 assert!(output.status.success());
1027 assert_eq!(
1028 String::from_utf8_lossy(&output.stdout).trim(),
1029 "--prefix world"
1030 );
1031 }
1032
1033 #[test]
1034 fn debug_redacts_access_tokens() {
1035 let config: Config =
1036 toml::from_str::<ConfigBuilder>("[access_tokens]\n\"github.com\" = \"supersecret\"\n")
1037 .unwrap()
1038 .build()
1039 .unwrap();
1040 let debug = format!("{config:?}");
1041 assert!(!debug.contains("supersecret"));
1042 assert!(debug.contains("access_tokens"));
1043 }
1044}