use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
const GENERATED_SUBDIR: &str = "cmake_build/ccgo_generated";
#[derive(Debug, Clone)]
pub struct GeneratedVerinfo {
pub header: PathBuf,
pub source: PathBuf,
pub identity: String,
}
pub fn generate(
project_root: &Path,
header_include_subdir: &str,
project_name: &str,
identity: &str,
) -> Result<GeneratedVerinfo> {
let generated_root = project_root.join(GENERATED_SUBDIR);
let header_dir = generated_root.join("include").join(header_include_subdir);
let source_dir = generated_root.join("src");
fs::create_dir_all(&header_dir).with_context(|| {
format!(
"Failed to create verinfo header dir {}",
header_dir.display()
)
})?;
fs::create_dir_all(&source_dir).with_context(|| {
format!(
"Failed to create verinfo source dir {}",
source_dir.display()
)
})?;
let macro_name = format!("{}_CCGO_PROJECT_VERIDENTITY", project_name.to_uppercase());
let symbol_name = format!("{}_ccgo_project_veridentity", project_name.to_lowercase());
let header_path = header_dir.join("verinfo_ccgo_gen.h");
let header = render_header(¯o_name, &symbol_name, identity);
write_if_changed(&header_path, &header)?;
let source_path = source_dir.join("verinfo_ccgo_gen.cc");
let source = render_source(¯o_name, &symbol_name, identity, project_name);
write_if_changed(&source_path, &source)?;
Ok(GeneratedVerinfo {
header: header_path,
source: source_path,
identity: identity.to_string(),
})
}
fn render_header(macro_name: &str, symbol_name: &str, identity: &str) -> String {
format!(
r#"/*
* Auto-generated by ccgo — do not edit.
* Regenerated on every `ccgo build`.
*
* Build identity macro + symbol declaration.
*/
#ifndef CCGO_VERINFO_CCGO_GEN_H_
#define CCGO_VERINFO_CCGO_GEN_H_
#define {macro_name}_STRING "{identity}"
#define {macro_name} "{macro_name}=" {macro_name}_STRING
#ifdef __cplusplus
extern "C" {{
#endif
/* Symbol always present in the compiled artifact; `strings` can find it. */
extern const char {symbol_name}[];
#ifdef __cplusplus
}}
#endif
#endif /* CCGO_VERINFO_CCGO_GEN_H_ */
"#
)
}
fn render_source(
macro_name: &str,
symbol_name: &str,
identity: &str,
project_name: &str,
) -> String {
let note_name = project_name.to_lowercase();
format!(
r#"/*
* Auto-generated by ccgo — do not edit.
* Regenerated on every `ccgo build`.
*
* Embeds `{macro_name}` into the compiled artifact so the source state a
* binary came from can be recovered post-ship:
*
* strings libfoo.so | grep VERIDENTITY=
*
* On ELF targets the identity is additionally placed in the
* `.note.ccgo.project` section, which `readelf -n` can pretty-print.
*
* Compiled as C++ on every platform (Apple included) — the code body is
* pure C, so the .cc extension is safe everywhere and lets the CMake
* template pick up a single translation unit without per-platform fan-out.
*/
#include <stddef.h>
#ifdef __cplusplus
extern "C" {{
#endif
/* Keep the identity string in the final artifact through every stage:
* - `used` defeats compile-time DCE
* - `visibility("default")` overrides `-fvisibility=hidden`
* - `retain` blocks the linker's dead-strip
* (Clang 13+/GCC 11+ attribute; silently ignored by older toolchains
* which already preserved the symbol thanks to `used`).
* We also add a self-referencing anchor function marked `used` so that even
* with LTO the const is proven reachable from the public symbol graph. */
#if defined(__clang__) || (defined(__GNUC__) && __GNUC__ >= 11)
# define CCGO_USED __attribute__((used, retain, visibility("default")))
#elif defined(__clang__) || defined(__GNUC__)
# define CCGO_USED __attribute__((used, visibility("default")))
#else
# define CCGO_USED
#endif
/* Prefixed so `strings | grep VERIDENTITY=` returns a self-identifying line. */
CCGO_USED const char {symbol_name}[] = "{macro_name}={identity}";
/* Exported accessor — keeps the const reachable through LTO. */
CCGO_USED const char *{symbol_name}_get(void) {{
return {symbol_name};
}}
/* Library-load constructor — forces the linker to pull this translation
* unit into the final dylib/so, even when no other object references
* {symbol_name} directly. Constructors are never dead-stripped by ld.
* Made non-static + visibility("default") so the symbol is externally
* visible and Apple's `ld` treats it as an anchor for the .o. The asm
* volatile reference defeats LTO's attempt to fold it away. */
#if defined(__clang__) || defined(__GNUC__)
CCGO_USED __attribute__((constructor))
void {symbol_name}_register(void) {{
__asm__ volatile ("" : : "r"({symbol_name}) : "memory");
}}
#endif
#if defined(__ELF__)
/* ELF note: name="{note_name}\0", desc="{identity}\0", type=0 (ccgo-defined). */
struct ccgo_note_{note_name} {{
unsigned int namesz;
unsigned int descsz;
unsigned int type;
char name[sizeof("{note_name}")];
char desc[sizeof("{identity}")];
}};
CCGO_USED
__attribute__((section(".note.ccgo.project"), aligned(4)))
static const struct ccgo_note_{note_name} ccgo_note_{note_name}_instance = {{
sizeof("{note_name}"),
sizeof("{identity}"),
0,
"{note_name}",
"{identity}"
}};
#endif
#ifdef __cplusplus
}} /* extern "C" */
#endif
"#
)
}
fn write_if_changed(path: &Path, content: &str) -> Result<()> {
if let Ok(existing) = fs::read_to_string(path) {
if existing == content {
return Ok(());
}
}
fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn generates_expected_files() {
let tmp = TempDir::new().unwrap();
let out = generate(
tmp.path(),
"foo/base/",
"foo",
"1.2.3-20260101000000-abc1234",
)
.unwrap();
let expected_header = tmp
.path()
.join("cmake_build/ccgo_generated/include/foo/base/verinfo_ccgo_gen.h");
let expected_source = tmp
.path()
.join("cmake_build/ccgo_generated/src/verinfo_ccgo_gen.cc");
assert_eq!(out.header, expected_header);
assert_eq!(out.source, expected_source);
assert!(out.header.is_file());
assert!(out.source.is_file());
let header = fs::read_to_string(&out.header).unwrap();
assert!(header.contains("FOO_CCGO_PROJECT_VERIDENTITY_STRING"));
assert!(header.contains("1.2.3-20260101000000-abc1234"));
assert!(header.contains("CCGO_VERINFO_CCGO_GEN_H_"));
let source = fs::read_to_string(&out.source).unwrap();
assert!(source.contains("foo_ccgo_project_veridentity"));
assert!(source.contains(".note.ccgo.project"));
}
#[test]
fn write_if_changed_skips_identical_content() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("f.txt");
write_if_changed(&path, "hello").unwrap();
let mtime1 = fs::metadata(&path).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
write_if_changed(&path, "hello").unwrap();
let mtime2 = fs::metadata(&path).unwrap().modified().unwrap();
assert_eq!(mtime1, mtime2);
}
}