mirror of
https://github.com/nix-community/nurl.git
synced 2024-08-17 05:50:22 +03:00
add --arg to pass additional arguments to the fetcher
This commit is contained in:
parent
1f3b38eeb7
commit
7952a1ede5
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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),
|
||||
))
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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(())
|
||||
|
Loading…
Reference in New Issue
Block a user