feat(nixgen): write the hash of the schema as the first line of the output

Consumers that check whether generated options are older than their schema
(komorebi-for-kde's `schema-fresh`) compare a `# schema-sha256: <hex>` first
line with the hash of schema.json. That line was added by hand in the
consumer's justfile with `printf` and `nix hash file`, so the tool's own output
could never pass the check.

Write the line from the tool. `schema_hash_header` returns it for the raw bytes
of the schema file, in lowercase hex, which is the digest `nix hash file --type
sha256 --base16` prints. The binary puts it before the generated code; the
library functions that return generated code are unchanged, so library users get
no new line unless they ask for it.

Adds the sha2 dependency.

Verified: cargo test passes (49, including two known SHA-256 vectors). On the
komorebi-for-kde schema.json the first line equals `nix hash file --type sha256
--base16 schema.json`.
This commit is contained in:
LGUG2Z committed 2026-10-31 08:40:11 -07:00
1 parent e28a8b7956
commit 50ce2860c9
4 files changed
+110 -2

No files matched your search

Generated
+72
View File
@@ -88,6 +88,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -168,6 +177,35 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dyn-clone"
version = "1.0.20"
@@ -194,6 +232,16 @@ dependencies = [
"once_cell",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "gimli"
version = "0.32.3"
@@ -375,6 +423,7 @@ dependencies = [
"schemars",
"serde",
"serde_json_lenient",
"sha2",
"thiserror",
]
@@ -456,6 +505,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -562,6 +622,12 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.22"
@@ -580,6 +646,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
+1
View File
@@ -18,4 +18,5 @@ color-eyre = "0.6"
schemars = "1.1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
sha2 = "0.10"
thiserror = "2"
+29
View File
@@ -32,6 +32,22 @@ pub trait SchemaExt: JsonSchema {
// Implement SchemaExt for all types that implement JsonSchema
impl<T: JsonSchema> SchemaExt for T {}
/// The first line of a generated file: the SHA-256 of the schema file's bytes, in lowercase hex
///
/// This is the same digest as `nix hash file --type sha256 --base16 <schema>`, so a check can compare
/// the header of a generated file with the hash of the schema it says it was made from.
pub fn schema_hash_header(schema_bytes: &[u8]) -> String {
use sha2::Digest;
use std::fmt::Write;
let mut header = String::from("# schema-sha256: ");
for byte in sha2::Sha256::digest(schema_bytes) {
write!(header, "{byte:02x}").expect("writing to a String cannot fail");
}
header.push('\n');
header
}
/// Generate Nix options from a JSON schema value
pub fn generate_from_schema(
schema: &Value,
@@ -56,6 +72,19 @@ mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_schema_hash_header() {
// the SHA-256 of the empty input and of "abc"
assert_eq!(
schema_hash_header(b""),
"# schema-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n"
);
assert_eq!(
schema_hash_header(b"abc"),
"# schema-sha256: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad\n"
);
}
#[test]
fn test_generate_from_schema() {
let schema = json!({
+8 -2
View File
@@ -1,6 +1,7 @@
use clap::Parser;
use schemars_nixgen::GeneratorOptions;
use schemars_nixgen::generate_from_schema;
use schemars_nixgen::schema_hash_header;
use std::fs;
use std::io::Write;
use std::io::{self};
@@ -42,8 +43,13 @@ fn main() -> color_eyre::Result<()> {
root_description: cli.description,
};
// Generate Nix code
let nix_code = generate_from_schema(&schema, &options)?;
// Generate Nix code, with the hash of the schema file first so a check can tell when the options
// are older than the schema
let nix_code = format!(
"{}{}",
schema_hash_header(schema_str.as_bytes()),
generate_from_schema(&schema, &options)?
);
// Write output
if let Some(output_path) = cli.output {