Replace curl with ureq

The HTTP client is now pure Rust
master
Muhammad Hamza 2 years ago
parent cea88f0c9d
commit 33856b04f2
No known key found for this signature in database
GPG Key ID: B7812BE5DBACA4E0
  1. 2
      .gitignore
  2. 150
      Cargo.lock
  3. 15
      Cargo.toml
  4. 15
      src/install/krate.rs
  5. 1
      src/lib.rs
  6. 34
      src/manifest/mod.rs
  7. 15
      src/test/webdriver.rs
  8. 19
      src/test/webdriver/chromedriver.rs
  9. 44
      src/test/webdriver/geckodriver.rs

2
.gitignore vendored

@ -5,3 +5,5 @@ tests/bin
/build-installer /build-installer
docs/book docs/book
docs/installer docs/installer
.idea

150
Cargo.lock generated

@ -160,9 +160,15 @@ dependencies = [
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.0" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
@ -814,9 +820,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -1186,14 +1192,13 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.6" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" checksum = "eebffdb73fe72e917997fad08bdbf31ac50b0fa91cec93e69a0662e4264d454c"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.45.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -1272,9 +1277,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.52" version = "0.10.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" checksum = "12df40a956736488b7b44fe79fe12d4f245bb5b3f5a1f6095e499760015be392"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@ -1302,24 +1307,14 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.25.3+1.1.1t"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.87" version = "0.9.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -1549,11 +1544,11 @@ checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.17" version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
dependencies = [ dependencies = [
"base64", "base64 0.21.2",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -1584,6 +1579,21 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
@ -1604,6 +1614,18 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "rustls"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.13" version = "1.0.13"
@ -1640,11 +1662,21 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.8.2" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
@ -1655,9 +1687,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.8.0" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1817,6 +1849,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -1993,9 +2031,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.28.1" version = "1.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -2093,9 +2131,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.30" version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
] ]
@ -2145,6 +2183,30 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
dependencies = [
"base64 0.13.1",
"flate2",
"log",
"once_cell",
"rustls",
"serde",
"serde_json",
"url",
"webpki",
"webpki-roots",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.3.1"
@ -2307,14 +2369,12 @@ dependencies = [
"cargo_metadata", "cargo_metadata",
"chrono", "chrono",
"console", "console",
"curl",
"dialoguer", "dialoguer",
"env_logger", "env_logger",
"glob", "glob",
"human-panic", "human-panic",
"lazy_static", "lazy_static",
"log", "log",
"openssl",
"parking_lot", "parking_lot",
"predicates 2.1.5", "predicates 2.1.5",
"reqwest", "reqwest",
@ -2329,6 +2389,7 @@ dependencies = [
"structopt", "structopt",
"tempfile", "tempfile",
"toml 0.5.11", "toml 0.5.11",
"ureq",
"walkdir", "walkdir",
"which", "which",
] ]
@ -2343,6 +2404,25 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]] [[package]]
name = "which" name = "which"
version = "4.4.0" version = "4.4.0"

@ -17,15 +17,12 @@ binary-install = "0.1.0"
cargo_metadata = "0.15.2" cargo_metadata = "0.15.2"
chrono = "0.4.23" chrono = "0.4.23"
console = "0.15.5" console = "0.15.5"
curl = "0.4.44"
dialoguer = "0.10.3" dialoguer = "0.10.3"
env_logger = { version = "0.10.0", default-features = false } env_logger = { version = "0.10.0", default-features = false }
glob = "0.3.1" glob = "0.3.1"
human-panic = "1.0.3" human-panic = "1.0.3"
log = "0.4.17" log = "0.4.17"
openssl = { version = '0.10.48', optional = true }
parking_lot = "0.12.1" parking_lot = "0.12.1"
reqwest = { version = "0.11.14", features = ["blocking"] }
semver = "1.0.16" semver = "1.0.16"
serde = "1.0.152" serde = "1.0.152"
serde_derive = "1.0.152" serde_derive = "1.0.152"
@ -35,6 +32,7 @@ siphasher = "0.3.10"
strsim = "0.10.0" strsim = "0.10.0"
structopt = "0.3.26" structopt = "0.3.26"
toml = "0.5.11" toml = "0.5.11"
ureq = { version = "2.6.2", features = ["json"] }
walkdir = "2.3.2" walkdir = "2.3.2"
which = "4.4.0" which = "4.4.0"
@ -44,14 +42,5 @@ lazy_static = "1.4.0"
predicates = "2.1.5" predicates = "2.1.5"
serial_test = "1.0.0" serial_test = "1.0.0"
tempfile = "3.3.0" tempfile = "3.3.0"
reqwest = { version = "0.11.14", features = ["blocking"] }
[features]
# OpenSSL is vendored by default, can use system OpenSSL through feature flag.
default = ['openssl/vendored']
# Treat compiler warnings as a build error.
# This only runs in CI by default
strict = ['openssl/vendored']
sys-openssl = ['openssl']
# Keeping feature for users already using this feature flag
vendored-openssl = ['openssl/vendored']

@ -1,6 +1,5 @@
use crate::install::Tool; use crate::install::Tool;
use anyhow::Result; use anyhow::Result;
use reqwest::header::USER_AGENT;
use serde::Deserialize; use serde::Deserialize;
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
@ -18,16 +17,14 @@ pub struct KrateResponse {
impl Krate { impl Krate {
pub fn new(name: &Tool) -> Result<Krate> { pub fn new(name: &Tool) -> Result<Krate> {
let krate_address = format!("https://crates.io/api/v1/crates/{}", name); let krate_address = format!("https://crates.io/api/v1/crates/{}", name);
let client = reqwest::blocking::Client::new(); let res = ureq::get(&krate_address)
let res = client .set(
.get(&krate_address) "user-agent",
.header( &format!("wasm-pack/{}", VERSION.unwrap_or("unknown")),
USER_AGENT,
format!("wasm-pack/{}", VERSION.unwrap_or("unknown")),
) )
.send()?; .call()?;
let kr: KrateResponse = serde_json::from_str(&res.text()?)?; let kr: KrateResponse = res.into_json()?;
Ok(kr.krate) Ok(kr.krate)
} }
} }

@ -19,7 +19,6 @@ extern crate serde_json;
extern crate structopt; extern crate structopt;
extern crate binary_install; extern crate binary_install;
extern crate chrono; extern crate chrono;
extern crate curl;
extern crate dialoguer; extern crate dialoguer;
extern crate log; extern crate log;
extern crate toml; extern crate toml;

@ -20,7 +20,6 @@ use crate::PBAR;
use cargo_metadata::Metadata; use cargo_metadata::Metadata;
use chrono::offset; use chrono::offset;
use chrono::DateTime; use chrono::DateTime;
use curl::easy;
use serde::{self, Deserialize}; use serde::{self, Deserialize};
use serde_json; use serde_json;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -120,15 +119,6 @@ struct CargoWasmPackProfileWasmBindgen {
dwarf_debug_info: Option<bool>, dwarf_debug_info: Option<bool>,
} }
struct Collector(Vec<u8>);
impl easy::Handler for Collector {
fn write(&mut self, data: &[u8]) -> Result<usize, easy::WriteError> {
self.0.extend_from_slice(data);
Ok(data.len())
}
}
/// Struct for storing information received from crates.io /// Struct for storing information received from crates.io
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Crate { pub struct Crate {
@ -233,26 +223,24 @@ impl Crate {
/// Call to the crates.io api and return the latest version of `wasm-pack` /// Call to the crates.io api and return the latest version of `wasm-pack`
fn check_wasm_pack_latest_version() -> Result<Crate> { fn check_wasm_pack_latest_version() -> Result<Crate> {
let url = "https://crates.io/api/v1/crates/wasm-pack"; let url = "https://crates.io/api/v1/crates/wasm-pack";
let agent = ureq::builder()
let mut easy = easy::Easy2::new(Collector(Vec::new())); .user_agent(&format!(
easy.useragent(&format!(
"wasm-pack/{} ({})", "wasm-pack/{} ({})",
WASM_PACK_VERSION.unwrap_or_else(|| "unknown"), WASM_PACK_VERSION.unwrap_or_else(|| "unknown"),
WASM_PACK_REPO_URL WASM_PACK_REPO_URL
))?; ))
.build();
easy.url(url)?; let resp = agent
easy.get(true)?; .get(url)
easy.perform()?; .call()
.context("failed to get wasm-pack version")?;
let status_code = easy.response_code()?; let status_code = resp.status();
if 200 <= status_code && status_code < 300 { if 200 <= status_code && status_code < 300 {
let contents = easy.get_ref(); let json = resp.into_json()?;
let result = String::from_utf8_lossy(&contents.0);
Ok(serde_json::from_str(result.into_owned().as_str())?) Ok(json)
} else { } else {
bail!( bail!(
"Received a bad HTTP status code ({}) when checking for newer wasm-pack version at: {}", "Received a bad HTTP status code ({}) when checking for newer wasm-pack version at: {}",

@ -34,18 +34,3 @@ fn get_and_notify(
None => Ok(None), None => Ok(None),
} }
} }
struct Collector(Vec<u8>);
impl Collector {
pub fn take_content(&mut self) -> Vec<u8> {
std::mem::take(&mut self.0)
}
}
impl curl::easy::Handler for Collector {
fn write(&mut self, data: &[u8]) -> Result<usize, curl::easy::WriteError> {
self.0.extend_from_slice(data);
Ok(data.len())
}
}

@ -1,4 +1,4 @@
use super::{get_and_notify, Collector}; use super::get_and_notify;
use crate::install::InstallMode; use crate::install::InstallMode;
use crate::stamps; use crate::stamps;
use crate::target; use crate::target;
@ -117,17 +117,12 @@ fn should_load_chromedriver_version_from_stamp(json: &serde_json::Value) -> bool
} }
fn fetch_chromedriver_version() -> Result<String> { fn fetch_chromedriver_version() -> Result<String> {
let mut handle = curl::easy::Easy2::new(Collector(Vec::new())); let version = ureq::get("https://chromedriver.storage.googleapis.com/LATEST_RELEASE")
handle .call()
.url("https://chromedriver.storage.googleapis.com/LATEST_RELEASE") .context("fetching of chromedriver's LATEST_RELEASE failed")?
.context("URL to fetch chromedriver's LATEST_RELEASE is invalid")?; .into_string()
handle .context("converting chromedriver version response to string failed")?;
.perform()
.context("fetching of chromedriver's LATEST_RELEASE failed")?;
let content = handle.get_mut().take_content();
let version =
String::from_utf8(content).context("chromedriver's LATEST_RELEASE is not valid UTF-8")?;
Ok(version) Ok(version)
} }

@ -1,4 +1,4 @@
use super::{get_and_notify, Collector}; use super::get_and_notify;
use crate::install::InstallMode; use crate::install::InstallMode;
use crate::stamps; use crate::stamps;
use crate::target; use crate::target;
@ -77,11 +77,8 @@ pub fn install_geckodriver(cache: &Cache, installation_allowed: bool) -> Result<
/// - it should be relatively safe because each `geckodriver` supports many `Firefox` versions: /// - it should be relatively safe because each `geckodriver` supports many `Firefox` versions:
/// https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html#supported-platforms /// https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html#supported-platforms
fn get_geckodriver_url(target: &str, ext: &str) -> String { fn get_geckodriver_url(target: &str, ext: &str) -> String {
let fetch_and_save_version = || { let fetch_and_save_version =
fetch_latest_geckodriver_tag_json() || fetch_latest_geckodriver_tag_json().and_then(save_geckodriver_version);
.and_then(get_version_from_json)
.and_then(save_geckodriver_version)
};
let geckodriver_version = if target::WINDOWS { let geckodriver_version = if target::WINDOWS {
log::info!( log::info!(
@ -142,37 +139,18 @@ fn should_load_geckodriver_version_from_stamp(json: &serde_json::Value) -> bool
} }
fn fetch_latest_geckodriver_tag_json() -> Result<String> { fn fetch_latest_geckodriver_tag_json() -> Result<String> {
let mut headers = curl::easy::List::new(); let content: serde_json::Value =
headers ureq::get("https://github.com/mozilla/geckodriver/releases/latest")
.append("Accept: application/json") .set("Accept", "application/json")
.context("cannot fetch geckodriver's latest release data - appending header failed")?; .call()
.context("fetching of geckodriver's latest release data failed")?
let mut handle = curl::easy::Easy2::new(Collector(Vec::new())); .into_json()?;
handle
.url("https://github.com/mozilla/geckodriver/releases/latest")
.context("URL to fetch geckodriver's latest release data is invalid")?;
handle
.http_headers(headers)
.context("cannot fetch geckodriver's latest release data - setting headers failed")?;
// We will be redirected from the `latest` placeholder to the specific tag name.
handle
.follow_location(true)
.context("cannot fetch geckodriver's latest release data - enabling redirects failed")?;
handle
.perform()
.context("fetching of geckodriver's latest release data failed")?;
let content = handle.get_mut().take_content();
let version = String::from_utf8(content)
.context("geckodriver's latest release data is not valid UTF-8")?;
Ok(version) get_version_from_json(content)
} }
/// JSON example: `{"id":15227534,"tag_name":"v0.24.0","update_url":"/mozzila...` /// JSON example: `{"id":15227534,"tag_name":"v0.24.0","update_url":"/mozzila...`
fn get_version_from_json(json: impl AsRef<str>) -> Result<String> { fn get_version_from_json(json: serde_json::Value) -> Result<String> {
let json: serde_json::Value = serde_json::from_str(json.as_ref())
.context("geckodriver's latest release data is not valid JSON")?;
json.get("tag_name") json.get("tag_name")
.and_then(|tag_name| tag_name.as_str().map(ToOwned::to_owned)) .and_then(|tag_name| tag_name.as_str().map(ToOwned::to_owned))
.ok_or_else(|| anyhow!("cannot get `tag_name` from geckodriver's latest release data")) .ok_or_else(|| anyhow!("cannot get `tag_name` from geckodriver's latest release data"))

Loading…
Cancel
Save