add --arg to pass additional arguments to the fetcher

This commit is contained in:
figsoda 2022-12-31 00:40:48 -05:00
parent 1f3b38eeb7
commit 7952a1ede5
10 changed files with 358 additions and 42 deletions

16
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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<String>,
/// List all available fetchers
#[arg(short, long, group = "command")]
pub list_fetchers: bool,

View File

@ -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";
}

View File

@ -1,13 +1,19 @@
use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher};
use crate::{
fetcher::{SimpleFetcher, SimpleFlakeFetcher},
impl_fetcher,
};
pub struct FetchFromGitHub(pub Option<String>);
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<String> {
&self.0
}
}
impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitHub {
const FLAKE_TYPE: &'static str = "github";
}

View File

@ -1,13 +1,19 @@
use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher};
use crate::{
fetcher::{SimpleFetcher, SimpleFlakeFetcher},
impl_fetcher,
};
pub struct FetchFromGitLab(pub Option<String>);
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<String> {
&self.0
}
}
impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitLab {
const FLAKE_TYPE: &'static str = "gitlab";
}

View File

@ -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";
}

View File

@ -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<String>;
@ -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 <nixpkgs> {{}}).{}{{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<String> {
let mut expr = format!(
r#"(import <nixpkgs> {{}}).{}{{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<String> {
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<String> {
)?
.hash)
}
pub fn fod_prefetch(expr: String) -> Result<String> {
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),
))
}

View File

@ -1,13 +1,19 @@
use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher};
use crate::{
fetcher::{SimpleFetcher, SimpleFlakeFetcher},
impl_fetcher,
};
pub struct FetchFromSourcehut(pub Option<String>);
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<String> {
&self.0
}
}
impl<'a> SimpleFlakeFetcher<'a> for FetchFromSourcehut {
const FLAKE_TYPE: &'static str = "sourcehut";
}

View File

@ -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(())