Merge pull request #124 from DeterminateSystems/cel-expressions

This commit is contained in:
Luc Perkins 2024-07-02 07:54:49 -07:00 committed by GitHub
commit 6ba8ec538e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1299 additions and 420 deletions

View File

@ -36,6 +36,21 @@ jobs:
- name: cargo test - name: cargo test
run: nix develop -c cargo test run: nix develop -c cargo test
check-flake-cel-condition:
name: Check flake.lock test (CEL condition)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Check flake.lock
run: |
nix develop -c \
cargo run -- \
--condition "supportedRefs.contains(gitRef) && numDaysOld < 30 && owner == 'NixOS'" \
./tests/flake.cel.0.lock
check-flake-dirty: check-flake-dirty:
name: Check flake.lock test (dirty 😈) name: Check flake.lock test (dirty 😈)
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04

879
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
[package] [package]
name = "flake-checker" name = "flake-checker"
version = "0.1.20" version = "0.2.0"
edition = "2021" edition = "2021"
[workspace] [workspace]
resolver = "2"
members = [".", "parse-flake-lock"] members = [".", "parse-flake-lock"]
[workspace.dependencies] [workspace.dependencies]
@ -13,11 +14,8 @@ serde_json = { version = "1.0.100", default-features = false, features = [
] } ] }
thiserror = { version = "1.0.40", default-features = false } thiserror = { version = "1.0.40", default-features = false }
[features]
default = []
allowed-refs = []
[dependencies] [dependencies]
cel-interpreter = { version = "0.7.1", default-features = false }
chrono = { version = "0.4.25", default-features = false, features = ["clock"] } chrono = { version = "0.4.25", default-features = false, features = ["clock"] }
clap = { version = "4.3.0", default-features = false, features = [ clap = { version = "4.3.0", default-features = false, features = [
"derive", "derive",
@ -27,8 +25,7 @@ clap = { version = "4.3.0", default-features = false, features = [
] } ] }
handlebars = { version = "4.3.7", default-features = false } handlebars = { version = "4.3.7", default-features = false }
is_ci = "1.1.1" is_ci = "1.1.1"
once_cell = { version = "1.19.0", default-features = false } parse-flake-lock = { path = "./parse-flake-lock" }
parse-flake-lock = { path = "parse-flake-lock" }
reqwest = { version = "0.11.18", default-features = false, features = [ reqwest = { version = "0.11.18", default-features = false, features = [
"blocking", "blocking",
"json", "json",
@ -38,3 +35,7 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
sha2 = { version = "0.10.6", default-features = false } sha2 = { version = "0.10.6", default-features = false }
thiserror = { workspace = true } thiserror = { workspace = true }
[features]
default = []
allowed-refs = []

View File

@ -14,21 +14,83 @@ nix run github:DeterminateSystems/flake-checker
nix run github:DeterminateSystems/flake-checker /path/to/flake.lock nix run github:DeterminateSystems/flake-checker /path/to/flake.lock
``` ```
Nix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs and checks that: Nix Flake Checker looks at your `flake.lock`'s root-level [Nixpkgs] inputs.
There are two ways to express flake policies:
- Any explicit Nixpkgs Git refs are in this list: * Via [config parameters](#parameters).
- `nixos-23.11` * Via [policy conditions](#policy-conditions) using [Common Expression Language][cel] (CEL).
- `nixos-23.11-small`
- `nixos-unstable`
- `nixos-unstable-small`
- `nixpkgs-23.11-darwin`
- `nixpkgs-unstable`
- Any Nixpkgs dependencies are less than 30 days old
- Any Nixpkgs dependencies have the [`NixOS`][nixos-org] org as the GitHub owner (and thus that the dependency isn't a fork or non-upstream variant)
If you're running it locally, Nix Flake Checker reports any issues via text output in your terminal. If you're running it locally, Nix Flake Checker reports any issues via text output in your terminal.
But you can also use Nix Flake Checker [in CI](#the-flake-checker-action). But you can also use Nix Flake Checker [in CI](#the-flake-checker-action).
## Supported branches
At any given time, [Nixpkgs] has a bounded set of branches that are considered *supported*.
The current list:
* `nixos-23.11`
* `nixos-23.11-small`
* `nixos-24.05`
* `nixos-24.05-small`
* `nixos-unstable`
* `nixos-unstable-small`
* `nixpkgs-23.11-darwin`
* `nixpkgs-24.05-darwin`
* `nixpkgs-unstable`
## Parameters
By default, Flake Checker verifies that:
- Any explicit Nixpkgs Git refs are in the [supported list](#supported-branches).
- Any Nixpkgs dependencies are less than 30 days old.
- Any Nixpkgs dependencies have the [`NixOS`][nixos-org] org as the GitHub owner (and thus that the dependency isn't a fork or non-upstream variant).
You can adjust this behavior via configuration (all are enabled by default but you can disable them):
Flag | Environment variable | Action | Default
:----|:---------------------|:-------|:-------
`--check-outdated` | `NIX_FLAKE_CHECKER_CHECK_OUTDATED` | Check for outdated Nixpkgs inputs | `true`
`--check-owner` | `NIX_FLAKE_CHECKER_CHECK_OWNER` | Check that Nixpkgs inputs have `NixOS` as the GitHub owner | `true`
`--check-supported` | `NIX_FLAKE_CHECKER_CHECK_SUPPORTED` | Check that Git refs for Nixpkgs inputs are supported | `true`
## Policy conditions
You can apply a CEL condition to your flake using the `--condition` flag.
Here's an example:
```shell
flake-checker --condition "has(numDaysOld) && numDaysOld < 365"
```
This would check that each Nixpkgs input in your `flake.lock` is less than 365 days old.
These variables are available in each condition:
Variable | Description
:--------|:-----------
`gitRef` | The Git reference of the input.
`numDaysOld` | The number of days old the input is.
`owner` | The input's owner (if a GitHub input).
`supportedRefs` | A list of [supported Git refs](#supported-branches) (all are branch names).
We recommend a condition *at least* this stringent:
```ruby
supportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 30) && owner == 'NixOS'
```
Note that not all Nixpkgs inputs have a `numDaysOld` field, so make sure to ensure that that field exists when checking for the number of days.
Here are some other example conditions:
```ruby
# Updated in the last two weeks
supportedRefs.contains(gitRef) && (has(numDaysOld) && numDaysOld < 14) && owner == 'NixOS'
# Check for most recent stable Nixpkgs
gitRef.contains("24.05")
```
## The Nix Flake Checker Action ## The Nix Flake Checker Action
You can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows: You can automate Nix Flake Checker by adding Determinate Systems' [Nix Flake Checker Action][action] to your GitHub Actions workflows:
@ -96,6 +158,7 @@ The `parse-flake-lock` crate doesn't yet exhaustively parse all input node types
If you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome. If you'd like to help make the parser more exhaustive, [pull requests][prs] are quite welcome.
[action]: https://github.com/DeterminateSystems/flake-checker-action [action]: https://github.com/DeterminateSystems/flake-checker-action
[cel]: https://cel.dev
[detsys]: https://determinate.systems [detsys]: https://determinate.systems
[flakes]: https://zero-to-nix.com/concepts/flakes [flakes]: https://zero-to-nix.com/concepts/flakes
[install]: https://zero-to-nix.com/start/install [install]: https://zero-to-nix.com/start/install

View File

@ -12,7 +12,7 @@
let let
inherit (stdenv.hostPlatform) system; inherit (stdenv.hostPlatform) system;
nightlyVersion = "2023-05-01"; nightlyVersion = "2024-06-13";
rustNightly = pkgs.rust-bin.nightly.${nightlyVersion}.default.override { rustNightly = pkgs.rust-bin.nightly.${nightlyVersion}.default.override {
extensions = [ "rust-src" "rust-analyzer-preview" ]; extensions = [ "rust-src" "rust-analyzer-preview" ];
targets = cargoTargets; targets = cargoTargets;
@ -80,7 +80,7 @@ let
# The Rust toolchain from rust-overlay has a dynamic libiconv in depsTargetTargetPropagated # The Rust toolchain from rust-overlay has a dynamic libiconv in depsTargetTargetPropagated
# Our static libiconv needs to take precedence # Our static libiconv needs to take precedence
++ lib.optionals pkgs.stdenv.isDarwin [ ++ lib.optionals pkgs.stdenv.isDarwin [
(libiconv.override { enableStatic = true; enableShared = false; }) libiconv
]; ];
cargoExtraArgs = "--target ${crossPlatform.rustTargetSpec}"; cargoExtraArgs = "--target ${crossPlatform.rustTargetSpec}";

View File

@ -2,31 +2,26 @@
"nodes": { "nodes": {
"crane": { "crane": {
"inputs": { "inputs": {
"flake-compat": [
"flake-compat"
],
"flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ]
"rust-overlay": "rust-overlay"
}, },
"locked": { "locked": {
"narHash": "sha256-ASliYUzlN/aTGDZ2d0FIqxq5fiz+Cwk0q2rYXgy4pB0=", "lastModified": 1717383740,
"rev": "8cb0282cb7c7b5ad7ce1c47d48f647836f8924a0", "narHash": "sha256-559HbY4uhNeoYvK3H6AMZAtVfmR3y8plXZ1x6ON/cWU=",
"revCount": 432, "rev": "b65673fce97d277934488a451724be94cc62499a",
"revCount": 580,
"type": "tarball", "type": "tarball",
"url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.14.2/018b3503-625a-71c8-96ff-c86e61bd12f7/source.tar.gz" "url": "https://api.flakehub.com/f/pinned/ipetkov/crane/0.17.3/018fdc0e-176b-7a0f-92ce-cc2d0db7b735/source.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
"url": "https://flakehub.com/f/ipetkov/crane/0.14.%2A.tar.gz" "url": "https://flakehub.com/f/ipetkov/crane/0.17.%2A"
} }
}, },
"flake-compat": { "flake-compat": {
"locked": { "locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57, "revCount": 57,
@ -35,89 +30,43 @@
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.0.1.tar.gz" "url": "https://flakehub.com/f/edolstra/flake-compat/1.0.1"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1704290814, "lastModified": 1717952948,
"narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", "narHash": "sha256-mJi4/gjiwQlSaxjA6AusXBN/6rQRaPCycR7bd8fydnQ=",
"rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", "rev": "2819fffa7fa42156680f0d282c60d81e8fb185b7",
"revCount": 492897, "revCount": 631440,
"type": "tarball", "type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2305.492897%2Brev-70bdadeb94ffc8806c0570eb5c2695ad29f0e421/018ce318-b896-7d27-b495-cc2cdb39d680/source.tar.gz" "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.631440%2Brev-2819fffa7fa42156680f0d282c60d81e8fb185b7/0190034c-678d-7039-b45c-fa38168f2500/source.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.2305.%2A.tar.gz" "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2405.%2A"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"crane": "crane", "crane": "crane",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay_2" "rust-overlay": "rust-overlay"
} }
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": [
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1696299134,
"narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1697422411, "lastModified": 1719800573,
"narHash": "sha256-eCj20wEwATLm7Bd/+/wOIdbqq9jgvS6ZxMrxujX2DxU=", "narHash": "sha256-9DLgG4T6l7cc4pJNOCcXGUwHsFfUp8KLsiwed65MdHk=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "056256f2fcf3c5a652dbc3edba9ec1a956d41f56", "rev": "648b25dd9c3acd255dc50c1eb3ca8b987856f675",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -125,21 +74,6 @@
"repo": "rust-overlay", "repo": "rust-overlay",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -1,25 +1,21 @@
{ {
inputs = { inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2305.*.tar.gz"; nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
}; };
crane = { crane = {
url = "https://flakehub.com/f/ipetkov/crane/0.14.*.tar.gz"; url = "https://flakehub.com/f/ipetkov/crane/0.17.*";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat";
inputs.flake-utils.follows = "flake-utils";
}; };
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.0.1.tar.gz"; flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.0.1";
}; };
outputs = { self, nixpkgs, flake-utils, rust-overlay, crane, ... }: outputs = { self, nixpkgs, rust-overlay, crane, ... }:
let let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec { forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "parse-flake-lock" name = "parse-flake-lock"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -107,7 +107,10 @@ impl<'de> Deserialize<'de> for FlakeLock {
let mut root_nodes = HashMap::new(); let mut root_nodes = HashMap::new();
let root_node = &nodes[&root]; let root_node = &nodes[&root];
let Node::Root(root_node) = root_node else { let Node::Root(root_node) = root_node else {
return Err(de::Error::custom(format!("root node was not a Root node, but was a {} node", root_node.variant()))); return Err(de::Error::custom(format!(
"root node was not a Root node, but was a {} node",
root_node.variant()
)));
}; };
for (root_name, root_input) in root_node.inputs.iter() { for (root_name, root_input) in root_node.inputs.iter() {
@ -204,7 +207,7 @@ pub enum Node {
Indirect(IndirectNode), Indirect(IndirectNode),
/// A [PathNode] flake input stemming from a filesystem path. /// A [PathNode] flake input stemming from a filesystem path.
Path(PathNode), Path(PathNode),
/// TODO /// Nodes that point to tarball paths.
Tarball(TarballNode), Tarball(TarballNode),
/// A "catch-all" variant for node types that don't (yet) have explicit struct definitions in /// A "catch-all" variant for node types that don't (yet) have explicit struct definitions in
/// this crate. /// this crate.
@ -372,6 +375,9 @@ pub struct TarballNode {
/// Information about the tarball input that's "locked" because it's supplied by Nix. /// Information about the tarball input that's "locked" because it's supplied by Nix.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct TarballLocked { pub struct TarballLocked {
/// The timestamp for when the input was last modified.
#[serde(alias = "lastModified")]
pub last_modified: Option<i64>,
/// The NAR hash of the input. /// The NAR hash of the input.
#[serde(alias = "narHash")] #[serde(alias = "narHash")]
pub nar_hash: String, pub nar_hash: String,

82
src/condition.rs Normal file
View File

@ -0,0 +1,82 @@
use cel_interpreter::{Context, Program, Value};
use parse_flake_lock::{FlakeLock, Node};
use crate::{
error::FlakeCheckerError,
flake::{nixpkgs_deps, num_days_old},
issue::{Issue, IssueKind},
};
const KEY_GIT_REF: &str = "gitRef";
const KEY_NUM_DAYS_OLD: &str = "numDaysOld";
const KEY_OWNER: &str = "owner";
const KEY_SUPPORTED_REFS: &str = "supportedRefs";
pub(super) fn evaluate_condition(
flake_lock: &FlakeLock,
nixpkgs_keys: &[String],
condition: &str,
supported_refs: Vec<String>,
) -> Result<Vec<Issue>, FlakeCheckerError> {
let mut issues: Vec<Issue> = vec![];
let mut ctx = Context::default();
ctx.add_variable_from_value(KEY_SUPPORTED_REFS, supported_refs);
let deps = nixpkgs_deps(flake_lock, nixpkgs_keys)?;
for (name, node) in deps {
let (git_ref, last_modified, owner) = match node {
Node::Repo(repo) => (
repo.original.git_ref,
Some(repo.locked.last_modified),
Some(repo.original.owner),
),
Node::Tarball(tarball) => (None, tarball.locked.last_modified, None),
_ => (None, None, None),
};
add_cel_variables(&mut ctx, git_ref, last_modified, owner);
match Program::compile(condition)?.execute(&ctx) {
Ok(result) => match result {
Value::Bool(b) if !b => {
issues.push(Issue {
input: name.clone(),
kind: IssueKind::Violation,
});
}
Value::Bool(b) if b => continue,
result => {
return Err(FlakeCheckerError::NonBooleanCondition(
result.type_of().to_string(),
))
}
},
Err(e) => return Err(FlakeCheckerError::CelExecution(e)),
}
}
Ok(issues)
}
fn add_cel_variables(
ctx: &mut Context,
git_ref: Option<String>,
last_modified: Option<i64>,
owner: Option<String>,
) {
ctx.add_variable_from_value(KEY_GIT_REF, value_or_empty_string(git_ref));
ctx.add_variable_from_value(
KEY_NUM_DAYS_OLD,
value_or_zero(last_modified.map(num_days_old)),
);
ctx.add_variable_from_value(KEY_OWNER, value_or_empty_string(owner));
}
fn value_or_empty_string(value: Option<String>) -> Value {
Value::from(value.unwrap_or(String::from("")))
}
fn value_or_zero(value: Option<i64>) -> Value {
Value::from(value.unwrap_or(0))
}

View File

@ -1,11 +1,17 @@
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum FlakeCheckerError { pub enum FlakeCheckerError {
#[error("CEL execution error: {0}")]
CelExecution(#[from] cel_interpreter::ExecutionError),
#[error("CEL parsing error: {0}")]
CelParse(#[from] cel_interpreter::ParseError),
#[error("env var error: {0}")] #[error("env var error: {0}")]
EnvVar(#[from] std::env::VarError), EnvVar(#[from] std::env::VarError),
#[error("couldn't parse flake.lock: {0}")] #[error("couldn't parse flake.lock: {0}")]
FlakeLock(#[from] parse_flake_lock::FlakeLockParseError), FlakeLock(#[from] parse_flake_lock::FlakeLockParseError),
#[error("http client error: {0}")] #[error("http client error: {0}")]
Http(#[from] reqwest::Error), Http(#[from] reqwest::Error),
#[error("CEL conditions must return a Boolean but returned {0} instead")]
NonBooleanCondition(String),
#[error("couldn't access flake.lock: {0}")] #[error("couldn't access flake.lock: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("couldn't parse flake.lock: {0}")] #[error("couldn't parse flake.lock: {0}")]

View File

@ -30,9 +30,9 @@ impl Default for FlakeCheckConfig {
} }
} }
fn nixpkgs_deps( pub(super) fn nixpkgs_deps(
flake_lock: &FlakeLock, flake_lock: &FlakeLock,
keys: Vec<String>, keys: &[String],
) -> Result<HashMap<String, Node>, FlakeCheckerError> { ) -> Result<HashMap<String, Node>, FlakeCheckerError> {
let mut deps: HashMap<String, Node> = HashMap::new(); let mut deps: HashMap<String, Node> = HashMap::new();
@ -49,7 +49,7 @@ fn nixpkgs_deps(
} }
} }
Node::Indirect(indirect_node) => { Node::Indirect(indirect_node) => {
if &indirect_node.original.id == key { if keys.contains(key) && &indirect_node.original.id == key {
deps.insert(key.to_string(), node); deps.insert(key.to_string(), node);
} }
} }
@ -83,14 +83,23 @@ pub(crate) fn check_flake_lock(
) -> Result<Vec<Issue>, FlakeCheckerError> { ) -> Result<Vec<Issue>, FlakeCheckerError> {
let mut issues = vec![]; let mut issues = vec![];
let deps = nixpkgs_deps(flake_lock, config.nixpkgs_keys.clone())?; let deps = nixpkgs_deps(flake_lock, &config.nixpkgs_keys)?;
for (name, node) in deps {
let (git_ref, last_modified, owner) = match node {
Node::Repo(repo) => (
repo.original.git_ref,
Some(repo.locked.last_modified),
Some(repo.original.owner),
),
Node::Tarball(tarball) => (None, tarball.locked.last_modified, None),
_ => (None, None, None),
};
for (name, dep) in deps {
if let Node::Repo(repo) = dep {
// Check if not explicitly supported // Check if not explicitly supported
if config.check_supported { if let Some(git_ref) = git_ref {
if let Some(ref git_ref) = repo.original.git_ref { // Check if not explicitly supported
if !allowed_refs.contains(git_ref) { if config.check_supported && !allowed_refs.contains(&git_ref) {
issues.push(Issue { issues.push(Issue {
input: name.clone(), input: name.clone(),
kind: IssueKind::Disallowed(Disallowed { kind: IssueKind::Disallowed(Disallowed {
@ -99,13 +108,11 @@ pub(crate) fn check_flake_lock(
}); });
} }
} }
}
if let Some(last_modified) = last_modified {
// Check if outdated // Check if outdated
if config.check_outdated { if config.check_outdated {
let now_timestamp = Utc::now().timestamp(); let num_days_old = num_days_old(last_modified);
let diff = now_timestamp - repo.locked.last_modified;
let num_days_old = Duration::seconds(diff).num_days();
if num_days_old > MAX_DAYS { if num_days_old > MAX_DAYS {
issues.push(Issue { issues.push(Issue {
@ -114,11 +121,11 @@ pub(crate) fn check_flake_lock(
}); });
} }
} }
}
if let Some(owner) = owner {
// Check that the GitHub owner is NixOS // Check that the GitHub owner is NixOS
if config.check_owner { if config.check_owner && owner.to_lowercase() != "nixos" {
let owner = repo.original.owner;
if owner.to_lowercase() != "nixos" {
issues.push(Issue { issues.push(Issue {
input: name.clone(), input: name.clone(),
kind: IssueKind::NonUpstream(NonUpstream { owner }), kind: IssueKind::NonUpstream(NonUpstream { owner }),
@ -126,27 +133,77 @@ pub(crate) fn check_flake_lock(
} }
} }
} }
}
Ok(issues) Ok(issues)
} }
pub(super) fn num_days_old(timestamp: i64) -> i64 {
let now_timestamp = Utc::now().timestamp();
let diff = now_timestamp - timestamp;
Duration::seconds(diff).num_days()
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::path::PathBuf; use std::path::PathBuf;
use crate::{ use crate::{
check_flake_lock, check_flake_lock,
condition::evaluate_condition,
issue::{Disallowed, Issue, IssueKind, NonUpstream}, issue::{Disallowed, Issue, IssueKind, NonUpstream},
FlakeCheckConfig, FlakeLock, FlakeCheckConfig, FlakeLock,
}; };
#[test] #[test]
fn test_clean_flake_locks() { fn cel_conditions() {
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json")) // (condition, expected)
.expect("couldn't deserialize allowed-refs.json file"); let cases: Vec<(&str, bool)> = vec![
(include_str!("../tests/cel-condition.txt"), true),
(
"has(gitRef) && has(numDaysOld) && has(owner) && has(supportedRefs) && supportedRefs.contains(gitRef) && owner != 'NixOS'",
false,
),
(
"has(gitRef) && has(numDaysOld) && has(owner) && has(supportedRefs) && supportedRefs.contains(gitRef) && owner != 'NixOS'",
false,
),
];
let supported_refs: Vec<String> =
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
let path = PathBuf::from("tests/flake.cel.0.lock");
for (condition, expected) in cases {
let flake_lock = FlakeLock::new(&path).unwrap();
let config = FlakeCheckConfig {
nixpkgs_keys: vec![String::from("nixpkgs")],
..Default::default()
};
let result = evaluate_condition(
&flake_lock,
&config.nixpkgs_keys,
condition,
supported_refs.clone(),
);
if expected {
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
} else {
assert!(!result.unwrap().is_empty());
}
}
}
#[test]
fn clean_flake_locks() {
let allowed_refs: Vec<String> =
serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
for n in 0..=7 { for n in 0..=7 {
let path = PathBuf::from(format!("tests/flake.clean.{n}.lock")); let path = PathBuf::from(format!("tests/flake.clean.{n}.lock"));
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock"); let flake_lock = FlakeLock::new(&path).unwrap();
let config = FlakeCheckConfig { let config = FlakeCheckConfig {
check_outdated: false, check_outdated: false,
..Default::default() ..Default::default()
@ -161,9 +218,9 @@ mod test {
} }
#[test] #[test]
fn test_dirty_flake_locks() { fn dirty_flake_locks() {
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json")) let allowed_refs: Vec<String> =
.expect("couldn't deserialize allowed-refs.json file"); serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
let cases: Vec<(&str, Vec<Issue>)> = vec![ let cases: Vec<(&str, Vec<Issue>)> = vec![
( (
"flake.dirty.0.lock", "flake.dirty.0.lock",
@ -203,22 +260,21 @@ mod test {
for (file, expected_issues) in cases { for (file, expected_issues) in cases {
let path = PathBuf::from(format!("tests/{file}")); let path = PathBuf::from(format!("tests/{file}"));
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock"); let flake_lock = FlakeLock::new(&path).unwrap();
let config = FlakeCheckConfig { let config = FlakeCheckConfig {
check_outdated: false, check_outdated: false,
..Default::default() ..Default::default()
}; };
let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()) let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()).unwrap();
.expect("couldn't run check_flake_lock function");
dbg!(&path); dbg!(&path);
assert_eq!(issues, expected_issues); assert_eq!(issues, expected_issues);
} }
} }
#[test] #[test]
fn test_explicit_nixpkgs_keys() { fn explicit_nixpkgs_keys() {
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json")) let allowed_refs: Vec<String> =
.expect("couldn't deserialize allowed-refs.json file"); serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
let cases: Vec<(&str, Vec<String>, Vec<Issue>)> = vec![( let cases: Vec<(&str, Vec<String>, Vec<Issue>)> = vec![(
"flake.explicit-keys.0.lock", "flake.explicit-keys.0.lock",
vec![String::from("nixpkgs"), String::from("nixpkgs-alt")], vec![String::from("nixpkgs"), String::from("nixpkgs-alt")],
@ -232,22 +288,21 @@ mod test {
for (file, nixpkgs_keys, expected_issues) in cases { for (file, nixpkgs_keys, expected_issues) in cases {
let path = PathBuf::from(format!("tests/{file}")); let path = PathBuf::from(format!("tests/{file}"));
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock"); let flake_lock = FlakeLock::new(&path).unwrap();
let config = FlakeCheckConfig { let config = FlakeCheckConfig {
check_outdated: false, check_outdated: false,
nixpkgs_keys, nixpkgs_keys,
..Default::default() ..Default::default()
}; };
let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()) let issues = check_flake_lock(&flake_lock, &config, allowed_refs.clone()).unwrap();
.expect("couldn't run check_flake_lock function");
assert_eq!(issues, expected_issues); assert_eq!(issues, expected_issues);
} }
} }
#[test] #[test]
fn test_missing_nixpkgs_keys() { fn missing_nixpkgs_keys() {
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json")) let allowed_refs: Vec<String> =
.expect("couldn't deserialize allowed-refs.json file"); serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
let cases: Vec<(&str, Vec<String>, String)> = vec![( let cases: Vec<(&str, Vec<String>, String)> = vec![(
"flake.clean.0.lock", "flake.clean.0.lock",
vec![String::from("nixpkgs"), String::from("foo"), String::from("bar")], vec![String::from("nixpkgs"), String::from("foo"), String::from("bar")],
@ -260,7 +315,7 @@ mod test {
)]; )];
for (file, nixpkgs_keys, expected_err) in cases { for (file, nixpkgs_keys, expected_err) in cases {
let path = PathBuf::from(format!("tests/{file}")); let path = PathBuf::from(format!("tests/{file}"));
let flake_lock = FlakeLock::new(&path).expect("couldn't create flake.lock"); let flake_lock = FlakeLock::new(&path).unwrap();
let config = FlakeCheckConfig { let config = FlakeCheckConfig {
check_outdated: false, check_outdated: false,
nixpkgs_keys, nixpkgs_keys,

View File

@ -12,6 +12,7 @@ pub(crate) enum IssueKind {
Disallowed(Disallowed), Disallowed(Disallowed),
Outdated(Outdated), Outdated(Outdated),
NonUpstream(NonUpstream), NonUpstream(NonUpstream),
Violation,
} }
#[derive(Clone, Debug, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize)]
@ -41,4 +42,8 @@ impl IssueKind {
pub(crate) fn is_non_upstream(&self) -> bool { pub(crate) fn is_non_upstream(&self) -> bool {
matches!(self, Self::NonUpstream(_)) matches!(self, Self::NonUpstream(_))
} }
pub(crate) fn is_violation(&self) -> bool {
matches!(self, Self::Violation)
}
} }

View File

@ -1,5 +1,6 @@
#[cfg(feature = "allowed-refs")] #[cfg(feature = "allowed-refs")]
mod allowed_refs; mod allowed_refs;
mod condition;
mod error; mod error;
mod flake; mod flake;
mod issue; mod issue;
@ -16,6 +17,8 @@ use std::process::ExitCode;
use clap::Parser; use clap::Parser;
use parse_flake_lock::FlakeLock; use parse_flake_lock::FlakeLock;
use crate::condition::evaluate_condition;
/// A flake.lock checker for Nix projects. /// A flake.lock checker for Nix projects.
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -86,6 +89,10 @@ struct Cli {
)] )]
markdown_summary: bool, markdown_summary: bool,
/// The Common Expression Language (CEL) policy to apply to each Nixpkgs input.
#[arg(long, short, env = "NIX_FLAKE_CHECKER_CONDITION")]
condition: Option<String>,
#[cfg(feature = "allowed-refs")] #[cfg(feature = "allowed-refs")]
// Check to make sure that Flake Checker is aware of the current supported branches. // Check to make sure that Flake Checker is aware of the current supported branches.
#[arg(long, hide = true)] #[arg(long, hide = true)]
@ -98,8 +105,8 @@ struct Cli {
} }
fn main() -> Result<ExitCode, FlakeCheckerError> { fn main() -> Result<ExitCode, FlakeCheckerError> {
let allowed_refs: Vec<String> = serde_json::from_str(include_str!("../allowed-refs.json")) let allowed_refs: Vec<String> =
.expect("couldn't deserialize allowed-refs.json file"); serde_json::from_str(include_str!("../allowed-refs.json")).unwrap();
let Cli { let Cli {
no_telemetry, no_telemetry,
@ -111,6 +118,7 @@ fn main() -> Result<ExitCode, FlakeCheckerError> {
fail_mode, fail_mode,
nixpkgs_keys, nixpkgs_keys,
markdown_summary, markdown_summary,
condition,
#[cfg(feature = "allowed-refs")] #[cfg(feature = "allowed-refs")]
check_allowed_refs, check_allowed_refs,
#[cfg(feature = "allowed-refs")] #[cfg(feature = "allowed-refs")]
@ -166,17 +174,27 @@ fn main() -> Result<ExitCode, FlakeCheckerError> {
check_supported, check_supported,
check_outdated, check_outdated,
check_owner, check_owner,
nixpkgs_keys, nixpkgs_keys: nixpkgs_keys.clone(),
fail_mode, fail_mode,
}; };
let issues = check_flake_lock(&flake_lock, &flake_check_config, allowed_refs.clone())?; let issues = if let Some(condition) = &condition {
evaluate_condition(&flake_lock, &nixpkgs_keys, condition, allowed_refs.clone())?
} else {
check_flake_lock(&flake_lock, &flake_check_config, allowed_refs.clone())?
};
if !no_telemetry { if !no_telemetry {
telemetry::TelemetryReport::make_and_send(&issues); telemetry::TelemetryReport::make_and_send(&issues);
} }
let summary = Summary::new(&issues, flake_lock_path, flake_check_config, allowed_refs); let summary = Summary::new(
&issues,
flake_lock_path,
flake_check_config,
allowed_refs,
condition,
);
if std::env::var("GITHUB_ACTIONS").is_ok() { if std::env::var("GITHUB_ACTIONS").is_ok() {
if markdown_summary { if markdown_summary {

View File

@ -10,14 +10,24 @@ use std::path::PathBuf;
use handlebars::Handlebars; use handlebars::Handlebars;
use serde_json::json; use serde_json::json;
static MARKDOWN_TEMPLATE: &str = include_str!(concat!( static CEL_MARKDOWN_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/src/templates/summary_md.hbs" "/src/templates/summary.cel.md.hbs"
)); ));
static TEXT_TEMPLATE: &str = include_str!(concat!( static CEL_TEXT_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/src/templates/summary_txt.hbs" "/src/templates/summary.cel.txt.hbs"
));
static STANDARD_MARKDOWN_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/templates/summary.standard.md.hbs"
));
static STANDARD_TEXT_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/templates/summary.standard.txt.hbs"
)); ));
pub(crate) struct Summary { pub(crate) struct Summary {
@ -25,6 +35,7 @@ pub(crate) struct Summary {
data: serde_json::Value, data: serde_json::Value,
flake_lock_path: PathBuf, flake_lock_path: PathBuf,
flake_check_config: FlakeCheckConfig, flake_check_config: FlakeCheckConfig,
condition: Option<String>,
} }
impl Summary { impl Summary {
@ -33,18 +44,41 @@ impl Summary {
flake_lock_path: PathBuf, flake_lock_path: PathBuf,
flake_check_config: FlakeCheckConfig, flake_check_config: FlakeCheckConfig,
allowed_refs: Vec<String>, allowed_refs: Vec<String>,
condition: Option<String>,
) -> Self { ) -> Self {
let disallowed: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_disallowed()).collect(); let num_issues = issues.len();
let clean = issues.is_empty();
let issue_word = if issues.len() == 1 { "issue" } else { "issues" };
let data = if let Some(condition) = &condition {
let inputs_with_violations: Vec<String> = issues
.iter()
.filter(|i| i.kind.is_violation())
.map(|i| i.input.to_owned())
.collect();
json!({
"issues": issues,
"num_issues": num_issues,
"clean": clean,
"dirty": !clean,
"issue_word": issue_word,
"condition": condition,
"inputs_with_violations": inputs_with_violations,
})
} else {
let disallowed: Vec<&Issue> =
issues.iter().filter(|i| i.kind.is_disallowed()).collect();
let outdated: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_outdated()).collect(); let outdated: Vec<&Issue> = issues.iter().filter(|i| i.kind.is_outdated()).collect();
let non_upstream: Vec<&Issue> = let non_upstream: Vec<&Issue> =
issues.iter().filter(|i| i.kind.is_non_upstream()).collect(); issues.iter().filter(|i| i.kind.is_non_upstream()).collect();
let data = json!({ json!({
"issues": issues, "issues": issues,
"num_issues": issues.len(), "num_issues": num_issues,
"clean": issues.is_empty(), "clean": clean,
"dirty": !issues.is_empty(), "dirty": !clean,
"issue_word": if issues.len() == 1 { "issue" } else { "issues" }, "issue_word": issue_word,
// Disallowed refs // Disallowed refs
"has_disallowed": !disallowed.is_empty(), "has_disallowed": !disallowed.is_empty(),
"disallowed": disallowed, "disallowed": disallowed,
@ -57,13 +91,15 @@ impl Summary {
// Constants // Constants
"max_days": MAX_DAYS, "max_days": MAX_DAYS,
"supported_ref_names": allowed_refs, "supported_ref_names": allowed_refs,
}); })
};
Self { Self {
issues: issues.to_vec(), issues: issues.to_vec(),
data, data,
flake_lock_path, flake_lock_path,
flake_check_config, flake_check_config,
condition,
} }
} }
@ -72,6 +108,18 @@ impl Summary {
if self.issues.is_empty() { if self.issues.is_empty() {
println!("The Determinate Nix Flake Checker scanned {file} and found no issues"); println!("The Determinate Nix Flake Checker scanned {file} and found no issues");
return Ok(());
}
if let Some(condition) = &self.condition {
println!(
"You supplied this CEL condition for your flake:\n\n{}",
condition
);
println!("The following inputs violate that condition:\n");
for issue in self.issues.iter() {
println!("* {}", issue.input);
}
} else { } else {
let level = if self.flake_check_config.fail_mode { let level = if self.flake_check_config.fail_mode {
"error" "error"
@ -113,6 +161,7 @@ impl Summary {
None None
} }
} }
IssueKind::Violation => Some(String::from("policy violation")),
}; };
if let Some(message) = message { if let Some(message) = message {
@ -124,10 +173,16 @@ impl Summary {
} }
pub fn generate_markdown(&self) -> Result<(), FlakeCheckerError> { pub fn generate_markdown(&self) -> Result<(), FlakeCheckerError> {
let template = if self.condition.is_some() {
CEL_MARKDOWN_TEMPLATE
} else {
STANDARD_MARKDOWN_TEMPLATE
};
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
handlebars handlebars
.register_template_string("summary.md", MARKDOWN_TEMPLATE) .register_template_string("summary.md", template)
.map_err(Box::new)?; .map_err(Box::new)?;
let summary_md = handlebars.render("summary.md", &self.data)?; let summary_md = handlebars.render("summary.md", &self.data)?;
@ -142,9 +197,15 @@ impl Summary {
} }
pub fn generate_text(&self) -> Result<(), FlakeCheckerError> { pub fn generate_text(&self) -> Result<(), FlakeCheckerError> {
let template = if self.condition.is_some() {
CEL_TEXT_TEMPLATE
} else {
STANDARD_TEXT_TEMPLATE
};
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
handlebars handlebars
.register_template_string("summary.txt", TEXT_TEMPLATE) .register_template_string("summary.txt", template)
.map_err(Box::new)?; .map_err(Box::new)?;
let summary_txt = handlebars.render("summary.txt", &self.data)?; let summary_txt = handlebars.render("summary.txt", &self.data)?;

View File

@ -0,0 +1,23 @@
# ![](https://avatars.githubusercontent.com/u/80991770?s=30) Flake checkup
{{#if clean}}
The Determinate Flake Checker Action scanned your `flake.lock` and didn't identify any issues.
All Nixpkgs inputs conform to the flake policy expressed in your supplied [Common Expression Language](https://cel.dev) condition.
{{/if}}
{{#if dirty}}
⚠️ The Determinate Nix Installer Action scanned your `flake.lock` and discovered {{num_issues}} {{issue_word}} that we recommend looking into.
You supplied this CEL condition:
```ruby
{{condition}}
```
The following inputs violate that condition:
{{#each inputs_with_violations}}
* `{{this}}`
{{/each}}
{{/if}}
<p>Feedback? Let us know at <a href="https://github.com/DeterminateSystems/flake-checker">DeterminateSystems/flake-checker</a>.</p>

View File

@ -0,0 +1,19 @@
Flake checker results:
{{#if clean}}
The flake checker scanned your flake.lock and didn't identify any issues. You specified this CEL
condition:
{{{condition}}}
All Nixpkgs inputs satisfy this condition.
{{/if}}
{{#if dirty}}
The flake checker scanned your flake.lock and discovered {{num_issues}} {{issue_word}}
that we recommend looking into. Here are the inputs that violate your supplied
condition:
{{#each inputs_with_violations}}
* {{this}}
{{/each}}
{{/if}}

6
tests/cel-condition.txt Normal file
View File

@ -0,0 +1,6 @@
supportedRefs == ['nixos-24.05', 'nixos-24.05-small', 'nixos-unstable', 'nixos-unstable-small', 'nixpkgs-24.05-darwin', 'nixpkgs-unstable']
&& owner == 'NixOS'
&& gitRef == 'nixos-unstable'
&& supportedRefs.contains(gitRef)
&& has(numDaysOld)
&& numDaysOld > 0

154
tests/flake.cel.0.lock Normal file
View File

@ -0,0 +1,154 @@
{
"nodes": {
"crane": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1684468982,
"narHash": "sha256-EoC1N5sFdmjuAP3UOkyQujSOT6EdcXTnRw8hPjJkEgc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "99de890b6ef4b4aab031582125b6056b792a4a30",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1686960236,
"narHash": "sha256-AYCC9rXNLpUWzD9hm+askOfpliLEC9kwAo7ITJc4HIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "04af42f3b31dba0ef742d254456dc4c14eedac86",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay_2"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1683080331,
"narHash": "sha256-nGDvJ1DAxZIwdn6ww8IFwzoHb2rqBP4wv/65Wt5vflk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d59c3fa0cba8336e115b376c2d9e91053aa59e56",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1684808436,
"narHash": "sha256-WG5LgB1+Oguj4H4Bpqr5GoLSc382LyGlaToiOw5xhwA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a227d4571dd1f948138a40ea8b0d0c413eefb44b",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}