2025-02-01 00:55:10 +01:00
|
|
|
use anyhow::{
|
|
|
|
anyhow,
|
|
|
|
Context,
|
|
|
|
Result,
|
|
|
|
};
|
|
|
|
use clap::{
|
|
|
|
Parser,
|
|
|
|
};
|
|
|
|
use serde::{
|
|
|
|
Deserialize,
|
|
|
|
};
|
2025-02-01 02:10:06 +01:00
|
|
|
use sqlx::{
|
|
|
|
sqlite::SqlitePoolOptions,
|
|
|
|
};
|
2025-02-01 00:55:10 +01:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::process::Command;
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct FlakeMetadata {
|
|
|
|
lastModified: i64,
|
|
|
|
locked: FlakeLockedInfo,
|
|
|
|
locks: FlakeLocks,
|
|
|
|
original: FlakeSource,
|
|
|
|
path: String,
|
|
|
|
resolved: FlakeSource,
|
|
|
|
revision: String,
|
|
|
|
}
|
|
|
|
|
2025-02-01 02:10:06 +01:00
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
2025-02-01 00:55:10 +01:00
|
|
|
struct FlakeSource {
|
|
|
|
owner: Option<String>,
|
|
|
|
repo: Option<String>,
|
|
|
|
r#ref: Option<String>,
|
|
|
|
rev: Option<String>,
|
|
|
|
r#type: String,
|
|
|
|
url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn assemble_flake_git_uri(flake_source: &FlakeSource) -> Result<String> {
|
|
|
|
let mut out = String::new();
|
|
|
|
out.push_str("git+");
|
|
|
|
out.push_str(&flake_source.url.clone()
|
|
|
|
.context("Flake git uri does not contain an url")?);
|
|
|
|
|
|
|
|
let mut query: Vec<String> = Vec::new();
|
|
|
|
|
|
|
|
if let Some(r#ref) = &flake_source.r#ref {
|
|
|
|
query.push(format!("ref={}", r#ref));
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(rev) = &flake_source.rev {
|
|
|
|
query.push(format!("rev={}", rev));
|
|
|
|
}
|
|
|
|
|
|
|
|
if query.len() > 0 {
|
|
|
|
out.push_str("?");
|
|
|
|
out.push_str(&query.join("&"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn assemble_flake_github_uri(flake_source: &FlakeSource) -> Result<String> {
|
|
|
|
let mut out = String::new();
|
|
|
|
out.push_str("github:");
|
|
|
|
out.push_str(&flake_source.owner.clone()
|
|
|
|
.context("Flake github uri does not contain an owner")?);
|
|
|
|
out.push_str("/");
|
|
|
|
out.push_str(&flake_source.repo.clone()
|
|
|
|
.context("Flake github uri does not contain an repo")?);
|
|
|
|
|
|
|
|
if let Some(rev) = &flake_source.rev {
|
|
|
|
out.push_str("/");
|
|
|
|
out.push_str(&rev);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn assemble_flake_tarball_uri(flake_source: &FlakeSource) -> Result<String> {
|
|
|
|
let mut out = String::new();
|
|
|
|
out.push_str(&flake_source.url.clone()
|
|
|
|
.context("Flake tarball uri does not contain an url")?);
|
|
|
|
|
|
|
|
if let Some(rev) = &flake_source.rev {
|
|
|
|
out.push_str("?rev=");
|
|
|
|
out.push_str(rev);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
trait FlakeUri {
|
|
|
|
fn flake_uri(&self) -> Result<String>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FlakeUri for FlakeSource {
|
|
|
|
fn flake_uri(&self) -> Result<String> {
|
|
|
|
match self.r#type.as_str() {
|
|
|
|
"git" => assemble_flake_git_uri(self),
|
|
|
|
"github" => assemble_flake_github_uri(self),
|
|
|
|
"tarball" => assemble_flake_tarball_uri(self),
|
|
|
|
other => Err(anyhow!("Unsupported flake uri type {}", other)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-01 02:10:06 +01:00
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
2025-02-01 00:55:10 +01:00
|
|
|
struct FlakeLockedInfo {
|
|
|
|
lastModified: i64,
|
|
|
|
narHash: String,
|
|
|
|
owner: Option<String>,
|
|
|
|
repo: Option<String>,
|
|
|
|
r#ref: Option<String>,
|
|
|
|
rev: Option<String>,
|
|
|
|
r#type: String,
|
|
|
|
url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FlakeUri for FlakeLockedInfo {
|
|
|
|
fn flake_uri(&self) -> Result<String> {
|
|
|
|
Into::<FlakeSource>::into(self).flake_uri()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Into<FlakeSource> for &FlakeLockedInfo {
|
|
|
|
fn into(self) -> FlakeSource {
|
|
|
|
FlakeSource {
|
|
|
|
owner: self.owner.clone(),
|
|
|
|
repo: self.repo.clone(),
|
|
|
|
r#ref: self.r#ref.clone(),
|
|
|
|
rev: self.rev.clone(),
|
|
|
|
r#type: self.r#type.clone(),
|
|
|
|
url: self.url.clone(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct FlakeLocks {
|
|
|
|
nodes: HashMap<String, FlakeLocksNode>,
|
|
|
|
root: String,
|
|
|
|
version: u64,
|
|
|
|
}
|
|
|
|
|
2025-02-01 02:10:06 +01:00
|
|
|
#[derive(Deserialize, Debug, Clone)]
|
2025-02-01 00:55:10 +01:00
|
|
|
#[serde(untagged)]
|
|
|
|
enum FlakeLocksNodeInputs {
|
|
|
|
String(String),
|
|
|
|
Array(Vec<String>),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct FlakeLocksNode {
|
|
|
|
inputs: Option<HashMap<String, FlakeLocksNodeInputs>>,
|
|
|
|
locked: Option<FlakeLockedInfo>,
|
|
|
|
original: Option<FlakeSource>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Parser)]
|
|
|
|
#[command(version, about, long_about = None)]
|
|
|
|
struct Cli {
|
|
|
|
flake_uri: String,
|
|
|
|
}
|
|
|
|
|
2025-02-01 02:10:06 +01:00
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> Result<()> {
|
2025-02-01 00:55:10 +01:00
|
|
|
let cli = Cli::parse();
|
|
|
|
|
2025-02-01 02:10:06 +01:00
|
|
|
let db = SqlitePoolOptions::new().connect("sqlite://flake-tracker.db").await?;
|
|
|
|
|
2025-02-01 00:55:10 +01:00
|
|
|
let flake_metadata_raw = Command::new("nix")
|
|
|
|
.arg("flake")
|
|
|
|
.arg("metadata")
|
|
|
|
.arg("--json")
|
|
|
|
.arg(cli.flake_uri)
|
|
|
|
.output()
|
|
|
|
.context("Failed to fetch flake metadata")?;
|
|
|
|
|
|
|
|
let flake_metadata: FlakeMetadata = serde_json::from_slice(&flake_metadata_raw.stdout)
|
|
|
|
.context("Failed to parse flake metadata")?;
|
|
|
|
|
2025-02-01 02:10:06 +01:00
|
|
|
sqlx::query("INSERT INTO flake_revisions (revision_uri, uri, nix_store_path, revision, nar_hash, last_modified)
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
|
|
ON CONFLICT(revision_uri) DO UPDATE SET
|
|
|
|
uri=excluded.uri,
|
|
|
|
nix_store_path=excluded.nix_store_path,
|
|
|
|
revision=excluded.revision,
|
|
|
|
nar_hash=excluded.nar_hash,
|
|
|
|
last_modified=excluded.last_modified
|
|
|
|
")
|
|
|
|
.bind(&flake_metadata.locked.flake_uri()?)
|
|
|
|
.bind(&flake_metadata.resolved.flake_uri()?)
|
|
|
|
.bind(&flake_metadata.path)
|
|
|
|
.bind(&flake_metadata.revision)
|
|
|
|
.bind(&flake_metadata.locked.narHash)
|
|
|
|
.bind(&flake_metadata.locked.lastModified)
|
|
|
|
.execute(&db).await?;
|
|
|
|
|
|
|
|
let locks_root_name = &flake_metadata.locks.root;
|
|
|
|
let locks_root_node = flake_metadata.locks.nodes.get(locks_root_name)
|
|
|
|
.context("Failed to get locks root node")?;
|
|
|
|
|
|
|
|
for (input_name, locks_input_name) in locks_root_node.inputs.clone().context("No inputs found for flake")? {
|
|
|
|
|
|
|
|
if let FlakeLocksNodeInputs::String(locks_input_name) = locks_input_name {
|
|
|
|
let locks_input_node = flake_metadata.locks.nodes.get(&locks_input_name)
|
|
|
|
.context("Failed to find lock of input")?;
|
|
|
|
|
|
|
|
sqlx::query("INSERT INTO flake_revisions_inputs (flake_revision_uri, input_name, revision_uri, uri, nar_hash, last_modified)
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
|
|
ON CONFLICT(flake_revision_uri, input_name) DO UPDATE SET
|
|
|
|
revision_uri=excluded.revision_uri,
|
|
|
|
uri=excluded.uri,
|
|
|
|
nar_hash=excluded.nar_hash,
|
|
|
|
last_modified=excluded.last_modified
|
|
|
|
")
|
|
|
|
.bind(flake_metadata.locked.flake_uri()?)
|
|
|
|
.bind(input_name)
|
|
|
|
.bind(locks_input_node.locked.clone().context("Unexpected missing lock")?.flake_uri()?)
|
|
|
|
.bind(locks_input_node.original.clone().context("Unexpected missing lock")?.flake_uri()?)
|
|
|
|
.bind(locks_input_node.locked.clone().context("Unexpected missing lock")?.narHash)
|
|
|
|
.bind(locks_input_node.locked.clone().context("Unexpected missing lock")?.lastModified)
|
|
|
|
.execute(&db).await?;
|
2025-02-01 00:55:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|