1mod read;
2mod target;
3mod version_masking;
4
5use crate::skeleton::target::{Target, TargetKind};
6use crate::OptimisationProfile;
7use anyhow::Context;
8use cargo_manifest::Product;
9use fs_err as fs;
10use globwalk::GlobWalkerBuilder;
11use guppy::graph::{DependencyDirection, PackageGraph};
12use pathdiff::diff_paths;
13use serde::{Deserialize, Serialize};
14use std::collections::HashSet;
15use std::path::{Path, PathBuf};
16
17#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
18pub struct Skeleton {
19 pub manifests: Vec<Manifest>,
20 pub config_file: Option<String>,
21 pub lock_file: Option<String>,
22 pub rust_toolchain_file: Option<(RustToolchainFile, String)>,
23}
24
25#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
26pub enum RustToolchainFile {
27 Bare,
28 Toml,
29}
30
31#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
32pub struct Manifest {
33 pub relative_path: PathBuf,
35 pub contents: String,
36 pub targets: Vec<Target>,
37}
38
39pub(in crate::skeleton) struct ParsedManifest {
40 relative_path: PathBuf,
41 contents: toml::Value,
42 targets: Vec<Target>,
43}
44
45impl Skeleton {
46 pub fn derive<P: AsRef<Path>>(
48 base_path: P,
49 member: Option<String>,
50 ) -> Result<Self, anyhow::Error> {
51 let no_deps = member.is_none() && base_path.as_ref().join("Cargo.lock").exists();
55 let graph = extract_package_graph(base_path.as_ref(), no_deps)?;
56
57 let config_file = read::config(&base_path)?;
59 let mut manifests = read::manifests(&base_path, &graph)?;
60 let mut lock_file = read::lockfile(&base_path)?;
61 if let Some(member) = &member {
62 filter_to_member_closure(&mut manifests, &mut lock_file, &graph, member)?;
63 }
64 let rust_toolchain_file = read::rust_toolchain(&base_path)?;
65
66 version_masking::mask_local_crate_versions(&mut manifests, &mut lock_file);
67
68 let lock_file = lock_file.map(|l| toml::to_string(&l)).transpose()?;
69
70 let mut serialised_manifests = serialize_manifests(manifests)?;
71 serialised_manifests.sort_by_key(|m| m.relative_path.clone());
74
75 Ok(Skeleton {
76 manifests: serialised_manifests,
77 config_file,
78 lock_file,
79 rust_toolchain_file,
80 })
81 }
82
83 pub fn build_minimum_project(
90 &self,
91 base_path: &Path,
92 no_std: bool,
93 ) -> Result<(), anyhow::Error> {
94 if let Some(lock_file) = &self.lock_file {
96 let lock_file_path = base_path.join("Cargo.lock");
97 fs::write(lock_file_path, lock_file.as_str())?;
98 }
99
100 if let Some((file_kind, content)) = &self.rust_toolchain_file {
102 let file_name = match file_kind {
103 RustToolchainFile::Bare => "rust-toolchain",
104 RustToolchainFile::Toml => "rust-toolchain.toml",
105 };
106 let path = base_path.join(file_name);
107 fs::write(path, content.as_str())?;
108 }
109
110 if let Some(config_file) = &self.config_file {
112 let parent_dir = base_path.join(".cargo");
113 let config_file_path = parent_dir.join("config.toml");
114 fs::create_dir_all(parent_dir)?;
115 fs::write(config_file_path, config_file.as_str())?;
116 }
117
118 const NO_STD_ENTRYPOINT: &str = "#![no_std]
119#![no_main]
120
121#[panic_handler]
122fn panic(_: &core::panic::PanicInfo) -> ! {
123 loop {}
124}
125";
126 const NO_STD_HARNESS_ENTRYPOINT: &str = r#"#![no_std]
127#![no_main]
128#![feature(custom_test_frameworks)]
129#![test_runner(test_runner)]
130
131#[no_mangle]
132pub extern "C" fn _init() {}
133
134fn test_runner(_: &[&dyn Fn()]) {}
135
136#[panic_handler]
137fn panic(_: &core::panic::PanicInfo) -> ! {
138 loop {}
139}
140"#;
141
142 let get_test_like_entrypoint = |harness: bool| -> &str {
143 match (no_std, harness) {
144 (true, true) => NO_STD_HARNESS_ENTRYPOINT,
145 (true, false) => NO_STD_ENTRYPOINT,
146 (false, true) => "",
147 (false, false) => "fn main() {}",
148 }
149 };
150
151 for manifest in &self.manifests {
153 let manifest_path = base_path.join(&manifest.relative_path);
155 let parent_directory = if let Some(parent_directory) = manifest_path.parent() {
156 fs::create_dir_all(parent_directory)?;
157 parent_directory.to_path_buf()
158 } else {
159 base_path.to_path_buf()
160 };
161 fs::write(&manifest_path, &manifest.contents)?;
162 let parsed_manifest =
163 cargo_manifest::Manifest::from_slice(manifest.contents.as_bytes())?;
164
165 let is_harness = |products: &[Product], name: &str| -> bool {
166 products
167 .iter()
168 .find(|product| product.name.as_deref() == Some(name))
169 .map(|p| p.harness)
170 .unwrap_or(true)
171 };
172
173 for target in &manifest.targets {
175 let content = match target.kind {
176 TargetKind::BuildScript => "fn main() {}",
177 TargetKind::Bin | TargetKind::Example => {
178 if no_std {
179 NO_STD_ENTRYPOINT
180 } else {
181 "fn main() {}"
182 }
183 }
184 TargetKind::Lib { is_proc_macro } => {
185 if no_std && !is_proc_macro {
186 "#![no_std]"
187 } else {
188 ""
189 }
190 }
191 TargetKind::Bench => {
192 get_test_like_entrypoint(is_harness(&parsed_manifest.bench, &target.name))
193 }
194 TargetKind::Test => {
195 get_test_like_entrypoint(is_harness(&parsed_manifest.test, &target.name))
196 }
197 };
198 let path = parent_directory.join(&target.path);
199 if let Some(dir) = path.parent() {
200 fs::create_dir_all(dir)?;
201 }
202 fs::write(&path, content)?;
203 }
204 }
205 Ok(())
206 }
207
208 pub fn remove_compiled_dummies<P: AsRef<Path>>(
213 &self,
214 base_path: P,
215 profile: OptimisationProfile,
216 target: Option<Vec<String>>,
217 target_dir: Option<PathBuf>,
218 ) -> Result<(), anyhow::Error> {
219 let target_dir = match target_dir {
220 None => base_path.as_ref().join("target"),
221 Some(target_dir) => target_dir,
222 };
223
224 let profile_dir = match &profile {
231 OptimisationProfile::Release => "release",
232 OptimisationProfile::Debug => "debug",
233 OptimisationProfile::Other(profile) if profile == "bench" => "release",
234 OptimisationProfile::Other(profile) if profile == "dev" || profile == "test" => "debug",
235 OptimisationProfile::Other(custom_profile) => custom_profile,
236 };
237
238 let target_directories: Vec<PathBuf> = target
239 .map_or(vec![target_dir.clone()], |targets| {
240 targets
241 .iter()
242 .map(|target| target_dir.join(target_str(target)))
243 .collect()
244 })
245 .iter()
246 .map(|path| path.join(profile_dir))
247 .collect();
248
249 for manifest in &self.manifests {
250 let parsed_manifest =
251 cargo_manifest::Manifest::from_slice(manifest.contents.as_bytes())?;
252 if let Some(package) = parsed_manifest.package.as_ref() {
253 for target_directory in &target_directories {
254 if let Some(lib) = &parsed_manifest.lib {
256 let library_name =
257 lib.name.as_ref().unwrap_or(&package.name).replace('-', "_");
258 let walker = GlobWalkerBuilder::from_patterns(
259 target_directory,
260 &[
261 format!("/**/lib{library_name}.*"),
262 format!("/**/lib{library_name}-*"),
263 ],
264 )
265 .build()?;
266 for file in walker {
267 let file = file?;
268 if file.file_type().is_file() {
269 fs::remove_file(file.path())?;
270 } else if file.file_type().is_dir() {
271 fs::remove_dir_all(file.path())?;
272 }
273 }
274 }
275
276 if package.build.is_some() {
278 let walker = GlobWalkerBuilder::new(
279 target_directory,
280 format!("/build/{}-*/build[-_]script[-_]build*", package.name),
281 )
282 .build()?;
283 for file in walker {
284 let file = file?;
285 fs::remove_file(file.path())?;
286 }
287 }
288 }
289 }
290 }
291
292 Ok(())
293 }
294}
295
296fn target_str(target: &str) -> &str {
301 target.trim_end_matches(".json")
302}
303
304fn serialize_manifests(manifests: Vec<ParsedManifest>) -> Result<Vec<Manifest>, anyhow::Error> {
305 let mut serialised_manifests = vec![];
306 for manifest in manifests {
307 let contents = toml::to_string(&manifest.contents)?;
309 serialised_manifests.push(Manifest {
310 relative_path: manifest.relative_path,
311 contents,
312 targets: manifest.targets,
313 });
314 }
315 Ok(serialised_manifests)
316}
317
318fn extract_package_graph(path: &Path, no_deps: bool) -> Result<PackageGraph, anyhow::Error> {
319 let mut cmd = guppy::MetadataCommand::new();
320 cmd.current_dir(path);
321 if no_deps {
322 cmd.no_deps();
323 }
324 cmd.build_graph().context("Cannot extract package graph")
325}
326
327fn filter_to_member_closure(
337 manifests: &mut Vec<ParsedManifest>,
338 lock_file: &mut Option<toml::Value>,
339 graph: &PackageGraph,
340 member: &str,
341) -> Result<(), anyhow::Error> {
342 let ws = graph.workspace();
343 let pkg = match ws.member_by_name(member) {
346 Ok(pkg) => pkg,
347 Err(_) => ws
348 .iter()
349 .find(|pkg| {
350 pkg.build_targets().any(|t| {
351 matches!(t.id(), guppy::graph::BuildTargetId::Binary(name) if name == member)
352 })
353 })
354 .ok_or_else(|| {
355 anyhow::anyhow!("No workspace package or binary target named '{member}'")
356 })?,
357 };
358
359 let resolved = graph.query_forward(std::iter::once(pkg.id()))?.resolve();
361
362 let closure_members: HashSet<String> = ws
364 .iter()
365 .filter(|ws_pkg| resolved.contains(ws_pkg.id()).unwrap_or(false))
366 .map(|ws_pkg| ws_pkg.name().to_string())
367 .collect();
368
369 manifests.retain(|m| {
371 extract_pkg_name(&m.contents).is_none_or(|name| closure_members.contains(&name))
372 });
373
374 let referenced_workspace_deps = collect_workspace_dep_keys(manifests);
376
377 update_workspace_members(
379 manifests,
380 graph,
381 &closure_members,
382 &referenced_workspace_deps,
383 );
384
385 let closure_packages: HashSet<(String, String)> = resolved
387 .packages(DependencyDirection::Forward)
388 .map(|pkg| (pkg.name().to_string(), pkg.version().to_string()))
389 .collect();
390
391 if let Some(lockfile) = lock_file {
393 filter_lockfile_packages(lockfile, &closure_packages);
394 }
395
396 Ok(())
397}
398
399fn extract_pkg_name(contents: &toml::Value) -> Option<String> {
401 contents
402 .get("package")?
403 .get("name")?
404 .as_str()
405 .map(|s| s.to_string())
406}
407
408fn update_workspace_members(
412 manifests: &mut [ParsedManifest],
413 graph: &PackageGraph,
414 closure_members: &HashSet<String>,
415 referenced_workspace_deps: &HashSet<String>,
416) {
417 let workspace_toml = manifests
418 .iter_mut()
419 .find(|manifest| manifest.relative_path == std::path::Path::new("Cargo.toml"));
420
421 if let Some(workspace) = workspace_toml.and_then(|toml| toml.contents.get_mut("workspace")) {
422 if let Some(members) = workspace.get_mut("members") {
423 let ws = graph.workspace();
424 let workspace_root = ws.root();
425
426 let member_paths: Vec<toml::Value> = ws
427 .iter()
428 .filter(|pkg| closure_members.contains(pkg.name()))
429 .filter_map(|pkg| {
430 let cargo_path = diff_paths(pkg.manifest_path(), workspace_root)?;
431 let dir = cargo_path.parent()?;
432 Some(toml::Value::String(dir.to_str()?.to_string()))
433 })
434 .collect();
435
436 *members = toml::Value::Array(member_paths);
437 }
438 if let Some(workspace) = workspace.as_table_mut() {
439 workspace.remove("default-members");
440 if let Some(deps) = workspace
441 .get_mut("dependencies")
442 .and_then(|d| d.as_table_mut())
443 {
444 deps.retain(|key, _| referenced_workspace_deps.contains(key));
445 }
446 }
447 }
448}
449
450fn collect_workspace_dep_keys(manifests: &[ParsedManifest]) -> HashSet<String> {
453 let mut keys = HashSet::new();
454 for manifest in manifests {
455 if extract_pkg_name(&manifest.contents).is_none() {
456 continue; }
458 collect_workspace_keys_from(&manifest.contents, &mut keys);
459 if let Some(targets) = manifest.contents.get("target").and_then(|t| t.as_table()) {
460 for (_, target_config) in targets {
461 collect_workspace_keys_from(target_config, &mut keys);
462 }
463 }
464 }
465 keys
466}
467
468fn collect_workspace_keys_from(value: &toml::Value, keys: &mut HashSet<String>) {
469 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
470 if let Some(deps) = value.get(section).and_then(|d| d.as_table()) {
471 for (key, dep) in deps {
472 if dep
473 .get("workspace")
474 .and_then(|w| w.as_bool())
475 .unwrap_or(false)
476 {
477 keys.insert(key.clone());
478 }
479 }
480 }
481 }
482}
483
484fn filter_lockfile_packages(
487 lockfile: &mut toml::Value,
488 closure_packages: &HashSet<(String, String)>,
489) {
490 if let Some(packages) = lockfile.get_mut("package").and_then(|p| p.as_array_mut()) {
491 packages.retain(|pkg| {
492 let name = pkg.get("name").and_then(|n| n.as_str());
493 let version = pkg.get("version").and_then(|v| v.as_str());
494 match (name, version) {
495 (Some(n), Some(v)) => closure_packages.contains(&(n.to_string(), v.to_string())),
496 _ => true,
497 }
498 });
499 }
500}