use super::{build_http_client, BlobRef, ClosureRef, ExtRecipe, PhpRecipe};
use bougie_errors::BougieError;
use bougie_fetch::ArchiveKind;
use bougie_index::{
build_verifier,
fetch::{fetch_manifest, fetch_root, fetch_section},
};
use bougie_index::host_to_dirname;
use bougie_index::wire::Root;
use bougie_paths::Paths;
use bougie_version::request::{Flavor, VersionLike};
use bougie_resolver::{resolve_extension, resolve_php, ResolveOptions, Selected};
use bougie_version::version::PartialVersion;
use eyre::{eyre, Result};
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
const SECTION_NAME: &str = "interpreter/php";
#[derive(Debug)]
pub struct BougieIndexBackend {
client: reqwest::blocking::Client,
host: String,
target: String,
cache_root: PathBuf,
root_cache: RefCell<Option<Rc<Root>>>,
}
impl BougieIndexBackend {
pub fn new(paths: &Paths, host: &str, target: &str) -> Result<Self> {
let client = build_http_client("bougie index")?;
let cache_root = paths.cache_index(&host_to_dirname(host));
Ok(Self {
client,
host: host.to_owned(),
target: target.to_owned(),
cache_root,
root_cache: RefCell::new(None),
})
}
fn root(&self) -> Result<Rc<Root>> {
if let Some(root) = self.root_cache.borrow().as_ref() {
return Ok(Rc::clone(root));
}
let fetched = fetch_root(&self.client, &self.host, &self.cache_root, build_verifier)?;
let root = Rc::new(fetched.root);
*self.root_cache.borrow_mut() = Some(Rc::clone(&root));
Ok(root)
}
}
impl super::Backend for BougieIndexBackend {
fn client(&self) -> &reqwest::blocking::Client {
&self.client
}
fn resolve_php(
&self,
spec: &VersionLike,
flavor: Flavor,
opts: ResolveOptions,
) -> Result<PhpRecipe> {
let root = self.root()?;
let target_entry = root.targets.get(&self.target).ok_or_else(|| {
let available: Vec<String> = root.targets.keys().cloned().collect();
target_not_served(&self.host, &self.target, &available)
})?;
let section_ref =
target_entry
.sections
.get(SECTION_NAME)
.ok_or_else(|| BougieError::Resolution {
kind: "section".into(),
detail: format!(
"the index at {} has no `{SECTION_NAME}` section under target {}",
self.host, self.target,
),
})?;
let section = fetch_section(
&self.client,
&self.host,
&self.cache_root,
&root.version,
&self.target,
SECTION_NAME,
§ion_ref.sha256,
)?;
let selected: Selected<'_> = resolve_php(§ion, spec, flavor, opts)?;
let manifest = fetch_manifest(
&self.client,
&self.host,
&self.cache_root,
&selected.artifact.manifest.path,
&selected.artifact.manifest.sha256,
)?;
manifest.validate()?;
Ok(PhpRecipe {
version: selected.version,
flavor,
blob: BlobRef {
url: manifest.blob.url.clone(),
sha256: manifest.blob.sha256.clone(),
size: manifest.blob.size,
archive: ArchiveKind::TarZst,
strip_prefix: "install".to_owned(),
},
frozen_warning: selected.frozen_warning,
})
}
fn resolve_extension(
&self,
name: &str,
php_minor: PartialVersion,
flavor: Flavor,
version_pin: Option<&str>,
opts: ResolveOptions,
) -> Result<ExtRecipe> {
let section_name = format!("extension/{name}");
let root = self.root()?;
let target_entry = root.targets.get(&self.target).ok_or_else(|| {
let available: Vec<String> = root.targets.keys().cloned().collect();
target_not_served(&self.host, &self.target, &available)
})?;
let section_ref = target_entry
.sections
.get(§ion_name)
.ok_or_else(|| BougieError::Resolution {
kind: "extension".into(),
detail: format!(
"the index at {} has no `{section_name}` section under target {} — \
run `bougie ext list --only-available` to see what's published",
self.host, self.target,
),
})?;
let section = fetch_section(
&self.client,
&self.host,
&self.cache_root,
&root.version,
&self.target,
§ion_name,
§ion_ref.sha256,
)?;
let selected: Selected<'_> =
resolve_extension(§ion, php_minor, flavor, version_pin, opts)?;
let manifest = fetch_manifest(
&self.client,
&self.host,
&self.cache_root,
&selected.artifact.manifest.path,
&selected.artifact.manifest.sha256,
)?;
manifest.validate()?;
let ext_ref = manifest.extension.as_ref().ok_or_else(|| {
eyre!(
"manifest for {} is missing the `extension` field — \
publisher bug: an extension-kind manifest must declare its `.so` path",
manifest.tag
)
})?;
Ok(ExtRecipe {
name: manifest.name.clone(),
version: selected.version,
php_minor,
flavor,
blob: BlobRef {
url: manifest.blob.url.clone(),
sha256: manifest.blob.sha256.clone(),
size: manifest.blob.size,
archive: ArchiveKind::TarZst,
strip_prefix: String::new(),
},
artifact_rel: PathBuf::from(&ext_ref.path),
load: ext_ref.load,
closure: manifest.closure.iter().map(ClosureRef::from).collect(),
needs_store_on_path: false,
frozen_warning: selected.frozen_warning,
})
}
}
fn target_not_served(host: &str, target: &str, available: &[String]) -> BougieError {
let mut hint = String::new();
if target.contains("musl") {
hint.push_str(
"this is a musl-libc platform (e.g. Alpine Linux), which bougie has no build for. ",
);
}
hint.push_str(&format!(
"Targets this index ({host}) provides: {}",
available.join(", "),
));
BougieError::UnknownTarget { triple: target.to_owned(), hint }
}
#[cfg(test)]
mod tests {
use super::*;
fn hint_of(err: &BougieError) -> String {
match err {
BougieError::UnknownTarget { hint, .. } => hint.clone(),
other => panic!("expected UnknownTarget, got {other:?}"),
}
}
#[test]
fn musl_target_gets_an_alpine_pointer_and_the_dynamic_list() {
let available = vec![
"aarch64-apple-darwin".to_owned(),
"x86_64-unknown-linux-gnu".to_owned(),
];
let err = target_not_served(
"https://index.bougie.tools",
"x86_64-unknown-linux-musl",
&available,
);
let hint = hint_of(&err);
assert!(hint.contains("musl"), "{hint}");
assert!(hint.contains("Alpine"), "{hint}");
assert!(!hint.contains('—'), "no em dash in the hint: {hint}");
assert!(hint.contains("x86_64-unknown-linux-gnu"), "{hint}");
assert!(hint.contains("aarch64-apple-darwin"), "{hint}");
match err {
BougieError::UnknownTarget { triple, .. } => {
assert_eq!(triple, "x86_64-unknown-linux-musl");
}
_ => unreachable!(),
}
}
#[test]
fn non_musl_target_lists_targets_without_the_alpine_pointer() {
let available = vec!["x86_64-unknown-linux-gnu".to_owned()];
let hint = hint_of(&target_not_served(
"https://index.example",
"powerpc64-unknown-linux-gnu",
&available,
));
assert!(!hint.contains("Alpine"), "{hint}");
assert!(hint.contains("x86_64-unknown-linux-gnu"), "{hint}");
assert!(hint.contains("https://index.example"), "{hint}");
}
}