1 parent
d5ff5ea1a5
commit
9f76cca8b4
107 files changed
+2940
-2660
No files matched your search
+92
-25
@@ -1,40 +1,107 @@
|
||||
//! `cargo xtask assets`: every non-code asset in the repository must have a row in
|
||||
//! ATTRIBUTION.md (author, source, licence). Icons are drawn in code and need no file.
|
||||
|
||||
//! Asset provenance gate. ATTRIBUTION.md is the single inventory of collected files.
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
const ASSET_EXT: &[&str] =
|
||||
&["png", "jpg", "jpeg", "svg", "ico", "icns", "ttf", "otf", "woff", "woff2", "gif", "bmp", "webp", "wav", "mp3", "stl", "step", "stp", "obj"];
|
||||
const SKIP_DIRS: &[&str] = &["target", ".git", "plan", "dist"];
|
||||
const ASSET_EXT: &[&str] = &[
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"svg",
|
||||
"ico",
|
||||
"icns",
|
||||
"car",
|
||||
"vectorcraft",
|
||||
"ai",
|
||||
"psd",
|
||||
"afdesign",
|
||||
"fig",
|
||||
"sketch",
|
||||
"ttf",
|
||||
"otf",
|
||||
"woff",
|
||||
"woff2",
|
||||
"gif",
|
||||
"bmp",
|
||||
"webp",
|
||||
"wav",
|
||||
"mp3",
|
||||
"mp4",
|
||||
"stl",
|
||||
"step",
|
||||
"stp",
|
||||
"obj",
|
||||
];
|
||||
|
||||
fn walk(dir: &Path, root: &Path, out: &mut Vec<String>) {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for e in rd.flatten() {
|
||||
fn walk(dir: &Path, root: &Path, out: &mut Vec<String>) -> Result<(), String> {
|
||||
for e in std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))? {
|
||||
let e = e.map_err(|e| e.to_string())?;
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
if name.starts_with('.') || name.starts_with("target") || matches!(name.as_str(), "plan" | "dist" | "node_modules") {
|
||||
continue;
|
||||
}
|
||||
let p = e.path();
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if p.is_dir() {
|
||||
if !SKIP_DIRS.contains(&name.as_str()) && !name.starts_with('.') {
|
||||
walk(&p, root, out);
|
||||
let kind = e.file_type().map_err(|e| e.to_string())?;
|
||||
if kind.is_symlink() {
|
||||
if p.starts_with(root.join("assets")) {
|
||||
return Err(format!("resource symlink: {}", p.display()));
|
||||
}
|
||||
} else if let Some(ext) = p.extension().map(|x| x.to_string_lossy().to_ascii_lowercase())
|
||||
&& ASSET_EXT.contains(&ext.as_str())
|
||||
&& let Ok(rel) = p.strip_prefix(root)
|
||||
{
|
||||
out.push(rel.to_string_lossy().replace('\\', "/"));
|
||||
} else if kind.is_dir() {
|
||||
walk(&p, root, out)?;
|
||||
} else if kind.is_file() {
|
||||
out.push(p.strip_prefix(root).map_err(|e| e.to_string())?.to_string_lossy().replace('\\', "/"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn needs_attribution(path: &str) -> bool {
|
||||
// Include code masters, native work, catalogs, metadata and licence texts in the collection.
|
||||
if path.starts_with("assets/") {
|
||||
return !path.ends_with(".md");
|
||||
}
|
||||
Path::new(path).extension().is_some_and(|ext| ASSET_EXT.contains(&ext.to_string_lossy().to_ascii_lowercase().as_str()))
|
||||
}
|
||||
|
||||
fn missing<'a>(files: &'a [String], attribution: &str) -> Vec<&'a str> {
|
||||
let rows: BTreeSet<_> =
|
||||
attribution.lines().filter(|line| line.starts_with('|')).filter_map(|line| line.split('|').nth(1)).map(str::trim).collect();
|
||||
files.iter().map(String::as_str).filter(|path| needs_attribution(path) && !rows.contains(path)).collect()
|
||||
}
|
||||
|
||||
pub fn run(root: &Path) -> Result<(), String> {
|
||||
let attribution = std::fs::read_to_string(root.join("ATTRIBUTION.md")).map_err(|e| format!("ATTRIBUTION.md: {e}"))?;
|
||||
let mut files = Vec::new();
|
||||
walk(root, root, &mut files);
|
||||
walk(root, root, &mut files)?;
|
||||
files.sort();
|
||||
let missing: Vec<&String> = files.iter().filter(|f| !attribution.contains(f.as_str())).collect();
|
||||
if missing.is_empty() {
|
||||
println!("assets: {} asset file(s), all attributed", files.len());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("assets missing from ATTRIBUTION.md:\n{}", missing.iter().map(|m| format!(" {m}")).collect::<Vec<_>>().join("\n")))
|
||||
let missing = missing(&files, &attribution);
|
||||
if !missing.is_empty() {
|
||||
return Err(format!("assets missing attribution rows:\n{}", missing.iter().map(|path| format!(" {path}")).collect::<Vec<_>>().join("\n")));
|
||||
}
|
||||
println!("assets: {} files, all attributed", files.iter().filter(|path| needs_attribution(path)).count());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn collected_code_native_work_and_metadata_require_rows() {
|
||||
for path in
|
||||
["assets/icons/source/icon.rs", "assets/work/project.ai", "assets/fonts/OFL.txt", "assets/locales/en.json", "assets/macos/Assets.car"]
|
||||
{
|
||||
assert!(needs_attribution(path), "{path}");
|
||||
}
|
||||
assert!(needs_attribution("docs/screenshot.png"));
|
||||
assert!(needs_attribution("design/project.vectorcraft"));
|
||||
assert!(!needs_attribution("assets/README.md"));
|
||||
assert!(!needs_attribution("crates/ui/src/widget.rs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incidental_mentions_do_not_replace_attribution_rows() {
|
||||
let files = ["assets/source.svg".to_string(), "assets/README.md".to_string()];
|
||||
assert_eq!(missing(&files, "Mention assets/source.svg"), ["assets/source.svg"]);
|
||||
assert!(missing(&files, "| assets/source.svg | author | original | MIT |\n").is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user