Skip to main content

osdk_core/backend/
yarn.rs

1//! yarn backend: installs both yarn lines natively (no corepack), verified by
2//! the npm registry's Subresource Integrity (SRI).
3//!
4//! - classic (1.x): the `yarn` npm package
5//! - berry (2+): the `@yarnpkg/cli-dist` npm package (the same packaged bundle
6//!   corepack uses)
7//!
8//! yarn is a JavaScript bundle that runs on Node, so we generate small launchers
9//! that run the extracted CLI entry with the active node.
10
11use std::path::PathBuf;
12
13use async_trait::async_trait;
14
15use crate::backend::{Backend, Ctx, InstallCtx};
16use crate::error::{Error, Result};
17use crate::pipeline::{self, ArchiveKind, InstallPlan, PipelineCtx};
18use crate::platform::Os;
19use crate::source::Source;
20use crate::version::{ToolVersion, VersionInfo};
21
22pub struct YarnBackend;
23
24impl YarnBackend {
25    /// The npm package a yarn version ships in: classic (1.x) -> `yarn`,
26    /// berry (2+) -> `@yarnpkg/cli-dist`.
27    fn npm_package(version: &str) -> &'static str {
28        let major = version
29            .split('.')
30            .next()
31            .and_then(|s| s.parse::<u64>().ok())
32            .unwrap_or(1);
33        if major >= 2 {
34            "@yarnpkg/cli-dist"
35        } else {
36            "yarn"
37        }
38    }
39}
40
41#[async_trait]
42impl Backend for YarnBackend {
43    fn id(&self) -> &str {
44        "yarn"
45    }
46
47    fn default_sources(&self) -> Vec<Source> {
48        vec![
49            Source::mirror("npmmirror", "https://registry.npmmirror.com/", 5)
50                .with_index("https://registry.npmmirror.com/yarn"),
51            Source::official("npm", "https://registry.npmjs.org/")
52                .with_index("https://registry.npmjs.org/yarn"),
53        ]
54    }
55
56    fn probe_url(&self, _ctx: &Ctx, source: &Source) -> Option<String> {
57        source.index_url.clone()
58    }
59
60    async fn list_remote_versions(&self, ctx: &Ctx) -> Result<Vec<VersionInfo>> {
61        // Merge classic (`yarn`) and berry (`@yarnpkg/cli-dist`) version lines.
62        use std::collections::BTreeSet;
63        let sources = crate::source::select::ranked_source_list(ctx, self).await?;
64        let mut set: BTreeSet<String> = BTreeSet::new();
65        if let Ok(classic) = crate::npm::list_versions(ctx, &sources, "yarn").await {
66            for v in classic {
67                if v.starts_with('1') || v.starts_with('0') {
68                    set.insert(v);
69                }
70            }
71        }
72        if let Ok(berry) = crate::npm::list_versions(ctx, &sources, "@yarnpkg/cli-dist").await {
73            for v in berry {
74                // stable berry tags only (skip git snapshots like 4.9.1-git.*)
75                if !v.contains('-') {
76                    set.insert(v);
77                }
78            }
79        }
80        let mut out: Vec<VersionInfo> = set
81            .into_iter()
82            .map(|v| VersionInfo {
83                version: v,
84                stable: true,
85                lts: None,
86            })
87            .collect();
88        out.sort_by(|a, b| crate::backend::python::cmp_versions(&a.version, &b.version));
89        Ok(out)
90    }
91
92    async fn install(&self, ictx: &InstallCtx<'_>, tv: &ToolVersion) -> Result<()> {
93        let ctx = ictx.ctx;
94        let plan = if let Some(plan) = pipeline::locked_install_plan(self.id(), tv, true)? {
95            plan
96        } else {
97            let package = Self::npm_package(&tv.version);
98            let sources = crate::source::select::ranked_source_list(ctx, self).await?;
99            let dist = crate::npm::resolve_dist(ctx, &sources, package, &tv.version).await?;
100            InstallPlan {
101                tool: self.id().to_string(),
102                version: tv.version.clone(),
103                urls: dist.urls,
104                file_name: format!("yarn-{}.tgz", tv.version),
105                kind: ArchiveKind::TarGz,
106                checksum: dist.checksum,
107                strip_root: true,
108                subdir: None,
109            }
110        };
111        let pctx = PipelineCtx {
112            client: &ctx.client,
113            dirs: &ctx.dirs,
114            cas: &ctx.cas,
115            link_mode: ctx.config.settings.link_mode,
116            show_progress: ctx.show_progress,
117            offline: ctx.config.settings.offline,
118            require_checksums: ctx.config.settings.require_checksums,
119        };
120        let install_dir = pipeline::run(&plan, &pctx).await?;
121
122        // Generate node launchers pointing at the extracted CLI entry. Both
123        // `yarn` (classic) and `@yarnpkg/cli-dist` (berry) ship `bin/yarn.js`.
124        let entry = if install_dir.join("bin/yarn.js").is_file() {
125            install_dir.join("bin/yarn.js")
126        } else {
127            install_dir.join("lib/cli.js")
128        };
129        let bin_dir = install_dir.join("bin");
130        crate::dirs::create_dir_all(&bin_dir)?;
131        write_launcher(&bin_dir, &entry, ctx.platform.os)?;
132        Ok(())
133    }
134
135    fn bin_paths(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
136        Ok(vec![ctx
137            .dirs
138            .install_path(self.id(), &tv.version)
139            .join("bin")])
140    }
141
142    fn exec_env(
143        &self,
144        ctx: &Ctx,
145        tv: &ToolVersion,
146    ) -> Result<std::collections::BTreeMap<String, String>> {
147        let mapping = cache_mapping(&tv.version);
148        Ok(crate::cache::manager_exec_env(&ctx.dirs.cache, &[mapping]))
149    }
150
151    fn bin_names(&self, _ctx: &Ctx, _tv: &ToolVersion) -> Result<Vec<String>> {
152        Ok(vec!["yarn".into(), "yarnpkg".into()])
153    }
154}
155
156fn yarn_major(version: &str) -> u64 {
157    version
158        .trim_start_matches('v')
159        .split('.')
160        .next()
161        .and_then(|part| part.parse().ok())
162        .unwrap_or(1)
163}
164
165fn cache_mapping(version: &str) -> (&'static str, &'static str) {
166    if yarn_major(version) >= 2 {
167        ("YARN_GLOBAL_FOLDER", "yarn")
168    } else {
169        ("YARN_CACHE_FOLDER", "yarn-classic")
170    }
171}
172
173#[cfg(unix)]
174fn write_launcher(bin_dir: &std::path::Path, js: &std::path::Path, _os: Os) -> Result<()> {
175    use std::os::unix::fs::PermissionsExt;
176    for name in ["yarn", "yarnpkg"] {
177        let p = bin_dir.join(name);
178        let script = format!("#!/bin/sh\nexec node \"{}\" \"$@\"\n", js.display());
179        std::fs::write(&p, script).map_err(|e| Error::io(&p, e))?;
180        let _ = std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755));
181    }
182    Ok(())
183}
184
185#[cfg(windows)]
186fn write_launcher(bin_dir: &std::path::Path, js: &std::path::Path, _os: Os) -> Result<()> {
187    for name in ["yarn", "yarnpkg"] {
188        let p = bin_dir.join(format!("{name}.cmd"));
189        let script = format!("@echo off\r\nnode \"{}\" %*\r\n", js.display());
190        std::fs::write(&p, script).map_err(|e| Error::io(&p, e))?;
191    }
192    Ok(())
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn routes_classic_vs_berry_to_npm_package() {
201        assert_eq!(YarnBackend::npm_package("1.22.22"), "yarn");
202        assert_eq!(YarnBackend::npm_package("2.4.3"), "@yarnpkg/cli-dist");
203        assert_eq!(YarnBackend::npm_package("4.10.3"), "@yarnpkg/cli-dist");
204        // malformed -> classic default
205        assert_eq!(YarnBackend::npm_package("weird"), "yarn");
206        assert_eq!(
207            cache_mapping("1.22.22"),
208            ("YARN_CACHE_FOLDER", "yarn-classic")
209        );
210        assert_eq!(cache_mapping("4.10.3"), ("YARN_GLOBAL_FOLDER", "yarn"));
211    }
212}