Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0379597
feat: record git status.
sayakpaul Jun 19, 2026
a6fa638
up
sayakpaul Jun 19, 2026
b638689
Merge branch 'main' into dirty
sayakpaul Jun 19, 2026
cd94930
docs: regenerate builder-cli.md for create-pyproject provenance flags
sayakpaul Jun 19, 2026
27a2009
nix: format gen-flake-outputs.nix with nixfmt
sayakpaul Jun 19, 2026
b6e800e
formatting
sayakpaul Jun 19, 2026
2f18556
Merge branch 'main' into dirty
sayakpaul Jun 19, 2026
7c1176e
resolve conflicts.
sayakpaul Jul 6, 2026
d497ce9
Fix build_info wiring dropped during merge conflict resolution
sayakpaul Jul 6, 2026
e52cdc2
Scope PR to metadata capture only; defer dirty-build warnings
sayakpaul Jul 6, 2026
56fba60
keep op identifier atomic.
sayakpaul Jul 6, 2026
0c2b0ea
burn
sayakpaul Jul 6, 2026
9b6705c
use crates
sayakpaul Jul 6, 2026
2662a65
GitHash.
sayakpaul Jul 6, 2026
a58e16f
GitHash into KernelBuilderInfo.
sayakpaul Jul 6, 2026
2eb585e
KernelBuilderVersion
sayakpaul Jul 6, 2026
83b1c10
use provenance in place of buildinfo.
sayakpaul Jul 6, 2026
d40ad4b
make provenance args atomic.
sayakpaul Jul 6, 2026
f5ab629
pay attention to defaults.
sayakpaul Jul 6, 2026
cdf7710
put abstractions in the right place.
sayakpaul Jul 6, 2026
ebfedf0
document create-pyproject ping pong
sayakpaul Jul 6, 2026
26a5229
Simplify flake-provenance.nix
danieldk Jul 6, 2026
3463a54
Do not set bogus provenance fields when the builder provenance is null
danieldk Jul 6, 2026
91b7290
Avoid overlay and pass through provenance in a more focused way
danieldk Jul 6, 2026
c97b27d
Merge branch 'main' into dirty
sayakpaul Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/source/builder-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ Generate CMake files for a kernel extension build

* `-f`, `--force` — Force-overwrite existing files
* `--unique-id <UNIQUE_ID>` — This is an optional unique identifier that is suffixed to the kernel name to avoid name collisions. (e.g. Git SHA)
* `--kernel-sha <KERNEL_SHA>` — Full commit SHA of the kernel source, recorded in the build metadata. When absent, it is detected from the kernel's git repository (used by Nix builds where the source has no `.git`)
* `--kernel-dirty` — Mark the kernel source as having uncommitted changes in the build metadata. Only meaningful together with `--kernel-sha`



Expand Down
25 changes: 25 additions & 0 deletions docs/source/kernel-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@ metadata. Currently the following top-level keys are supported:
- `digest` (`Digest`, required): hash digest of the kernel files.
- `python-depends` (`list[str]`, optional): list of Python dependencies
from a curated set of Python dependencies.
- `provenance` (`dict`, optional): provenance of the build, used to flag
non-reproducible (dirty) builds. It contains two optional sub-objects:
- `kernel-builder`: the `kernel-builder` that produced the build, with its
`version` (`str`), the `sha` (`str`) of the `kernel-builder` source it was
built from (when known), and a `dirty` (`bool`) flag that is `true` when
`kernel-builder` was built from a source tree with uncommitted changes.
- `kernel`: the kernel source that was built, with its commit `sha` (`str`)
and a `dirty` (`bool`) flag that is `true` when the kernel source had
uncommitted changes.

When either `dirty` flag is set, the kernel was built from uncommitted
sources and cannot be reliably reproduced.

> **Note:** For Nix builds, dirtiness follows Nix's flake tree status, which
> also counts **untracked** files (including an uncommitted `flake.lock`).
> Commit your `flake.lock` (and avoid stray untracked files) so that clean
> builds are not flagged as dirty. Local `create-pyproject` runs only
> consider changes to tracked files.

> **Note:** The kernel `sha`/`dirty` are captured at the moment
> `create-pyproject` runs, so they describe the source tree as it was *then*.
> Running `create-pyproject` and committing afterwards is bad practice: the
> recorded provenance keeps pointing at the pre-commit state (a stale `sha`,
> and `dirty: true` if the tree was dirty) even though the committed source
> differs. Generate the metadata from the final, committed source instead.

Example `metadata.json`:

Expand Down
9 changes: 8 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@
rust-overlay,
}:
let
# Git provenance (`{ sha, dirty }` or `null`) of the `kernel-builder`
# flake itself, so it can be burned into the binary at build time and
# recorded in the build metadata of the kernels it builds.
builderProvenance = import ./nix-builder/lib/flake-provenance.nix {
inherit (nixpkgs) lib;
} self;

inherit
(import ./nix-builder/lib/build-sets.nix {
inherit nixpkgs rust-overlay;
inherit nixpkgs rust-overlay builderProvenance;
})
mkBuildSets
partitionBuildSetsBySystem
Expand Down
1 change: 1 addition & 0 deletions kernel-builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ reqwest = { version = "0.13", features = ["blocking"] }
walkdir = "2"

[build-dependencies]
built = { version = "0.8", features = ["git2"] }
minijinja-embed = "2.5"

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions kernel-builder/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
fn main() {
minijinja_embed::embed_templates!("src/pyproject/templates");
built::write_built_file().expect("Failed to acquire build-time information");
}
29 changes: 28 additions & 1 deletion kernel-builder/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ use validate_builds::check_builds;

use crate::util::parse_and_validate_compat;

/// Build-time metadata gathered by the [`built`](https://crates.io/crates/built)
/// crate (see `build.rs`), including the `kernel-builder` git provenance that is
/// recorded in the build metadata of the kernels it builds.
mod built_info {
include!(concat!(env!("OUT_DIR"), "/built.rs"));
}

#[derive(Args, Debug)]
struct NixArgs {
/// Maximum number of parallel Nix build jobs.
Expand Down Expand Up @@ -174,6 +181,17 @@ enum Commands {
/// kernel name to avoid name collisions. (e.g. Git SHA)
#[arg(long)]
unique_id: Option<String>,

/// Full commit SHA of the kernel source, recorded in the build
/// metadata. When absent, it is detected from the kernel's git
/// repository (used by Nix builds where the source has no `.git`).
#[arg(long)]
kernel_sha: Option<String>,

/// Mark the kernel source as having uncommitted changes in the build
/// metadata. Only meaningful together with `--kernel-sha`.
#[arg(long)]
kernel_dirty: bool,
},

/// Spawn a kernel development shell.
Expand Down Expand Up @@ -365,7 +383,16 @@ fn main() -> Result<()> {
force,
target_dir,
unique_id,
} => create_pyproject(kernel_dir, target_dir, force, unique_id),
kernel_sha,
kernel_dirty,
} => create_pyproject(
kernel_dir,
target_dir,
force,
unique_id,
kernel_sha,
kernel_dirty,
),
Commands::Devshell {
kernel_dir,
variant,
Expand Down
22 changes: 20 additions & 2 deletions kernel-builder/src/pyproject/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use eyre::Result;
use itertools::Itertools;

use kernels_data::config::{Backend, Build};
use kernels_data::metadata::Metadata;
use kernels_data::metadata::{GitHash, KernelBuilderVersion, Metadata, Provenance};

use crate::pyproject::ops_identifier::KernelIdentifier;
use crate::pyproject::FileSet;
Expand All @@ -20,16 +20,34 @@ pub fn write_compat_py(file_set: &mut FileSet) -> Result<()> {
Ok(())
}

/// `kernel-builder` provenance baked in at compile time.
///
/// The git provenance is gathered by the `built` crate (see `build.rs`). In
/// build sandboxes without a `.git` (e.g. Nix), the derivation supplies it via
/// `built`'s `BUILT_OVERRIDE_hf_kernel_builder_GIT_*` environment variables.
pub(crate) fn kernel_builder_version() -> KernelBuilderVersion {
let git = crate::built_info::GIT_COMMIT_HASH.map(|sha| GitHash {
sha: sha.to_owned(),
dirty: crate::built_info::GIT_DIRTY.unwrap_or(false),
});
KernelBuilderVersion {
version: env!("CARGO_PKG_VERSION").to_owned(),
git,
}
}

pub fn write_metadata(
build: &Build,
kernel_id: &KernelIdentifier,
provenance: Option<&Provenance>,
file_set: &mut FileSet,
) -> Result<()> {
for backend in &Backend::all() {
let writer = file_set.entry(format!("metadata-{backend}.json"));

let metadata =
let mut metadata =
Metadata::for_backend(build, kernel_id.to_string_for_backend(*backend), *backend)?;
metadata.provenance = provenance.cloned();

serde_json::to_writer_pretty(writer, &metadata)?;
}
Expand Down
37 changes: 31 additions & 6 deletions kernel-builder/src/pyproject/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{

use eyre::{bail, Result};
use kernels_data::config::{Build, Framework};
use kernels_data::metadata::{GitHash, Provenance};
use minijinja::Environment;

use crate::{
Expand All @@ -23,17 +24,21 @@ mod tvm_ffi;

pub use fileset::FileSet;

pub fn create_pyproject_file_set(build: Build, kernel_id: &KernelIdentifier) -> Result<FileSet> {
pub fn create_pyproject_file_set(
build: Build,
kernel_id: &KernelIdentifier,
provenance: Option<&Provenance>,
) -> Result<FileSet> {
let mut env = Environment::new();
env.set_trim_blocks(true);
minijinja_embed::load_templates!(&mut env);

let file_set = if matches!(build.framework, Framework::TvmFfi(_)) {
tvm_ffi::write_tvm_ffi_ext(&env, &build, kernel_id)?
tvm_ffi::write_tvm_ffi_ext(&env, &build, kernel_id, provenance)?
} else if build.is_noarch() {
torch::write_torch_ext_noarch(&env, &build, kernel_id)?
torch::write_torch_ext_noarch(&env, &build, kernel_id, provenance)?
} else {
torch::write_torch_ext(&env, &build, kernel_id)?
torch::write_torch_ext(&env, &build, kernel_id, provenance)?
};

Ok(file_set)
Expand All @@ -44,12 +49,31 @@ pub fn create_pyproject(
target_dir: Option<PathBuf>,
force: bool,
unique_id: Option<String>,
kernel_sha: Option<String>,
kernel_dirty: bool,
Comment on lines +52 to +53

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can lead to incorrect metadata in case someone generates pyproject.toml and then adds more commits. So doing this from CMake (and then giving the kernel build sandbox access to git files) seems like the way to do it. For the Nix build (since we probably do not want to copy all the git objects to the sandbox), we could pass the SHA to CMake. Not sure how important this is though.

This is currently also an issue for the sha hash we use in the ops name. We are generating it when making the project files, but it should probably be done inside CMake (but would better be a separate PR to avoid PRs becoming too big).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will table it for later as mentioned.

We go with our approach and document the bad practice of running create-pyproject and committing after that.

) -> Result<()> {
let kernel_dir = check_or_infer_kernel_dir(kernel_dir)?;
let target_dir = check_or_infer_target_dir(&kernel_dir, target_dir)?;
let build = parse_build(&kernel_dir)?;

// Assemble build provenance. Prefer an explicitly provided kernel git
// provenance (e.g. passed by Nix builds, where the source tree has no
// `.git`); fall back to detecting it from the kernel's git repository. The
// `kernel-builder` provenance is always the one baked into this binary at
// compile time.
let kernel = kernel_sha
.map(|sha| GitHash {
sha,
dirty: kernel_dirty,
})
.or_else(|| ops_identifier::git_hash(&kernel_dir));
let provenance = Provenance {
kernel_builder: common::kernel_builder_version(),
kernel,
};

let kernel_id = KernelIdentifier::new(&kernel_dir, build.general.name.python_name(), unique_id);
let file_set = create_pyproject_file_set(build, &kernel_id)?;
let file_set = create_pyproject_file_set(build, &kernel_id, Some(&provenance))?;
file_set.write(&target_dir, force)?;

Ok(())
Expand All @@ -65,9 +89,10 @@ pub fn clean_pyproject(
let kernel_dir = check_or_infer_kernel_dir(kernel_dir)?;
let target_dir = check_or_infer_target_dir(&kernel_dir, target_dir)?;
let build = parse_build(&kernel_dir)?;
// Provenance is irrelevant when computing the set of files to clean.
let kernel_id = KernelIdentifier::new(&kernel_dir, build.general.name.python_name(), unique_id);

let generated_files = create_pyproject_file_set(build, &kernel_id)?.into_names();
let generated_files = create_pyproject_file_set(build, &kernel_id, None)?.into_names();

if generated_files.is_empty() {
eprintln!("No generated artifacts found to clean.");
Expand Down
40 changes: 40 additions & 0 deletions kernel-builder/src/pyproject/ops_identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::Path;
use eyre::{Result, WrapErr};
use git2::Repository;
use kernels_data::config::Backend;
use kernels_data::metadata::GitHash;
use rand::Rng;

pub fn random_identifier() -> String {
Expand All @@ -28,6 +29,25 @@ pub fn git_identifier(target_dir: impl AsRef<Path>) -> Result<String> {
Ok(if dirty { format!("{rev}_dirty") } else { rev })
}

pub fn git_hash(target_dir: impl AsRef<Path>) -> Option<GitHash> {
let repo = Repository::discover(target_dir.as_ref()).ok()?;
let head = repo.head().ok()?;
let commit = head.peel_to_commit().ok()?;
let sha = commit.id().to_string();

let mut status_options = git2::StatusOptions::new();
status_options.include_untracked(false); // Ignore untracked files (like generated CMake files)
status_options.exclude_submodules(true);
let dirty = !repo.statuses(Some(&mut status_options)).ok()?.is_empty();

Some(GitHash { sha, dirty })
}

/// Uniquely identifies a kernel for the purpose of ops-name generation.
///
/// This type is intentionally small and atomic: it carries only what is
/// needed to name a kernel's ops. Build provenance (git SHA, dirty flags,
/// `kernel-builder` version) is handled separately when writing metadata.
pub struct KernelIdentifier {
name: String,
unique_id: String,
Expand Down Expand Up @@ -63,3 +83,23 @@ impl KernelIdentifier {
&self.unique_id
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn kernel_identifier_uses_provided_unique_id() {
let tmp = tempfile::tempdir().unwrap();
let id = KernelIdentifier::new(tmp.path(), "relu".to_owned(), Some("rev123".to_owned()));

assert_eq!(id.unique_id(), "rev123");
assert_eq!(id.to_string_for_backend(Backend::Cuda), "_relu_cuda_rev123");
}

#[test]
fn git_hash_is_none_in_non_git_dir() {
let tmp = tempfile::tempdir().unwrap();
assert!(git_hash(tmp.path()).is_none());
}
}
4 changes: 3 additions & 1 deletion kernel-builder/src/pyproject/torch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::path::PathBuf;
use eyre::{bail, Context, Result};
use itertools::Itertools;
use kernels_data::config::{Backend, Build, General, Torch};
use kernels_data::metadata::Provenance;
use minijinja::context;

use crate::pyproject::common::{
Expand Down Expand Up @@ -271,6 +272,7 @@ pub fn write_torch_ext(
env: &minijinja::Environment,
build: &Build,
kernel_id: &KernelIdentifier,
provenance: Option<&Provenance>,
) -> Result<FileSet> {
let torch_ext = match build.framework.torch() {
Some(torch_ext) => torch_ext,
Expand All @@ -296,7 +298,7 @@ pub fn write_torch_ext(

write_torch_registration_macros(&mut file_set)?;

write_metadata(build, kernel_id, &mut file_set)?;
write_metadata(build, kernel_id, provenance, &mut file_set)?;

Ok(file_set)
}
Loading
Loading