Skip to main content

bake_readme/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Reusable `readme.md` maintenance tasks for Bake.
5//!
6//! The task adds a short summary of recent entries from `releases.md` when a
7//! project `readme.md` does not already contain an authored Releases section. When
8//! the project root has a Cargo package manifest, it also adds or refreshes a
9//! generated package entry in the `readme.md` See Also section when the source
10//! repository link is not already present elsewhere in the readme.
11mod document;
12mod metadata;
13
14pub use document::{
15    PackageMetadata, Release, ensure_releases_section, recent_releases, update_document,
16    update_document_with_releases, update_releases_section,
17};
18
19mod readme_support {
20    use bake::{Context, Error, Result};
21    use std::fs;
22    use std::io::Write;
23    use std::path::PathBuf;
24
25    use super::{metadata::read_project_package, recent_releases, update_document_with_releases};
26
27    trait TemporaryFile: Write {
28        fn set_permissions(&self, permissions: fs::Permissions) -> std::io::Result<()>;
29        fn sync_all(&self) -> std::io::Result<()>;
30    }
31
32    impl TemporaryFile for fs::File {
33        fn set_permissions(&self, permissions: fs::Permissions) -> std::io::Result<()> {
34            fs::File::set_permissions(self, permissions)
35        }
36
37        fn sync_all(&self) -> std::io::Result<()> {
38            fs::File::sync_all(self)
39        }
40    }
41
42    fn prepare_temporary_file(
43        file: &mut impl TemporaryFile,
44        path: &std::path::Path,
45        document: &str,
46    ) -> Result<()> {
47        file.write_all(document.as_bytes())?;
48        file.set_permissions(fs::metadata(path)?.permissions())?;
49        file.sync_all()?;
50        Ok(())
51    }
52
53    fn write_document_atomically(path: &std::path::Path, document: &str) -> Result<()> {
54        let directory = path
55            .parent()
56            .expect("a canonical readme file path should have a parent directory");
57        let mut temporary = tempfile::NamedTempFile::new_in(directory)?;
58        prepare_temporary_file(temporary.as_file_mut(), path, document)?;
59        temporary
60            .persist(path)
61            .map_err(|error| Error::from(error.error))?;
62        Ok(())
63    }
64
65    /// Add recent release notes and Cargo package links to `readme.md` when needed.
66    pub(super) fn update(context: &mut Context, path: PathBuf) -> Result<()> {
67        let path = context.root().join(path).canonicalize()?;
68        let document = fs::read_to_string(&path)
69            .map_err(|error| Error::new(format!("{}: {error}", path.display())))?;
70        let package = read_project_package(context)?;
71        let release_path = context.root().join("releases.md");
72        let releases = if release_path.is_file() {
73            let release_document = fs::read_to_string(&release_path)
74                .map_err(|error| Error::new(format!("{}: {error}", release_path.display())))?;
75            recent_releases(&release_document)
76        } else {
77            Vec::new()
78        };
79        let updated = update_document_with_releases(&document, package.as_ref(), &releases);
80
81        if updated == document {
82            return Ok(());
83        }
84
85        write_document_atomically(&path, &updated)
86    }
87
88    #[cfg(test)]
89    #[path = "readme_tests.rs"]
90    mod tests;
91}
92
93/// Add recent release notes and Cargo package links to `readme.md` when needed.
94#[bake::task]
95pub fn update(
96    context: &mut bake::Context,
97    #[bake(
98        default = "readme.md",
99        help = "readme file relative to the project root."
100    )]
101    path: std::path::PathBuf,
102) -> bake::Result<()> {
103    readme_support::update(context, path)
104}