mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-04-22 13:38:43 +00:00

* docstrings for things that I was curious about * report to new stats server, with status * Arc<Self> needs a clone * fix stats url in docs * fix stats url handling
383 lines
12 KiB
Rust
383 lines
12 KiB
Rust
use std::{
|
|
borrow::Cow,
|
|
path::Path,
|
|
sync::{Arc, Mutex, OnceLock},
|
|
};
|
|
|
|
use binstalk_downloader::remote::Method;
|
|
use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta, PkgSigning, Strategy};
|
|
use tokio::sync::OnceCell;
|
|
use tracing::{error, info, trace};
|
|
use url::Url;
|
|
|
|
use crate::{
|
|
common::*, Data, FetchError, SignaturePolicy, SignatureVerifier, SigningAlgorithm,
|
|
TargetDataErased,
|
|
};
|
|
|
|
const BASE_URL: &str = "https://github.com/cargo-bins/cargo-quickinstall/releases/download";
|
|
pub const QUICKINSTALL_STATS_URL: &str =
|
|
"https://cargo-quickinstall-stats-server.fly.dev/record-install";
|
|
|
|
const QUICKINSTALL_SIGN_KEY: Cow<'static, str> =
|
|
Cow::Borrowed("RWTdnnab2pAka9OdwgCMYyOE66M/BlQoFWaJ/JjwcPV+f3n24IRTj97t");
|
|
const QUICKINSTALL_SUPPORTED_TARGETS_URL: &str =
|
|
"https://raw.githubusercontent.com/cargo-bins/cargo-quickinstall/main/supported-targets";
|
|
|
|
fn is_universal_macos(target: &str) -> bool {
|
|
["universal-apple-darwin", "universal2-apple-darwin"].contains(&target)
|
|
}
|
|
|
|
async fn get_quickinstall_supported_targets(
|
|
client: &Client,
|
|
) -> Result<&'static [CompactString], FetchError> {
|
|
static SUPPORTED_TARGETS: OnceCell<Box<[CompactString]>> = OnceCell::const_new();
|
|
|
|
SUPPORTED_TARGETS
|
|
.get_or_try_init(|| async {
|
|
let bytes = client
|
|
.get(Url::parse(QUICKINSTALL_SUPPORTED_TARGETS_URL)?)
|
|
.send(true)
|
|
.await?
|
|
.bytes()
|
|
.await?;
|
|
|
|
let mut v: Vec<CompactString> = String::from_utf8_lossy(&bytes)
|
|
.split_whitespace()
|
|
.map(CompactString::new)
|
|
.collect();
|
|
v.sort_unstable();
|
|
v.dedup();
|
|
Ok(v.into())
|
|
})
|
|
.await
|
|
.map(Box::as_ref)
|
|
}
|
|
|
|
pub struct QuickInstall {
|
|
client: Client,
|
|
gh_api_client: GhApiClient,
|
|
is_supported_v: OnceCell<bool>,
|
|
|
|
data: Arc<Data>,
|
|
package: String,
|
|
package_url: Url,
|
|
signature_url: Url,
|
|
signature_policy: SignaturePolicy,
|
|
|
|
target_data: Arc<TargetDataErased>,
|
|
|
|
signature_verifier: OnceLock<SignatureVerifier>,
|
|
status: Mutex<Status>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum Status {
|
|
Start,
|
|
NotFound,
|
|
Found,
|
|
AttemptingInstall,
|
|
InvalidSignature,
|
|
InstalledFromTarball,
|
|
}
|
|
|
|
impl Status {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Status::Start => "start",
|
|
Status::NotFound => "not-found",
|
|
Status::Found => "found",
|
|
Status::AttemptingInstall => "attempting-install",
|
|
Status::InvalidSignature => "invalid-signature",
|
|
Status::InstalledFromTarball => "installed-from-tarball",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl QuickInstall {
|
|
async fn is_supported(&self) -> Result<bool, FetchError> {
|
|
self.is_supported_v
|
|
.get_or_try_init(|| async {
|
|
Ok(get_quickinstall_supported_targets(&self.client)
|
|
.await?
|
|
.binary_search(&CompactString::new(&self.target_data.target))
|
|
.is_ok())
|
|
})
|
|
.await
|
|
.copied()
|
|
}
|
|
|
|
fn download_signature(
|
|
self: Arc<Self>,
|
|
) -> AutoAbortJoinHandle<Result<SignatureVerifier, FetchError>> {
|
|
AutoAbortJoinHandle::spawn(async move {
|
|
if self.signature_policy == SignaturePolicy::Ignore {
|
|
Ok(SignatureVerifier::Noop)
|
|
} else {
|
|
debug!(url=%self.signature_url, "Downloading signature");
|
|
match Download::new(self.client.clone(), self.signature_url.clone())
|
|
.into_bytes()
|
|
.await
|
|
{
|
|
Ok(signature) => {
|
|
trace!(?signature, "got signature contents");
|
|
let config = PkgSigning {
|
|
algorithm: SigningAlgorithm::Minisign,
|
|
pubkey: QUICKINSTALL_SIGN_KEY,
|
|
file: None,
|
|
};
|
|
SignatureVerifier::new(&config, &signature)
|
|
}
|
|
Err(err) => {
|
|
if self.signature_policy == SignaturePolicy::Require {
|
|
error!("Failed to download signature: {err}");
|
|
Err(FetchError::MissingSignature)
|
|
} else {
|
|
debug!("Failed to download signature, skipping verification: {err}");
|
|
Ok(SignatureVerifier::Noop)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn get_status(&self) -> Status {
|
|
*self.status.lock().unwrap()
|
|
}
|
|
|
|
fn set_status(&self, status: Status) {
|
|
*self.status.lock().unwrap() = status;
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl super::Fetcher for QuickInstall {
|
|
fn new(
|
|
client: Client,
|
|
gh_api_client: GhApiClient,
|
|
data: Arc<Data>,
|
|
target_data: Arc<TargetDataErased>,
|
|
signature_policy: SignaturePolicy,
|
|
) -> Arc<dyn super::Fetcher> {
|
|
let crate_name = &data.name;
|
|
let version = &data.version;
|
|
let target = &target_data.target;
|
|
|
|
let package = format!("{crate_name}-{version}-{target}");
|
|
|
|
let url = format!("{BASE_URL}/{crate_name}-{version}/{package}.tar.gz");
|
|
|
|
Arc::new(Self {
|
|
client,
|
|
data,
|
|
gh_api_client,
|
|
is_supported_v: OnceCell::new(),
|
|
|
|
package_url: Url::parse(&url)
|
|
.expect("package_url is pre-generated and should never be invalid url"),
|
|
signature_url: Url::parse(&format!("{url}.sig"))
|
|
.expect("signature_url is pre-generated and should never be invalid url"),
|
|
package,
|
|
signature_policy,
|
|
|
|
target_data,
|
|
|
|
signature_verifier: OnceLock::new(),
|
|
status: Mutex::new(Status::Start),
|
|
})
|
|
}
|
|
|
|
fn find(self: Arc<Self>) -> JoinHandle<Result<bool, FetchError>> {
|
|
tokio::spawn(async move {
|
|
if !self.is_supported().await? {
|
|
return Ok(false);
|
|
}
|
|
|
|
let download_signature_task = self.clone().download_signature();
|
|
|
|
let is_found = does_url_exist(
|
|
self.client.clone(),
|
|
self.gh_api_client.clone(),
|
|
&self.package_url,
|
|
)
|
|
.await?;
|
|
|
|
if !is_found {
|
|
self.set_status(Status::NotFound);
|
|
return Ok(false);
|
|
}
|
|
|
|
if self
|
|
.signature_verifier
|
|
.set(download_signature_task.flattened_join().await?)
|
|
.is_err()
|
|
{
|
|
panic!("<QuickInstall as Fetcher>::find is run twice");
|
|
}
|
|
|
|
self.set_status(Status::Found);
|
|
Ok(true)
|
|
})
|
|
}
|
|
|
|
fn report_to_upstream(self: Arc<Self>) {
|
|
if cfg!(debug_assertions) {
|
|
debug!("Not sending quickinstall report in debug mode");
|
|
} else if is_universal_macos(&self.target_data.target) {
|
|
debug!(
|
|
r#"Not sending quickinstall report for universal-apple-darwin
|
|
and universal2-apple-darwin.
|
|
Quickinstall does not support these targets, it only supports targets supported
|
|
by rust officially."#,
|
|
);
|
|
} else if self.is_supported_v.get().copied() != Some(false) {
|
|
tokio::spawn(async move {
|
|
if let Err(err) = self.report().await {
|
|
warn!(
|
|
"Failed to send quickinstall report for package {} (NOTE that this does not affect package resolution): {err}",
|
|
self.package
|
|
)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async fn fetch_and_extract(&self, dst: &Path) -> Result<ExtractedFiles, FetchError> {
|
|
self.set_status(Status::AttemptingInstall);
|
|
let Some(verifier) = self.signature_verifier.get() else {
|
|
panic!("<QuickInstall as Fetcher>::find has not been called yet!")
|
|
};
|
|
|
|
debug!(url=%self.package_url, "Downloading package");
|
|
let mut data_verifier = verifier.data_verifier()?;
|
|
let files = Download::new_with_data_verifier(
|
|
self.client.clone(),
|
|
self.package_url.clone(),
|
|
data_verifier.as_mut(),
|
|
)
|
|
.and_extract(self.pkg_fmt(), dst)
|
|
.await?;
|
|
trace!("validating signature (if any)");
|
|
if data_verifier.validate() {
|
|
if let Some(info) = verifier.info() {
|
|
info!("Verified signature for package '{}': {info}", self.package);
|
|
}
|
|
self.set_status(Status::InstalledFromTarball);
|
|
Ok(files)
|
|
} else {
|
|
self.set_status(Status::InvalidSignature);
|
|
Err(FetchError::InvalidSignature)
|
|
}
|
|
}
|
|
|
|
fn pkg_fmt(&self) -> PkgFmt {
|
|
PkgFmt::Tgz
|
|
}
|
|
|
|
fn target_meta(&self) -> PkgMeta {
|
|
let mut meta = self.target_data.meta.clone();
|
|
meta.pkg_fmt = Some(self.pkg_fmt());
|
|
meta.bin_dir = Some("{ bin }{ binary-ext }".to_string());
|
|
meta
|
|
}
|
|
|
|
fn source_name(&self) -> CompactString {
|
|
CompactString::from("QuickInstall")
|
|
}
|
|
|
|
fn fetcher_name(&self) -> &'static str {
|
|
"QuickInstall"
|
|
}
|
|
|
|
fn strategy(&self) -> Strategy {
|
|
Strategy::QuickInstall
|
|
}
|
|
|
|
fn is_third_party(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn target(&self) -> &str {
|
|
&self.target_data.target
|
|
}
|
|
|
|
fn target_data(&self) -> &Arc<TargetDataErased> {
|
|
&self.target_data
|
|
}
|
|
}
|
|
|
|
impl QuickInstall {
|
|
pub async fn report(&self) -> Result<(), FetchError> {
|
|
if !self.is_supported().await? {
|
|
debug!(
|
|
"Not sending quickinstall report for {} since Quickinstall does not support these targets.",
|
|
self.target_data.target
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let mut url = Url::parse(QUICKINSTALL_STATS_URL)
|
|
.expect("stats_url is pre-generated and should never be invalid url");
|
|
url.query_pairs_mut()
|
|
.append_pair("crate", &self.data.name)
|
|
.append_pair("version", &self.data.version)
|
|
.append_pair("target", &self.target_data.target)
|
|
.append_pair(
|
|
"agent",
|
|
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
|
|
)
|
|
.append_pair("status", self.get_status().as_str());
|
|
debug!("Sending installation report to quickinstall ({url})");
|
|
|
|
self.client.request(Method::POST, url).send(true).await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::{get_quickinstall_supported_targets, Client, CompactString};
|
|
use std::num::NonZeroU16;
|
|
|
|
/// Mark this as an async fn so that you won't accidentally use it in
|
|
/// sync context.
|
|
async fn create_client() -> Client {
|
|
Client::new(
|
|
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
|
|
None,
|
|
NonZeroU16::new(10).unwrap(),
|
|
1.try_into().unwrap(),
|
|
[],
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_quickinstall_supported_targets() {
|
|
let supported_targets = get_quickinstall_supported_targets(&create_client().await)
|
|
.await
|
|
.unwrap();
|
|
|
|
[
|
|
"x86_64-pc-windows-msvc",
|
|
"x86_64-apple-darwin",
|
|
"aarch64-apple-darwin",
|
|
"x86_64-unknown-linux-gnu",
|
|
"x86_64-unknown-linux-musl",
|
|
"aarch64-unknown-linux-gnu",
|
|
"aarch64-unknown-linux-musl",
|
|
"aarch64-pc-windows-msvc",
|
|
"armv7-unknown-linux-musleabihf",
|
|
"armv7-unknown-linux-gnueabihf",
|
|
]
|
|
.into_iter()
|
|
.for_each(|known_supported_target| {
|
|
supported_targets
|
|
.binary_search(&CompactString::new(known_supported_target))
|
|
.unwrap();
|
|
});
|
|
}
|
|
}
|