1use 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}