Skip to main content

osdk_core/backend/
go.rs

1//! Go backend: downloads official archives from go.dev/dl (or a mirror),
2//! verified against the per-file sha256 in the JSON index.
3
4use 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, // e.g. "go1.22.5"
23    #[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, // "archive" | "installer" | "source"
38}
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            // Aliyun mirrors the archives; it has no ?mode=json index, so we
63            // reuse the official index for discovery and only swap the download
64            // host. (index_url points at official.)
65            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            // go.dev lists newest-first; produce oldest-first.
93            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        // Discover the exact archive filename + sha256 from an index.
132        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, // archives wrap everything in a `go/` dir
157            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        // Archive contains `go/bin`, and we strip the `go/` root, so bin is at
174        // <install>/bin.
175        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    /// Find the platform archive file for `go_ver` (e.g. "go1.22.5") by trying
205    /// each source's index in order.
206    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
239/// Strip the leading `go` from a go.dev version string: "go1.22.5" -> "1.22.5".
240fn normalize_go_version(v: &str) -> String {
241    v.strip_prefix("go").unwrap_or(v).to_string()
242}
243
244/// Go binaries live at archive root on Windows too (go/bin), so no special case
245/// beyond the shared `bin` join is needed.
246#[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}