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}