diff --git a/migrations/20260725025954_flake_inputs.down.sql b/migrations/20260725025954_flake_inputs.down.sql new file mode 100644 index 0000000..1fb4321 --- /dev/null +++ b/migrations/20260725025954_flake_inputs.down.sql @@ -0,0 +1,2 @@ +DROP INDEX ix_flake_inputs_build_id; +DROP TABLE flake_inputs; diff --git a/migrations/20260725025954_flake_inputs.up.sql b/migrations/20260725025954_flake_inputs.up.sql new file mode 100644 index 0000000..fb73c29 --- /dev/null +++ b/migrations/20260725025954_flake_inputs.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE flake_inputs ( + id INTEGER NOT NULL PRIMARY KEY, + build_id INTEGER NOT NULL, + input_name TEXT NOT NULL, + rev TEXT NOT NULL, + FOREIGN KEY(build_id) REFERENCES build(id) +) STRICT; + +CREATE INDEX ix_flake_inputs_build_id ON flake_inputs(build_id); diff --git a/src/command/build/runner.rs b/src/command/build/runner.rs index ad11657..5517691 100644 --- a/src/command/build/runner.rs +++ b/src/command/build/runner.rs @@ -1,3 +1,6 @@ +use sqlx::Row; +use tracing::info; + use crate::Result; use crate::cli::parameters::BuildArgs; use crate::config::Config; @@ -7,8 +10,10 @@ use crate::fs_util::assert_directory; use crate::fs_util::is_git_repo; use crate::git_util::git_force_into_state; use crate::git_util::git_init_at_rev; +use crate::nix_util::FlakeLock; use crate::nix_util::nix_flake_update; use crate::nix_util::nixos_build_target; +use crate::nix_util::parse_flake_lock; pub(crate) async fn run_build(args: BuildArgs) -> Result<()> { println!("{:?}", args); @@ -101,14 +106,61 @@ async fn build_target( build_directory.to_string_lossy() ); + let target_name = target_config.get_name()?; + + let build_id: i64 = sqlx::query( + r#"INSERT INTO build (start_time, target) SELECT unixepoch('now'), ? RETURNING id"#, + ) + .bind(target_name) + .fetch_one(&db_handle.conn) + .await? + .try_get("id")?; + + let flake_lock = parse_flake_lock(&flake_directory).await?; + write_input_revs_to_db(db_handle, &flake_lock, build_id).await?; + nixos_build_target( db_handle, build_directory, flake_directory, target_config.get_attr()?, - target_config.get_name()?, + target_name, + build_id, ) .await?; Ok(()) } + +async fn write_input_revs_to_db( + db_handle: &DbHandle, + flake_lock: &FlakeLock, + build_id: i64, +) -> Result<()> { + let root_node = flake_lock.get_root_node()?; + let direct_inputs = flake_lock.get_inputs(root_node)?; + + let mut tx = db_handle.conn.begin().await?; + for (input, node) in direct_inputs.iter() { + let locked = match &node.locked { + Some(locked) => locked, + None => { + info!( + "Not recording rev for input {} because it lacks a locked section.", + input + ); + continue; + } + }; + let rev = &locked.rev; + info!("input {} {}", input, rev); + sqlx::query(r#"INSERT INTO flake_inputs (build_id, input_name, rev) VALUES (?, ?, ?);"#) + .bind(build_id) + .bind(input) + .bind(rev) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + Ok(()) +} diff --git a/src/command/feed_logs/runner.rs b/src/command/feed_logs/runner.rs index 16f8563..de5f543 100644 --- a/src/command/feed_logs/runner.rs +++ b/src/command/feed_logs/runner.rs @@ -14,12 +14,10 @@ use crate::nix_util::NixOutputStream; use crate::nix_util::OutputLine; use crate::nix_util::OutputLineStream; use crate::nix_util::RunningBuild; -use crate::nix_util::RunningUpdate; pub(crate) async fn feed_logs(args: FeedLogArgs) -> Result<()> { let db_handle = DbHandle::new::(None).await?; - // let mut running_build = RunningUpdate::new()?; - let mut running_build = RunningBuild::new(&db_handle)?; + let mut running_build = RunningBuild::new(&db_handle, -1)?; let file_stream = FileStream::new(args.input).await?; let mut nix_output_stream = NixOutputStream::new(file_stream); while let Some(message) = nix_output_stream.next().await? { diff --git a/src/nix_util/high_level.rs b/src/nix_util/high_level.rs index aa2e76e..82653f6 100644 --- a/src/nix_util/high_level.rs +++ b/src/nix_util/high_level.rs @@ -17,6 +17,7 @@ pub(crate) async fn nixos_build_target( flake_path: F, attr: A, target_name: TN, + build_id: i64, ) -> Result<()> where B: AsRef, @@ -55,7 +56,7 @@ where let child = command.spawn()?; - let mut running_build = RunningBuild::new(db_handle)?; + let mut running_build = RunningBuild::new(db_handle, build_id)?; running_build.run_to_completion(child, target_name).await?; Ok(()) diff --git a/src/nix_util/lock_parser.rs b/src/nix_util/lock_parser.rs new file mode 100644 index 0000000..0006c4e --- /dev/null +++ b/src/nix_util/lock_parser.rs @@ -0,0 +1,150 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use serde::Deserialize; +use serde::Serialize; + +use crate::Result; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct FlakeLock { + pub(crate) root: String, + pub(crate) version: u8, + pub(crate) nodes: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct FlakeLockNode { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) locked: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) original: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) inputs: Option>, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) flake: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct FlakeLockNodeLocked { + #[serde(rename = "lastModified")] + pub(crate) last_modified: i64, + + #[serde(rename = "narHash")] + pub(crate) nar_hash: String, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) url: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) owner: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) repo: Option, + + #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")] + pub(crate) git_ref: Option, + + pub(crate) rev: String, + + #[serde(rename = "revCount", default, skip_serializing_if = "Option::is_none")] + pub(crate) rev_count: Option, + + #[serde(rename = "type")] + pub(crate) repo_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)] +pub(crate) enum FlakeLockNodeOriginal { + Github { + owner: String, + + repo: String, + + #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")] + git_ref: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + rev: Option, + }, + Git { + url: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +pub(crate) enum FlakeLockInputValue { + String(String), + List(Vec), +} + +pub(crate) async fn parse_flake_lock(flake_path: F) -> Result +where + F: AsRef, +{ + let lock_file_path = flake_path.as_ref().join("flake.lock"); + let contents = tokio::fs::read_to_string(&lock_file_path).await?; + let parsed_flake_lock = serde_json::from_str(&contents)?; + Ok(parsed_flake_lock) +} + +impl FlakeLock { + pub(crate) fn get_root_node(&self) -> Result<&FlakeLockNode> { + let root_node = self.nodes.get(&self.root).ok_or("Root node not found.")?; + Ok(root_node) + } + + pub(crate) fn get_inputs<'lock>( + &'lock self, + source_node: &'lock FlakeLockNode, + ) -> Result> { + let direct_inputs = match &source_node.inputs { + Some(inputs) => inputs, + None => { + return Ok(BTreeMap::new()); + } + }; + let mut inputs = BTreeMap::new(); + + for (input_name, input_id_or_path) in direct_inputs.iter() { + inputs.insert(input_name, self.resolve_path(input_id_or_path)?); + } + + Ok(inputs) + } + + fn resolve_path<'lock>( + &'lock self, + input_id_or_path: &'lock FlakeLockInputValue, + ) -> Result<&'lock FlakeLockNode> { + match input_id_or_path { + FlakeLockInputValue::String(input_id) => { + // If the input value is a plain string, then it refers to a node. + let node = self.nodes.get(input_id).ok_or("Input not found.")?; + Ok(node) + } + FlakeLockInputValue::List(items) => { + // If the input value is a list, then it refers to a path to an input. + let mut current_node = self.get_root_node()?; + for step in items { + let child_input_id_or_path = current_node + .inputs + .as_ref() + .ok_or("Node without inputs while walking input tree.")? + .get(step) + .ok_or("Step not found in inputs while walking input tree")?; + current_node = self.resolve_path(child_input_id_or_path)?; + } + Ok(current_node) + } + } + } +} diff --git a/src/nix_util/mod.rs b/src/nix_util/mod.rs index 8073d73..603e8cb 100644 --- a/src/nix_util/mod.rs +++ b/src/nix_util/mod.rs @@ -2,6 +2,7 @@ mod activity; mod activity_tree; mod activity_tree_stream; mod high_level; +mod lock_parser; mod nix_output_stream; mod output_stream; mod running_build; @@ -10,6 +11,8 @@ mod tree_iter; pub(crate) use activity_tree::ActivityIdAlreadyInTreeError; pub(crate) use activity_tree::ActivityIdNotInTreeError; pub(crate) use high_level::*; +pub(crate) use lock_parser::FlakeLock; +pub(crate) use lock_parser::parse_flake_lock; pub(crate) use nix_output_stream::NixOutputStream; pub(crate) use output_stream::OutputLine; pub(crate) use output_stream::OutputLineStream; diff --git a/src/nix_util/running_build.rs b/src/nix_util/running_build.rs index e9abb93..f5b84ef 100644 --- a/src/nix_util/running_build.rs +++ b/src/nix_util/running_build.rs @@ -26,14 +26,16 @@ pub(crate) struct RunningBuild<'db> { db_handle: &'db DbHandle, activity_tree: ActivityTreeStream, last_announce: Option, + build_id: i64, } impl<'db> RunningBuild<'db> { - pub(crate) fn new(db_handle: &'db DbHandle) -> Result { + pub(crate) fn new(db_handle: &'db DbHandle, build_id: i64) -> Result { Ok(RunningBuild { db_handle, activity_tree: ActivityTreeStream::new(), last_announce: None, + build_id, }) } @@ -45,16 +47,6 @@ impl<'db> RunningBuild<'db> { where TN: AsRef, { - let foo = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - let now = Instant::now(); - let build_id: i64 = sqlx::query( - r#"INSERT INTO build (start_time, target) SELECT unixepoch('now'), ? RETURNING id"#, - ) - .bind(target_name.as_ref()) - .fetch_one(&self.db_handle.conn) - .await? - .try_get("id")?; - let output_stream = OutputStream::from_child(&mut child)?; let mut nix_output_stream: NixOutputStream = NixOutputStream::new(output_stream); @@ -81,7 +73,7 @@ impl<'db> RunningBuild<'db> { .code() .expect("Process should have an exit code."), ) - .bind(build_id) + .bind(self.build_id) .execute(&self.db_handle.conn) .await? .rows_affected();