Skip to main content

zlayer_builder/
harvest.rs

1//! Cross-search: when the package resolver can't map a Linux package to a
2//! Homebrew/Chocolatey equivalent, fire a lightweight "unfulfilled request" to
3//! `ZPackageIndex` (`{base}/linux/request`). A server-side hourly scheduler
4//! drains the queue and resolves the mapping (Repology), so a FUTURE native
5//! sandbox/HCS build resolves the package without the VM fallback. The build
6//! itself does zero apt/buildah work — this is one cheap HMAC POST.
7//!
8//! There is exactly one HMAC signer in the workspace
9//! ([`zlayer_toolchain::package_index::sign`]) and the endpoint base lives in
10//! [`zlayer_types::package_index::PackageIndexConfig`]; this module reuses both
11//! rather than re-deriving either.
12
13use zlayer_toolchain::package_index::sign;
14use zlayer_types::package_index::PackageIndexConfig;
15
16/// Build-time reposync HMAC secret; `None` disables the POST. The single home
17/// for the *value* — the signing *algorithm* lives in `zlayer-toolchain`.
18const REPOSYNC_HMAC_SECRET: Option<&str> = option_env!("ZLAYER_REPOSYNC_HMAC_SECRET");
19
20/// Fire-and-forget: report a Linux package the resolver could not map, so the
21/// index can backfill it. `manager` is `"apt"`|`"apk"`; `distro` e.g.
22/// `"debian-12"`. No-op when the HMAC secret isn't baked in. Never
23/// blocks/fails the build.
24pub fn report_unfulfilled(distro: &str, manager: &str, name: &str) {
25    let Some(secret) = REPOSYNC_HMAC_SECRET.filter(|s| !s.is_empty()) else {
26        return;
27    };
28    let (distro, manager, name) = (distro.to_string(), manager.to_string(), name.to_string());
29    let endpoint = PackageIndexConfig::from_env().linux_request_url();
30    tokio::spawn(async move {
31        // JSON-escape via serde_json to be safe with package names.
32        let payload =
33            serde_json::json!({ "distro": distro, "manager": manager, "name": name }).to_string();
34        let signature = sign(secret, payload.as_bytes());
35        let _ = reqwest::Client::new()
36            .post(&endpoint)
37            .header("x-reposync-signature", signature)
38            .header("content-type", "application/json")
39            .body(payload)
40            .send()
41            .await;
42    });
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn shared_signer_matches_reposync_reference_vector() {
51        // Same reference vector the toolchain signer is tested against:
52        //   crypto.createHmac('sha256','test-secret').update('{"foo":"bar"}').digest('hex')
53        // Proves the DRY consolidation onto zlayer_toolchain::package_index::sign
54        // preserves the exact signature the index expects.
55        let sig = sign("test-secret", br#"{"foo":"bar"}"#);
56        assert_eq!(
57            sig,
58            "sha256=9b1abf7d901bda91325d00f6b397fb0dc257937939b27d4dc67848ab9e08f6c0"
59        );
60    }
61
62    #[test]
63    fn payload_shape_is_stable() {
64        // The request body is a flat {distro, manager, name} object; serde_json
65        // escapes package names safely (no manual string interpolation).
66        let payload = serde_json::json!({
67            "distro": "debian-12",
68            "manager": "apt",
69            "name": "lib\"weird\"-dev",
70        })
71        .to_string();
72        let parsed: serde_json::Value = serde_json::from_str(&payload).expect("valid JSON");
73        assert_eq!(parsed["distro"], "debian-12");
74        assert_eq!(parsed["manager"], "apt");
75        assert_eq!(parsed["name"], "lib\"weird\"-dev");
76    }
77
78    #[test]
79    fn endpoint_derives_from_index_base() {
80        // The request endpoint is the PackageIndexConfig-derived URL, not a
81        // hardcoded const.
82        assert_eq!(
83            PackageIndexConfig::default().linux_request_url(),
84            "https://packages.zlayer.dev/linux/request"
85        );
86    }
87}