const DEPRECATE_BY_VERSION: Option<&str> = Some("v3.0.0");
const DEPRECATE_FORCE: bool = true;
fn main() {
git_version();
log_directives();
deprecate();
}
fn git_version() {
let git_hash = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|o| if o.status.success() { Some(o.stdout) } else { None })
.and_then(|stdout| String::from_utf8(stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
println!("cargo:rustc-env=GIT_HASH={git_hash}");
}
fn log_directives() {
println!("cargo:rerun-if-changed=.cargo/log_directives");
if let Ok(directives) = std::fs::read_to_string(".cargo/log_directives") {
let directives = directives.trim();
if !directives.is_empty() {
println!("cargo:rustc-env=LOG_DIRECTIVES={directives}");
}
}
}
fn deprecate() {
let pkg_version = env!("CARGO_PKG_VERSION");
let current = parse_semver(pkg_version);
let default_deprecate_at = DEPRECATE_BY_VERSION.map(parse_semver);
let src_dir = std::path::Path::new("src");
if src_dir.exists() {
if DEPRECATE_FORCE {
if let Some(target_version) = DEPRECATE_BY_VERSION {
let mut force_updates = Vec::new();
collect_force_updates(src_dir, target_version, &mut force_updates);
for (path, line_num, old_line) in &force_updates {
if let Err(e) = update_since_in_file(path, *line_num, old_line, target_version) {
eprintln!("Warning: failed to update {}: {}", path, e);
}
}
return;
}
}
let mut expired_items = Vec::new();
let mut missing_since = Vec::new();
find_deprecated_attrs(src_dir, current, default_deprecate_at, &mut expired_items, &mut missing_since);
if !missing_since.is_empty() && DEPRECATE_BY_VERSION.is_none() {
eprintln!("\n\x1b[1;31mDeprecated items missing `since` attribute!\x1b[0m\n");
for loc in &missing_since {
eprintln!(" - {}", loc);
}
eprintln!("\nEither add `since = \"VERSION\"` to each #[deprecated] attribute,");
eprintln!("or configure a default version: {{ deprecate = {{ by_version = \"X.Y.Z\"; }}; }}");
panic!("Deprecated items must have `since` attribute or a default version configured");
}
if !expired_items.is_empty() {
eprintln!("\n\x1b[1;31mDeprecated items past their removal deadline!\x1b[0m\n");
for (loc, version) in &expired_items {
eprintln!(" - {} (should be removed by {})", loc, version);
}
eprintln!("\nRemove these items before proceeding with version {}.", pkg_version);
panic!("Deprecated items must be removed");
}
}
}
fn parse_semver(version: &str) -> (u32, u32, u32) {
let version = version.strip_prefix('v').unwrap_or(version);
let parts: Vec<&str> = version.split('.').collect();
let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
(major, minor, patch)
}
fn find_deprecated_attrs(
dir: &std::path::Path,
current: (u32, u32, u32),
default_deprecate_at: Option<(u32, u32, u32)>,
expired: &mut Vec<(String, String)>,
missing_since: &mut Vec<String>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
find_deprecated_attrs(&path, current, default_deprecate_at, expired, missing_since);
} else if path.extension().is_some_and(|ext| ext == "rs") {
if let Ok(content) = std::fs::read_to_string(&path) {
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("#[deprecated") {
let loc = format!("{}:{}", path.display(), line_num + 1);
if let Some(since_version) = extract_since(trimmed) {
let deprecate_at = parse_semver(since_version);
if current >= deprecate_at {
expired.push((loc, since_version.to_string()));
}
} else {
if let Some(default_at) = default_deprecate_at {
if current >= default_at {
expired.push((loc, DEPRECATE_BY_VERSION.unwrap().to_string()));
}
} else {
missing_since.push(loc);
}
}
} else if trimmed.starts_with("#[allow(deprecated") {
if let Some(default_at) = default_deprecate_at {
if current >= default_at {
expired.push((format!("{}:{}", path.display(), line_num + 1), DEPRECATE_BY_VERSION.unwrap().to_string()));
}
}
}
}
}
}
}
}
fn collect_force_updates(dir: &std::path::Path, target_version: &str, updates: &mut Vec<(String, usize, String)>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_force_updates(&path, target_version, updates);
} else if path.extension().is_some_and(|ext| ext == "rs") {
if let Ok(content) = std::fs::read_to_string(&path) {
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("#[deprecated") {
match extract_since(trimmed) {
Some(v) if v == target_version => {} _ => updates.push((path.display().to_string(), line_num, line.to_string())),
}
}
}
}
}
}
}
fn extract_since(attr: &str) -> Option<&str> {
let start = attr.find("since")? + 5;
let rest = &attr[start..];
let rest = rest.trim_start();
let rest = rest.strip_prefix('=')?;
let rest = rest.trim_start();
let quote_char = rest.chars().next()?;
if quote_char != '"' {
return None;
}
let rest = &rest[1..];
let end = rest.find('"')?;
Some(&rest[..end])
}
fn update_since_in_file(path: &str, line_num: usize, old_line: &str, target_version: &str) -> Result<(), std::io::Error> {
let content = std::fs::read_to_string(path)?;
let lines: Vec<&str> = content.lines().collect();
if lines.get(line_num) != Some(&old_line.as_ref()) {
return Ok(()); }
let new_line = update_deprecated_since(old_line, target_version);
let mut new_lines: Vec<&str> = lines.clone();
let new_line_ref: &str = &new_line;
new_lines[line_num] = new_line_ref;
let new_content = new_lines.join("\n");
let new_content = if content.ends_with('\n') { new_content + "\n" } else { new_content };
std::fs::write(path, new_content)
}
fn update_deprecated_since(line: &str, version: &str) -> String {
let trimmed = line.trim_start();
let indent = &line[..line.len() - trimmed.len()];
if let Some(since_start) = trimmed.find("since") {
let before_since = &trimmed[..since_start];
let after_since = &trimmed[since_start + 5..];
if let Some(eq_pos) = after_since.find('=') {
let after_eq = &after_since[eq_pos + 1..].trim_start();
if after_eq.starts_with('"') {
if let Some(end_quote) = after_eq[1..].find('"') {
let after_value = &after_eq[end_quote + 2..];
return format!("{}{}since = \"{}\"{}", indent, before_since, version, after_value);
}
}
}
}
if trimmed == "#[deprecated]" {
return format!("{}#[deprecated(since = \"{}\")]", indent, version);
}
if let Some(paren_start) = trimmed.find('(') {
let before_paren = &trimmed[..paren_start + 1];
let inside = &trimmed[paren_start + 1..];
return format!("{}{}since = \"{}\", {}", indent, before_paren, version, inside);
}
line.to_string()
}