Skip to main content

osdk_core/backend/
bun.rs

1//! Bun backend: installs the official platform package published to npm. This
2//! avoids the GitHub Releases API and verifies the tarball with npm SRI.
3
4use std::path::PathBuf;
5
6use async_trait::async_trait;
7
8use crate::backend::{Backend, Ctx, InstallCtx};
9use crate::error::{Error, Result};
10use crate::pipeline::{self, ArchiveKind, InstallPlan, PipelineCtx};
11use crate::platform::{Arch, Libc, Os};
12use crate::source::Source;
13use crate::version::{ToolRequest, ToolVersion, VersionInfo};
14
15pub struct BunBackend;
16
17const CACHE_ENV: &[(&str, &str)] = &[("BUN_INSTALL_CACHE_DIR", "bun")];
18
19impl BunBackend {
20    fn platform_package(ctx: &Ctx) -> Option<&'static str> {
21        Some(
22            match (ctx.platform.os, ctx.platform.arch, ctx.platform.libc) {
23                (Os::Linux, Arch::X64, Libc::Musl) => "@oven/bun-linux-x64-musl",
24                (Os::Linux, Arch::Arm64, Libc::Musl) => "@oven/bun-linux-aarch64-musl",
25                (Os::Linux, Arch::X64, _) => "@oven/bun-linux-x64",
26                (Os::Linux, Arch::Arm64, _) => "@oven/bun-linux-aarch64",
27                (Os::Macos, Arch::X64, _) => "@oven/bun-darwin-x64",
28                (Os::Macos, Arch::Arm64, _) => "@oven/bun-darwin-aarch64",
29                (Os::Windows, Arch::X64, _) => "@oven/bun-windows-x64",
30                (Os::Windows, Arch::Arm64, _) => "@oven/bun-windows-aarch64",
31                _ => return None,
32            },
33        )
34    }
35}
36
37#[async_trait]
38impl Backend for BunBackend {
39    fn id(&self) -> &str {
40        "bun"
41    }
42
43    fn default_sources(&self) -> Vec<Source> {
44        vec![
45            Source::mirror("npmmirror", "https://registry.npmmirror.com/", 5)
46                .with_index("https://registry.npmmirror.com/bun"),
47            Source::official("npm", "https://registry.npmjs.org/")
48                .with_index("https://registry.npmjs.org/bun"),
49        ]
50    }
51
52    fn probe_url(&self, _ctx: &Ctx, source: &Source) -> Option<String> {
53        source.index_url.clone()
54    }
55
56    async fn list_remote_versions(&self, ctx: &Ctx) -> Result<Vec<VersionInfo>> {
57        let sources = crate::source::select::ranked_source_list(ctx, self).await?;
58        let package = Self::platform_package(ctx).ok_or_else(|| Error::UnsupportedPlatform {
59            os: format!("{:?}", ctx.platform.os),
60            arch: format!("{:?}", ctx.platform.arch),
61        })?;
62        let packument = crate::npm::packument(ctx, &sources, package).await?;
63        Ok(packument
64            .versions
65            .into_iter()
66            .map(|version| VersionInfo {
67                stable: semver::Version::parse(&version)
68                    .map(|version| version.pre.is_empty())
69                    .unwrap_or(false),
70                version,
71                lts: None,
72            })
73            .collect())
74    }
75
76    async fn resolve_version(&self, ctx: &Ctx, request: &ToolRequest) -> Result<ToolVersion> {
77        let package = Self::platform_package(ctx).ok_or_else(|| Error::UnsupportedPlatform {
78            os: format!("{:?}", ctx.platform.os),
79            arch: format!("{:?}", ctx.platform.arch),
80        })?;
81        let sources = crate::source::select::ranked_source_list(ctx, self).await?;
82        crate::npm::resolve_package_version(ctx, &sources, package, self.id(), request).await
83    }
84
85    async fn install(&self, ictx: &InstallCtx<'_>, tv: &ToolVersion) -> Result<()> {
86        let ctx = ictx.ctx;
87        let plan = if let Some(plan) = pipeline::locked_install_plan(self.id(), tv, true)? {
88            plan
89        } else {
90            let package =
91                Self::platform_package(ctx).ok_or_else(|| Error::UnsupportedPlatform {
92                    os: format!("{:?}", ctx.platform.os),
93                    arch: format!("{:?}", ctx.platform.arch),
94                })?;
95            let sources = crate::source::select::ranked_source_list(ctx, self).await?;
96            let dist = crate::npm::resolve_dist(ctx, &sources, package, &tv.version).await?;
97            InstallPlan {
98                tool: self.id().to_string(),
99                version: tv.version.clone(),
100                urls: dist.urls,
101                file_name: format!("bun-{}.tgz", tv.version),
102                kind: ArchiveKind::TarGz,
103                checksum: dist.checksum,
104                strip_root: true,
105                subdir: None,
106            }
107        };
108        let pctx = PipelineCtx {
109            client: &ctx.client,
110            dirs: &ctx.dirs,
111            cas: &ctx.cas,
112            link_mode: ctx.config.settings.link_mode,
113            show_progress: ctx.show_progress,
114            offline: ctx.config.settings.offline,
115            require_checksums: ctx.config.settings.require_checksums,
116        };
117        pipeline::run(&plan, &pctx).await?;
118        ensure_executable(
119            &ctx.dirs.install_path(self.id(), &tv.version),
120            ctx.platform.os,
121        );
122        Ok(())
123    }
124
125    fn bin_paths(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
126        Ok(vec![ctx
127            .dirs
128            .install_path(self.id(), &tv.version)
129            .join("bin")])
130    }
131
132    fn exec_env(
133        &self,
134        ctx: &Ctx,
135        _tv: &ToolVersion,
136    ) -> Result<std::collections::BTreeMap<String, String>> {
137        Ok(crate::cache::manager_exec_env(&ctx.dirs.cache, CACHE_ENV))
138    }
139
140    fn bin_names(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<String>> {
141        let paths = self.bin_paths(ctx, tv)?;
142        Ok(exposed_bin_names(crate::backend::bin_names_in_dirs(&paths)))
143    }
144
145    fn idiomatic_files(&self) -> &[&str] {
146        &[".bun-version"]
147    }
148}
149
150fn exposed_bin_names(discovered: Vec<String>) -> Vec<String> {
151    let mut names = discovered
152        .into_iter()
153        .collect::<std::collections::BTreeSet<_>>();
154    names.extend(["bun".into(), "bunx".into()]);
155    names.into_iter().collect()
156}
157
158fn ensure_executable(install_dir: &std::path::Path, os: Os) {
159    if os == Os::Windows {
160        return;
161    }
162    #[cfg(unix)]
163    {
164        use std::os::unix::fs::PermissionsExt;
165        let bin = install_dir.join("bin/bun");
166        if let Ok(meta) = std::fs::metadata(&bin) {
167            let mut perms = meta.permissions();
168            perms.set_mode(perms.mode() | 0o755);
169            let _ = std::fs::set_permissions(&bin, perms);
170        }
171    }
172    #[cfg(not(unix))]
173    {
174        let _ = install_dir;
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::platform::Platform;
182
183    #[test]
184    fn maps_platform_packages() {
185        let linux = CtxPlatform {
186            os: Os::Linux,
187            arch: Arch::X64,
188            libc: Libc::Glibc,
189        };
190        let musl = CtxPlatform {
191            libc: Libc::Musl,
192            ..linux
193        };
194        assert_eq!(
195            BunBackend::platform_package(&ctx(linux)),
196            Some("@oven/bun-linux-x64")
197        );
198        assert_eq!(
199            BunBackend::platform_package(&ctx(musl)),
200            Some("@oven/bun-linux-x64-musl")
201        );
202    }
203
204    #[test]
205    fn uses_shared_bun_package_cache_without_overriding_user_value() {
206        let ctx = ctx(Platform {
207            os: Os::Linux,
208            arch: Arch::X64,
209            libc: Libc::Glibc,
210        });
211        let managed = crate::cache::manager_env(&ctx.dirs.cache, CACHE_ENV, |_| None);
212        assert_eq!(
213            PathBuf::from(managed.get("BUN_INSTALL_CACHE_DIR").unwrap()),
214            ctx.dirs.cache.join("pkg/bun")
215        );
216
217        let user = crate::cache::manager_env(&ctx.dirs.cache, CACHE_ENV, |key| {
218            (key == "BUN_INSTALL_CACHE_DIR").then(|| "/custom/bun-cache".into())
219        });
220        assert!(!user.contains_key("BUN_INSTALL_CACHE_DIR"));
221    }
222
223    #[test]
224    fn exposes_bunx_as_a_routing_alias() {
225        assert_eq!(
226            exposed_bin_names(vec!["bun".into()]),
227            vec!["bun".to_string(), "bunx".to_string()]
228        );
229    }
230
231    type CtxPlatform = Platform;
232
233    fn ctx(platform: Platform) -> Ctx {
234        let dirs = crate::dirs::Dirs::resolve_from(|key| match key {
235            "OSDK_DATA_DIR" => Some("/tmp/osdk-bun-test/data".into()),
236            "OSDK_CACHE_DIR" => Some("/tmp/osdk-bun-test/cache".into()),
237            "OSDK_CONFIG_DIR" => Some("/tmp/osdk-bun-test/config".into()),
238            _ => None,
239        })
240        .unwrap();
241        Ctx {
242            dirs: dirs.clone(),
243            platform,
244            config: crate::config::Config {
245                settings: Default::default(),
246                sources: Default::default(),
247                tools: Default::default(),
248                tool_configs: Default::default(),
249                global_tools: Default::default(),
250                global_tool_configs: Default::default(),
251                tool_origins: Default::default(),
252                aliases: Default::default(),
253                project_config_path: None,
254            },
255            client: reqwest::Client::new(),
256            cas: std::sync::Arc::new(crate::store::Cas::new(dirs.store)),
257            show_progress: false,
258        }
259    }
260}