Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ tokio = { version = "1", features = [
trybuild = "1"

toml = "0.9"
tempfile = "3"

[build-dependencies]
serde = { version = "1", features = ["derive"] }
Expand Down
31 changes: 29 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::{env, error::Error, path::PathBuf, process};
use std::{
env,
error::Error,
path::{Component, Path, PathBuf},
process
};

#[path = "build/readme.rs"]
mod readme;
Expand All @@ -16,6 +21,28 @@ fn run() -> Result<(), Box<dyn Error>> {
println!("cargo:rerun-if-changed=build/readme.rs");

let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
readme::sync_readme(&manifest_dir)?;
if is_packaged_manifest(&manifest_dir) {
readme::verify_readme(&manifest_dir)?;
} else {
readme::sync_readme(&manifest_dir)?;
}
Ok(())
}

fn is_packaged_manifest(manifest_dir: &Path) -> bool {
let mut seen_target = false;
for component in manifest_dir.components() {
match component {
Component::Normal(name) => {
if seen_target && name == "package" {
return true;
}
seen_target = name == "target";
Comment on lines +32 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Detect packaged builds without assuming default target dir

The new is_packaged_manifest helper only flags packaging when the manifest directory contains consecutive path components named target and package. cargo package indeed produces …/target/package/… by default, but when a user sets CARGO_TARGET_DIR to a custom directory (e.g., CARGO_TARGET_DIR=build on CI), the generated path becomes …/build/package/… and the function never sees a "target" component. In those setups the build script will continue to call sync_readme during packaging, rewriting files inside the package and re‑introducing the original failure. The detection needs to look for the package component irrespective of the parent directory or use an explicit environment signal from Cargo.

Useful? React with 👍 / 👎.

}
_ => {
seen_target = false;
}
}
}
false
}
45 changes: 43 additions & 2 deletions build/readme.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
collections::{BTreeMap, BTreeSet},
fs, io,
path::Path
path::{Path, PathBuf}
};

use serde::Deserialize;
Expand All @@ -26,7 +26,9 @@ pub enum ReadmeError {
/// Feature snippet group must be greater than zero.
InvalidSnippetGroup,
/// Placeholder in the template was not substituted.
UnresolvedPlaceholder(String)
UnresolvedPlaceholder(String),
/// README on disk differs from generated template output.
OutOfSync { path: PathBuf }
}

impl std::fmt::Display for ReadmeError {
Expand Down Expand Up @@ -63,6 +65,15 @@ impl std::fmt::Display for ReadmeError {
"Template placeholder '{{{{{name}}}}}' was not substituted"
)
}
Self::OutOfSync {
path
} => {
write!(
f,
"README at {} is out of sync; run `cargo build` in the repository root to refresh it",
path.display()
)
}
}
}
}
Expand Down Expand Up @@ -210,6 +221,36 @@ pub fn sync_readme(manifest_dir: &Path) -> Result<(), ReadmeError> {
write_if_changed(&output_path, &readme)
}

/// Verify that README.md matches the generated output without writing to disk.
///
/// # Errors
///
/// Returns an error if the README differs from the generated template or if any
/// IO/TOML operations fail.
///
/// # Examples
///
/// ```ignore
/// use std::path::Path;
///
/// let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
/// build::readme::verify_readme(manifest_dir)?;
/// ```
pub(crate) fn verify_readme(manifest_dir: &Path) -> Result<(), ReadmeError> {
let manifest_path = manifest_dir.join("Cargo.toml");
let template_path = manifest_dir.join("README.template.md");
let output_path = manifest_dir.join("README.md");
let readme = generate_readme(&manifest_path, &template_path)?;
let actual = fs::read_to_string(&output_path)?;
if actual == readme {
Ok(())
} else {
Err(ReadmeError::OutOfSync {
path: output_path
})
}
}

fn collect_feature_docs(
feature_table: &BTreeMap<String, Vec<String>>,
readme_meta: &ReadmeMetadata
Expand Down
60 changes: 60 additions & 0 deletions tests/readme_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@ mod readme;

use std::{error::Error, fs, io, path::PathBuf};

use tempfile::tempdir;

const MINIMAL_MANIFEST: &str = r#"[package]
name = "demo"
version = "1.2.3"
rust-version = "1.89"
edition = "2024"

[features]
default = []

[package.metadata.masterror.readme]
feature_order = []
conversion_lines = []
feature_snippet_group = 2

[package.metadata.masterror.readme.features]
"#;

const MINIMAL_TEMPLATE: &str = "# Demo\\n\\nVersion {{CRATE_VERSION}}\\nMSRV {{MSRV}}\\n\\nFeatures\\n{{FEATURE_BULLETS}}\\n\\nSnippet\\n{{FEATURE_SNIPPET}}\\n\\nConversions\\n{{CONVERSION_BULLETS}}\\n";

#[test]
fn readme_is_in_sync() -> Result<(), Box<dyn Error>> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
Expand All @@ -21,3 +42,42 @@ fn readme_is_in_sync() -> Result<(), Box<dyn Error>> {

Ok(())
}

#[test]
fn verify_readme_succeeds_when_in_sync() -> Result<(), Box<dyn Error>> {
let tmp = tempdir()?;
let manifest_path = tmp.path().join("Cargo.toml");
let template_path = tmp.path().join("README.template.md");
let readme_path = tmp.path().join("README.md");

fs::write(&manifest_path, MINIMAL_MANIFEST)?;
fs::write(&template_path, MINIMAL_TEMPLATE)?;
let generated = readme::generate_readme(&manifest_path, &template_path)?;
fs::write(&readme_path, generated)?;

readme::verify_readme(tmp.path()).map_err(|err| io::Error::other(err.to_string()))?;
Ok(())
}

#[test]
fn verify_readme_detects_out_of_sync() -> Result<(), Box<dyn Error>> {
let tmp = tempdir()?;
let manifest_path = tmp.path().join("Cargo.toml");
let template_path = tmp.path().join("README.template.md");
let readme_path = tmp.path().join("README.md");

fs::write(&manifest_path, MINIMAL_MANIFEST)?;
fs::write(&template_path, MINIMAL_TEMPLATE)?;
fs::write(&readme_path, "outdated")?;

match readme::verify_readme(tmp.path()) {
Err(readme::ReadmeError::OutOfSync {
path
}) => {
assert_eq!(path, readme_path);
Ok(())
}
Err(err) => Err(io::Error::other(format!("unexpected error: {err}")).into()),
Ok(_) => Err(io::Error::other("expected mismatch error").into())
}
}