Ensure that version resolution works offline
And so, even for unpinned package. In this case, we can't do a HEAD request. So we fallback by looking at what's available in the cache and using the most recently downloaded version from the cache. This is only a best effort as the most recently downloaded one may not be the actual latest. But common, this is a case where (a) someone didn't pin any version, (b) is trying to build on in an offline setup. We could possibly make that edge-case better but, let's see if anyone ever complains about it first.
This commit is contained in:
parent
87087a1811
commit
76ff09ba0e
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
package_name::PackageName,
|
package_name::PackageName,
|
||||||
paths,
|
paths,
|
||||||
telemetry::{Event, EventListener},
|
telemetry::{DownloadSource, Event, EventListener},
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
package_name::PackageName,
|
package_name::PackageName,
|
||||||
paths::{self, CacheKey},
|
paths::{self, CacheKey},
|
||||||
|
telemetry::EventListener,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::manifest::Package;
|
use super::manifest::Package;
|
||||||
|
|
|
@ -107,6 +107,13 @@ pub enum Error {
|
||||||
)]
|
)]
|
||||||
UnknownPackageVersion { package: Package },
|
UnknownPackageVersion { package: Package },
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"I need to resolve a package {}/{}, but couldn't find it.",
|
||||||
|
package.name.owner,
|
||||||
|
package.name.repo,
|
||||||
|
)]
|
||||||
|
UnableToResolvePackage { package: Package },
|
||||||
|
|
||||||
#[error("I couldn't parse the provided stake address.")]
|
#[error("I couldn't parse the provided stake address.")]
|
||||||
MalformedStakeAddress {
|
MalformedStakeAddress {
|
||||||
error: Option<pallas::ledger::addresses::Error>,
|
error: Option<pallas::ledger::addresses::Error>,
|
||||||
|
@ -188,6 +195,7 @@ impl GetSource for Error {
|
||||||
Error::ZipExtract(_) => None,
|
Error::ZipExtract(_) => None,
|
||||||
Error::JoinError(_) => None,
|
Error::JoinError(_) => None,
|
||||||
Error::UnknownPackageVersion { .. } => None,
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
@ -213,6 +221,7 @@ impl GetSource for Error {
|
||||||
Error::ZipExtract(_) => None,
|
Error::ZipExtract(_) => None,
|
||||||
Error::JoinError(_) => None,
|
Error::JoinError(_) => None,
|
||||||
Error::UnknownPackageVersion { .. } => None,
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
@ -247,6 +256,7 @@ impl Diagnostic for Error {
|
||||||
Error::ZipExtract(_) => None,
|
Error::ZipExtract(_) => None,
|
||||||
Error::JoinError(_) => None,
|
Error::JoinError(_) => None,
|
||||||
Error::UnknownPackageVersion { .. } => Some(Box::new("aiken::packages::resolve")),
|
Error::UnknownPackageVersion { .. } => Some(Box::new("aiken::packages::resolve")),
|
||||||
|
Error::UnableToResolvePackage { .. } => Some(Box::new("aiken::package::download")),
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
@ -306,6 +316,7 @@ impl Diagnostic for Error {
|
||||||
Error::ZipExtract(_) => None,
|
Error::ZipExtract(_) => None,
|
||||||
Error::JoinError(_) => None,
|
Error::JoinError(_) => None,
|
||||||
Error::UnknownPackageVersion{..} => Some(Box::new("Perhaps, double-check the package repository and version?")),
|
Error::UnknownPackageVersion{..} => Some(Box::new("Perhaps, double-check the package repository and version?")),
|
||||||
|
Error::UnableToResolvePackage{..} => Some(Box::new("The network is unavailable and the package isn't in the local cache either. Try connecting to the Internet so I can look it up?")),
|
||||||
Error::Json(error) => Some(Box::new(format!("{error}"))),
|
Error::Json(error) => Some(Box::new(format!("{error}"))),
|
||||||
Error::MalformedStakeAddress { error } => Some(Box::new(format!("A stake address must be provided either as a base16-encoded string, or as a bech32-encoded string with the 'stake' or 'stake_test' prefix.{hint}", hint = match error {
|
Error::MalformedStakeAddress { error } => Some(Box::new(format!("A stake address must be provided either as a base16-encoded string, or as a bech32-encoded string with the 'stake' or 'stake_test' prefix.{hint}", hint = match error {
|
||||||
Some(error) => format!("\n\nHere's the error I encountered: {error}"),
|
Some(error) => format!("\n\nHere's the error I encountered: {error}"),
|
||||||
|
@ -366,6 +377,7 @@ impl Diagnostic for Error {
|
||||||
Error::ZipExtract(_) => None,
|
Error::ZipExtract(_) => None,
|
||||||
Error::JoinError(_) => None,
|
Error::JoinError(_) => None,
|
||||||
Error::UnknownPackageVersion { .. } => None,
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
@ -391,6 +403,7 @@ impl Diagnostic for Error {
|
||||||
Error::ZipExtract(_) => None,
|
Error::ZipExtract(_) => None,
|
||||||
Error::JoinError(_) => None,
|
Error::JoinError(_) => None,
|
||||||
Error::UnknownPackageVersion { .. } => None,
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
@ -416,6 +429,7 @@ impl Diagnostic for Error {
|
||||||
Error::ZipExtract { .. } => None,
|
Error::ZipExtract { .. } => None,
|
||||||
Error::JoinError { .. } => None,
|
Error::JoinError { .. } => None,
|
||||||
Error::UnknownPackageVersion { .. } => None,
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
@ -441,6 +455,7 @@ impl Diagnostic for Error {
|
||||||
Error::ZipExtract { .. } => None,
|
Error::ZipExtract { .. } => None,
|
||||||
Error::JoinError { .. } => None,
|
Error::JoinError { .. } => None,
|
||||||
Error::UnknownPackageVersion { .. } => None,
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
Error::Json { .. } => None,
|
Error::Json { .. } => None,
|
||||||
Error::MalformedStakeAddress { .. } => None,
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
Error::NoValidatorNotFound { .. } => None,
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
use crate::deps::manifest::Package;
|
use crate::deps::manifest::Package;
|
||||||
use crate::{error::Error, package_name::PackageName};
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
package_name::PackageName,
|
||||||
|
telemetry::{Event, EventListener},
|
||||||
|
};
|
||||||
|
use regex::Regex;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::path::PathBuf;
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
pub fn project_config() -> PathBuf {
|
pub fn project_config() -> PathBuf {
|
||||||
PathBuf::from("aiken.toml")
|
PathBuf::from("aiken.toml")
|
||||||
|
@ -28,7 +33,7 @@ pub fn build_deps_package(package_name: &PackageName) -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn package_cache_zipball(cache_key: &CacheKey) -> PathBuf {
|
pub fn package_cache_zipball(cache_key: &CacheKey) -> PathBuf {
|
||||||
packages_cache().join(cache_key.get_key())
|
packages_cache().join(format!("{}.zip", cache_key.get_key()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn packages_cache() -> PathBuf {
|
pub fn packages_cache() -> PathBuf {
|
||||||
|
@ -47,37 +52,36 @@ pub struct CacheKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CacheKey {
|
impl CacheKey {
|
||||||
pub async fn new(http: &Client, package: &Package) -> Result<CacheKey, Error> {
|
pub async fn new<T>(
|
||||||
let version = match hex::decode(&package.version) {
|
http: &Client,
|
||||||
Ok(..) => Ok(package.version.to_string()),
|
event_listener: &T,
|
||||||
Err(..) => {
|
package: &Package,
|
||||||
let url = format!(
|
) -> Result<CacheKey, Error>
|
||||||
"https://api.github.com/repos/{}/{}/zipball/{}",
|
where
|
||||||
package.name.owner, package.name.repo, package.version
|
T: EventListener,
|
||||||
);
|
{
|
||||||
let response = http
|
Ok(CacheKey::from_package(
|
||||||
.head(url)
|
package,
|
||||||
.header("User-Agent", "aiken-lang")
|
if is_git_sha_or_tag(&package.version) {
|
||||||
.send()
|
Ok(package.version.to_string())
|
||||||
.await?;
|
} else {
|
||||||
let etag = response
|
match new_cache_key_from_network(http, package).await {
|
||||||
.headers()
|
Err(_) => {
|
||||||
.get("etag")
|
event_listener.handle_event(Event::PackageResolveFallback {
|
||||||
.ok_or(Error::UnknownPackageVersion {
|
name: format!("{}", package.name),
|
||||||
package: package.clone(),
|
});
|
||||||
})?
|
new_cache_key_from_cache(package)
|
||||||
.to_str()
|
}
|
||||||
.unwrap()
|
Ok(cache_key) => Ok(cache_key),
|
||||||
.replace('"', "");
|
}
|
||||||
Ok(format!("main@{etag}"))
|
}?,
|
||||||
}
|
))
|
||||||
};
|
}
|
||||||
version.map(|version| CacheKey {
|
|
||||||
key: format!(
|
fn from_package(package: &Package, version: String) -> CacheKey {
|
||||||
"{}-{}-{}.zip",
|
CacheKey {
|
||||||
package.name.owner, package.name.repo, version
|
key: format!("{}-{}-{}", package.name.owner, package.name.repo, version),
|
||||||
),
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_key(&self) -> &str {
|
pub fn get_key(&self) -> &str {
|
||||||
|
@ -85,6 +89,73 @@ impl CacheKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn new_cache_key_from_network(http: &Client, package: &Package) -> Result<String, Error> {
|
||||||
|
let url = format!(
|
||||||
|
"https://api.github.com/repos/{}/{}/zipball/{}",
|
||||||
|
package.name.owner, package.name.repo, package.version
|
||||||
|
);
|
||||||
|
let response = http
|
||||||
|
.head(url)
|
||||||
|
.header("User-Agent", "aiken-lang")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let etag = response
|
||||||
|
.headers()
|
||||||
|
.get("etag")
|
||||||
|
.ok_or(Error::UnknownPackageVersion {
|
||||||
|
package: package.clone(),
|
||||||
|
})?
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.replace('"', "");
|
||||||
|
Ok(format!(
|
||||||
|
"{version}@{etag}",
|
||||||
|
version = package.version.replace('/', "_")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_cache_key_from_cache(target: &Package) -> Result<String, Error> {
|
||||||
|
let packages = fs::read_dir(packages_cache())?;
|
||||||
|
|
||||||
|
let prefix = CacheKey::from_package(target, target.version.replace('/', "_"))
|
||||||
|
.get_key()
|
||||||
|
.to_string();
|
||||||
|
let mut most_recently_modified_date = None;
|
||||||
|
let mut most_recently_modified = None;
|
||||||
|
|
||||||
|
for pkg in packages {
|
||||||
|
let entry = pkg.unwrap();
|
||||||
|
|
||||||
|
let filename = entry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.expect("cache filename are valid utf8 strings");
|
||||||
|
|
||||||
|
if filename.starts_with(&prefix) {
|
||||||
|
let last_modified = entry.metadata()?.modified()?;
|
||||||
|
if Some(last_modified) > most_recently_modified_date {
|
||||||
|
most_recently_modified_date = Some(last_modified);
|
||||||
|
most_recently_modified = Some(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match most_recently_modified {
|
||||||
|
None => Err(Error::UnableToResolvePackage {
|
||||||
|
package: target.clone(),
|
||||||
|
}),
|
||||||
|
Some(pkg) => Ok(format!(
|
||||||
|
"{version}{etag}",
|
||||||
|
version = target.version,
|
||||||
|
etag = pkg
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.expect("cache filename starts with a valid version prefix")
|
||||||
|
.strip_suffix(".zip")
|
||||||
|
.expect("cache files are all zip archives")
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Best-effort to assert whether a version refers is a git sha digest or a tag. When it is, we
|
// Best-effort to assert whether a version refers is a git sha digest or a tag. When it is, we
|
||||||
// avoid re-downloading it if it's already fetched. But when it isn't, and thus refer to a branch,
|
// avoid re-downloading it if it's already fetched. But when it isn't, and thus refer to a branch,
|
||||||
// we always re-download it. Note however that the download might be short-circuited by the
|
// we always re-download it. Note however that the download might be short-circuited by the
|
||||||
|
|
Loading…
Reference in New Issue