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::fs;
17use crate::lua_version::LuaVersion;
18use crate::package::RemotePackageTypeFilterSpec;
19use crate::project::TomlDeError;
20use crate::tree::{Tree, TreeError};
21use crate::variables::GetVariableError;
22use crate::{build::utils, variables::HasVariables};
23
24pub mod build;
25pub mod external_deps;
26pub mod tree;
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 namespace: Option<String>,
48 lua_dir: Option<PathBuf>,
49 lua_version: Option<LuaVersion>,
50 user_tree: PathBuf,
51 workspace_tree: Option<PathBuf>,
52 verbose: bool,
53 no_progress: bool,
55 no_prompt: bool,
57 timeout: Duration,
58 max_jobs: usize,
59 variables: HashMap<String, String>,
60 external_deps: ExternalDependencySearchConfig,
61
62 build: BuildConfig,
63 entrypoint_layout: RockLayoutConfig,
64
65 cache_dir: PathBuf,
66 data_dir: PathBuf,
67 vendor_dir: Option<PathBuf>,
68
69 user_agent: String,
70
71 generate_luarc: bool,
72 luarc_file_name: String,
73 wrap_bin_scripts: bool,
74 package_types: RemotePackageTypeFilterSpec,
75 no_tfa: bool,
76}
77
78impl Config {
79 pub(crate) fn project_dirs() -> Result<ProjectDirs, NoValidHomeDirectory> {
81 directories::ProjectDirs::from("org", "lumenlabs", "lux").ok_or(NoValidHomeDirectory)
82 }
83
84 fn default_cache_path() -> Result<PathBuf, NoValidHomeDirectory> {
86 let project_dirs = Config::project_dirs()?;
87 Ok(project_dirs.cache_dir().to_path_buf())
88 }
89
90 fn default_data_path() -> Result<PathBuf, NoValidHomeDirectory> {
92 let project_dirs = Config::project_dirs()?;
93 Ok(project_dirs.data_local_dir().to_path_buf())
94 }
95
96 pub fn with_lua_version(self, lua_version: LuaVersion) -> Self {
98 Self {
99 lua_version: Some(lua_version),
100 ..self
101 }
102 }
103
104 pub fn with_tree(self, tree: PathBuf) -> Self {
106 Self {
107 user_tree: tree,
108 ..self
109 }
110 }
111
112 pub fn with_workspace_tree(self, tree: Option<PathBuf>) -> Self {
114 Self {
115 workspace_tree: tree,
116 ..self
117 }
118 }
119
120 pub fn server(&self) -> &Url {
122 &self.server
123 }
124
125 pub fn extra_servers(&self) -> &Vec<Url> {
127 self.extra_servers.as_ref()
128 }
129
130 pub fn enabled_dev_servers(&self) -> Result<Vec<Url>, ConfigError> {
132 let mut enabled_dev_servers = Vec::new();
133 if self.enable_development_packages {
134 let config_file = ConfigBuilder::config_file()
135 .map(|p| p.to_string_lossy().to_string())
136 .unwrap_or_default();
137 enabled_dev_servers.push(self.server().join(DEV_PATH).map_err(|source| {
138 ConfigError::UrlParseError {
139 source,
140 help: Some(format!("check the `server` URL in {config_file}")),
141 }
142 })?);
143 for server in self.extra_servers() {
144 enabled_dev_servers.push(server.join(DEV_PATH).map_err(|source| {
145 ConfigError::UrlParseError {
146 source,
147 help: Some(format!("check the `extra_servers` URLs in {config_file}")),
148 }
149 })?);
150 }
151 }
152 Ok(enabled_dev_servers)
153 }
154
155 pub fn namespace(&self) -> Option<&String> {
157 self.namespace.as_ref()
158 }
159
160 pub fn lua_dir(&self) -> Option<&PathBuf> {
162 self.lua_dir.as_ref()
163 }
164
165 pub fn lua_version(&self) -> Option<&LuaVersion> {
167 self.lua_version.as_ref()
168 }
169
170 pub fn user_tree(&self, version: LuaVersion) -> Result<Tree, TreeError> {
173 Tree::new(self.user_tree.clone(), version, self)
174 }
175
176 pub fn workspace_tree(&self) -> Option<&PathBuf> {
178 self.workspace_tree.as_ref()
179 }
180
181 pub fn verbose(&self) -> bool {
183 self.verbose
184 }
185
186 pub fn no_progress(&self) -> bool {
188 self.no_progress
189 }
190
191 pub fn no_prompt(&self) -> bool {
193 self.no_prompt
194 }
195
196 pub fn timeout(&self) -> &Duration {
199 &self.timeout
200 }
201
202 pub fn max_jobs(&self) -> usize {
205 self.max_jobs
206 }
207
208 pub fn make_cmd(&self) -> String {
210 match self.variables.get("MAKE") {
211 Some(make) => make.clone(),
212 None => "make".into(),
213 }
214 }
215
216 pub fn cmake_cmd(&self) -> String {
218 match self.variables.get("CMAKE") {
219 Some(cmake) => cmake.clone(),
220 None => "cmake".into(),
221 }
222 }
223
224 pub(crate) fn wrapped_command<P, A>(&self, program: P, args: A) -> Command
230 where
231 P: AsRef<OsStr>,
232 A: IntoIterator,
233 A::Item: AsRef<OsStr>,
234 {
235 if self.build.runner.is_empty() {
236 let mut cmd = Command::new(program);
237 cmd.args(args);
238 cmd
239 } else {
240 let runner = &self.build.runner;
241 let mut cmd = Command::new(&runner[0]);
242 cmd.args(&runner[1..]).arg(program).args(args);
243 cmd
244 }
245 }
246
247 pub(crate) fn build_profile(&self) -> build::Profile {
249 self.build
250 .profile
251 .as_ref()
252 .cloned()
253 .unwrap_or(build::Profile::Release)
254 }
255
256 pub fn variables(&self) -> &HashMap<String, String> {
260 &self.variables
261 }
262
263 pub fn external_deps(&self) -> &ExternalDependencySearchConfig {
264 &self.external_deps
265 }
266
267 pub fn entrypoint_layout(&self) -> &RockLayoutConfig {
270 &self.entrypoint_layout
271 }
272
273 pub fn cache_dir(&self) -> &PathBuf {
275 &self.cache_dir
276 }
277
278 pub fn data_dir(&self) -> &PathBuf {
280 &self.data_dir
281 }
282
283 pub fn vendor_dir(&self) -> Option<&PathBuf> {
287 self.vendor_dir.as_ref()
288 }
289
290 pub fn user_agent(&self) -> &str {
292 &self.user_agent
293 }
294
295 pub fn generate_luarc(&self) -> bool {
297 self.generate_luarc
298 }
299
300 pub fn luarc_file_name(&self) -> &str {
302 &self.luarc_file_name
303 }
304
305 pub fn wrap_bin_scripts(&self) -> bool {
309 self.wrap_bin_scripts
310 }
311
312 pub fn package_types(&self) -> &RemotePackageTypeFilterSpec {
314 &self.package_types
315 }
316 pub fn no_tfa(&self) -> bool {
318 self.no_tfa
319 }
320}
321
322impl HasVariables for Config {
323 #[tracing::instrument(level = "trace")]
324 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
325 Ok(self.variables.get(input).cloned())
326 }
327}
328
329#[derive(Error, Debug, Diagnostic)]
330pub enum ConfigError {
331 #[error(transparent)]
332 #[diagnostic(transparent)]
333 Fs(#[from] fs::FsError),
334 #[error(transparent)]
335 #[diagnostic(transparent)]
336 NoValidHomeDirectory(#[from] NoValidHomeDirectory),
337 #[error("error parsing {config_file}")]
338 Deserialize {
339 config_file: String,
340 #[diagnostic_source]
341 source: TomlDeError,
342 },
343 #[error("error parsing URL: {source}")]
344 UrlParseError {
345 source: url::ParseError,
346 #[help]
347 help: Option<String>,
348 },
349}
350
351#[derive(Debug, Clone, Default, Deserialize, Serialize)]
358pub struct ConfigBuilder {
359 #[serde(
360 default,
361 deserialize_with = "deserialize_url",
362 serialize_with = "serialize_url"
363 )]
364 server: Option<Url>,
365 #[serde(
366 default,
367 deserialize_with = "deserialize_url_vec",
368 serialize_with = "serialize_url_vec"
369 )]
370 extra_servers: Option<Vec<Url>>,
371 namespace: Option<String>,
372 lua_version: Option<LuaVersion>,
373 user_tree: Option<PathBuf>,
374 workspace_tree: Option<PathBuf>,
375 lua_dir: Option<PathBuf>,
376 cache_dir: Option<PathBuf>,
377 data_dir: Option<PathBuf>,
378 vendor_dir: Option<PathBuf>,
379 enable_development_packages: Option<bool>,
380 verbose: Option<bool>,
381 no_progress: Option<bool>,
382 no_prompt: Option<bool>,
383 timeout: Option<Duration>,
384 max_jobs: Option<usize>,
385 variables: Option<HashMap<String, String>>,
386 #[serde(default)]
387 external_deps: ExternalDependencySearchConfig,
388 #[serde(default)]
389 build: BuildConfig,
390
391 #[serde(default)]
392 entrypoint_layout: RockLayoutConfig,
393 user_agent: Option<String>,
394 generate_luarc: Option<bool>,
395 luarc_file_name: Option<String>,
396 wrap_bin_scripts: Option<bool>,
397 package_types: Option<RemotePackageTypeFilterSpec>,
398 no_tfa: Option<bool>,
399}
400
401impl ConfigBuilder {
403 pub fn new() -> Result<Self, ConfigError> {
406 let config_file = Self::config_file()?;
407 if config_file.is_file() {
408 Self::from_file(&config_file)
409 } else {
410 Ok(Self::default())
411 }
412 }
413
414 pub(crate) fn from_file(config_file: &Path) -> Result<Self, ConfigError> {
415 let config_file_name = config_file.to_string_lossy().to_string();
416 let content = fs::sync::read_to_string(config_file)?;
417 crate::project::parse_toml(&config_file_name, &content).map_err(|source| {
418 ConfigError::Deserialize {
419 config_file: config_file_name,
420 source,
421 }
422 })
423 }
424
425 pub fn config_file() -> Result<PathBuf, NoValidHomeDirectory> {
427 let project_dirs = directories::ProjectDirs::from("org", "lumenlabs", "lux")
428 .ok_or(NoValidHomeDirectory)?;
429 Ok(project_dirs.config_dir().join("config.toml").to_path_buf())
430 }
431
432 pub fn dev(self, dev: Option<bool>) -> Self {
435 Self {
436 enable_development_packages: dev.or(self.enable_development_packages),
437 ..self
438 }
439 }
440
441 pub fn server(self, server: Option<Url>) -> Self {
444 Self {
445 server: server.or(self.server),
446 ..self
447 }
448 }
449
450 pub fn extra_servers(self, extra_servers: Option<Vec<Url>>) -> Self {
452 Self {
453 extra_servers: extra_servers.or(self.extra_servers),
454 ..self
455 }
456 }
457
458 pub fn namespace(self, namespace: Option<String>) -> Self {
460 Self {
461 namespace: namespace.or(self.namespace),
462 ..self
463 }
464 }
465
466 pub fn lua_dir(self, lua_dir: Option<PathBuf>) -> Self {
468 Self {
469 lua_dir: lua_dir.or(self.lua_dir),
470 ..self
471 }
472 }
473
474 pub fn lua_version(self, lua_version: Option<LuaVersion>) -> Self {
477 Self {
478 lua_version: lua_version.or(self.lua_version),
479 ..self
480 }
481 }
482
483 pub fn user_tree(self, tree: Option<PathBuf>) -> Self {
485 Self {
486 user_tree: tree.or(self.user_tree),
487 ..self
488 }
489 }
490
491 pub fn workspace_tree(self, tree: Option<PathBuf>) -> Self {
494 Self {
495 workspace_tree: tree.or(self.workspace_tree),
496 ..self
497 }
498 }
499
500 pub fn variables(self, variables: Option<HashMap<String, String>>) -> Self {
504 Self {
505 variables: variables.or(self.variables),
506 ..self
507 }
508 }
509
510 pub fn verbose(self, verbose: Option<bool>) -> Self {
513 Self {
514 verbose: verbose.or(self.verbose),
515 ..self
516 }
517 }
518
519 pub fn no_progress(self, no_progress: Option<bool>) -> Self {
522 Self {
523 no_progress: no_progress.or(self.no_progress),
524 ..self
525 }
526 }
527
528 pub fn no_prompt(self, no_prompt: Option<bool>) -> Self {
531 Self {
532 no_prompt: no_prompt.or(self.no_prompt),
533 ..self
534 }
535 }
536
537 pub fn timeout(self, timeout: Option<Duration>) -> Self {
541 Self {
542 timeout: timeout.or(self.timeout),
543 ..self
544 }
545 }
546
547 pub fn max_jobs(self, max_jobs: Option<usize>) -> Self {
551 Self {
552 max_jobs: max_jobs.or(self.max_jobs),
553 ..self
554 }
555 }
556
557 pub fn cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
559 Self {
560 cache_dir: cache_dir.or(self.cache_dir),
561 ..self
562 }
563 }
564
565 pub fn data_dir(self, data_dir: Option<PathBuf>) -> Self {
567 Self {
568 data_dir: data_dir.or(self.data_dir),
569 ..self
570 }
571 }
572
573 pub fn vendor_dir(self, vendor_dir: Option<PathBuf>) -> Self {
577 Self {
578 vendor_dir: vendor_dir.or(self.vendor_dir),
579 ..self
580 }
581 }
582
583 pub fn entrypoint_layout(self, rock_layout: RockLayoutConfig) -> Self {
586 Self {
587 entrypoint_layout: rock_layout,
588 ..self
589 }
590 }
591
592 pub fn user_agent(self, user_agent: Option<String>) -> Self {
595 Self {
596 user_agent: user_agent.or(self.user_agent),
597 ..self
598 }
599 }
600
601 pub fn generate_luarc(self, generate: Option<bool>) -> Self {
604 Self {
605 generate_luarc: generate.or(self.generate_luarc),
606 ..self
607 }
608 }
609
610 pub fn luarc_file_name(self, file: Option<String>) -> Self {
613 Self {
614 luarc_file_name: file.or(self.luarc_file_name),
615 ..self
616 }
617 }
618
619 pub fn wrap_bin_scripts(self, generate: Option<bool>) -> Self {
625 Self {
626 wrap_bin_scripts: generate.or(self.generate_luarc),
627 ..self
628 }
629 }
630 pub fn no_tfa(self, tfa: Option<bool>) -> Self {
633 Self {
634 no_tfa: tfa.or(self.no_tfa),
635 ..self
636 }
637 }
638
639 pub fn build_runner(self, runner: Option<Vec<String>>) -> Self {
689 Self {
690 build: BuildConfig {
691 runner: runner.unwrap_or(self.build.runner),
692 ..self.build
693 },
694 ..self
695 }
696 }
697
698 pub fn build_profile(self, profile: Option<build::Profile>) -> Self {
701 Self {
702 build: BuildConfig {
703 profile: profile.or(self.build.profile),
704 ..self.build
705 },
706 ..self
707 }
708 }
709
710 pub fn default_build_profile(self, profile: build::Profile) -> Self {
712 Self {
713 build: BuildConfig {
714 profile: self.build.profile.or(Some(profile)),
715 ..self.build
716 },
717 ..self
718 }
719 }
720
721 pub fn merge(self, other: Self) -> Self {
723 Self {
724 server: other.server.or(self.server),
725 extra_servers: other.extra_servers.or(self.extra_servers),
726 namespace: other.namespace.or(self.namespace),
727 lua_version: other.lua_version.or(self.lua_version),
728 user_tree: other.user_tree.or(self.user_tree),
729 workspace_tree: other.workspace_tree.or(self.workspace_tree),
730 lua_dir: other.lua_dir.or(self.lua_dir),
731 cache_dir: other.cache_dir.or(self.cache_dir),
732 data_dir: other.data_dir.or(self.data_dir),
733 vendor_dir: other.vendor_dir.or(self.vendor_dir),
734 enable_development_packages: other
735 .enable_development_packages
736 .or(self.enable_development_packages),
737 verbose: other.verbose.or(self.verbose),
738 no_progress: other.no_progress.or(self.no_progress),
739 no_prompt: other.no_prompt.or(self.no_prompt),
740 timeout: other.timeout.or(self.timeout),
741 max_jobs: other.max_jobs.or(self.max_jobs),
742 variables: other.variables.or(self.variables),
743 external_deps: other.external_deps,
744 build: BuildConfig {
745 profile: other.build.profile.or(self.build.profile),
746 ..other.build
747 },
748 entrypoint_layout: other.entrypoint_layout,
749 user_agent: other.user_agent.or(self.user_agent),
750 generate_luarc: other.generate_luarc.or(self.generate_luarc),
751 luarc_file_name: other.luarc_file_name.or(self.luarc_file_name),
752 wrap_bin_scripts: other.wrap_bin_scripts.or(self.wrap_bin_scripts),
753 package_types: other.package_types.or(self.package_types),
754 no_tfa: other.no_tfa.or(self.no_tfa),
755 }
756 }
757
758 #[tracing::instrument(level = "trace")]
759 pub fn build(self) -> Result<Config, ConfigError> {
760 let data_dir = self.data_dir.unwrap_or(Config::default_data_path()?);
761 let cache_dir = self.cache_dir.unwrap_or(Config::default_cache_path()?);
762 let user_tree = self.user_tree.unwrap_or(data_dir.join("tree"));
763
764 let lua_version = self
765 .lua_version
766 .or(crate::lua_installation::detect_installed_lua_version());
767
768 Ok(Config {
769 enable_development_packages: self.enable_development_packages.unwrap_or(false),
770 server: self.server.unwrap_or_else(|| unsafe {
771 Url::parse("https://luarocks.org/").unwrap_unchecked()
772 }),
773 extra_servers: self.extra_servers.unwrap_or_default(),
774 namespace: self.namespace,
775 lua_dir: self.lua_dir,
776 lua_version,
777 user_tree,
778 workspace_tree: self.workspace_tree,
779 verbose: self.verbose.unwrap_or(false),
780 no_progress: self.no_progress.unwrap_or(false),
781 no_prompt: self.no_prompt.unwrap_or(false),
782 timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
783 max_jobs: match self.max_jobs.unwrap_or(usize::MAX) {
784 0 => usize::MAX,
785 max_jobs => max_jobs,
786 },
787 variables: default_variables()
788 .chain(self.variables.unwrap_or_default())
789 .collect(),
790 external_deps: self.external_deps,
791 build: self.build,
792 entrypoint_layout: self.entrypoint_layout,
793 cache_dir,
794 data_dir,
795 vendor_dir: self.vendor_dir,
796 user_agent: self.user_agent.unwrap_or(DEFAULT_USER_AGENT.into()),
797 generate_luarc: self.generate_luarc.unwrap_or(true),
798 luarc_file_name: self
799 .luarc_file_name
800 .unwrap_or_else(|| ".luarc.json".to_string()),
801 wrap_bin_scripts: self.wrap_bin_scripts.unwrap_or(true),
802 package_types: self.package_types.unwrap_or_default(),
803 no_tfa: self.no_tfa.unwrap_or(false),
804 })
805 }
806}
807
808impl From<Config> for ConfigBuilder {
810 fn from(value: Config) -> Self {
811 ConfigBuilder {
812 enable_development_packages: Some(value.enable_development_packages),
813 server: Some(value.server),
814 extra_servers: Some(value.extra_servers),
815 namespace: value.namespace,
816 lua_dir: value.lua_dir,
817 lua_version: value.lua_version,
818 user_tree: Some(value.user_tree),
819 workspace_tree: value.workspace_tree,
820 verbose: Some(value.verbose),
821 no_progress: Some(value.no_progress),
822 no_prompt: Some(value.no_prompt),
823 timeout: Some(value.timeout),
824 max_jobs: if value.max_jobs == usize::MAX {
825 None
826 } else {
827 Some(value.max_jobs)
828 },
829 variables: Some(value.variables),
830 cache_dir: Some(value.cache_dir),
831 data_dir: Some(value.data_dir),
832 vendor_dir: value.vendor_dir,
833 external_deps: value.external_deps,
834 build: value.build,
835 entrypoint_layout: value.entrypoint_layout,
836 user_agent: Some(value.user_agent),
837 generate_luarc: Some(value.generate_luarc),
838 luarc_file_name: Some(value.luarc_file_name),
839 wrap_bin_scripts: Some(value.wrap_bin_scripts),
840 package_types: Some(value.package_types),
841 no_tfa: Some(value.no_tfa),
842 }
843 }
844}
845
846fn default_variables() -> impl Iterator<Item = (String, String)> {
847 let cflags = env::var("CFLAGS").unwrap_or(utils::default_cflags().into());
848 let ldflags = env::var("LDFLAGS").unwrap_or("".into());
849 vec![
850 ("MAKE".into(), "make".into()),
851 ("CMAKE".into(), "cmake".into()),
852 ("LIB_EXTENSION".into(), utils::c_dylib_extension().into()),
853 ("OBJ_EXTENSION".into(), utils::c_obj_extension().into()),
854 ("CFLAGS".into(), cflags),
855 ("LDFLAGS".into(), ldflags),
856 ("LIBFLAG".into(), utils::default_libflag().into()),
857 ]
858 .into_iter()
859}
860
861fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
862where
863 D: serde::Deserializer<'de>,
864{
865 let s = Option::<String>::deserialize(deserializer)?;
866 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
867 .transpose()
868}
869
870fn serialize_url<S>(url: &Option<Url>, serializer: S) -> Result<S::Ok, S::Error>
871where
872 S: Serializer,
873{
874 match url {
875 Some(url) => serializer.serialize_some(url.as_str()),
876 None => serializer.serialize_none(),
877 }
878}
879
880fn deserialize_url_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Url>>, D::Error>
881where
882 D: serde::Deserializer<'de>,
883{
884 let s = Option::<Vec<String>>::deserialize(deserializer)?;
885 s.map(|v| {
886 v.into_iter()
887 .map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
888 .try_collect()
889 })
890 .transpose()
891}
892
893fn serialize_url_vec<S>(urls: &Option<Vec<Url>>, serializer: S) -> Result<S::Ok, S::Error>
894where
895 S: Serializer,
896{
897 match urls {
898 Some(urls) => {
899 let url_strings: Vec<String> = urls.iter().map(|url| url.to_string()).collect();
900 serializer.serialize_some(&url_strings)
901 }
902 None => serializer.serialize_none(),
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909
910 #[tokio::test]
911 async fn wrapped_command_without_runner() {
912 let echo = which::which("echo").unwrap();
913 let config = ConfigBuilder::default().build().unwrap();
914 let output = config
915 .wrapped_command(&echo, ["hello", "world"])
916 .output()
917 .await
918 .unwrap();
919 assert!(output.status.success());
920 assert_eq!(
921 String::from_utf8_lossy(&output.stdout).trim(),
922 "hello world"
923 );
924 }
925
926 #[tokio::test]
927 async fn wrapped_command_prepends_runner_argv() {
928 let echo = which::which("echo").unwrap();
929 let config = ConfigBuilder::default()
930 .build_runner(Some(vec![echo.to_string_lossy().into(), "--prefix".into()]))
931 .build()
932 .unwrap();
933 let output = config
934 .wrapped_command("world", Vec::<String>::new())
935 .output()
936 .await
937 .unwrap();
938 assert!(output.status.success());
939 assert_eq!(
940 String::from_utf8_lossy(&output.stdout).trim(),
941 "--prefix world"
942 );
943 }
944}