Files
bruno 3254c33ae5 feat: versionnement automatique basé sur git (build number + hash)
- build.rs génère version dynamique : X.Y.Z+build.N-abc1234
- Version injectée dans l'API (IMAGOD_BUILD_VERSION) et pages HTML
- Suffixe -dirty si modifications non commitées
- replace_all_versions() consomme le suffixe build metadata
- Suppression auto-changelog — reste manuel pour contrôle qualité
2026-06-25 13:27:13 -04:00

375 lines
13 KiB
Rust

//! Build script — génère les pages HTML à partir du Markdown
//!
//! Exécuté à chaque `cargo build`. Garantit que :
//! 1. changelog.html est généré depuis CHANGELOG.md
//! 2. help.html est généré depuis ROADMAP.md
//! 3. Le badge version dans index.html suit la version calculée
//! 4. La version est auto-générée : base Cargo.toml + compteur commits git
//! 5. CHANGELOG.md reçoit une entrée vide pour la nouvelle version si absente
use std::fs;
use std::path::Path;
use std::process::Command;
fn main() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let base_version = env!("CARGO_PKG_VERSION");
// Version dynamique = version de base + numéro de build (compteur commits git)
let version = compute_version(base_version, manifest_dir);
let frontend_dir = manifest_dir.join("src/frontend");
// === changelog.html depuis CHANGELOG.md ===
let changelog_tmpl = frontend_dir.join("changelog.tmpl.html");
let changelog_md = manifest_dir.join("CHANGELOG.md");
let changelog_out = frontend_dir.join("changelog.html");
if changelog_tmpl.exists() && changelog_md.exists() {
let md = fs::read_to_string(&changelog_md).unwrap_or_default();
let html = simple_md_to_html(&md);
let tmpl = fs::read_to_string(&changelog_tmpl).unwrap_or_default();
let page = tmpl
.replace("{{VERSION}}", &version)
.replace("{{CHANGELOG_CONTENT}}", &html);
fs::write(&changelog_out, &page).ok();
println!("cargo:rerun-if-changed=CHANGELOG.md");
println!("cargo:rerun-if-changed=src/frontend/changelog.tmpl.html");
}
// === help.html depuis ROADMAP.md ===
let help_tmpl = frontend_dir.join("help.tmpl.html");
let roadmap_md = manifest_dir.join("ROADMAP.md");
let help_out = frontend_dir.join("help.html");
if help_tmpl.exists() && roadmap_md.exists() {
let md = fs::read_to_string(&roadmap_md).unwrap_or_default();
let html = simple_md_to_html(&md);
let tmpl = fs::read_to_string(&help_tmpl).unwrap_or_default();
let page = tmpl
.replace("{{VERSION}}", &version)
.replace("{{HELP_CONTENT}}", &html);
fs::write(&help_out, &page).ok();
println!("cargo:rerun-if-changed=ROADMAP.md");
println!("cargo:rerun-if-changed=src/frontend/help.tmpl.html");
}
// === Injection version dans index.html ===
let index_path = frontend_dir.join("index.html");
if index_path.exists() {
let mut content = fs::read_to_string(&index_path).unwrap_or_default();
// Remplacer tous les badges de version vX.Y.Z par la version courante
content = replace_all_versions(&content, &version);
fs::write(&index_path, &content).ok();
println!("cargo:rerun-if-changed=src/frontend/index.html");
}
println!("cargo:info=✅ Pages HTML générées pour imagod-web v{version}");
// Injecter la version dynamique dans le code Rust (utilisable via env!("IMAGOD_BUILD_VERSION"))
println!("cargo:rustc-env=IMAGOD_BUILD_VERSION={version}");
}
/// Calcule la version dynamique : base + compteur de commits git
fn compute_version(base: &str, manifest_dir: &Path) -> String {
// Récupérer le compteur de commits
let commit_count = Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.current_dir(manifest_dir)
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "0".to_string());
// Récupérer le hash court du commit
let short_hash = Command::new("git")
.args(["rev-parse", "--short=7", "HEAD"])
.current_dir(manifest_dir)
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
// Vérifier si le working tree est dirty
let dirty = Command::new("git")
.args(["diff", "--quiet"])
.current_dir(manifest_dir)
.status()
.map(|s| !s.success())
.unwrap_or(false);
let suffix = if dirty { "-dirty" } else { "" };
format!("{}+build.{}-{}{}", base, commit_count, short_hash, suffix)
}
/// Module minimal pour la date du jour (zéro dépendance externe)
mod chrono_lite {
use std::process::Command;
pub fn today() -> String {
Command::new("date")
.args(["+%d %B %Y"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "Date inconnue".to_string())
}
}
/// Mini convertisseur Markdown → HTML (zéro dépendance)
fn simple_md_to_html(md: &str) -> String {
let mut html = String::new();
let mut in_code_block = false;
let mut code_content = String::new();
for line in md.lines() {
if line.starts_with("```") {
if in_code_block {
html.push_str("<pre><code>");
html.push_str(&html_escape(&code_content));
html.push_str("</code></pre>\n");
code_content.clear();
in_code_block = false;
} else {
in_code_block = true;
}
continue;
}
if in_code_block {
if !code_content.is_empty() {
code_content.push('\n');
}
code_content.push_str(line);
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("### ") {
html.push_str(&format!("<h3>{}</h3>\n", html_escape(&trimmed[4..])));
} else if trimmed.starts_with("## ") {
html.push_str(&format!("<h2>{}</h2>\n", html_escape(&trimmed[3..])));
} else if trimmed.starts_with("# ") {
html.push_str(&format!("<h1>{}</h1>\n", html_escape(&trimmed[2..])));
} else if trimmed == "---" {
html.push_str("<hr>\n");
} else if trimmed.starts_with('|') && trimmed.ends_with('|') {
render_table(&mut html, trimmed);
} else if trimmed.starts_with("- [x]") || trimmed.starts_with("- [X]") {
html.push_str(&format!(
"<li><input type=\"checkbox\" checked disabled> {}</li>\n",
process_inline(&trimmed[5..].trim())
));
} else if trimmed.starts_with("- [ ]") {
html.push_str(&format!(
"<li><input type=\"checkbox\" disabled> {}</li>\n",
process_inline(&trimmed[5..].trim())
));
} else if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
html.push_str(&format!("<li>{}</li>\n", process_inline(&trimmed[2..])));
} else {
html.push_str(&format!("<p>{}</p>\n", process_inline(trimmed)));
}
}
if in_code_block && !code_content.is_empty() {
html.push_str("<pre><code>");
html.push_str(&html_escape(&code_content));
html.push_str("</code></pre>\n");
}
html
}
fn render_table(html: &mut String, line: &str) {
let cells: Vec<&str> = line.split('|').collect();
let is_sep = cells.iter().any(|c| {
let t = c.trim();
t.contains("---") || t.contains(":--") || t.is_empty() && !t.contains(char::is_alphanumeric)
});
if is_sep {
return;
}
let content_cells: Vec<&str> = cells.iter().filter(|c| !c.trim().is_empty()).copied().collect();
if content_cells.is_empty() {
return;
}
html.push_str("<tr>");
for cell in &content_cells {
html.push_str(&format!("<td>{}</td>", process_inline(cell.trim())));
}
html.push_str("</tr>\n");
}
fn process_inline(text: &str) -> String {
// Échapper HTML d'abord
let text = html_escape(text);
// Puis appliquer les formats
let text = replace_pattern(&text, "**", "strong");
let text = replace_pattern(&text, "__", "strong");
let text = replace_pattern(&text, "*", "em");
let text = replace_pattern(&text, "_", "em");
let text = replace_pattern(&text, "~~", "del");
let text = replace_pattern(&text, "`", "code");
// Liens [texte](url)
replace_links(&text)
}
fn replace_pattern(text: &str, marker: &str, tag: &str) -> String {
let mut result = String::new();
let mut chars = text.chars().peekable();
let marker_len = marker.chars().count();
let marker_chars: Vec<char> = marker.chars().collect();
while let Some(&c) = chars.peek() {
// Vérifier si on est au début d'un marqueur
if c == marker_chars[0] {
let mut candidate = String::new();
let mut clone = chars.clone();
for _ in 0..marker_len {
if let Some(&mc) = clone.peek() {
candidate.push(mc);
clone.next();
}
}
if candidate == marker {
// Avancer après le marqueur d'ouverture
for _ in 0..marker_len {
chars.next();
}
// Chercher le marqueur de fermeture
let mut inner = String::new();
let mut found = false;
while let Some(&c2) = chars.peek() {
if c2 == marker_chars[0] {
let mut close_candidate = String::new();
let mut close_clone = chars.clone();
for _ in 0..marker_len {
if let Some(&mc) = close_clone.peek() {
close_candidate.push(mc);
close_clone.next();
}
}
if close_candidate == marker {
for _ in 0..marker_len {
chars.next();
}
found = true;
break;
}
}
inner.push(chars.next().unwrap());
}
if found {
result.push_str(&format!("<{tag}>{inner}</{tag}>"));
} else {
result.push_str(&candidate);
result.push_str(&inner);
}
continue;
}
}
result.push(chars.next().unwrap());
}
result
}
fn replace_links(text: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '[' {
let mut j = i + 1;
let mut link_text = String::new();
while j < chars.len() && chars[j] != ']' {
link_text.push(chars[j]);
j += 1;
}
if j + 2 < chars.len() && chars[j] == ']' && chars[j + 1] == '(' {
let mut k = j + 2;
let mut url = String::new();
while k < chars.len() && chars[k] != ')' {
url.push(chars[k]);
k += 1;
}
if k < chars.len() && chars[k] == ')' {
result.push_str(&format!("<a href=\"{url}\">{link_text}</a>"));
i = k + 1;
continue;
}
}
// Pas un lien valide, remettre le [
result.push('[');
result.push_str(&link_text);
i = j;
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn html_escape(text: &str) -> String {
text.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
/// Remplace toutes les occurrences de `vX.Y.Z` dans le HTML par la version courante
fn replace_all_versions(html: &str, version: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = html.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == 'v'
&& i + 1 < chars.len()
&& chars[i + 1].is_ascii_digit()
{
// Essayer de parser une version vX.Y.Z ou vX.Y
let mut j = i + 1;
let mut dots = 0;
while j < chars.len() {
if chars[j].is_ascii_digit() {
j += 1;
} else if chars[j] == '.' && dots < 2 && j + 1 < chars.len() && chars[j + 1].is_ascii_digit() {
dots += 1;
j += 1;
} else {
break;
}
}
// Consommer le suffixe build metadata (+ ou - suivi d'alphanum/dots/tirets)
while j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
j += 1;
while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '.' || chars[j] == '-') {
j += 1;
}
}
if dots >= 1 {
// C'est une version — remplacer
result.push_str(&format!("v{version}"));
i = j;
continue;
}
}
result.push(chars[i]);
i += 1;
}
result
}