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, time::Duration};
11use thiserror::Error;
12use tokio::process::Command;
13use tree::RockLayoutConfig;
14use url::Url;
15
16use crate::config::access_tokens::AccessToken;
17use crate::fs;
18use crate::lua_version::LuaVersion;
19use crate::package::RemotePackageTypeFilterSpec;
20use crate::project::TomlDeError;
21use crate::tree::{Tree, TreeError};
22use crate::variables::GetVariableError;
23use crate::{build::utils, variables::HasVariables};
24
25pub mod access_tokens;
26pub mod build;
27pub mod external_deps;
28pub mod tree;
29
30const DEV_PATH: &str = "dev/";
31const DEFAULT_USER_AGENT: &str = concat!("lux-lib/", env!("CARGO_PKG_VERSION"));
32
33#[derive(Error, Debug, Diagnostic)]
34#[error("could not find a valid home directory")]
35#[diagnostic(
36 code(lux_lib::no_home_directory),
37 help("this usually means you're running Lux in a managed environment like LDAP or a live session.")
38)]
39pub struct NoValidHomeDirectory;
40
41#[derive(Debug, Clone)]
45pub struct Config {
46 enable_development_packages: bool,
47 server: Url,
48 extra_servers: 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: RockLayoutConfig,
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").ok_or(NoValidHomeDirectory)
85 }
86
87 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
89 let project_dirs = Config::project_dirs()?;
90 Ok(project_dirs.cache_dir().to_path_buf())
91 }
92
93 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
95 let project_dirs = Config::project_dirs()?;
96 Ok(project_dirs.data_local_dir().to_path_buf())
97 }
98
99 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
101 Self {
102 lua_version: Some(lua_version),
103 ..self
104 }
105 }
106
107 pub fn with_tree(self, tree: PathBuf) -> Self {
109 Self {
110 user_tree: tree,
111 ..self
112 }
113 }
114
115 pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
117 Self {
118 workspace_tree: tree,
119 ..self
120 }
121 }
122
123 pub fn server(&self) -> &Url {
125 &self.server
126 }
127
128 pub fn extra_servers(&self) -> &Vec<Url> {
130 self.extra_servers.as_ref()
131 }
132
133 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
135 let mut enabled_dev_servers = Vec::new();
136 if self.enable_development_packages {
137 let config_file = ConfigBuilder::config_file()
138 .map(|p| p.to_string_lossy().to_string())
139 .unwrap_or_default();
140 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
141 ConfigError::UrlParseError {
142 source,
143 help: Some(format!("check the `server` URL in {config_file}")),
144 }
145 })?);
146 for server in self.extra_servers() {
147 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
148 ConfigError::UrlParseError {
149 source,
150 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
151 }
152 })?);
153 }
154 }
155 Ok(enabled_dev_servers)
156 }
157
158 pub fn namespace(&self) -> Option<&String> {
160 self.namespace.as_ref()
161 }
162
163 pub fn lua_dir(&self) -> Option<&PathBuf> {
165 self.lua_dir.as_ref()
166 }
167
168 pub fn lua_version(&self) -> Option<&LuaVersion> {
170 self.lua_version.as_ref()
171 }
172
173 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
176 Tree::new(self.user_tree.clone(), version, self)
177 }
178
179 pub fn workspace_tree(&self) -> Option<&PathBuf> {
181 self.workspace_tree.as_ref()
182 }
183
184 pub fn verbose(&self) -> bool {
186 self.verbose
187 }
188
189 pub fn no_progress(&self) -> bool {
191 self.no_progress
192 }
193
194 pub fn no_prompt(&self) -> bool {
196 self.no_prompt
197 }
198
199 pub fn timeout(&self) -> &Duration {
202 &self.timeout
203 }
204
205 pub fn max_jobs(&self) -> usize {
208 self.max_jobs
209 }
210
211 pub fn make_cmd(&self) -> String {
213 match self.variables.get("MAKE") {
214 Some(make) => make.clone(),
215 None => "make".into(),
216 }
217 }
218
219 pub fn cmake_cmd(&self) -> String {
221 match self.variables.get("CMAKE") {
222 Some(cmake) => cmake.clone(),
223 None => "cmake".into(),
224 }
225 }
226
227 pub(crate) fn wrapped_command<P, A>(&self, program: P, args: A) -> Command
233 where
234 P: AsRef<OsStr>,
235 A: IntoIterator,
236 A::Item: AsRef<OsStr>,
237 {
238 if self.build.runner.is_empty() {
239 let mut cmd = Command::new(program);
240 cmd.args(args);
241 cmd
242 } else {
243 let runner = &self.build.runner;
244 let mut cmd = Command::new(&runner[0]);
245 cmd.args(&runner[1..]).arg(program).args(args);
246 cmd
247 }
248 }
249
250 pub(crate) fn build_profile(&self) -> build::Profile {
252 self.build
253 .profile
254 .as_ref()
255 .cloned()
256 .unwrap_or(build::Profile::Release)
257 }
258
259 pub fn variables(&self) -> &HashMap<String, String> {
263 &self.variables
264 }
265
266 pub fn access_token(&self, host: &str) -> Option<AccessToken> {
271 access_tokens::env_access_tokens()
272 .get(host)
273 .cloned()
274 .or_else(|| self.access_tokens.get(host).cloned())
275 .or_else(|| {
276 (host == "github.com")
277 .then(|| env::var("GITHUB_TOKEN").ok())
278 .flatten()
279 .map(|raw| AccessToken::from(raw.as_str()))
280 })
281 }
282
283 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
284 &self.external_deps
285 }
286
287 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
290 &self.entrypoint_layout
291 }
292
293 pub fn cache_dir(&self) -> &PathBuf {
295 &self.cache_dir
296 }
297
298 pub fn data_dir(&self) -> &PathBuf {
300 &self.data_dir
301 }
302
303 pub fn vendor_dir(&self) -> Option<&PathBuf> {
307 self.vendor_dir.as_ref()
308 }
309
310 pub fn user_agent(&self) -> &str {
312 &self.user_agent
313 }
314
315 pub fn generate_luarc(&self) -> bool {
317 self.generate_luarc
318 }
319
320 pub fn luarc_file_name(&self) -> &str {
322 &self.luarc_file_name
323 }
324
325 pub fn wrap_bin_scripts(&self) -> bool {
329 self.wrap_bin_scripts
330 }
331
332 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
334 &self.package_types
335 }
336 pub fn no_tfa(&self) -> bool {
338 self.no_tfa
339 }
340}
341
342impl HasVariables for Config {
343 #[tracing::instrument(level = "trace")]
344 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
345 Ok(self.variables.get(input).cloned())
346 }
347}
348
349#[derive(Error, Debug, Diagnostic)]
350pub enum ConfigError {
351 #[error(transparent)]
352 #[diagnostic(transparent)]
353 Fs(#[from] fs::FsError),
354 #[error(transparent)]
355 #[diagnostic(transparent)]
356 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
357 #[error("error parsing {config_file}")]
358 Deserialize {
359 config_file: String,
360 #[diagnostic_source]
361 source: TomlDeError,
362 },
363 #[error("error parsing URL: {source}")]
364 UrlParseError {
365 source: url::ParseError,
366 #[help]
367 help: Option<String>,
368 },
369}
370
371#[derive(Debug, Clone, Default, Deserialize, Serialize)]
378pub struct ConfigBuilder {
379 #[serde(
380 default,
381 deserialize_with = "deserialize_url",
382 serialize_with = "serialize_url"
383 )]
384 server: Option<Url>,
385 #[serde(
386 default,
387 deserialize_with = "deserialize_url_vec",
388 serialize_with = "serialize_url_vec"
389 )]
390 extra_servers: Option<Vec<Url>>,
391 namespace: Option<String>,
392 lua_version: Option<LuaVersion>,
393 user_tree: Option<PathBuf>,
394 workspace_tree: Option<PathBuf>,
395 lua_dir: Option<PathBuf>,
396 cache_dir: Option<PathBuf>,
397 data_dir: Option<PathBuf>,
398 vendor_dir: Option<PathBuf>,
399 enable_development_packages: Option<bool>,
400 verbose: Option<bool>,
401 no_progress: Option<bool>,
402 no_prompt: Option<bool>,
403 timeout: Option<Duration>,
404 max_jobs: Option<usize>,
405 variables: Option<HashMap<String, String>>,
406 #[serde(default, skip_serializing)]
409 access_tokens: Option<HashMap<String, AccessToken>>,
410 #[serde(default)]
411 external_deps: ExternalDependencySearchConfig,
412 #[serde(default)]
413 build: BuildConfig,
414
415 #[serde(default)]
416 entrypoint_layout: RockLayoutConfig,
417 user_agent: Option<String>,
418 generate_luarc: Option<bool>,
419 luarc_file_name: Option<String>,
420 wrap_bin_scripts: Option<bool>,
421 package_types: Option<RemotePackageTypeFilterSpec>,
422 no_tfa: Option<bool>,
423}
424
425impl ConfigBuilder {
427 pub fn new() -> Result<Self, ConfigError> {
430 let config_file = Self::config_file()?;
431 if config_file.is_file() {
432 Self::from_file(&config_file)
433 } else {
434 Ok(Self::default())
435 }
436 }
437
438 pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
439 let config_file_name = config_file.to_string_lossy().to_string();
440 let content = fs::sync::read_to_string(config_file)?;
441 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
442 ConfigError::Deserialize {
443 config_file: config_file_name,
444 source,
445 }
446 })
447 }
448
449 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
451 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
452 .ok_or(NoValidHomeDirectory)?;
453 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
454 }
455
456 pub fn dev(self, dev: Option<bool>) -> Self {
459 Self {
460 enable_development_packages: dev.or(self.enable_development_packages),
461 ..self
462 }
463 }
464
465 pub fn server(self, server: Option<Url>) -> Self {
468 Self {
469 server: server.or(self.server),
470 ..self
471 }
472 }
473
474 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
476 Self {
477 extra_servers: extra_servers.or(self.extra_servers),
478 ..self
479 }
480 }
481
482 pub fn namespace(self, namespace: Option<String>) -> Self {
484 Self {
485 namespace: namespace.or(self.namespace),
486 ..self
487 }
488 }
489
490 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
492 Self {
493 lua_dir: lua_dir.or(self.lua_dir),
494 ..self
495 }
496 }
497
498 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
501 Self {
502 lua_version: lua_version.or(self.lua_version),
503 ..self
504 }
505 }
506
507 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
509 Self {
510 user_tree: tree.or(self.user_tree),
511 ..self
512 }
513 }
514
515 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
518 Self {
519 workspace_tree: tree.or(self.workspace_tree),
520 ..self
521 }
522 }
523
524 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
528 Self {
529 variables: variables.or(self.variables),
530 ..self
531 }
532 }
533
534 pub fn verbose(self, verbose: Option<bool>) -> Self {
537 Self {
538 verbose: verbose.or(self.verbose),
539 ..self
540 }
541 }
542
543 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
546 Self {
547 no_progress: no_progress.or(self.no_progress),
548 ..self
549 }
550 }
551
552 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
555 Self {
556 no_prompt: no_prompt.or(self.no_prompt),
557 ..self
558 }
559 }
560
561 pub fn timeout(self, timeout: Option<Duration>) -> Self {
565 Self {
566 timeout: timeout.or(self.timeout),
567 ..self
568 }
569 }
570
571 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
575 Self {
576 max_jobs: max_jobs.or(self.max_jobs),
577 ..self
578 }
579 }
580
581 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
583 Self {
584 cache_dir: cache_dir.or(self.cache_dir),
585 ..self
586 }
587 }
588
589 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
591 Self {
592 data_dir: data_dir.or(self.data_dir),
593 ..self
594 }
595 }
596
597 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
601 Self {
602 vendor_dir: vendor_dir.or(self.vendor_dir),
603 ..self
604 }
605 }
606
607 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
610 Self {
611 entrypoint_layout: rock_layout,
612 ..self
613 }
614 }
615
616 pub fn user_agent(self, user_agent: Option<String>) -> Self {
619 Self {
620 user_agent: user_agent.or(self.user_agent),
621 ..self
622 }
623 }
624
625 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
628 Self {
629 generate_luarc: generate.or(self.generate_luarc),
630 ..self
631 }
632 }
633
634 pub fn luarc_file_name(self, file: Option<String>) -> Self {
637 Self {
638 luarc_file_name: file.or(self.luarc_file_name),
639 ..self
640 }
641 }
642
643 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
649 Self {
650 wrap_bin_scripts: generate.or(self.generate_luarc),
651 ..self
652 }
653 }
654 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
657 Self {
658 no_tfa: tfa.or(self.no_tfa),
659 ..self
660 }
661 }
662
663 pub fn build_runner(self, runner: Option<Vec<String>>) -> Self {
713 Self {
714 build: BuildConfig {
715 runner: runner.unwrap_or(self.build.runner),
716 ..self.build
717 },
718 ..self
719 }
720 }
721
722 pub fn build_profile(self, profile: Option<build::Profile>) -> Self {
725 Self {
726 build: BuildConfig {
727 profile: profile.or(self.build.profile),
728 ..self.build
729 },
730 ..self
731 }
732 }
733
734 pub fn default_build_profile(self, profile: build::Profile) -> Self {
736 Self {
737 build: BuildConfig {
738 profile: self.build.profile.or(Some(profile)),
739 ..self.build
740 },
741 ..self
742 }
743 }
744
745 pub fn merge(self, other: Self) -> Self {
747 Self {
748 server: other.server.or(self.server),
749 extra_servers: other.extra_servers.or(self.extra_servers),
750 namespace: other.namespace.or(self.namespace),
751 lua_version: other.lua_version.or(self.lua_version),
752 user_tree: other.user_tree.or(self.user_tree),
753 workspace_tree: other.workspace_tree.or(self.workspace_tree),
754 lua_dir: other.lua_dir.or(self.lua_dir),
755 cache_dir: other.cache_dir.or(self.cache_dir),
756 data_dir: other.data_dir.or(self.data_dir),
757 vendor_dir: other.vendor_dir.or(self.vendor_dir),
758 enable_development_packages: other
759 .enable_development_packages
760 .or(self.enable_development_packages),
761 verbose: other.verbose.or(self.verbose),
762 no_progress: other.no_progress.or(self.no_progress),
763 no_prompt: other.no_prompt.or(self.no_prompt),
764 timeout: other.timeout.or(self.timeout),
765 max_jobs: other.max_jobs.or(self.max_jobs),
766 variables: other.variables.or(self.variables),
767 access_tokens: other.access_tokens.or(self.access_tokens),
768 external_deps: other.external_deps,
769 build: BuildConfig {
770 profile: other.build.profile.or(self.build.profile),
771 ..other.build
772 },
773 entrypoint_layout: other.entrypoint_layout,
774 user_agent: other.user_agent.or(self.user_agent),
775 generate_luarc: other.generate_luarc.or(self.generate_luarc),
776 luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
777 wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
778 package_types: other.package_types.or(self.package_types),
779 no_tfa: other.no_tfa.or(self.no_tfa),
780 }
781 }
782
783 #[tracing::instrument(level = "trace")]
784 pub fn build(self) -> Result<Config, ConfigError> {
785 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
786 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
787 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
788
789 let lua_version = self
790 .lua_version
791 .or(crate::lua_installation::detect_installed_lua_version());
792
793 Ok(Config {
794 enable_development_packages: self.enable_development_packages.unwrap_or(false),
795 server: self.server.unwrap_or_else(|| unsafe {
796 Url::parse("https://luarocks.org/").unwrap_unchecked()
797 }),
798 extra_servers: self.extra_servers.unwrap_or_default(),
799 namespace: self.namespace,
800 lua_dir: self.lua_dir,
801 lua_version,
802 user_tree,
803 workspace_tree: self.workspace_tree,
804 verbose: self.verbose.unwrap_or(false),
805 no_progress: self.no_progress.unwrap_or(false),
806 no_prompt: self.no_prompt.unwrap_or(false),
807 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
808 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
809 0 => usize::MAX,
810 max_jobs => max_jobs,
811 },
812 variables: default_variables()
813 .chain(self.variables.unwrap_or_default())
814 .collect(),
815 access_tokens: self.access_tokens.unwrap_or_default(),
816 external_deps: self.external_deps,
817 build: self.build,
818 entrypoint_layout: self.entrypoint_layout,
819 cache_dir,
820 data_dir,
821 vendor_dir: self.vendor_dir,
822 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
823 generate_luarc: self.generate_luarc.unwrap_or(true),
824 luarc_file_name: self
825 .luarc_file_name
826 .unwrap_or_else(|| ".luarc.json".to_string()),
827 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
828 package_types: self.package_types.unwrap_or_default(),
829 no_tfa: self.no_tfa.unwrap_or(false),
830 })
831 }
832}
833
834impl From<Config> for ConfigBuilder {
836 fn from(value: Config) -> Self {
837 ConfigBuilder {
838 enable_development_packages: Some(value.enable_development_packages),
839 server: Some(value.server),
840 extra_servers: Some(value.extra_servers),
841 namespace: value.namespace,
842 lua_dir: value.lua_dir,
843 lua_version: value.lua_version,
844 user_tree: Some(value.user_tree),
845 workspace_tree: value.workspace_tree,
846 verbose: Some(value.verbose),
847 no_progress: Some(value.no_progress),
848 no_prompt: Some(value.no_prompt),
849 timeout: Some(value.timeout),
850 max_jobs: if value.max_jobs == usize::MAX {
851 None
852 } else {
853 Some(value.max_jobs)
854 },
855 variables: Some(value.variables),
856 access_tokens: Some(value.access_tokens),
857 cache_dir: Some(value.cache_dir),
858 data_dir: Some(value.data_dir),
859 vendor_dir: value.vendor_dir,
860 external_deps: value.external_deps,
861 build: value.build,
862 entrypoint_layout: value.entrypoint_layout,
863 user_agent: Some(value.user_agent),
864 generate_luarc: Some(value.generate_luarc),
865 luarc_file_name: Some(value.luarc_file_name),
866 wrap_bin_scripts: Some(value.wrap_bin_scripts),
867 package_types: Some(value.package_types),
868 no_tfa: Some(value.no_tfa),
869 }
870 }
871}
872
873fn default_variables() -> impl Iterator<Item = (String, String)> {
874 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
875 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
876 vec![
877 ("MAKE".into(), "make".into()),
878 ("CMAKE".into(), "cmake".into()),
879 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
880 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
881 ("CFLAGS".into(), cflags),
882 ("LDFLAGS".into(), ldflags),
883 ("LIBFLAG".into(), utils::default_libflag().into()),
884 ]
885 .into_iter()
886}
887
888fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
889where
890 D: serde::Deserializer<'de>,
891{
892 let s = Option::<String>::deserialize(deserializer)?;
893 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
894 .transpose()
895}
896
897fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
898where
899 S: Serializer,
900{
901 match url {
902 Some(url) => serializer.serialize_some(url.as_str()),
903 None => serializer.serialize_none(),
904 }
905}
906
907fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
908where
909 D: serde::Deserializer<'de>,
910{
911 let s = Option::<Vec<String>>::deserialize(deserializer)?;
912 s.map(|v| {
913 v.into_iter()
914 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
915 .try_collect()
916 })
917 .transpose()
918}
919
920fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
921where
922 S: Serializer,
923{
924 match urls {
925 Some(urls) => {
926 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
927 serializer.serialize_some(&url_strings)
928 }
929 None => serializer.serialize_none(),
930 }
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936
937 #[tokio::test]
938 async fn wrapped_command_without_runner() {
939 let echo = which::which("echo").unwrap();
940 let config = ConfigBuilder::default().build().unwrap();
941 let output = config
942 .wrapped_command(&echo, ["hello", "world"])
943 .output()
944 .await
945 .unwrap();
946 assert!(output.status.success());
947 assert_eq!(
948 String::from_utf8_lossy(&output.stdout).trim(),
949 "hello world"
950 );
951 }
952
953 #[tokio::test]
954 async fn wrapped_command_prepends_runner_argv() {
955 let echo = which::which("echo").unwrap();
956 let config = ConfigBuilder::default()
957 .build_runner(Some(vec![echo.to_string_lossy().into(), "--prefix".into()]))
958 .build()
959 .unwrap();
960 let output = config
961 .wrapped_command("world", Vec::<String>::new())
962 .output()
963 .await
964 .unwrap();
965 assert!(output.status.success());
966 assert_eq!(
967 String::from_utf8_lossy(&output.stdout).trim(),
968 "--prefix world"
969 );
970 }
971
972 #[test]
973 fn debug_redacts_access_tokens() {
974 let config: Config =
975 toml::from_str::<ConfigBuilder>("[access_tokens]\n\"github.com\" = \"supersecret\"\n")
976 .unwrap()
977 .build()
978 .unwrap();
979 let debug = format!("{config:?}");
980 assert!(!debug.contains("supersecret"));
981 assert!(debug.contains("access_tokens"));
982 }
983}