Skip to main content

bougie_backend/
lib.rs

1//! Pluggable PHP-distribution backends.
2//!
3//! A `Backend` resolves a user-facing request (`bougie php install 8.4`)
4//! into a [`PhpRecipe`] — a self-contained description of one blob to
5//! download and how to extract it. Today there's exactly one
6//! implementation, [`bougie_index::BougieIndexBackend`], which talks to
7//! `index.bougie.tools` (or a configured mirror) using the signed
8//! root → section → manifest protocol. Phase 3 of `WINDOWS_PLAN.md` adds
9//! `WindowsPhpNetBackend`, which fetches `releases.json` from
10//! windows.php.net and synthesizes a `PhpRecipe` for that distribution.
11//!
12//! The trait deliberately keeps the surface narrow: resolution is
13//! pure-ish (network I/O, no filesystem mutation). The recipe carries
14//! everything `install_php` needs to drive [`bougie_fetch::fetch_blob`],
15//! so the install code is identical across backends — the only
16//! per-backend choice is which `Backend` impl to construct.
17//!
18//! The trait grows symmetrically for extensions: `resolve_extension`
19//! returns an [`ExtRecipe`] that carries everything `install_extension`
20//! needs to drive the same fetch + place + conf.d dance regardless of
21//! backend. Closure peers (bougie-index only) ride on the recipe as a
22//! [`ClosureRef`] list — windows.php.net always returns an empty one,
23//! and the install code's closure-handling step is a no-op for it.
24
25pub mod bougie_index_backend;
26pub mod nodejs_org;
27pub mod windows_php_net;
28
29pub use bougie_index_backend::BougieIndexBackend;
30pub use nodejs_org::{NodeRecipe, NodeRequest, NodeVersion, NodejsOrgBackend};
31pub use windows_php_net::WindowsPhpNetBackend;
32
33use bougie_fetch::{fetch_blob, ArchiveKind, BlobOutcome, DownloadBar};
34use bougie_index::wire::LoadDirective;
35use bougie_paths::Paths;
36use bougie_version::request::{Flavor, VersionLike};
37use bougie_resolver::ResolveOptions;
38use bougie_platform::target::{Os, Triple};
39use bougie_version::version::{PartialVersion, Version};
40use eyre::Result;
41use std::path::{Path, PathBuf};
42
43/// A source of PHP interpreter artifacts. One concrete impl per
44/// distribution channel — bougie's own signed index, windows.php.net.
45pub trait Backend {
46    /// Resolve a user-facing request into a [`PhpRecipe`] ready to
47    /// hand off to the extract pipeline. Network I/O happens here
48    /// (root + section + manifest fetches for `BougieIndexBackend`;
49    /// `releases.json` fetch for `WindowsPhpNetBackend`); no filesystem
50    /// state under `$BOUGIE_HOME` is mutated.
51    fn resolve_php(
52        &self,
53        spec: &VersionLike,
54        flavor: Flavor,
55        opts: ResolveOptions,
56    ) -> Result<PhpRecipe>;
57
58    /// Resolve a user-facing extension request into an [`ExtRecipe`].
59    /// Same contract as [`resolve_php`]: network I/O lives here (index
60    /// section + manifest fetch for `BougieIndexBackend`; static table
61    /// lookup for `WindowsPhpNetBackend`), no filesystem mutation under
62    /// `$BOUGIE_HOME`. `version_pin` and `opts` are bougie-index
63    /// concepts; the windows.php.net backend ignores them (the
64    /// compile-time `WINDOWS_PECL_VERSIONS` table is the version oracle
65    /// — see `WINDOWS_PLAN.md` §Phase 4).
66    ///
67    /// [`resolve_php`]: Self::resolve_php
68    fn resolve_extension(
69        &self,
70        name: &str,
71        php_minor: PartialVersion,
72        flavor: Flavor,
73        version_pin: Option<&str>,
74        opts: ResolveOptions,
75    ) -> Result<ExtRecipe>;
76
77    /// Borrow the backend's HTTP client. Exposed so [`fetch_into`]'s
78    /// default impl (and the test harness) can drive
79    /// [`bougie_fetch::fetch_blob`] without re-building a client.
80    ///
81    /// [`fetch_into`]: Self::fetch_into
82    fn client(&self) -> &reqwest::blocking::Client;
83
84    /// Fetch the recipe's blob into `install_root` and extract.
85    ///
86    /// The default impl extracts directly into `install_root` — right
87    /// for backends whose blobs already wrap their contents into a
88    /// bougie-shaped tree (`install/bin/...`, stripped via the
89    /// recipe's `strip_prefix`). Backends whose blobs ship a different
90    /// shape override this to relocate the extracted tree (the
91    /// windows.php.net backend extracts into `install_root/bin/` so
92    /// `php.exe` and its colocated DLLs land where the rest of bougie
93    /// expects to find them).
94    fn fetch_into(
95        &self,
96        blob: &BlobRef,
97        install_root: &Path,
98        partial_dir: &Path,
99        bar: &DownloadBar,
100    ) -> Result<BlobOutcome> {
101        let spec = blob.as_blob_spec(partial_dir, install_root);
102        fetch_blob(self.client(), &spec, bar)
103    }
104}
105
106/// Pick the right backend for the host target.
107///
108/// `windows.*` triples go through [`WindowsPhpNetBackend`] regardless
109/// of `host` (`$BOUGIE_INDEX_URL` is a bougie-index concept). Everything
110/// else uses [`BougieIndexBackend`] pointed at `host`. Returning a
111/// boxed trait object lets `install_php` stay branch-free.
112pub fn select(target: &Triple, host: &str, paths: &Paths) -> Result<Box<dyn Backend>> {
113    if target.os == Os::Windows {
114        Ok(Box::new(WindowsPhpNetBackend::new(paths, target)?))
115    } else {
116        Ok(Box::new(BougieIndexBackend::new(
117            paths,
118            host,
119            &target.to_string(),
120        )?))
121    }
122}
123
124pub(crate) fn build_http_client(_label: &'static str) -> Result<reqwest::blocking::Client> {
125    bougie_fetch::default_client()
126}
127
128/// One blob to fetch and extract.
129///
130/// Carries exactly the information [`bougie_fetch::fetch_blob`] needs:
131/// URL + sha256 for verification, byte size for the progress bar,
132/// archive kind so the extractor knows how to decode, and a strip
133/// prefix so the unwrapping directory disappears. The fields map 1:1
134/// onto [`bougie_fetch::BlobSpec`]; see [`extract`] for the wiring.
135#[derive(Debug, Clone)]
136pub struct BlobRef {
137    pub url: String,
138    pub sha256: String,
139    pub size: u64,
140    pub archive: ArchiveKind,
141    /// Leading path component the extractor strips from every entry.
142    /// `"install"` for bougie's own tar.zst interpreter tarballs;
143    /// `"php-<ver>"` for windows.php.net's interpreter ZIPs;
144    /// `""` for flat archives. See [`bougie_fetch::BlobSpec::strip_prefix`].
145    pub strip_prefix: String,
146}
147
148/// A resolved PHP interpreter, ready to install.
149///
150/// The version comes back from the backend (not the request) because
151/// the request may have been a constraint like `^8.4` that the backend
152/// pinned to a concrete `8.4.3`. `flavor` round-trips so the caller
153/// doesn't have to thread it separately when computing
154/// [`crate::store::install_dir`].
155///
156/// `frozen_warning` propagates from bougie-index frozen artifacts; for
157/// non-index backends (windows.php.net) it's always `false`.
158#[derive(Debug, Clone)]
159pub struct PhpRecipe {
160    pub version: Version,
161    pub flavor: Flavor,
162    pub blob: BlobRef,
163    pub frozen_warning: bool,
164}
165
166impl BlobRef {
167    /// Build a [`bougie_fetch::BlobSpec`] borrowing from this recipe.
168    /// The caller supplies `partial_dir` (where in-flight downloads
169    /// stage) and `dest` (the final install path) — those are
170    /// filesystem concerns the backend doesn't know about.
171    pub fn as_blob_spec<'a>(
172        &'a self,
173        partial_dir: &'a std::path::Path,
174        dest: &'a std::path::Path,
175    ) -> bougie_fetch::BlobSpec<'a> {
176        bougie_fetch::BlobSpec {
177            url: &self.url,
178            hash: bougie_fetch::Hash::sha256(&self.sha256),
179            partial_dir,
180            dest,
181            strip_prefix: &self.strip_prefix,
182            archive: self.archive,
183            auth_header: None,
184            auth_header_name: None,
185        }
186    }
187}
188
189/// A resolved PHP extension, ready to fetch + place into the
190/// content-addressed store.
191///
192/// `name`/`version`/`php_minor`/`flavor` round-trip from the request so
193/// `install_extension` doesn't have to thread them separately when
194/// computing the store directory. `blob` is the single archive to
195/// fetch; on bougie-index that's the per-extension `.so` tarball, on
196/// windows.php.net the flat PECL `.zip`. `artifact_rel` is the path of
197/// the loadable file relative to the extracted store dir (`lib/extensions/<api>/<name>.so`
198/// for bougie-index manifests, `php_<name>.dll` for PECL zips); the
199/// conf.d emitter joins it onto the store dir to get the absolute path
200/// it writes into the `extension=` / `zend_extension=` directive.
201///
202/// `closure` carries the bundled-library closure for bougie-index
203/// artifacts — see [`ClosureRef`]. windows.php.net always returns an
204/// empty vec; dependent DLLs ride inside the same PECL zip and are
205/// handled via `needs_store_on_path` (see [`super::WindowsPhpNetBackend`]).
206///
207/// `frozen_warning` propagates the bougie-index frozen flag; always
208/// false for non-index backends.
209#[derive(Debug, Clone)]
210pub struct ExtRecipe {
211    pub name: String,
212    pub version: Version,
213    pub php_minor: PartialVersion,
214    pub flavor: Flavor,
215    pub blob: BlobRef,
216    pub artifact_rel: PathBuf,
217    pub load: LoadDirective,
218    pub closure: Vec<ClosureRef>,
219    pub needs_store_on_path: bool,
220    pub frozen_warning: bool,
221}
222
223/// One bundled-library entry an extension `.so` depends on at runtime.
224///
225/// Mirrors [`bougie_index::wire::Closure`] field-for-field, but lives
226/// here so the [`Backend`] trait can stay agnostic of the index wire
227/// schema (the windows.php.net backend depends on none of it). The
228/// install code uses these to materialize the install-shaped
229/// `store/<name>-<version>-<hash>/` peer layout the `.so`'s RPATH was
230/// compiled against — see [`crate::install::install_closure_peers`].
231#[derive(Debug, Clone)]
232pub struct ClosureRef {
233    pub name: String,
234    pub version: String,
235    pub hash: String,
236    pub sha256: String,
237    pub url: String,
238    pub size: u64,
239}
240
241impl From<&bougie_index::wire::Closure> for ClosureRef {
242    fn from(c: &bougie_index::wire::Closure) -> Self {
243        Self {
244            name: c.name.clone(),
245            version: c.version.clone(),
246            hash: c.hash.clone(),
247            sha256: c.sha256.clone(),
248            url: c.url.clone(),
249            size: c.size,
250        }
251    }
252}