chef/skeleton/
mod.rs

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    /// Relative path with respect to the project root.
33    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    /// Find all Cargo.toml files in `base_path` by traversing sub-directories recursively.
46    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        // Read relevant files from the filesystem
53        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        // We don't want an ordering issue (e.g. related to how files are read from the filesystem)
68        // to make our skeleton generation logic non-reproducible - therefore we sort!
69        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    /// Given the manifests in the current skeleton, create the minimum set of files required to
80    /// have a valid Rust project (i.e. write all manifests to disk and create dummy `lib.rs`,
81    /// `main.rs` and `build.rs` files where needed).
82    ///
83    /// This function should be called on an empty canvas - i.e. an empty directory apart from
84    /// the recipe file used to restore the skeleton.
85    pub fn build_minimum_project(
86        &self,
87        base_path: &Path,
88        no_std: bool,
89    ) -> Result<(), anyhow::Error> {
90        // Save lockfile to disk, if available
91        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        // Save rust-toolchain or rust-toolchain.toml to disk, if available
97        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        // save config file to disk, if available
107        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        // Save all manifests to disks
148        for manifest in &self.manifests {
149            // Persist manifest
150            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            // Create dummy entrypoints for all targets
170            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    /// Scan the target directory and remove all compilation artifacts for libraries and build
205    /// scripts from the current workspace.
206    /// Given the usage of dummy `lib.rs` and `build.rs` files, keeping them around leads to funny
207    /// compilation errors.
208    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        // https://doc.rust-lang.org/cargo/guide/build-cache.html
221        // > For historical reasons, the `dev` and `test` profiles are stored
222        // > in the `debug` directory, and the `release` and `bench` profiles are
223        // > stored in the `release` directory. User-defined profiles are
224        // > stored in a directory with the same name as the profile.
225
226        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                    // Remove dummy libraries.
251                    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                    // Remove dummy build.rs script artifacts.
273                    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
292/// If a custom target spec file is used,
293/// (Part of the unstable cargo feature 'build-std'; c.f. https://doc.rust-lang.org/rustc/targets/custom.html )
294/// the `--target` flag refers to a `.json` file in the current directory.
295/// In this case, the actual name of the target is the value of `--target` without the `.json` suffix.
296fn 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        // The serialised contents might be different from the original manifest!
304        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
322/// If the top-level `Cargo.toml` has a `members` field, replace it with
323/// a list consisting of just the path to the package.
324///
325/// Also deletes the `default-members` field because it does not play nicely
326/// with a modified `members` field and has no effect on cooking the final recipe.
327fn 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                // Make this a relative path to the workspace, and remove the `Cargo.toml` child.
346                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}