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