mirror of
https://github.com/DeterminateSystems/flake-checker.git
synced 2024-09-11 14:25:44 +03:00
Merge pull request #49 from DeterminateSystems/flake-lock-lib
Create a separate crate for the flake.lock parsing logic
This commit is contained in:
commit
e915cfa5c6
@ -11,3 +11,6 @@ insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
|
||||
[*.hbs]
|
||||
insert_final_newline = false
|
||||
|
42
Cargo.lock
generated
42
Cargo.lock
generated
@ -210,6 +210,7 @@ dependencies = [
|
||||
"clap",
|
||||
"handlebars",
|
||||
"is_ci",
|
||||
"parse-flake-lock",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -574,6 +575,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "parse-flake-lock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
@ -638,18 +648,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.59"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.28"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -815,18 +825,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.170"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "a56657f512baabca8f840542f9ca8152aecf182c473c26e46e58d6aab4f6e439"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.163"
|
||||
version = "1.0.170"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
checksum = "77d477848e6b23adba0db397777d5aad864555bc17fd9c89abb3b8009788b7b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -835,9 +845,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.96"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -894,9 +904,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.18"
|
||||
version = "2.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -915,18 +925,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.40"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
||||
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.40"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3,11 +3,18 @@ name = "flake-checker"
|
||||
version = "0.1.11"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"parse-flake-lock"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.25", default-features = false, features = [ "clock" ] }
|
||||
clap = { version = "4.3.0", default-features = false, features = [ "derive", "env", "std", "wrap_help" ] }
|
||||
handlebars = { version = "4.3.7", default-features = false }
|
||||
is_ci = "1.1.1"
|
||||
parse-flake-lock = { path = "./parse-flake-lock" }
|
||||
reqwest = { version = "0.11.18", default-features = false, features = [ "blocking", "rustls-tls-native-roots" ]}
|
||||
serde = { version = "1.0.163", features = [ "derive" ] }
|
||||
serde_json = { version = "1.0.96", default-features = false }
|
||||
|
35
README.md
35
README.md
@ -64,6 +64,38 @@ To disable diagnostic reporting, set the diagnostics URL to an empty string by p
|
||||
|
||||
You can read the full privacy policy for [Determinate Systems][detsys], the creators of this tool and the [Determinate Nix Installer][installer], [here][privacy].
|
||||
|
||||
## Rust library
|
||||
|
||||
The Nix Flake Checker is written in [Rust].
|
||||
This repo exposes a [`parse-flake-lock`](./parse-flake-lock) crate that you can use to parse [`flake.lock` files][lockfile] in your own Rust projects.
|
||||
To add that dependency:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
parse-flake-lock = { git = "https://github.com/DeterminateSystems/flake-checker", branch = "main" }
|
||||
```
|
||||
|
||||
Here's an example usage:
|
||||
|
||||
```rust
|
||||
use std::path::Path;
|
||||
|
||||
use parse_flake_lock::{FlakeLock, FlakeLockParseError};
|
||||
|
||||
fn main() -> Result<(), FlakeLockParseError> {
|
||||
let flake_lock = FlakeLock::new(Path::new("flake.lock"))?;
|
||||
println!("flake.lock info:");
|
||||
println!("version: {version}", version=flake_lock.version);
|
||||
println!("root node: {root:?}", root=flake_lock.root);
|
||||
println!("all nodes: {nodes:?}", nodes=flake_lock.nodes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The `parse-flake-lock` crate doesn't yet exhaustively parse all input node types, instead using a "fallthrough" mechanism that parses input types that don't yet have explicit struct definitions to a [`serde_json::value::Value`][val].
|
||||
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
|
||||
[detsys]: https://determinate.systems
|
||||
[flakes]: https://zero-to-nix.com/concepts/flakes
|
||||
@ -74,4 +106,7 @@ You can read the full privacy policy for [Determinate Systems][detsys], the crea
|
||||
[nixos-org]: https://github.com/NixOS
|
||||
[nixpkgs]: https://github.com/NixOS/nixpkgs
|
||||
[privacy]: https://determinate.systems/privacy
|
||||
[prs]: /pulls
|
||||
[rust]: https://rust-lang.org
|
||||
[telemetry]: https://github.com/DeterminateSystems/nix-flake-checker/blob/main/src/telemetry.rs#L29-L43
|
||||
[val]: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html
|
||||
|
9
parse-flake-lock/Cargo.toml
Normal file
9
parse-flake-lock/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "parse-flake-lock"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.170", features = ["derive"] }
|
||||
serde_json = "1.0.100"
|
||||
thiserror = "1.0.43"
|
254
parse-flake-lock/src/lib.rs
Normal file
254
parse-flake-lock/src/lib.rs
Normal file
@ -0,0 +1,254 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::de::{self, MapAccess, Visitor};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// A custom error type for the `parse-flake-lock` crate.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FlakeLockParseError {
|
||||
/// The `flake.lock` can be parsed as JSON but is nonetheless invalid.
|
||||
#[error("invalid flake.lock file: {0}")]
|
||||
Invalid(String),
|
||||
/// The `flake.lock` file couldn't be found.
|
||||
#[error("couldn't find the flake.lock file: {0}")]
|
||||
NotFound(#[from] std::io::Error),
|
||||
/// The specified `flake.lock` file couldn't be parsed as JSON.
|
||||
#[error("couldn't parse the flake.lock file as json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// A Rust representation of a Nix [`flake.lock`
|
||||
/// file](https://zero-to-nix.com/concepts/flakes#lockfile).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FlakeLock {
|
||||
/// The `nodes` field of the `flake.lock`, representing all input [Node]s for the flake.
|
||||
pub nodes: HashMap<String, Node>,
|
||||
/// The `root` of the `flake.lock` with all input references resolved into the corresponding
|
||||
/// [Node]s represented by the `nodes` field.
|
||||
pub root: HashMap<String, Node>,
|
||||
/// The version of the `flake.lock` (incremented whenever the `flake.nix` dependencies are
|
||||
/// updated).
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
/// A custom [Deserializer] for `flake.lock` files, which are standard JSON but require some special
|
||||
/// logic to create a meaningful Rust representation.
|
||||
impl<'de> Deserialize<'de> for FlakeLock {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(field_identifier, rename_all = "lowercase")]
|
||||
enum Field {
|
||||
Nodes,
|
||||
Root,
|
||||
Version,
|
||||
}
|
||||
|
||||
struct FlakeLockVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FlakeLockVisitor {
|
||||
type Value = FlakeLock;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("struct FlakeLock")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut nodes = None;
|
||||
let mut root = None;
|
||||
let mut version = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::Nodes => {
|
||||
if nodes.is_some() {
|
||||
return Err(de::Error::duplicate_field("nodes"));
|
||||
}
|
||||
nodes = Some(map.next_value()?);
|
||||
}
|
||||
Field::Root => {
|
||||
if root.is_some() {
|
||||
return Err(de::Error::duplicate_field("root"));
|
||||
}
|
||||
root = Some(map.next_value()?);
|
||||
}
|
||||
Field::Version => {
|
||||
if version.is_some() {
|
||||
return Err(de::Error::duplicate_field("version"));
|
||||
}
|
||||
version = Some(map.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
let nodes: HashMap<String, Node> =
|
||||
nodes.ok_or_else(|| de::Error::missing_field("nodes"))?;
|
||||
let root: String = root.ok_or_else(|| de::Error::missing_field("root"))?;
|
||||
let version: usize = version.ok_or_else(|| de::Error::missing_field("version"))?;
|
||||
|
||||
let mut root_nodes = HashMap::new();
|
||||
let root_node = &nodes[&root];
|
||||
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())));
|
||||
};
|
||||
|
||||
for (root_name, root_input) in root_node.inputs.iter() {
|
||||
let inputs: VecDeque<String> = match root_input.clone() {
|
||||
Input::String(s) => [s].into(),
|
||||
Input::List(keys) => keys.into(),
|
||||
};
|
||||
|
||||
let real_node = chase_input_node(&nodes, inputs).map_err(|e| {
|
||||
de::Error::custom(format!("failed to chase input {}: {:?}", root_name, e))
|
||||
})?;
|
||||
root_nodes.insert(root_name.clone(), real_node.clone());
|
||||
}
|
||||
|
||||
Ok(FlakeLock {
|
||||
nodes,
|
||||
root: root_nodes,
|
||||
version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(FlakeLockVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
fn chase_input_node(
|
||||
nodes: &HashMap<String, Node>,
|
||||
mut inputs: VecDeque<String>,
|
||||
) -> Result<&Node, FlakeLockParseError> {
|
||||
let Some(next_input) = inputs.pop_front() else {
|
||||
unreachable!("there should always be at least one input");
|
||||
};
|
||||
|
||||
let mut node = &nodes[&next_input];
|
||||
for input in inputs {
|
||||
let maybe_node_inputs = match node {
|
||||
Node::Repo(node) => node.inputs.to_owned(),
|
||||
Node::Fallthrough(node) => match node.get("inputs") {
|
||||
Some(node_inputs) => serde_json::from_value(node_inputs.clone())
|
||||
.map_err(FlakeLockParseError::Json)?,
|
||||
None => None,
|
||||
},
|
||||
Node::Root(_) => None,
|
||||
};
|
||||
|
||||
let node_inputs = match maybe_node_inputs {
|
||||
Some(node_inputs) => node_inputs,
|
||||
None => {
|
||||
return Err(FlakeLockParseError::Invalid(format!(
|
||||
"lock node should have had some inputs but had none:\n{:?}",
|
||||
node
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let next_inputs = &node_inputs[&input];
|
||||
node = match next_inputs {
|
||||
Input::String(s) => &nodes[s],
|
||||
Input::List(inputs) => chase_input_node(nodes, inputs.to_owned().into())?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
impl FlakeLock {
|
||||
/// Instantiate a new [FlakeLock] from the provided [Path].
|
||||
pub fn new(path: &Path) -> Result<Self, FlakeLockParseError> {
|
||||
let flake_lock_file = read_to_string(path)?;
|
||||
let flake_lock: FlakeLock = serde_json::from_str(&flake_lock_file)?;
|
||||
Ok(flake_lock)
|
||||
}
|
||||
}
|
||||
|
||||
/// A flake input [node]. This enum represents two concrete node types, [RepoNode] and [RootNode],
|
||||
/// and uses the `Fallthrough` variant to capture node types that don't have explicitly defined
|
||||
/// structs in this library, representing them as raw [Value][serde_json::value::Value]s.
|
||||
///
|
||||
/// [node]: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#lock-files
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Node {
|
||||
/// A [RepoNode] flake input for a [Git](https://git-scm.com) repository (or another version
|
||||
/// control system).
|
||||
Repo(Box<RepoNode>),
|
||||
/// A [RootNode] specifying an [Input] map.
|
||||
Root(RootNode),
|
||||
/// A "catch-all" variant for node types that don't (yet) have explicit struct definitions in
|
||||
/// this crate.
|
||||
Fallthrough(serde_json::value::Value), // Covers all other node types
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn variant(&self) -> &'static str {
|
||||
match self {
|
||||
Node::Root(_) => "Root",
|
||||
Node::Repo(_) => "Repo",
|
||||
Node::Fallthrough(_) => "Fallthrough", // Covers all other node types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum type representing node input references.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Input {
|
||||
String(String),
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
/// A flake [Node] representing a raw mapping of strings to [Input]s.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RootNode {
|
||||
/// A mapping of the flake's input [Node]s.
|
||||
pub inputs: HashMap<String, Input>,
|
||||
}
|
||||
|
||||
/// A [Node] representing a [Git](https://git-scm.com) repository (or another version control
|
||||
/// system).
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RepoNode {
|
||||
/// The node's inputs.
|
||||
pub inputs: Option<HashMap<String, Input>>,
|
||||
/// The "locked" attributes of the input (set by Nix).
|
||||
pub locked: RepoLocked,
|
||||
/// The "original" (user-supplied) attributes of the input.
|
||||
pub original: RepoOriginal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct RepoLocked {
|
||||
#[serde(alias = "lastModified")]
|
||||
pub last_modified: i64,
|
||||
#[serde(alias = "narHash")]
|
||||
pub nar_hash: String,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub rev: String,
|
||||
#[serde(alias = "type")]
|
||||
pub node_type: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct RepoOriginal {
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
#[serde(alias = "type")]
|
||||
pub node_type: String,
|
||||
#[serde(alias = "ref")]
|
||||
pub git_ref: Option<String>,
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
pub enum FlakeCheckerError {
|
||||
#[error("env var error: {0}")]
|
||||
EnvVar(#[from] std::env::VarError),
|
||||
#[error("couldn't parse flake.lock: {0}")]
|
||||
FlakeLock(#[from] parse_flake_lock::FlakeLockParseError),
|
||||
#[error("couldn't access flake.lock: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("couldn't parse flake.lock: {0}")]
|
||||
|
283
src/flake.rs
283
src/flake.rs
@ -1,15 +1,12 @@
|
||||
#![allow(dead_code)]
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::Path;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::issue::{Disallowed, Issue, IssueKind, NonUpstream, Outdated};
|
||||
use crate::FlakeCheckerError;
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
use serde::de::{self, Deserializer, MapAccess, Visitor};
|
||||
use serde::Deserialize;
|
||||
use parse_flake_lock::{FlakeLock, Node};
|
||||
|
||||
// Update this when necessary by running the get-allowed-refs.sh script to fetch
|
||||
// the current values from monitoring.nixos.org
|
||||
@ -46,13 +43,44 @@ impl Default for FlakeCheckConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn nixpkgs_deps(
|
||||
flake_lock: &FlakeLock,
|
||||
keys: Vec<String>,
|
||||
) -> Result<HashMap<String, Node>, FlakeCheckerError> {
|
||||
let mut deps: HashMap<String, Node> = HashMap::new();
|
||||
|
||||
for (key, node) in flake_lock.root.clone() {
|
||||
if let Node::Repo(_) = node {
|
||||
if keys.contains(&key) {
|
||||
deps.insert(key, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
let missing: Vec<String> = keys
|
||||
.iter()
|
||||
.filter(|k| !deps.contains_key(*k))
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
if !missing.is_empty() {
|
||||
let error_msg = format!(
|
||||
"no nixpkgs dependency found for specified {}: {}",
|
||||
if missing.len() > 1 { "keys" } else { "key" },
|
||||
missing.join(", ")
|
||||
);
|
||||
return Err(FlakeCheckerError::Invalid(error_msg));
|
||||
}
|
||||
|
||||
Ok(deps)
|
||||
}
|
||||
|
||||
pub(crate) fn check_flake_lock(
|
||||
flake_lock: &FlakeLock,
|
||||
config: &FlakeCheckConfig,
|
||||
) -> Result<Vec<Issue>, FlakeCheckerError> {
|
||||
let mut issues = vec![];
|
||||
|
||||
let deps = flake_lock.nixpkgs_deps(config.nixpkgs_keys.clone())?;
|
||||
let deps = nixpkgs_deps(flake_lock, config.nixpkgs_keys.clone())?;
|
||||
|
||||
for (name, dep) in deps {
|
||||
if let Node::Repo(repo) = dep {
|
||||
@ -99,247 +127,6 @@ pub(crate) fn check_flake_lock(
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct FlakeLock {
|
||||
nodes: HashMap<String, Node>,
|
||||
root: HashMap<String, Node>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FlakeLock {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(field_identifier, rename_all = "lowercase")]
|
||||
enum Field {
|
||||
Nodes,
|
||||
Root,
|
||||
Version,
|
||||
}
|
||||
|
||||
struct FlakeLockVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FlakeLockVisitor {
|
||||
type Value = FlakeLock;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("struct FlakeLock")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut nodes = None;
|
||||
let mut root = None;
|
||||
let mut version = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::Nodes => {
|
||||
if nodes.is_some() {
|
||||
return Err(de::Error::duplicate_field("nodes"));
|
||||
}
|
||||
nodes = Some(map.next_value()?);
|
||||
}
|
||||
Field::Root => {
|
||||
if root.is_some() {
|
||||
return Err(de::Error::duplicate_field("root"));
|
||||
}
|
||||
root = Some(map.next_value()?);
|
||||
}
|
||||
Field::Version => {
|
||||
if version.is_some() {
|
||||
return Err(de::Error::duplicate_field("version"));
|
||||
}
|
||||
version = Some(map.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
let nodes: HashMap<String, Node> =
|
||||
nodes.ok_or_else(|| de::Error::missing_field("nodes"))?;
|
||||
let root: String = root.ok_or_else(|| de::Error::missing_field("root"))?;
|
||||
let version: usize = version.ok_or_else(|| de::Error::missing_field("version"))?;
|
||||
|
||||
let mut root_nodes = HashMap::new();
|
||||
let root_node = &nodes[&root];
|
||||
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())));
|
||||
};
|
||||
|
||||
for (root_name, root_input) in root_node.inputs.iter() {
|
||||
let inputs: VecDeque<String> = match root_input.clone() {
|
||||
Input::String(s) => [s].into(),
|
||||
Input::List(keys) => keys.into(),
|
||||
};
|
||||
|
||||
let real_node = chase_input_node(&nodes, inputs).map_err(|e| {
|
||||
de::Error::custom(format!("failed to chase input {}: {:?}", root_name, e))
|
||||
})?;
|
||||
root_nodes.insert(root_name.clone(), real_node.clone());
|
||||
}
|
||||
|
||||
Ok(FlakeLock {
|
||||
nodes,
|
||||
root: root_nodes,
|
||||
version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(FlakeLockVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
fn chase_input_node(
|
||||
nodes: &HashMap<String, Node>,
|
||||
mut inputs: VecDeque<String>,
|
||||
) -> Result<&Node, FlakeCheckerError> {
|
||||
let Some(next_input) = inputs.pop_front() else {
|
||||
unreachable!("there should always be at least one input");
|
||||
};
|
||||
|
||||
let mut node = &nodes[&next_input];
|
||||
for input in inputs {
|
||||
let maybe_node_inputs = match node {
|
||||
Node::Repo(node) => node.inputs.to_owned(),
|
||||
Node::Fallthrough(node) => match node.get("inputs") {
|
||||
Some(node_inputs) => {
|
||||
serde_json::from_value(node_inputs.clone()).map_err(FlakeCheckerError::Json)?
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
Node::Root(_) => None,
|
||||
};
|
||||
|
||||
let node_inputs = match maybe_node_inputs {
|
||||
Some(node_inputs) => node_inputs,
|
||||
None => {
|
||||
return Err(FlakeCheckerError::Invalid(format!(
|
||||
"lock node should have had some inputs but had none:\n{:?}",
|
||||
node
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let next_inputs = &node_inputs[&input];
|
||||
node = match next_inputs {
|
||||
Input::String(s) => &nodes[s],
|
||||
Input::List(inputs) => chase_input_node(nodes, inputs.to_owned().into())?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
impl FlakeLock {
|
||||
pub(crate) fn new(path: &Path) -> Result<Self, FlakeCheckerError> {
|
||||
let flake_lock_file = read_to_string(path)?;
|
||||
let flake_lock: FlakeLock = serde_json::from_str(&flake_lock_file)?;
|
||||
Ok(flake_lock)
|
||||
}
|
||||
|
||||
fn nixpkgs_deps(&self, keys: Vec<String>) -> Result<HashMap<String, Node>, FlakeCheckerError> {
|
||||
let mut deps: HashMap<String, Node> = HashMap::new();
|
||||
|
||||
for (key, node) in self.root.clone() {
|
||||
if let Node::Repo(_) = node {
|
||||
if keys.contains(&key) {
|
||||
deps.insert(key, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
let missing: Vec<String> = keys
|
||||
.iter()
|
||||
.filter(|k| !deps.contains_key(*k))
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
if !missing.is_empty() {
|
||||
let error_msg = format!(
|
||||
"no nixpkgs dependency found for specified {}: {}",
|
||||
if missing.len() > 1 { "keys" } else { "key" },
|
||||
missing.join(", ")
|
||||
);
|
||||
return Err(FlakeCheckerError::Invalid(error_msg));
|
||||
}
|
||||
|
||||
Ok(deps)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Node {
|
||||
Repo(Box<RepoNode>),
|
||||
Root(RootNode),
|
||||
Fallthrough(serde_json::value::Value), // Covers all other node types
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn variant(&self) -> &'static str {
|
||||
match self {
|
||||
Node::Root(_) => "Root",
|
||||
Node::Repo(_) => "Repo",
|
||||
Node::Fallthrough(_) => "Fallthrough", // Covers all other node types
|
||||
}
|
||||
}
|
||||
|
||||
fn is_nixpkgs(&self) -> bool {
|
||||
match self {
|
||||
Self::Repo(repo) => {
|
||||
repo.locked.node_type == "github" && repo.original.repo == "nixpkgs"
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Input {
|
||||
String(String),
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct RootNode {
|
||||
pub(crate) inputs: HashMap<String, Input>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct RepoNode {
|
||||
pub(crate) inputs: Option<HashMap<String, Input>>,
|
||||
pub(crate) locked: RepoLocked,
|
||||
pub(crate) original: RepoOriginal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct RepoLocked {
|
||||
#[serde(alias = "lastModified")]
|
||||
pub(crate) last_modified: i64,
|
||||
#[serde(alias = "narHash")]
|
||||
pub(crate) nar_hash: String,
|
||||
pub(crate) owner: String,
|
||||
pub(crate) repo: String,
|
||||
pub(crate) rev: String,
|
||||
#[serde(alias = "type")]
|
||||
pub(crate) node_type: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct RepoOriginal {
|
||||
pub(crate) owner: String,
|
||||
pub(crate) repo: String,
|
||||
#[serde(alias = "type")]
|
||||
pub(crate) node_type: String,
|
||||
#[serde(alias = "ref")]
|
||||
pub(crate) git_ref: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
@ -5,13 +5,14 @@ mod summary;
|
||||
mod telemetry;
|
||||
|
||||
use error::FlakeCheckerError;
|
||||
use flake::{check_flake_lock, FlakeCheckConfig, FlakeLock};
|
||||
use flake::{check_flake_lock, FlakeCheckConfig};
|
||||
use summary::Summary;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use parse_flake_lock::FlakeLock;
|
||||
|
||||
/// A flake.lock checker for Nix projects.
|
||||
#[derive(Parser)]
|
||||
@ -19,7 +20,7 @@ use clap::Parser;
|
||||
struct Cli {
|
||||
/// Don't send aggregate sums of each issue type.
|
||||
///
|
||||
/// See: https://github.com/determinateSystems/flake-checker.
|
||||
/// See <https://github.com/determinateSystems/flake-checker>.
|
||||
#[arg(long, env = "NIX_FLAKE_CHECKER_NO_TELEMETRY", default_value_t = false)]
|
||||
no_telemetry: bool,
|
||||
|
||||
|
@ -110,4 +110,4 @@ While <a href="https://github.com/NixOS/nixpkgs">upstream Nixpkgs</a> isn't bull
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<p>Feedback? Let us know at <a href="https://github.com/DeterminateSystems/flake-checker">DeterminateSystems/flake-checker</a>.</p>
|
||||
<p>Feedback? Let us know at <a href="https://github.com/DeterminateSystems/flake-checker">DeterminateSystems/flake-checker</a>.</p>
|
@ -83,4 +83,4 @@ software is!) it has a wide range of security measures in place, most notably
|
||||
continuous integration testing with Hydra, that mitigate a great deal of supply
|
||||
chain risk.
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
Loading…
Reference in New Issue
Block a user