diff --git a/figure-out-flake-outputs.nix b/figure-out-flake-outputs.nix new file mode 100644 index 0000000..6a9fb72 --- /dev/null +++ b/figure-out-flake-outputs.nix @@ -0,0 +1,36 @@ +outputAttrs: let + + attrNamePathToString = attrNamePath: builtins.concatStringsSep "." attrNamePath; + + outputInfo = drv: builtins.listToAttrs (builtins.map (outputName: { + name = outputName; + value = { + store_path = drv."${outputName}".outPath; + }; + }) drv.outputs); + + recurseAttrs = attrs: attrNamePath: builtins.concatLists (builtins.attrValues ( + builtins.mapAttrs (attrName: attrValue: + if builtins.typeOf attrValue == "set" then + if builtins.hasAttr "type" attrValue then + if attrValue.type == "derivation" then + [ { + name = attrNamePathToString (attrNamePath ++ [ attrName ]); + value = { + name = attrValue.name; + derivation_path = attrValue.drvPath; + system = attrValue.system; + outputs = outputInfo attrValue; + }; + } ] + else + [ "unknown type" ] + else + recurseAttrs attrValue (attrNamePath ++ [ attrName ]) + else + [] + ) attrs + )); + +in + builtins.listToAttrs (recurseAttrs outputAttrs []) diff --git a/schema.sql b/schema.sql index b7dcf8f..d48c451 100644 --- a/schema.sql +++ b/schema.sql @@ -1,20 +1,20 @@ CREATE TABLE revisions ( revision_uri TEXT NOT NULL PRIMARY KEY, flake_uri TEXT NOT NULL, - store_path TEXT, - last_modified INT + store_path TEXT NOT NULL, + last_modified INT NOT NULL ); CREATE TABLE output_attributes ( revision_uri TEXT NOT NULL, output_attribute_name TEXT NOT NULL, - derivation_path TEXT, + derivation_path TEXT NOT NULL, PRIMARY KEY (revision_uri, output_attribute_name) ); CREATE TABLE build_outputs ( derivation_path TEXT NOT NULL, build_output_name TEXT NOT NULL, - store_path TEXT, + store_path TEXT NOT NULL, PRIMARY KEY (derivation_path, build_output_name) ); diff --git a/src/scan.rs b/src/scan.rs index 252087b..b098e54 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -13,12 +13,29 @@ use crate::{ }, storage::{ FlakeRow, + OutputAttributeRow, RevisionRow, Storage, }, }; +use serde::{ + Deserialize, +}; +use std::collections::HashMap; use std::process::Command; +#[derive(Deserialize, Debug)] +pub struct OutputAttribute { + pub derivation_path: String, + pub name: String, + pub outputs: HashMap, +} + +#[derive(Deserialize, Debug)] +pub struct BuildOutput { + pub store_path: String, +} + pub async fn scan_flake(storage: Storage, flake_uri: &str) -> Result<()> { let scan_time = Utc::now().timestamp(); @@ -30,6 +47,20 @@ pub async fn scan_flake(storage: Storage, flake_uri: &str) -> Result<()> { storage.set_revision(revision_row) .await?; + let flake_outputs = fetch_outputs(flake_uri) + .await?; + + for (output_attribute_name, derivation_info) in &flake_outputs { + let output_attribute_row = OutputAttributeRow { + revision_uri: flake_metadata.locked.flake_uri()?.clone(), + output_attribute_name: output_attribute_name.clone(), + derivation_path: derivation_info.derivation_path.clone(), + }; + + storage.set_output_attribute(output_attribute_row) + .await?; + } + Ok(()) } @@ -52,9 +83,28 @@ pub fn get_revision_from_metadata(flake_metadata: &FlakeMetadata) -> Result Result> { + let figure_out_flake_outputs = std::include_str!("../figure-out-flake-outputs.nix"); + + let flake_outputs_raw = Command::new("nix") + .arg("eval") + .arg("--json") + .arg(format!("{}#hydraJobs", flake_uri)) + .arg("--apply") + .arg(figure_out_flake_outputs) + .output() + .context("Failed to fetch flake outputs")?; + + println!("{}", str::from_utf8(&flake_outputs_raw.stdout)?); + let flake_outputs: HashMap = serde_json::from_slice(&flake_outputs_raw.stdout) + .context("Failed to parse flake outputs")?; + + Ok(flake_outputs) +} diff --git a/src/storage.rs b/src/storage.rs index 0201e1f..8b5e347 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -97,14 +97,44 @@ impl Storage { .await .context("Failed to fetch data from database") } + + pub async fn set_output_attribute(&self, output_attribute_row: OutputAttributeRow) -> Result { + sqlx::query("INSERT INTO output_attributes (revision_uri, output_attribute_name, derivation_path) + VALUES (?, ?, ?) + ON CONFLICT(revision_uri, output_attribute_name) DO UPDATE SET + derivation_path=excluded.derivation_path + ") + .bind(&output_attribute_row.revision_uri) + .bind(&output_attribute_row.output_attribute_name) + .bind(&output_attribute_row.derivation_path) + .execute(&self.db) + .await + .context("Failed to execute database query") + } + + pub async fn output_attributes_for_revision(&self, revision_uri: &str) -> Result> { + sqlx::query_as(" + SELECT + revision_uri, + output_attribute_name, + derivation_path + FROM output_attributes + WHERE revision_uri = ? + ORDER BY output_attribute_name DESC + ") + .bind(&revision_uri) + .fetch_all(&self.db) + .await + .context("Failed to fetch data from database") + } } #[derive(FromRow)] pub struct RevisionRow { pub revision_uri: String, pub flake_uri: String, - pub store_path: Option, - pub last_modified: Option, + pub store_path: String, + pub last_modified: i64, } impl RevisionRow { @@ -117,10 +147,7 @@ impl RevisionRow { } pub fn last_modified_time(&self) -> Option> { - match &self.last_modified { - Some(last_modified) => DateTime::from_timestamp(last_modified.clone(), 0), - None => None, - } + DateTime::from_timestamp(self.last_modified.clone(), 0) } } @@ -135,3 +162,33 @@ impl FlakeRow { } } +#[derive(FromRow)] +pub struct OutputAttributeRow { + pub revision_uri: String, + pub output_attribute_name: String, + pub derivation_path: String, +} + +impl OutputAttributeRow { + pub fn revision_link(&self) -> String { + format!("/revisions/{}", urlencode(&self.revision_uri)) + } + + pub fn derivation_link(&self) -> String { + format!("/derivations/{}", urlencode(&self.derivation_path)) + } +} + +#[derive(FromRow)] +pub struct BuildOutputRow { + pub derivation_path: String, + pub build_output_name: String, + pub store_path: String, +} + +impl BuildOutputRow { + pub fn derivation_link(&self ) -> String { + format!("/derivations/{}", urlencode(&self.derivation_path)) + } +} + diff --git a/src/templates.rs b/src/templates.rs index 6982a48..225a0a0 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -4,6 +4,7 @@ use askama::{ use crate::{ storage::{ FlakeRow, + OutputAttributeRow, RevisionRow, }, }; @@ -26,4 +27,5 @@ pub struct FlakeTemplate { pub struct RevisionTemplate { pub revision_uri: String, pub revision: RevisionRow, + pub output_attributes: Vec, } diff --git a/src/web.rs b/src/web.rs index eadf87e..66fb299 100644 --- a/src/web.rs +++ b/src/web.rs @@ -118,5 +118,6 @@ async fn route_revision( Ok(render_template(&RevisionTemplate { revision_uri: revision_uri.clone(), revision: state.storage.revision(&revision_uri).await?, + output_attributes: state.storage.output_attributes_for_revision(&revision_uri).await?, })?) } diff --git a/templates/revision.html b/templates/revision.html index f346760..2afa129 100644 --- a/templates/revision.html +++ b/templates/revision.html @@ -8,5 +8,12 @@
  • Revision of: {{ revision.flake_uri }} +

    Outputs

    + + {% endblock %}