Skip to content

Commit 2357881

Browse files
committed
add check-manifest, check-crlf.
1 parent 2d95b78 commit 2357881

File tree

4 files changed

+279
-0
lines changed

4 files changed

+279
-0
lines changed

src/cmd/check_crlf.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use crate::types::Context;
2+
use anyhow::{anyhow, Result};
3+
use std::fs;
4+
use std::path::Path;
5+
use walkdir::WalkDir;
6+
7+
#[derive(Debug, clap::Args)]
8+
/// Check that all files in the repository have LF line endings (no CRLF)
9+
pub struct Args;
10+
11+
pub fn run(ctx: &Context, _args: Args) -> Result<()> {
12+
let mut files_with_crlf = Vec::new();
13+
14+
// Walk through all files in the repository
15+
for entry in WalkDir::new(&ctx.root)
16+
.into_iter()
17+
.filter_entry(|e| !is_ignored_path(e.path()))
18+
{
19+
let entry = entry?;
20+
let path = entry.path();
21+
22+
// Only check regular files
23+
if !path.is_file() {
24+
continue;
25+
}
26+
27+
// Skip binary files by checking if they're likely text files
28+
if !is_likely_text_file(path) {
29+
continue;
30+
}
31+
32+
// Read file as bytes to detect CRLF
33+
match fs::read(path) {
34+
Ok(contents) => {
35+
if contains_crlf(&contents) {
36+
let relative_path = path.strip_prefix(&ctx.root)
37+
.unwrap_or(path)
38+
.display()
39+
.to_string();
40+
files_with_crlf.push(relative_path);
41+
}
42+
}
43+
Err(e) => {
44+
// Skip files we can't read (permissions, etc.)
45+
eprintln!("Warning: Could not read {}: {}", path.display(), e);
46+
}
47+
}
48+
}
49+
50+
if files_with_crlf.is_empty() {
51+
println!("✅ All text files have LF line endings!");
52+
Ok(())
53+
} else {
54+
for file in &files_with_crlf {
55+
eprintln!("❌ File has CRLF line endings: {}", file);
56+
}
57+
Err(anyhow!("Found {} files with CRLF line endings", files_with_crlf.len()))
58+
}
59+
}
60+
61+
fn is_ignored_path(path: &Path) -> bool {
62+
let path_str = path.to_string_lossy();
63+
64+
// Skip common directories and files that should be ignored
65+
path_str.contains("/.git/") ||
66+
path_str.contains("/target/") ||
67+
path_str.contains("/node_modules/") ||
68+
path_str.contains("/.cargo/") ||
69+
path_str.ends_with("/.DS_Store") ||
70+
path_str.ends_with("/Thumbs.db") ||
71+
path_str.contains("/__pycache__/") ||
72+
path_str.contains("/.pytest_cache/")
73+
}
74+
75+
fn is_likely_text_file(path: &Path) -> bool {
76+
if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
77+
// Common text file extensions
78+
matches!(extension.to_lowercase().as_str(),
79+
"rs" | "toml" | "md" | "txt" | "yml" | "yaml" | "json" |
80+
"js" | "ts" | "html" | "css" | "scss" | "xml" | "svg" |
81+
"py" | "sh" | "bash" | "zsh" | "fish" | "ps1" | "bat" |
82+
"c" | "cpp" | "cc" | "cxx" | "h" | "hpp" | "hxx" |
83+
"java" | "kt" | "scala" | "go" | "rb" | "php" | "cs" |
84+
"gitignore" | "gitattributes" | "dockerignore" | "dockerfile" |
85+
"makefile" | "cmake" | "gradle" | "sbt" | "pom" |
86+
"log" | "ini" | "cfg" | "conf" | "config"
87+
)
88+
} else {
89+
// Files without extensions - check if they're known text files
90+
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
91+
matches!(filename.to_lowercase().as_str(),
92+
"readme" | "license" | "changelog" | "authors" | "contributors" |
93+
"dockerfile" | "makefile" | "rakefile" | "gemfile" | "pipfile" |
94+
"cargo.lock" | "package.json" | "package-lock.json" |
95+
"yarn.lock" | "pnpm-lock.yaml" | ".gitignore" | ".gitattributes" |
96+
".dockerignore" | ".editorconfig" | ".rustfmt.toml"
97+
)
98+
} else {
99+
false
100+
}
101+
}
102+
}
103+
104+
fn contains_crlf(contents: &[u8]) -> bool {
105+
// Look for CRLF sequences (\r\n)
106+
contents.windows(2).any(|window| window == b"\r\n")
107+
}

src/cmd/check_manifest.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use crate::types::Context;
2+
use anyhow::{anyhow, Result};
3+
use std::collections::HashSet;
4+
use toml_edit::{DocumentMut, Item};
5+
6+
#[derive(Debug, clap::Args)]
7+
/// Check that all Cargo.toml files have correct metadata and feature configuration
8+
pub struct Args;
9+
10+
pub fn run(ctx: &Context, _args: Args) -> Result<()> {
11+
let mut errors = Vec::new();
12+
13+
for (crate_name, krate) in &ctx.crates {
14+
let cargo_toml_path = krate.path.join("Cargo.toml");
15+
let content = std::fs::read_to_string(&cargo_toml_path)
16+
.map_err(|e| anyhow!("Failed to read {}: {}", cargo_toml_path.display(), e))?;
17+
18+
let doc: DocumentMut = content.parse()
19+
.map_err(|e| anyhow!("Failed to parse {}: {}", cargo_toml_path.display(), e))?;
20+
21+
// Check package metadata
22+
if let Err(e) = check_package_metadata(&doc, crate_name, krate.publish) {
23+
errors.push(format!("{}: {}", crate_name, e));
24+
}
25+
26+
// Check features - only for publishable crates
27+
if krate.publish {
28+
if let Err(e) = check_features(&doc) {
29+
errors.push(format!("{}: {}", crate_name, e));
30+
}
31+
}
32+
}
33+
34+
if errors.is_empty() {
35+
println!("✅ All manifests are correct!");
36+
Ok(())
37+
} else {
38+
for error in &errors {
39+
eprintln!("❌ {}", error);
40+
}
41+
Err(anyhow!("Found {} manifest errors", errors.len()))
42+
}
43+
}
44+
45+
fn check_package_metadata(doc: &DocumentMut, crate_name: &str, is_publishable: bool) -> Result<()> {
46+
let package = doc.get("package")
47+
.ok_or_else(|| anyhow!("missing [package] section"))?
48+
.as_table()
49+
.ok_or_else(|| anyhow!("[package] is not a table"))?;
50+
51+
// Check license
52+
let license = package.get("license")
53+
.ok_or_else(|| anyhow!("missing license field"))?
54+
.as_str()
55+
.ok_or_else(|| anyhow!("license field is not a string"))?;
56+
57+
if license != "MIT OR Apache-2.0" {
58+
return Err(anyhow!("license should be 'MIT OR Apache-2.0', found '{}'", license));
59+
}
60+
61+
// Only check repository and documentation for publishable crates
62+
if is_publishable {
63+
// Check repository
64+
let repository = package.get("repository")
65+
.ok_or_else(|| anyhow!("missing repository field"))?
66+
.as_str()
67+
.ok_or_else(|| anyhow!("repository field is not a string"))?;
68+
69+
if repository != "https://github.com/embassy-rs/embassy" {
70+
return Err(anyhow!("repository should be 'https://github.com/embassy-rs/embassy', found '{}'", repository));
71+
}
72+
73+
// Check documentation
74+
let documentation = package.get("documentation")
75+
.ok_or_else(|| anyhow!("missing documentation field"))?
76+
.as_str()
77+
.ok_or_else(|| anyhow!("documentation field is not a string"))?;
78+
79+
let expected_documentation = format!("https://docs.embassy.dev/{}", crate_name);
80+
if documentation != expected_documentation {
81+
return Err(anyhow!("documentation should be '{}', found '{}'", expected_documentation, documentation));
82+
}
83+
}
84+
85+
Ok(())
86+
}
87+
88+
fn check_features(doc: &DocumentMut) -> Result<()> {
89+
// Get all optional dependencies
90+
let mut optional_deps: HashSet<String> = HashSet::new();
91+
92+
// Check dependencies
93+
if let Some(deps) = doc.get("dependencies").and_then(|d| d.as_table()) {
94+
for (name, value) in deps.iter() {
95+
if is_optional_dependency(value) {
96+
optional_deps.insert(name.to_string());
97+
}
98+
}
99+
}
100+
101+
// Check dev-dependencies
102+
if let Some(deps) = doc.get("dev-dependencies").and_then(|d| d.as_table()) {
103+
for (name, value) in deps.iter() {
104+
if is_optional_dependency(value) {
105+
optional_deps.insert(name.to_string());
106+
}
107+
}
108+
}
109+
110+
// Check build-dependencies
111+
if let Some(deps) = doc.get("build-dependencies").and_then(|d| d.as_table()) {
112+
for (name, value) in deps.iter() {
113+
if is_optional_dependency(value) {
114+
optional_deps.insert(name.to_string());
115+
}
116+
}
117+
}
118+
119+
if optional_deps.is_empty() {
120+
return Ok(()); // No optional dependencies to check
121+
}
122+
123+
// Get all features that reference dependencies
124+
let mut referenced_deps: HashSet<String> = HashSet::new();
125+
126+
if let Some(features) = doc.get("features").and_then(|f| f.as_table()) {
127+
for (_feature_name, feature_value) in features.iter() {
128+
if let Some(feature_list) = feature_value.as_array() {
129+
for item in feature_list.iter() {
130+
if let Some(item_str) = item.as_str() {
131+
if let Some(dep_name) = item_str.strip_prefix("dep:") {
132+
referenced_deps.insert(dep_name.to_string());
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
// Find unreferenced optional dependencies
141+
let unreferenced: Vec<String> = optional_deps.difference(&referenced_deps).cloned().collect();
142+
143+
if !unreferenced.is_empty() {
144+
return Err(anyhow!(
145+
"optional dependencies not referenced by any feature with 'dep:': {}",
146+
unreferenced.join(", ")
147+
));
148+
}
149+
150+
Ok(())
151+
}
152+
153+
fn is_optional_dependency(value: &Item) -> bool {
154+
match value {
155+
Item::Value(toml_edit::Value::InlineTable(table)) => {
156+
table.get("optional")
157+
.and_then(|v| v.as_bool())
158+
.unwrap_or(false)
159+
}
160+
_ => false
161+
}
162+
}

src/cmd/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod build;
22
pub mod bump;
3+
pub mod check_crlf;
4+
pub mod check_manifest;
35
pub mod dependencies;
46
pub mod dependents;
57
pub mod list;

src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ enum Command {
3030
Build(cmd::build::Args),
3131
SemverCheck(cmd::semver_check::Args),
3232
PrepareRelease(cmd::prepare_release::Args),
33+
CheckManifest(cmd::check_manifest::Args),
34+
CheckCrlf(cmd::check_crlf::Args),
3335
}
3436

3537
fn list_crates(root: &PathBuf) -> Result<BTreeMap<CrateId, Crate>> {
@@ -186,6 +188,12 @@ fn main() -> Result<()> {
186188
Command::PrepareRelease(args) => {
187189
cmd::prepare_release::run(&mut ctx, args)?;
188190
}
191+
Command::CheckManifest(args) => {
192+
cmd::check_manifest::run(&ctx, args)?;
193+
}
194+
Command::CheckCrlf(args) => {
195+
cmd::check_crlf::run(&ctx, args)?;
196+
}
189197
}
190198
Ok(())
191199
}

0 commit comments

Comments
 (0)