diff --git a/Cargo.lock b/Cargo.lock index 3f63f75..983f038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,12 @@ dependencies = [ "roff", ] +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "enum_dispatch" version = "0.3.9" @@ -174,6 +180,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.5" @@ -202,6 +217,7 @@ dependencies = [ "clap_mangen", "enum_dispatch", "indoc", + "itertools", "serde", "serde_json", "url", diff --git a/Cargo.toml b/Cargo.toml index 728aeb6..f77b834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ categories = ["command-line-utilities"] anyhow = "1.0.68" enum_dispatch = "0.3.9" indoc = "1.0.8" +itertools = "0.10.5" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" url = "2.3.1" diff --git a/src/cli.rs b/src/cli.rs index 16191ef..4831011 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -34,6 +34,10 @@ pub struct Opts { #[arg(short, long)] pub json: bool, + // additional arguments to pass to the fetcher + #[arg(short, long = "arg", num_args = 2, value_names = ["KEY", "VALUE"])] + pub args: Vec, + /// List all available fetchers #[arg(short, long, group = "command")] pub list_fetchers: bool, diff --git a/src/fetcher/git.rs b/src/fetcher/git.rs index 3d632f7..556e950 100644 --- a/src/fetcher/git.rs +++ b/src/fetcher/git.rs @@ -1,9 +1,15 @@ -use crate::{fetcher::UrlFlakeFetcher, impl_fetcher}; +use crate::{ + fetcher::{UrlFetcher, UrlFlakeFetcher}, + impl_fetcher, +}; pub struct Fetchgit; impl_fetcher!(Fetchgit); -impl UrlFlakeFetcher for Fetchgit { - const FLAKE_TYPE: &'static str = "git"; +impl UrlFetcher for Fetchgit { const NAME: &'static str = "fetchgit"; } + +impl UrlFlakeFetcher for Fetchgit { + const FLAKE_TYPE: &'static str = "git"; +} diff --git a/src/fetcher/github.rs b/src/fetcher/github.rs index 98c5178..b59359c 100644 --- a/src/fetcher/github.rs +++ b/src/fetcher/github.rs @@ -1,13 +1,19 @@ -use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher}; +use crate::{ + fetcher::{SimpleFetcher, SimpleFlakeFetcher}, + impl_fetcher, +}; pub struct FetchFromGitHub(pub Option); impl_fetcher!(FetchFromGitHub); -impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitHub { - const FLAKE_TYPE: &'static str = "github"; +impl<'a> SimpleFetcher<'a> for FetchFromGitHub { const NAME: &'static str = "fetchFromGitHub"; fn host(&'a self) -> &'a Option { &self.0 } } + +impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitHub { + const FLAKE_TYPE: &'static str = "github"; +} diff --git a/src/fetcher/gitlab.rs b/src/fetcher/gitlab.rs index 422c4db..a7e71ec 100644 --- a/src/fetcher/gitlab.rs +++ b/src/fetcher/gitlab.rs @@ -1,13 +1,19 @@ -use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher}; +use crate::{ + fetcher::{SimpleFetcher, SimpleFlakeFetcher}, + impl_fetcher, +}; pub struct FetchFromGitLab(pub Option); impl_fetcher!(FetchFromGitLab); -impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitLab { - const FLAKE_TYPE: &'static str = "gitlab"; +impl<'a> SimpleFetcher<'a> for FetchFromGitLab { const NAME: &'static str = "fetchFromGitLab"; fn host(&'a self) -> &'a Option { &self.0 } } + +impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitLab { + const FLAKE_TYPE: &'static str = "gitlab"; +} diff --git a/src/fetcher/hg.rs b/src/fetcher/hg.rs index abff606..224f405 100644 --- a/src/fetcher/hg.rs +++ b/src/fetcher/hg.rs @@ -1,9 +1,15 @@ -use crate::{fetcher::UrlFlakeFetcher, impl_fetcher}; +use crate::{ + fetcher::{UrlFetcher, UrlFlakeFetcher}, + impl_fetcher, +}; pub struct Fetchhg; impl_fetcher!(Fetchhg); -impl UrlFlakeFetcher for Fetchhg { - const FLAKE_TYPE: &'static str = "hg"; +impl UrlFetcher for Fetchhg { const NAME: &'static str = "fetchhg"; } + +impl UrlFlakeFetcher for Fetchhg { + const FLAKE_TYPE: &'static str = "hg"; +} diff --git a/src/fetcher/mod.rs b/src/fetcher/mod.rs index 65e6e73..d67c71e 100644 --- a/src/fetcher/mod.rs +++ b/src/fetcher/mod.rs @@ -4,28 +4,43 @@ mod gitlab; mod hg; mod sourcehut; -use enum_dispatch::enum_dispatch; pub use git::Fetchgit; pub use github::FetchFromGitHub; pub use gitlab::FetchFromGitLab; pub use hg::Fetchhg; -use indoc::writedoc; -use serde_json::json; pub use sourcehut::FetchFromSourcehut; -use anyhow::{bail, Context, Result}; +use enum_dispatch::enum_dispatch; +use indoc::writedoc; +use serde_json::json; + +use anyhow::{anyhow, bail, Context, Result}; use serde::Deserialize; use url::Url; use std::{ - io::Write, + fmt::Write as _, + io::{BufRead, Write}, process::{Command, Output, Stdio}, }; #[enum_dispatch] pub trait Fetcher { - fn fetch_nix(&self, out: &mut impl Write, url: Url, rev: String, indent: String) -> Result<()>; - fn fetch_json(&self, out: &mut impl Write, url: Url, rev: String) -> Result<()>; + fn fetch_nix( + &self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + indent: String, + ) -> Result<()>; + fn fetch_json( + &self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + ) -> Result<()>; } #[enum_dispatch(Fetcher)] @@ -46,9 +61,10 @@ macro_rules! impl_fetcher { out: &mut impl ::std::io::Write, url: ::url::Url, rev: String, + args: Vec<(String, String)>, indent: String, ) -> ::anyhow::Result<()> { - self.fetch_nix_impl(out, url, rev, indent) + self.fetch_nix_impl(out, url, rev, args, indent) } fn fetch_json( @@ -56,15 +72,15 @@ macro_rules! impl_fetcher { out: &mut impl ::std::io::Write, url: ::url::Url, rev: String, + args: Vec<(String, String)>, ) -> ::anyhow::Result<()> { - self.fetch_json_impl(out, url, rev) + self.fetch_json_impl(out, url, rev, args) } } }; } -pub trait SimpleFlakeFetcher<'a> { - const FLAKE_TYPE: &'static str; +pub trait SimpleFetcher<'a> { const NAME: &'static str; fn host(&'a self) -> &'a Option; @@ -79,6 +95,109 @@ pub trait SimpleFlakeFetcher<'a> { )) } + fn fetch_fod( + &'a self, + url: &Url, + rev: &str, + args: &[(String, String)], + ) -> Result<(String, String, String)> { + let (owner, repo) = self + .get_repo(url) + .with_context(|| format!("failed to parse {url}"))?; + + let mut expr = format!( + r#"(import {{}}).{}{{owner="{owner}";repo="{repo}";rev="{rev}";hash="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";"#, + Self::NAME + ); + if let Some(host) = self.host() { + write!(expr, r#"domain="{host}""#)?; + } + for (key, value) in args { + write!(expr, "{key}={value};")?; + } + expr.push('}'); + + let hash = fod_prefetch(expr)?; + + Ok((owner, repo, hash)) + } +} + +pub trait SimpleFodFetcher<'a>: SimpleFetcher<'a> { + fn fetch_nix_impl( + &'a self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + indent: String, + ) -> Result<()> { + let (owner, repo, hash) = self.fetch_fod(&url, &rev, &args)?; + + writeln!(out, "{} {{", Self::NAME)?; + + if let Some(domain) = self.host() { + writeln!(out, r#"{indent} domain = "{domain}";"#)?; + } + + writedoc!( + out, + r#" + {indent} owner = "{owner}"; + {indent} repo = "{repo}"; + {indent} rev = "{rev}"; + {indent} hash = "{hash}"; + "# + )?; + + for (key, value) in args { + writeln!(out, "{indent} {key} = {value};")?; + } + + write!(out, "{indent}}}")?; + + Ok(()) + } + + fn fetch_json_impl( + &'a self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + ) -> Result<()> { + let (owner, repo, hash) = self.fetch_fod(&url, &rev, &args)?; + + let mut fetcher_args = json! ({ + "owner": owner, + "repo": repo, + "rev": rev, + "hash": hash, + }); + + if let Some(host) = self.host() { + fetcher_args["host"] = json!(host); + } + + for (key, value) in args { + fetcher_args[key] = json!(value); + } + + serde_json::to_writer( + out, + &json!({ + "fetcher": Self::NAME, + "args": fetcher_args, + }), + )?; + + Ok(()) + } +} + +pub trait SimpleFlakeFetcher<'a>: SimpleFetcher<'a> { + const FLAKE_TYPE: &'static str; + fn fetch(&'a self, url: &Url, rev: &str) -> Result<(String, String, String)> { let (owner, repo) = self .get_repo(url) @@ -98,9 +217,14 @@ pub trait SimpleFlakeFetcher<'a> { out: &mut impl Write, url: Url, rev: String, + args: Vec<(String, String)>, indent: String, ) -> Result<()> { - let (owner, repo, hash) = self.fetch(&url, &rev)?; + let (owner, repo, hash) = if args.is_empty() { + self.fetch(&url, &rev)? + } else { + self.fetch_fod(&url, &rev, &args)? + }; writeln!(out, "{} {{", Self::NAME)?; @@ -115,16 +239,32 @@ pub trait SimpleFlakeFetcher<'a> { {indent} repo = "{repo}"; {indent} rev = "{rev}"; {indent} hash = "{hash}"; - {indent}}}"#, + "# )?; + for (key, value) in args { + writeln!(out, "{indent} {key} = {value};")?; + } + + write!(out, "{indent}}}")?; + Ok(()) } - fn fetch_json_impl(&'a self, out: &mut impl Write, url: Url, rev: String) -> Result<()> { - let (owner, repo, hash) = self.fetch(&url, &rev)?; + fn fetch_json_impl( + &'a self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + ) -> Result<()> { + let (owner, repo, hash) = if args.is_empty() { + self.fetch(&url, &rev)? + } else { + self.fetch_fod(&url, &rev, &args)? + }; - let mut args = json! ({ + let mut fetcher_args = json! ({ "owner": owner, "repo": repo, "rev": rev, @@ -132,14 +272,18 @@ pub trait SimpleFlakeFetcher<'a> { }); if let Some(host) = self.host() { - args["host"] = json!(host); + fetcher_args["host"] = json!(host); + } + + for (key, value) in args { + fetcher_args[key] = json!(value); } serde_json::to_writer( out, &json!({ "fetcher": Self::NAME, - "args": args, + "args": fetcher_args, }), )?; @@ -147,10 +291,57 @@ pub trait SimpleFlakeFetcher<'a> { } } -pub trait UrlFlakeFetcher { - const FLAKE_TYPE: &'static str; +pub trait UrlFetcher { const NAME: &'static str; + fn fetch_fod(&self, url: &Url, rev: &str, args: &[(String, String)]) -> Result { + let mut expr = format!( + r#"(import {{}}).{}{{url="{url}";rev="{rev}";hash="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";"#, + Self::NAME + ); + for (key, value) in args { + write!(expr, "{key}={value};")?; + } + expr.push('}'); + fod_prefetch(expr) + } +} + +pub trait UrlFodFetcher: UrlFetcher { + fn fetch_nix_impl( + &self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + indent: String, + ) -> Result<()> { + let hash = self.fetch_fod(&url, &rev, &args)?; + + writedoc!( + out, + r#" + {} {{ + {indent} url = "{url}"; + {indent} rev = "{rev}"; + {indent} hash = "{hash}"; + "#, + Self::NAME + )?; + + for (key, value) in args { + writeln!(out, "{indent} {key} = {value};")?; + } + + write!(out, "{indent}}}")?; + + Ok(()) + } +} + +pub trait UrlFlakeFetcher: UrlFetcher { + const FLAKE_TYPE: &'static str; + fn fetch(&self, url: &Url, rev: &str) -> Result { flake_prefetch(format!( "{}+{url}?{}={rev}", @@ -164,9 +355,14 @@ pub trait UrlFlakeFetcher { out: &mut impl Write, url: Url, rev: String, + args: Vec<(String, String)>, indent: String, ) -> Result<()> { - let hash = self.fetch(&url, &rev)?; + let hash = if args.is_empty() { + self.fetch(&url, &rev)? + } else { + self.fetch_fod(&url, &rev, &args)? + }; writedoc!( out, @@ -175,15 +371,41 @@ pub trait UrlFlakeFetcher { {indent} url = "{url}"; {indent} rev = "{rev}"; {indent} hash = "{hash}"; - {indent}}}"#, - Self::NAME, + "#, + Self::NAME )?; + for (key, value) in args { + writeln!(out, "{indent} {key} = {value};")?; + } + + write!(out, "{indent}}}")?; + Ok(()) } - fn fetch_json_impl(&self, out: &mut impl Write, url: Url, rev: String) -> Result<()> { - let hash = self.fetch(&url, &rev)?; + fn fetch_json_impl( + &self, + out: &mut impl Write, + url: Url, + rev: String, + args: Vec<(String, String)>, + ) -> Result<()> { + let hash = if args.is_empty() { + self.fetch(&url, &rev)? + } else { + self.fetch_fod(&url, &rev, &args)? + }; + + let mut fetcher_args = json!({ + "url": url.to_string(), + "rev": rev, + "hash": hash, + }); + + for (key, value) in args { + fetcher_args[key] = json!(value); + } serde_json::to_writer( out, @@ -232,3 +454,44 @@ pub fn flake_prefetch(flake_ref: String) -> Result { )? .hash) } + +pub fn fod_prefetch(expr: String) -> Result { + eprintln!("$ nix build --impure --no-link --expr '{expr}'"); + + let Output { + stdout, + stderr, + status, + } = Command::new("nix") + .arg("build") + .arg("--impure") + .arg("--no-link") + .arg("--expr") + .arg(expr) + .output()?; + + if status.success() { + bail!( + "command succeeded unexpectedly\nstdout:\n{}", + String::from_utf8_lossy(&stdout), + ); + } + + let mut lines = stderr.lines(); + while let Some(line) = lines.next() { + if !matches!(line, Ok(line) if line.trim_start().starts_with("specified:")) { + continue; + } + let Some(line) = lines.next() else { break; }; + if let Ok(line) = line { + let Some(hash) = line.trim_start().strip_prefix("got:") else { continue; }; + return Ok(hash.trim().into()); + } + } + + Err(anyhow!( + "failed to find the hash from error messages\nstdout: {}\nstderr:\n{}", + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr), + )) +} diff --git a/src/fetcher/sourcehut.rs b/src/fetcher/sourcehut.rs index d1dd10d..256c93e 100644 --- a/src/fetcher/sourcehut.rs +++ b/src/fetcher/sourcehut.rs @@ -1,13 +1,19 @@ -use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher}; +use crate::{ + fetcher::{SimpleFetcher, SimpleFlakeFetcher}, + impl_fetcher, +}; pub struct FetchFromSourcehut(pub Option); impl_fetcher!(FetchFromSourcehut); -impl<'a> SimpleFlakeFetcher<'a> for FetchFromSourcehut { - const FLAKE_TYPE: &'static str = "sourcehut"; +impl<'a> SimpleFetcher<'a> for FetchFromSourcehut { const NAME: &'static str = "fetchFromSourcehut"; fn host(&'a self) -> &'a Option { &self.0 } } + +impl<'a> SimpleFlakeFetcher<'a> for FetchFromSourcehut { + const FLAKE_TYPE: &'static str = "sourcehut"; +} diff --git a/src/main.rs b/src/main.rs index d637842..71298f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod fetcher; use anyhow::{bail, Result}; use clap::{Parser, ValueEnum}; +use itertools::Itertools; use url::Host; use crate::{ @@ -73,10 +74,11 @@ fn main() -> Result<()> { }; let out = &mut stdout().lock(); + let args = opts.args.into_iter().tuples().collect(); if opts.json { - fetcher.fetch_json(out, opts.url, opts.rev) + fetcher.fetch_json(out, opts.url, opts.rev, args) } else { - fetcher.fetch_nix(out, opts.url, opts.rev, " ".repeat(opts.indent)) + fetcher.fetch_nix(out, opts.url, opts.rev, args, " ".repeat(opts.indent)) }?; Ok(())