1use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7use async_trait::async_trait;
8use serde::Deserialize;
9
10use crate::backend::{Backend, Ctx, InstallCtx};
11use crate::error::{Error, Result};
12use crate::http;
13use crate::pipeline::{self, ArchiveKind, Checksum, HashAlgo, InstallPlan, PipelineCtx};
14use crate::platform::Os;
15use crate::source::Source;
16use crate::version::{ToolVersion, VersionInfo};
17
18pub struct GoBackend;
19
20#[derive(Debug, Deserialize)]
21struct GoRelease {
22 version: String, #[serde(default)]
24 stable: bool,
25 #[serde(default)]
26 files: Vec<GoFile>,
27}
28
29#[derive(Debug, Deserialize, Clone)]
30struct GoFile {
31 filename: String,
32 os: String,
33 arch: String,
34 #[serde(default)]
35 sha256: String,
36 #[serde(default)]
37 kind: String, }
39
40impl GoBackend {
41 fn matches_platform(f: &GoFile, ctx: &Ctx) -> bool {
42 f.kind == "archive"
43 && f.os == ctx.platform.os.go_token()
44 && f.arch == ctx.platform.arch.go_token()
45 }
46}
47
48#[async_trait]
49impl Backend for GoBackend {
50 fn id(&self) -> &str {
51 "go"
52 }
53
54 fn aliases(&self) -> &[&str] {
55 &["golang"]
56 }
57
58 fn default_sources(&self) -> Vec<Source> {
59 vec![
60 Source::official("official", "https://go.dev/dl/")
61 .with_index("https://go.dev/dl/?mode=json&include=all"),
62 Source::mirror("aliyun", "https://mirrors.aliyun.com/golang/", 10)
66 .with_index("https://go.dev/dl/?mode=json&include=all"),
67 Source::mirror("google-cn", "https://golang.google.cn/dl/", 20)
68 .with_index("https://golang.google.cn/dl/?mode=json&include=all"),
69 ]
70 }
71
72 fn probe_url(&self, _ctx: &Ctx, source: &Source) -> Option<String> {
73 source.index_url.clone()
74 }
75
76 async fn list_remote_versions(&self, ctx: &Ctx) -> Result<Vec<VersionInfo>> {
77 let sources = crate::source::select::ranked_source_list(ctx, self).await?;
78 let mut last_err: Option<Error> = None;
79 for source in &sources {
80 let index_url = match &source.index_url {
81 Some(u) => u.clone(),
82 None => continue,
83 };
84 let releases: Vec<GoRelease> = match http::get_cached_json(ctx, &index_url).await {
85 Ok(r) => r,
86 Err(e) => {
87 tracing::warn!(source = %source.id, "{}", crate::i18n::trf("log.go_index_fetch_failed", &[("err", &e.to_string())]));
88 last_err = Some(e);
89 continue;
90 }
91 };
92 let mut out: Vec<VersionInfo> = releases
94 .into_iter()
95 .rev()
96 .filter(|r| r.files.iter().any(|f| Self::matches_platform(f, ctx)))
97 .map(|r| VersionInfo {
98 version: normalize_go_version(&r.version),
99 stable: r.stable,
100 lts: None,
101 })
102 .collect();
103 out.retain(|v| !v.version.is_empty());
104 return Ok(out);
105 }
106 Err(last_err.unwrap_or_else(|| Error::NoUsableSource {
107 tool: self.id().to_string(),
108 tried: sources.len(),
109 }))
110 }
111
112 async fn install(&self, ictx: &InstallCtx<'_>, tv: &ToolVersion) -> Result<()> {
113 let ctx = ictx.ctx;
114 if let Some(plan) = pipeline::locked_install_plan(self.id(), tv, true)? {
115 let pctx = PipelineCtx {
116 client: &ctx.client,
117 dirs: &ctx.dirs,
118 cas: &ctx.cas,
119 link_mode: ctx.config.settings.link_mode,
120 show_progress: ctx.show_progress,
121 offline: ctx.config.settings.offline,
122 require_checksums: ctx.config.settings.require_checksums,
123 };
124 pipeline::run(&plan, &pctx).await?;
125 return Ok(());
126 }
127 let sources = crate::source::select::ranked_source_list(ctx, self).await?;
128 let version = &tv.version;
129 let go_ver = format!("go{version}");
130
131 let (file, _idx_source) = self.find_file(ctx, &sources, &go_ver).await?;
133
134 let urls: Vec<String> = sources
135 .iter()
136 .map(|s| http::join_url(&s.download_url, &file.filename))
137 .collect();
138
139 let kind = ArchiveKind::from_name(&file.filename)?;
140 let checksum = if file.sha256.is_empty() {
141 None
142 } else {
143 Some(Checksum {
144 algo: HashAlgo::Sha256,
145 hex: file.sha256.clone(),
146 })
147 };
148
149 let plan = InstallPlan {
150 tool: self.id().to_string(),
151 version: version.clone(),
152 urls,
153 file_name: file.filename.clone(),
154 kind,
155 checksum,
156 strip_root: true, subdir: None,
158 };
159 let pctx = PipelineCtx {
160 client: &ctx.client,
161 dirs: &ctx.dirs,
162 cas: &ctx.cas,
163 link_mode: ctx.config.settings.link_mode,
164 show_progress: ctx.show_progress,
165 offline: ctx.config.settings.offline,
166 require_checksums: ctx.config.settings.require_checksums,
167 };
168 pipeline::run(&plan, &pctx).await?;
169 Ok(())
170 }
171
172 fn bin_paths(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
173 Ok(vec![ctx
176 .dirs
177 .install_path(self.id(), &tv.version)
178 .join("bin")])
179 }
180
181 fn exec_env(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<BTreeMap<String, String>> {
182 let mut env = BTreeMap::new();
183 let root = ctx.dirs.install_path(self.id(), &tv.version);
184 env.insert("GOROOT".to_string(), root.display().to_string());
185 Ok(env)
186 }
187
188 fn bin_names(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<String>> {
189 let paths = self.bin_paths(ctx, tv)?;
190 let discovered = crate::backend::bin_names_in_dirs(&paths);
191 if discovered.is_empty() {
192 Ok(vec!["go".into(), "gofmt".into()])
193 } else {
194 Ok(discovered)
195 }
196 }
197
198 fn idiomatic_files(&self) -> &[&str] {
199 &["go.mod", ".go-version"]
200 }
201}
202
203impl GoBackend {
204 async fn find_file(
207 &self,
208 ctx: &Ctx,
209 sources: &[Source],
210 go_ver: &str,
211 ) -> Result<(GoFile, String)> {
212 let mut last_err: Option<Error> = None;
213 for source in sources {
214 let index_url = match &source.index_url {
215 Some(u) => u.clone(),
216 None => continue,
217 };
218 let releases: Vec<GoRelease> = match http::get_cached_json(ctx, &index_url).await {
219 Ok(r) => r,
220 Err(e) => {
221 last_err = Some(e);
222 continue;
223 }
224 };
225 if let Some(rel) = releases.iter().find(|r| r.version == go_ver) {
226 if let Some(f) = rel.files.iter().find(|f| Self::matches_platform(f, ctx)) {
227 return Ok((f.clone(), source.id.clone()));
228 }
229 }
230 }
231 Err(last_err.unwrap_or_else(|| Error::VersionResolve {
232 tool: self.id().to_string(),
233 spec: go_ver.to_string(),
234 hint: Some("no archive for this platform in any source index".into()),
235 }))
236 }
237}
238
239fn normalize_go_version(v: &str) -> String {
241 v.strip_prefix("go").unwrap_or(v).to_string()
242}
243
244#[allow(dead_code)]
247fn _windows_note(_os: Os) {}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn default_sources_exclude_unavailable_ustc_mirror() {
255 let sources = GoBackend.default_sources();
256 assert_eq!(
257 sources
258 .iter()
259 .map(|source| source.id.as_str())
260 .collect::<Vec<_>>(),
261 ["official", "aliyun", "google-cn"]
262 );
263 }
264}