From 346f02cd518577f89c4bf95851cc56810d63a1ce Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 12 Jan 2026 23:05:15 -0300 Subject: [PATCH 1/3] feat: Add `sync-langs.py` script to automate arborium feature sync --- crates/plotnik-langs/build.rs | 49 +++++++++++++++++++++++++++++ crates/plotnik-langs/src/builtin.rs | 3 ++ 2 files changed, 52 insertions(+) diff --git a/crates/plotnik-langs/build.rs b/crates/plotnik-langs/build.rs index c020b88..7f74f52 100644 --- a/crates/plotnik-langs/build.rs +++ b/crates/plotnik-langs/build.rs @@ -14,6 +14,9 @@ fn main() { }) .collect(); + // Check for features not defined in builtin.rs + check_lang_definitions(&enabled_features, &out_dir, &manifest_dir); + if enabled_features.is_empty() { println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=Cargo.toml"); @@ -139,6 +142,52 @@ fn feature_to_lang_key(feature: &str) -> String { } } +fn check_lang_definitions(enabled_features: &[String], out_dir: &Path, manifest_dir: &str) { + // Parse builtin.rs to find defined languages + let builtin_path = PathBuf::from(manifest_dir).join("src/builtin.rs"); + let builtin_src = std::fs::read_to_string(&builtin_path) + .expect("failed to read builtin.rs"); + + // Extract feature: "lang-*" patterns from define_langs! macro + let defined_langs: Vec<&str> = builtin_src + .lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with("feature:") { + // feature: "lang-foo", + trimmed + .strip_prefix("feature:") + .and_then(|s| s.trim().strip_prefix('"')) + .and_then(|s| s.strip_suffix(',').or(Some(s))) + .and_then(|s| s.strip_suffix('"')) + } else { + None + } + }) + .collect(); + + let mut errors = Vec::new(); + for feature in enabled_features { + if !defined_langs.contains(&feature.as_str()) { + errors.push(format!( + "compile_error!(\"Feature `{feature}` enabled but not defined in builtin.rs. \ + Add language metadata to define_langs! macro.\");" + )); + } + } + + let check_file = out_dir.join("lang_check.rs"); + if errors.is_empty() { + std::fs::write(&check_file, "// All enabled features are defined in builtin.rs\n") + .expect("failed to write lang_check.rs"); + } else { + std::fs::write(&check_file, errors.join("\n")) + .expect("failed to write lang_check.rs"); + } + + println!("cargo::rerun-if-changed={}", builtin_path.display()); +} + fn arborium_package_to_feature(package_name: &str) -> Option { const NON_LANGUAGE_PACKAGES: &[&str] = &[ "arborium-docsrs-demo", diff --git a/crates/plotnik-langs/src/builtin.rs b/crates/plotnik-langs/src/builtin.rs index f4e167e..6a6a4ed 100644 --- a/crates/plotnik-langs/src/builtin.rs +++ b/crates/plotnik-langs/src/builtin.rs @@ -2,6 +2,9 @@ use std::sync::{Arc, LazyLock}; use crate::{Lang, LangInner}; +// Build-time check: fails if Cargo.toml has features not defined in this file +include!(concat!(env!("OUT_DIR"), "/lang_check.rs")); + /// Language metadata for listing. #[derive(Debug, Clone)] pub struct LangInfo { From 06a27923c93b9c82c7f5e5fa0f387a51f52f6fdc Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 12 Jan 2026 22:26:53 -0300 Subject: [PATCH 2/3] fix: Add `toolchain: stable` input to pinned rust-toolchain action From 5d46fd4d8e05ef3166089982774040c955a7da53 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sun, 11 Jan 2026 19:19:54 -0300 Subject: [PATCH 3/3] refactor: Move lang consistency check from build.rs to Python script Add --ci flag to control PR comment posting. --- .github/workflows/sync-langs.yml | 2 +- crates/plotnik-langs/build.rs | 49 ----------------------- crates/plotnik-langs/src/builtin.rs | 3 -- scripts/sync-langs.py | 60 +++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/.github/workflows/sync-langs.yml b/.github/workflows/sync-langs.yml index 5b1162a..43b2f65 100644 --- a/.github/workflows/sync-langs.yml +++ b/.github/workflows/sync-langs.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Sync language features - run: python scripts/sync-langs.py + run: python3 scripts/sync-langs.py --ci - name: Commit if changed run: | diff --git a/crates/plotnik-langs/build.rs b/crates/plotnik-langs/build.rs index 7f74f52..c020b88 100644 --- a/crates/plotnik-langs/build.rs +++ b/crates/plotnik-langs/build.rs @@ -14,9 +14,6 @@ fn main() { }) .collect(); - // Check for features not defined in builtin.rs - check_lang_definitions(&enabled_features, &out_dir, &manifest_dir); - if enabled_features.is_empty() { println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=Cargo.toml"); @@ -142,52 +139,6 @@ fn feature_to_lang_key(feature: &str) -> String { } } -fn check_lang_definitions(enabled_features: &[String], out_dir: &Path, manifest_dir: &str) { - // Parse builtin.rs to find defined languages - let builtin_path = PathBuf::from(manifest_dir).join("src/builtin.rs"); - let builtin_src = std::fs::read_to_string(&builtin_path) - .expect("failed to read builtin.rs"); - - // Extract feature: "lang-*" patterns from define_langs! macro - let defined_langs: Vec<&str> = builtin_src - .lines() - .filter_map(|line| { - let trimmed = line.trim(); - if trimmed.starts_with("feature:") { - // feature: "lang-foo", - trimmed - .strip_prefix("feature:") - .and_then(|s| s.trim().strip_prefix('"')) - .and_then(|s| s.strip_suffix(',').or(Some(s))) - .and_then(|s| s.strip_suffix('"')) - } else { - None - } - }) - .collect(); - - let mut errors = Vec::new(); - for feature in enabled_features { - if !defined_langs.contains(&feature.as_str()) { - errors.push(format!( - "compile_error!(\"Feature `{feature}` enabled but not defined in builtin.rs. \ - Add language metadata to define_langs! macro.\");" - )); - } - } - - let check_file = out_dir.join("lang_check.rs"); - if errors.is_empty() { - std::fs::write(&check_file, "// All enabled features are defined in builtin.rs\n") - .expect("failed to write lang_check.rs"); - } else { - std::fs::write(&check_file, errors.join("\n")) - .expect("failed to write lang_check.rs"); - } - - println!("cargo::rerun-if-changed={}", builtin_path.display()); -} - fn arborium_package_to_feature(package_name: &str) -> Option { const NON_LANGUAGE_PACKAGES: &[&str] = &[ "arborium-docsrs-demo", diff --git a/crates/plotnik-langs/src/builtin.rs b/crates/plotnik-langs/src/builtin.rs index 6a6a4ed..f4e167e 100644 --- a/crates/plotnik-langs/src/builtin.rs +++ b/crates/plotnik-langs/src/builtin.rs @@ -2,9 +2,6 @@ use std::sync::{Arc, LazyLock}; use crate::{Lang, LangInner}; -// Build-time check: fails if Cargo.toml has features not defined in this file -include!(concat!(env!("OUT_DIR"), "/lang_check.rs")); - /// Language metadata for listing. #[derive(Debug, Clone)] pub struct LangInfo { diff --git a/scripts/sync-langs.py b/scripts/sync-langs.py index 7ccdf13..c9b2208 100644 --- a/scripts/sync-langs.py +++ b/scripts/sync-langs.py @@ -16,6 +16,24 @@ from pathlib import Path +def parse_builtin_langs(path: Path) -> set[str]: + """Extract lang-* features defined in builtin.rs.""" + content = path.read_text() + langs = set() + for line in content.splitlines(): + line = line.strip() + if line.startswith("feature:"): + # feature: "lang-foo", + rest = line[len("feature:"):].strip() + if rest.startswith('"'): + rest = rest[1:] + if '"' in rest: + lang = rest[:rest.index('"')] + if lang.startswith("lang-"): + langs.add(lang) + return langs + + def fetch_lang_features(version: str | None) -> tuple[str, list[str]]: """Fetch lang-* features from crates.io API.""" url = "https://crates.io/api/v1/crates/arborium" @@ -129,10 +147,40 @@ def update_plotnik_cli(path: Path, langs: list[str], dry_run: bool) -> bool: return True +def check_builtin_consistency(builtin_path: Path, langs: list[str]) -> tuple[list[str], list[str]]: + """Check if builtin.rs defines all langs from crates.io. + + Returns (added, removed) where: + - added: langs new in arborium, need to add to builtin.rs + - removed: langs removed from arborium, need to delete from builtin.rs + """ + defined = parse_builtin_langs(builtin_path) + expected = set(langs) + + added = sorted(expected - defined) + removed = sorted(defined - expected) + return added, removed + + +def post_pr_comment(added: list[str], removed: list[str]) -> None: + """Post a comment to the current PR about builtin.rs inconsistency.""" + import subprocess + + lines = ["Update `crates/plotnik-langs/src/builtin.rs`:", ""] + if added: + lines.append("Add: " + ", ".join(f"`{lang}`" for lang in added)) + if removed: + lines.append("Remove: " + ", ".join(f"`{lang}`" for lang in removed)) + + body = "\n".join(lines) + subprocess.run(["gh", "pr", "comment", "--body", body], check=True) + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--version", help="arborium version (default: latest)") parser.add_argument("--dry-run", action="store_true", help="print changes without writing") + parser.add_argument("--ci", action="store_true", help="CI mode: post PR comment on mismatch") args = parser.parse_args() version, langs = fetch_lang_features(args.version) @@ -141,6 +189,7 @@ def main(): root = Path(__file__).resolve().parent.parent langs_toml = root / "crates/plotnik-langs/Cargo.toml" cli_toml = root / "crates/plotnik-cli/Cargo.toml" + builtin_rs = root / "crates/plotnik-langs/src/builtin.rs" changed = False changed |= update_plotnik_langs(langs_toml, version, langs, args.dry_run) @@ -149,6 +198,17 @@ def main(): if args.dry_run and changed: print("\nRun without --dry-run to apply changes") + added, removed = check_builtin_consistency(builtin_rs, langs) + if added or removed: + print("\nbuiltin.rs is out of sync with arborium:") + for lang in added: + print(f" + {lang} (new in arborium, add to define_langs!)") + for lang in removed: + print(f" - {lang} (removed from arborium, delete from define_langs!)") + if args.ci: + post_pr_comment(added, removed) + raise SystemExit(1) + if __name__ == "__main__": main()