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 cargo_metadata::Metadata;
10use fs_err as fs;
11use globwalk::GlobWalkerBuilder;
12use pathdiff::diff_paths;
13use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15
16#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
17pub struct Skeleton {
18 pub manifests: Vec<Manifest>,
19 pub config_file: Option<String>,
20 pub lock_file: Option<String>,
21 pub rust_toolchain_file: Option<(RustToolchainFile, String)>,
22}
23
24#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
25pub enum RustToolchainFile {
26 Bare,
27 Toml,
28}
29
30#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
31pub struct Manifest {
32 pub relative_path: PathBuf,
34 pub contents: String,
35 pub targets: Vec<Target>,
36}
37
38pub(in crate::skeleton) struct ParsedManifest {
39 relative_path: PathBuf,
40 contents: toml::Value,
41 targets: Vec<Target>,
42}
43
44impl Skeleton {
45 pub fn derive<P: AsRef<Path>>(
47 base_path: P,
48 member: Option<String>,
49 ) -> Result<Self, anyhow::Error> {
50 let metadata = extract_cargo_metadata(base_path.as_ref())?;
51
52 let config_file = read::config(&base_path)?;
54 let mut manifests = read::manifests(&base_path, &metadata)?;
55 if let Some(member) = member {
56 ignore_all_members_except(&mut manifests, &metadata, member);
57 }
58
59 let mut lock_file = read::lockfile(&base_path)?;
60 let rust_toolchain_file = read::rust_toolchain(&base_path)?;
61
62 version_masking::mask_local_crate_versions(&mut manifests, &mut lock_file);
63
64 let lock_file = lock_file.map(|l| toml::to_string(&l)).transpose()?;
65
66 let mut serialised_manifests = serialize_manifests(manifests)?;
67 serialised_manifests.sort_by_key(|m| m.relative_path.clone());
70
71 Ok(Skeleton {
72 manifests: serialised_manifests,
73 config_file,
74 lock_file,
75 rust_toolchain_file,
76 })
77 }
78
79 pub fn build_minimum_project(
86 &self,
87 base_path: &Path,
88 no_std: bool,
89 ) -> Result<(), anyhow::Error> {
90 if let Some(lock_file) = &self.lock_file {
92 let lock_file_path = base_path.join("Cargo.lock");
93 fs::write(lock_file_path, lock_file.as_str())?;
94 }
95
96 if let Some((file_kind, content)) = &self.rust_toolchain_file {
98 let file_name = match file_kind {
99 RustToolchainFile::Bare => "rust-toolchain",
100 RustToolchainFile::Toml => "rust-toolchain.toml",
101 };
102 let path = base_path.join(file_name);
103 fs::write(path, content.as_str())?;
104 }
105
106 if let Some(config_file) = &self.config_file {
108 let parent_dir = base_path.join(".cargo");
109 let config_file_path = parent_dir.join("config.toml");
110 fs::create_dir_all(parent_dir)?;
111 fs::write(config_file_path, config_file.as_str())?;
112 }
113
114 const NO_STD_ENTRYPOINT: &str = "#![no_std]
115#![no_main]
116
117#[panic_handler]
118fn panic(_: &core::panic::PanicInfo) -> ! {
119 loop {}
120}
121";
122 const NO_STD_HARNESS_ENTRYPOINT: &str = r#"#![no_std]
123#![no_main]
124#![feature(custom_test_frameworks)]
125#![test_runner(test_runner)]
126
127#[no_mangle]
128pub extern "C" fn _init() {}
129
130fn test_runner(_: &[&dyn Fn()]) {}
131
132#[panic_handler]
133fn panic(_: &core::panic::PanicInfo) -> ! {
134 loop {}
135}
136"#;
137
138 let get_test_like_entrypoint = |harness: bool| -> &str {
139 match (no_std, harness) {
140 (true, true) => NO_STD_HARNESS_ENTRYPOINT,
141 (true, false) => NO_STD_ENTRYPOINT,
142 (false, true) => "",
143 (false, false) => "fn main() {}",
144 }
145 };
146
147 for manifest in &self.manifests {
149 let manifest_path = base_path.join(&manifest.relative_path);
151 let parent_directory = if let Some(parent_directory) = manifest_path.parent() {
152 fs::create_dir_all(parent_directory)?;
153 parent_directory.to_path_buf()
154 } else {
155 base_path.to_path_buf()
156 };
157 fs::write(&manifest_path, &manifest.contents)?;
158 let parsed_manifest =
159 cargo_manifest::Manifest::from_slice(manifest.contents.as_bytes())?;
160
161 let is_harness = |products: &[Product], name: &str| -> bool {
162 products
163 .iter()
164 .find(|product| product.name.as_deref() == Some(name))
165 .map(|p| p.harness)
166 .unwrap_or(true)
167 };
168
169 for target in &manifest.targets {
171 let content = match target.kind {
172 TargetKind::BuildScript => "fn main() {}",
173 TargetKind::Bin | TargetKind::Example => {
174 if no_std {
175 NO_STD_ENTRYPOINT
176 } else {
177 "fn main() {}"
178 }
179 }
180 TargetKind::Lib { is_proc_macro } => {
181 if no_std && !is_proc_macro {
182 "#![no_std]"
183 } else {
184 ""
185 }
186 }
187 TargetKind::Bench => {
188 get_test_like_entrypoint(is_harness(&parsed_manifest.bench, &target.name))
189 }
190 TargetKind::Test => {
191 get_test_like_entrypoint(is_harness(&parsed_manifest.test, &target.name))
192 }
193 };
194 let path = parent_directory.join(&target.path);
195 if let Some(dir) = path.parent() {
196 fs::create_dir_all(dir)?;
197 }
198 fs::write(&path, content)?;
199 }
200 }
201 Ok(())
202 }
203
204 pub fn remove_compiled_dummies<P: AsRef<Path>>(
209 &self,
210 base_path: P,
211 profile: OptimisationProfile,
212 target: Option<Vec<String>>,
213 target_dir: Option<PathBuf>,
214 ) -> Result<(), anyhow::Error> {
215 let target_dir = match target_dir {
216 None => base_path.as_ref().join("target"),
217 Some(target_dir) => target_dir,
218 };
219
220 let profile_dir = match &profile {
227 OptimisationProfile::Release => "release",
228 OptimisationProfile::Debug => "debug",
229 OptimisationProfile::Other(profile) if profile == "bench" => "release",
230 OptimisationProfile::Other(profile) if profile == "dev" || profile == "test" => "debug",
231 OptimisationProfile::Other(custom_profile) => custom_profile,
232 };
233
234 let target_directories: Vec<PathBuf> = target
235 .map_or(vec![target_dir.clone()], |targets| {
236 targets
237 .iter()
238 .map(|target| target_dir.join(target_str(target)))
239 .collect()
240 })
241 .iter()
242 .map(|path| path.join(profile_dir))
243 .collect();
244
245 for manifest in &self.manifests {
246 let parsed_manifest =
247 cargo_manifest::Manifest::from_slice(manifest.contents.as_bytes())?;
248 if let Some(package) = parsed_manifest.package.as_ref() {
249 for target_directory in &target_directories {
250 if let Some(lib) = &parsed_manifest.lib {
252 let library_name =
253 lib.name.as_ref().unwrap_or(&package.name).replace('-', "_");
254 let walker = GlobWalkerBuilder::from_patterns(
255 target_directory,
256 &[
257 format!("/**/lib{}.*", library_name),
258 format!("/**/lib{}-*", library_name),
259 ],
260 )
261 .build()?;
262 for file in walker {
263 let file = file?;
264 if file.file_type().is_file() {
265 fs::remove_file(file.path())?;
266 } else if file.file_type().is_dir() {
267 fs::remove_dir_all(file.path())?;
268 }
269 }
270 }
271
272 if package.build.is_some() {
274 let walker = GlobWalkerBuilder::new(
275 target_directory,
276 format!("/build/{}-*/build[-_]script[-_]build*", package.name),
277 )
278 .build()?;
279 for file in walker {
280 let file = file?;
281 fs::remove_file(file.path())?;
282 }
283 }
284 }
285 }
286 }
287
288 Ok(())
289 }
290}
291
292fn target_str(target: &str) -> &str {
297 target.trim_end_matches(".json")
298}
299
300fn serialize_manifests(manifests: Vec<ParsedManifest>) -> Result<Vec<Manifest>, anyhow::Error> {
301 let mut serialised_manifests = vec![];
302 for manifest in manifests {
303 let contents = toml::to_string(&manifest.contents)?;
305 serialised_manifests.push(Manifest {
306 relative_path: manifest.relative_path,
307 contents,
308 targets: manifest.targets,
309 });
310 }
311 Ok(serialised_manifests)
312}
313
314fn extract_cargo_metadata(path: &Path) -> Result<cargo_metadata::Metadata, anyhow::Error> {
315 let mut cmd = cargo_metadata::MetadataCommand::new();
316 cmd.current_dir(path);
317 cmd.no_deps();
318
319 cmd.exec().context("Cannot extract Cargo metadata")
320}
321
322fn ignore_all_members_except(
328 manifests: &mut [ParsedManifest],
329 metadata: &Metadata,
330 member: String,
331) {
332 let workspace_toml = manifests
333 .iter_mut()
334 .find(|manifest| manifest.relative_path == std::path::PathBuf::from("Cargo.toml"));
335
336 if let Some(workspace) = workspace_toml.and_then(|toml| toml.contents.get_mut("workspace")) {
337 if let Some(members) = workspace.get_mut("members") {
338 let workspace_root = &metadata.workspace_root;
339 let workspace_packages = metadata.workspace_packages();
340
341 if let Some(pkg) = workspace_packages
342 .into_iter()
343 .find(|pkg| pkg.name == member)
344 {
345 let member_cargo_path = diff_paths(pkg.manifest_path.as_os_str(), workspace_root);
347 let member_workspace_path = member_cargo_path
348 .as_ref()
349 .and_then(|path| path.parent())
350 .and_then(|dir| dir.to_str());
351
352 if let Some(member_path) = member_workspace_path {
353 *members =
354 toml::Value::Array(vec![toml::Value::String(member_path.to_string())]);
355 }
356 }
357 }
358 if let Some(workspace) = workspace.as_table_mut() {
359 workspace.remove("default-members");
360 }
361 }
362}