npmgen_core/target/
resolver.rs1use std::fs;
2use std::path::Path;
3
4use super::{Target, TargetError};
5use crate::config::Config;
6
7const CARGO_CONFIG_DIR: &str = ".cargo";
9const CARGO_CONFIG_FILES: &[&str] = &["config.toml", "config"];
10const BUILD_TABLE: &str = "build";
12const TARGET_KEY: &str = "target";
13
14#[derive(Debug)]
22pub struct TargetResolver<'a> {
23 config: &'a Config,
24 workspace_root: &'a Path,
25}
26
27impl<'a> TargetResolver<'a> {
28 pub fn new(config: &'a Config, workspace_root: &'a Path) -> Self {
29 Self {
30 config,
31 workspace_root,
32 }
33 }
34
35 pub fn resolve(&self, filter: &[String]) -> Result<Vec<Target>, TargetError> {
38 let mut targets = self.base_targets()?;
39 if !filter.is_empty() {
40 for key in filter {
41 if !targets.iter().any(|target| &target.key == key) {
42 return Err(TargetError::UnknownFilterKey { key: key.clone() });
43 }
44 }
45 targets.retain(|target| filter.iter().any(|key| key == &target.key));
46 }
47 Ok(targets)
48 }
49
50 fn base_targets(&self) -> Result<Vec<Target>, TargetError> {
51 if !self.config.targets.is_empty() {
52 return self.config.targets.iter().map(Target::from_spec).collect();
53 }
54 if let Some(triples) = self.cargo_targets()? {
55 return triples
56 .iter()
57 .map(|triple| Target::from_triple(triple))
58 .collect();
59 }
60 Ok(Target::defaults())
61 }
62
63 fn cargo_targets(&self) -> Result<Option<Vec<String>>, TargetError> {
70 for dir in self.workspace_root.ancestors() {
71 let cargo_dir = dir.join(CARGO_CONFIG_DIR);
72 let path = CARGO_CONFIG_FILES
73 .iter()
74 .map(|name| cargo_dir.join(name))
75 .find(|candidate| candidate.is_file());
76 let Some(path) = path else { continue };
77
78 let text = fs::read_to_string(&path).map_err(|source| TargetError::CargoConfig {
79 path: path.clone(),
80 source,
81 })?;
82 let value: toml::Value =
83 toml::from_str(&text).map_err(|source| TargetError::CargoConfigParse {
84 path: path.clone(),
85 source,
86 })?;
87
88 if let Some(target) = value
89 .get(BUILD_TABLE)
90 .and_then(|build| build.get(TARGET_KEY))
91 {
92 return Ok(Some(Self::triples(target)));
93 }
94 }
95 Ok(None)
96 }
97
98 fn triples(value: &toml::Value) -> Vec<String> {
100 match value {
101 toml::Value::String(triple) => vec![triple.clone()],
102 toml::Value::Array(items) => items
103 .iter()
104 .filter_map(|item| item.as_str().map(str::to_owned))
105 .collect(),
106 _ => Vec::new(),
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::config::TargetSpec;
115
116 fn spec(key: &str) -> TargetSpec {
117 TargetSpec {
118 key: key.to_owned(),
119 triple: None,
120 os: None,
121 cpu: None,
122 }
123 }
124
125 fn config_with(targets: Vec<TargetSpec>) -> Config {
126 Config {
127 targets,
128 ..Config::default()
129 }
130 }
131
132 fn scratch(tag: &str) -> std::path::PathBuf {
133 let dir = std::env::temp_dir().join(format!("npmgen-{}-{tag}", std::process::id()));
134 let _ = fs::remove_dir_all(&dir);
135 fs::create_dir_all(&dir).unwrap();
136 dir
137 }
138
139 #[test]
140 fn explicit_targets_take_precedence_and_skip_the_filesystem() {
141 let config = config_with(vec![spec("linux-x64")]);
142 let targets = TargetResolver::new(&config, Path::new("/does/not/exist"))
143 .resolve(&[])
144 .unwrap();
145 assert_eq!(targets.len(), 1);
146 assert_eq!(targets[0].triple, "x86_64-unknown-linux-gnu");
147 }
148
149 #[test]
150 fn filter_narrows_and_rejects_unknown_keys() {
151 let config = config_with(vec![spec("win32-x64"), spec("linux-x64")]);
152 let resolver = TargetResolver::new(&config, Path::new("/does/not/exist"));
153
154 let kept = resolver.resolve(&["linux-x64".to_owned()]).unwrap();
155 assert_eq!(kept.len(), 1);
156 assert_eq!(kept[0].key, "linux-x64");
157
158 assert!(resolver.resolve(&["bogus".to_owned()]).is_err());
159 }
160
161 #[test]
162 fn inherits_cargo_configured_target() {
163 let root = scratch("cargo-config");
164 fs::create_dir_all(root.join(".cargo")).unwrap();
165 fs::write(
166 root.join(".cargo").join("config.toml"),
167 "[build]\ntarget = \"aarch64-apple-darwin\"\n",
168 )
169 .unwrap();
170
171 let config = Config::default();
172 let targets = TargetResolver::new(&config, &root).resolve(&[]).unwrap();
173 assert_eq!(targets.len(), 1);
174 assert_eq!(targets[0].key, "darwin-arm64");
175
176 let _ = fs::remove_dir_all(&root);
177 }
178
179 #[test]
180 fn inherits_cargo_target_array() {
181 let root = scratch("cargo-array");
182 fs::create_dir_all(root.join(".cargo")).unwrap();
183 fs::write(
184 root.join(".cargo").join("config.toml"),
185 "[build]\ntarget = [\"x86_64-pc-windows-msvc\", \"x86_64-apple-darwin\"]\n",
186 )
187 .unwrap();
188
189 let config = Config::default();
190 let targets = TargetResolver::new(&config, &root).resolve(&[]).unwrap();
191 let keys: Vec<&str> = targets.iter().map(|target| target.key.as_str()).collect();
192 assert_eq!(keys, ["win32-x64", "darwin-x64"]);
193
194 let _ = fs::remove_dir_all(&root);
195 }
196
197 #[test]
198 fn explicit_config_beats_a_present_cargo_config() {
199 let root = scratch("config-beats-cargo");
200 fs::create_dir_all(root.join(".cargo")).unwrap();
201 fs::write(
202 root.join(".cargo").join("config.toml"),
203 "[build]\ntarget = \"aarch64-apple-darwin\"\n",
204 )
205 .unwrap();
206
207 let config = config_with(vec![spec("win32-x64")]);
208 let targets = TargetResolver::new(&config, &root).resolve(&[]).unwrap();
209 assert_eq!(targets.len(), 1);
210 assert_eq!(targets[0].key, "win32-x64");
211
212 let _ = fs::remove_dir_all(&root);
213 }
214
215 #[test]
216 fn nearest_cargo_config_wins_over_an_ancestor() {
217 let root = scratch("nested-cargo");
218 let inner = root.join("sub");
219 fs::create_dir_all(root.join(".cargo")).unwrap();
220 fs::create_dir_all(inner.join(".cargo")).unwrap();
221 fs::write(
222 root.join(".cargo").join("config.toml"),
223 "[build]\ntarget = \"x86_64-unknown-linux-gnu\"\n",
224 )
225 .unwrap();
226 fs::write(
227 inner.join(".cargo").join("config.toml"),
228 "[build]\ntarget = \"aarch64-apple-darwin\"\n",
229 )
230 .unwrap();
231
232 let config = Config::default();
233 let targets = TargetResolver::new(&config, &inner).resolve(&[]).unwrap();
234 assert_eq!(targets.len(), 1);
235 assert_eq!(targets[0].key, "darwin-arm64");
236
237 let _ = fs::remove_dir_all(&root);
238 }
239}