Skip to main content

scrollcase_consumer/contract/
targets.rs

1//! Mirror of the Scrollcase box-format target model.
2//!
3//! A target is the `(platform, arch, accelerator)` triple a box is built for, plus a CUDA ABI
4//! version when the accelerator is CUDA. [`box_target_id`] turns it into the canonical slug that
5//! appears in archive names, object keys and registry routes, so every implementation of the format
6//! must agree character for character. The golden cases in `fixtures/target-id-contract.json` are
7//! what "agree" means, and `tests/contract.rs` proves this mirror against them.
8//!
9//! The adapter describes what a target implies for the extracted tree. Only the parts a consumer
10//! relies on are carried here: the interpreter layout it must find, the inherited variables that can
11//! change which code that interpreter loads, and the platform assertion a self-test opens with. The
12//! builder's own adapter additionally names the archive backend, the conda subdir and the native
13//! library inspector — all of them decisions taken while a box is produced, none of them observable
14//! by something that only unpacks and runs one.
15//!
16//! The native host is expressed in Rust's own `OS`/`ARCH` vocabulary rather than Node's
17//! `darwin`/`arm64`. Those strings never appear in a signed document; they only answer "may this
18//! host run this box", and each implementation answers it in the terms its own runtime reports.
19
20use serde::{Deserialize, Serialize};
21
22use crate::error::{fail, Result};
23
24/// The `(platform, arch, accelerator)` triple a box is built for.
25#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct BoxTarget {
28    /// Operating system the box runs on.
29    pub platform: String,
30    /// CPU architecture the box runs on.
31    pub arch: String,
32    /// Compute backend the box was built against.
33    pub accelerator: String,
34    /// CUDA ABI version, required on a CUDA target and forbidden on every other.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub cuda_version: Option<String>,
37}
38
39/// Layout of the interpreter inside an extracted box.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct PythonLayout {
42    /// Directory the packed prefix was relocated into.
43    pub payload_root: &'static str,
44    /// Interpreter path, relative to the box root.
45    pub entry_point: &'static str,
46    /// Directory holding console scripts.
47    pub scripts_directory: &'static str,
48    /// Suffix an executable carries on this platform.
49    pub executable_suffix: &'static str,
50    /// Frozen wire string naming how launchers were repaired.
51    pub launcher_kind: &'static str,
52}
53
54/// What a target implies for the extracted tree.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct BoxTargetAdapter {
57    /// Canonical adapter id, for example `macos-aarch64`.
58    pub id: &'static str,
59    /// Operating system, as a scroll declares it.
60    pub platform: &'static str,
61    /// CPU architecture, as a scroll declares it.
62    pub arch: &'static str,
63    /// `std::env::consts::OS` value a host must report to run this box.
64    pub host_os: &'static str,
65    /// `std::env::consts::ARCH` value a host must report to run this box.
66    pub host_arch: &'static str,
67    /// Interpreter layout inside the box.
68    pub python: PythonLayout,
69    /// Inherited variables whose presence can change which code the interpreter loads.
70    pub execution_affecting_environment_variables: &'static [&'static str],
71    /// The platform assertion prepended to every self-test.
72    pub self_test_python: &'static str,
73}
74
75const PYTHON_EXECUTION_ENVIRONMENT: &[&str] = &[
76    "PYTHONPATH",
77    "PYTHONHOME",
78    "PYTHONSTARTUP",
79    "PYTHONBREAKPOINT",
80];
81
82const MACOS_EXECUTION_ENVIRONMENT: &[&str] = &[
83    "PYTHONPATH",
84    "PYTHONHOME",
85    "PYTHONSTARTUP",
86    "PYTHONBREAKPOINT",
87    "DYLD_INSERT_LIBRARIES",
88];
89
90const LINUX_EXECUTION_ENVIRONMENT: &[&str] = &[
91    "PYTHONPATH",
92    "PYTHONHOME",
93    "PYTHONSTARTUP",
94    "PYTHONBREAKPOINT",
95    "LD_PRELOAD",
96];
97
98const POSIX_PYTHON: PythonLayout = PythonLayout {
99    payload_root: "venv",
100    entry_point: "venv/bin/python",
101    scripts_directory: "venv/bin",
102    executable_suffix: "",
103    launcher_kind: "posix-polyglot",
104};
105
106const TARGET_ADAPTERS: &[BoxTargetAdapter] = &[
107    BoxTargetAdapter {
108        id: "macos-aarch64",
109        platform: "macos",
110        arch: "aarch64",
111        host_os: "macos",
112        host_arch: "aarch64",
113        python: POSIX_PYTHON,
114        execution_affecting_environment_variables: MACOS_EXECUTION_ENVIRONMENT,
115        self_test_python: "import sys; assert sys.platform == 'darwin'",
116    },
117    BoxTargetAdapter {
118        id: "linux-x86_64",
119        platform: "linux",
120        arch: "x86_64",
121        host_os: "linux",
122        host_arch: "x86_64",
123        python: POSIX_PYTHON,
124        execution_affecting_environment_variables: LINUX_EXECUTION_ENVIRONMENT,
125        self_test_python: "import sys; assert sys.platform.startswith('linux')",
126    },
127    BoxTargetAdapter {
128        id: "windows-x86_64",
129        platform: "windows",
130        arch: "x86_64",
131        host_os: "windows",
132        host_arch: "x86_64",
133        python: PythonLayout {
134            payload_root: "venv",
135            entry_point: "venv/python.exe",
136            scripts_directory: "venv/Scripts",
137            executable_suffix: ".exe",
138            // Reads like a stale reference to a tool this project does not use. It is a frozen wire
139            // string under the published format; it is not a typo and must not be "cleaned".
140            launcher_kind: "uv-windows-pe",
141        },
142        execution_affecting_environment_variables: PYTHON_EXECUTION_ENVIRONMENT,
143        self_test_python: "import sys; assert sys.platform == 'win32'",
144    },
145];
146
147/// The accelerators each `(platform, arch)` pair supports, in the order the format defines them.
148fn supported_accelerators(platform: &str, arch: &str) -> Option<&'static [&'static str]> {
149    match (platform, arch) {
150        ("macos", "aarch64") => Some(&["metal", "cpu"]),
151        ("linux" | "windows", "x86_64") => Some(&["cpu", "cuda"]),
152        _ => None,
153    }
154}
155
156/// Whether a CUDA version is the `major.minor` shape the format requires.
157///
158/// Hand-written rather than delegated to a regex engine: the pattern is
159/// `^[1-9][0-9]*\.[0-9]+$`, and carrying a regex dependency to answer it would be the whole cost of
160/// the crate's smallest question.
161fn is_cuda_version(value: &str) -> bool {
162    let Some((major, minor)) = value.split_once('.') else {
163        return false;
164    };
165    let major_valid = !major.is_empty()
166        && !major.starts_with('0')
167        && major.bytes().all(|byte| byte.is_ascii_digit());
168    let minor_valid = !minor.is_empty() && minor.bytes().all(|byte| byte.is_ascii_digit());
169    major_valid && minor_valid
170}
171
172/// Returns the canonical target slug used in box filenames, object keys and routes.
173///
174/// # Errors
175///
176/// When the target is outside the supported matrix, or its CUDA version is missing on a CUDA target
177/// or present on any other.
178pub fn box_target_id(target: &BoxTarget) -> Result<String> {
179    let accelerators = supported_accelerators(&target.platform, &target.arch);
180    if !accelerators.is_some_and(|values| values.contains(&target.accelerator.as_str())) {
181        fail!(
182            "Unsupported box target: {}/{}/{}",
183            target.platform,
184            target.arch,
185            target.accelerator
186        );
187    }
188    if target.accelerator == "cuda" {
189        let Some(version) = target.cuda_version.as_deref().filter(|v| is_cuda_version(v)) else {
190            fail!("A CUDA box target requires a numeric major.minor CUDA version");
191        };
192        return Ok(format!(
193            "{}-{}-cuda{version}",
194            target.platform, target.arch
195        ));
196    }
197    if target.cuda_version.is_some() {
198        fail!("Only CUDA box targets may declare a CUDA version");
199    }
200    Ok(format!(
201        "{}-{}-{}",
202        target.platform, target.arch, target.accelerator
203    ))
204}
205
206/// Returns the adapter for a validated box target.
207///
208/// # Errors
209///
210/// When the target is unsupported.
211pub fn box_target_adapter(target: &BoxTarget) -> Result<&'static BoxTargetAdapter> {
212    box_target_id(target)?;
213    let Some(adapter) = TARGET_ADAPTERS
214        .iter()
215        .find(|candidate| candidate.platform == target.platform && candidate.arch == target.arch)
216    else {
217        fail!(
218            "No box target adapter exists for {}/{}",
219            target.platform,
220            target.arch
221        );
222    };
223    Ok(adapter)
224}
225
226/// Lists every adapter, for contract tests and for callers enumerating supported targets.
227#[must_use]
228pub fn box_target_adapters() -> &'static [BoxTargetAdapter] {
229    TARGET_ADAPTERS
230}
231
232/// Ensures this host is the operating system and architecture the box ships for.
233///
234/// # Errors
235///
236/// When the current host is not the one the adapter requires.
237pub fn assert_native_host(adapter: &BoxTargetAdapter) -> Result<()> {
238    assert_host(adapter, std::env::consts::OS, std::env::consts::ARCH)
239}
240
241/// The host check with the host injected, so every target can be exercised on one machine.
242///
243/// # Errors
244///
245/// When the supplied host is not the one the adapter requires.
246pub fn assert_host(adapter: &BoxTargetAdapter, os: &str, arch: &str) -> Result<()> {
247    if os != adapter.host_os || arch != adapter.host_arch {
248        fail!(
249            "{} boxes cannot run on {os}/{arch}; they require {}/{}",
250            adapter.id,
251            adapter.host_os,
252            adapter.host_arch
253        );
254    }
255    Ok(())
256}
257
258/// Ensures a release's entry point agrees with the adapter's standalone Python layout.
259///
260/// # Errors
261///
262/// When the entry point is not the one the adapter defines.
263pub fn assert_python_entry_point(adapter: &BoxTargetAdapter, entry_point: &str) -> Result<()> {
264    if entry_point != adapter.python.entry_point {
265        fail!(
266            "{} boxes must use Python entry point {}",
267            adapter.id,
268            adapter.python.entry_point
269        );
270    }
271    Ok(())
272}
273
274#[cfg(test)]
275mod tests {
276    use super::{
277        assert_host, assert_python_entry_point, box_target_adapter, box_target_adapters,
278        box_target_id, is_cuda_version, BoxTarget,
279    };
280
281    fn target(platform: &str, arch: &str, accelerator: &str, cuda: Option<&str>) -> BoxTarget {
282        BoxTarget {
283            platform: platform.to_string(),
284            arch: arch.to_string(),
285            accelerator: accelerator.to_string(),
286            cuda_version: cuda.map(str::to_string),
287        }
288    }
289
290    #[test]
291    fn cuda_versions_follow_the_major_minor_rule() {
292        assert!(is_cuda_version("12.4"));
293        assert!(is_cuda_version("9.0"));
294        for invalid in ["", "12", "12.", ".4", "0.1", "01.2", "12.4.1", "12.x", " 12.4"] {
295            assert!(!is_cuda_version(invalid), "{invalid} was accepted");
296        }
297    }
298
299    #[test]
300    fn every_adapter_is_reachable_from_a_target() {
301        for adapter in box_target_adapters() {
302            let accelerator = if adapter.platform == "macos" {
303                "metal"
304            } else {
305                "cpu"
306            };
307            let resolved =
308                box_target_adapter(&target(adapter.platform, adapter.arch, accelerator, None))
309                    .unwrap();
310            assert_eq!(resolved.id, adapter.id);
311        }
312    }
313
314    #[test]
315    fn a_foreign_host_is_refused_with_the_shared_wording() {
316        let adapter = box_target_adapter(&target("linux", "x86_64", "cpu", None)).unwrap();
317        assert!(assert_host(adapter, "linux", "x86_64").is_ok());
318        let error = assert_host(adapter, "macos", "aarch64").unwrap_err();
319        assert!(error.message().contains("cannot run on"), "{error}");
320    }
321
322    #[test]
323    fn an_entry_point_from_another_platform_is_refused() {
324        let windows = box_target_adapter(&target("windows", "x86_64", "cpu", None)).unwrap();
325        assert!(assert_python_entry_point(windows, "venv/python.exe").is_ok());
326        assert!(assert_python_entry_point(windows, "venv/bin/python").is_err());
327    }
328
329    #[test]
330    fn a_target_id_is_never_produced_for_an_unsupported_triple() {
331        assert!(box_target_id(&target("macos", "x86_64", "cpu", None)).is_err());
332        assert!(box_target_id(&target("linux", "x86_64", "metal", None)).is_err());
333    }
334}