Skip to main content

goup_version/
version.rs

1use std::fs;
2use std::fs::DirEntry;
3use std::ops::Deref;
4use std::process::Command;
5use std::time::Duration;
6
7use anyhow::Result;
8use anyhow::anyhow;
9use regex::Regex;
10use reqwest::blocking::Client;
11use semver::Op;
12use semver::Version as SemVersion;
13use semver::VersionReq;
14use serde::{Deserialize, Serialize};
15use which::which;
16
17use super::Dir;
18use super::ToolchainFilter;
19use super::consts;
20
21const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
22
23#[derive(Serialize, Deserialize, Debug)]
24pub struct GoFile {
25    pub arch: String,
26    pub filename: String,
27    pub kind: String,
28    pub os: String,
29    pub sha256: String,
30    pub size: isize,
31    pub version: String,
32}
33
34#[derive(Serialize, Deserialize, Debug)]
35struct GoRelease {
36    pub version: String,
37    pub stable: bool,
38    // pub files: Vec<GoFile>,
39}
40
41#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
42pub struct Version {
43    // Version: 1.21.1
44    pub version: String,
45    // active or not
46    pub active: bool,
47}
48
49impl Version {
50    /// initializes the environment file.
51    pub fn init_env(s: &str) -> Result<(), anyhow::Error> {
52        let goup_home = Dir::goup_home()?;
53        if !goup_home.exists() {
54            fs::create_dir_all(&goup_home)?;
55        }
56        let env_file = goup_home.env();
57        fs::write(env_file, s)?;
58        Ok(())
59    }
60    pub fn list_upstream_go_versions_filter(
61        host: &str,
62        filter: Option<ToolchainFilter>,
63    ) -> Result<Vec<String>, anyhow::Error> {
64        let ver = Self::list_upstream_go_versions(host)?;
65        let re = filter.map_or_else(
66            || "(.+)".to_owned(),
67            |f| match f {
68                ToolchainFilter::Stable => {
69                    r#"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))?\b"#.to_string()
70                }
71                ToolchainFilter::Unstable => {
72                    r#"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))?(?:rc(?:0|[1-9]\d*))"#
73                        .to_string()
74                }
75                ToolchainFilter::Beta => {
76                    r#"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))?(?:beta(?:0|[1-9]\d*))"#
77                        .to_string()
78                }
79                ToolchainFilter::Filter(s) => format!("(.*{s}.*)"),
80            },
81        );
82        let re = Regex::new(&re)?;
83        Ok(ver
84            .into_iter()
85            .filter_map(|v| re.is_match(&v).then_some(v))
86            .collect())
87    }
88
89    /// list upstream go versions if get go version failure from http then fallback use git.
90    pub fn list_upstream_go_versions(host: &str) -> Result<Vec<String>, anyhow::Error> {
91        Self::list_upstream_go_versions_from_http(host).or_else(|e| {
92            which("git").map_or_else(|_| Err(e), |_| Self::list_upstream_go_versions_from_git())
93        })
94    }
95    /// list upstream go versions from http.
96    fn list_upstream_go_versions_from_http(host: &str) -> Result<Vec<String>, anyhow::Error> {
97        Ok(Client::builder()
98            .timeout(HTTP_TIMEOUT)
99            .build()?
100            .get(format!("{host}/dl/?mode=json&include=all"))
101            .send()?
102            .json::<Vec<GoRelease>>()?
103            .into_iter()
104            .map(|v| v.version.trim_start_matches("go").to_string())
105            .rev()
106            .collect())
107    }
108    /// list upstream go versions from git.
109    fn list_upstream_go_versions_from_git() -> Result<Vec<String>, anyhow::Error> {
110        let output = Command::new("git")
111            .args([
112                "ls-remote",
113                "--sort=version:refname",
114                "--tags",
115                &consts::go_source_git_url(),
116            ])
117            .output()?
118            .stdout;
119        Ok(Regex::new("refs/tags/go(.+)")?
120            .captures_iter(&String::from_utf8_lossy(&output))
121            .map(|capture| capture[1].to_string())
122            .collect())
123    }
124    pub fn match_version_req(host: &str, ver_pattern: &str) -> Result<String, anyhow::Error> {
125        log::debug!("version request pattern: {ver_pattern}");
126        let ver_req = VersionReq::parse(ver_pattern)?;
127        // 是否是精确匹配, 如果是则直接返回
128        if ver_req.comparators.iter().all(|v| v.op == Op::Exact) {
129            return Ok(ver_pattern.trim_start_matches('=').to_owned());
130        }
131        for ver in Self::list_upstream_go_versions(host)?.iter().rev() {
132            if ver_req.matches(&Self::semantic(ver)?) {
133                return Ok(ver.to_owned());
134            }
135        }
136        Err(anyhow!("not any match version!"))
137    }
138
139    /// get upstream latest go version.
140    pub fn get_upstream_latest_go_version(host: &str) -> Result<String, anyhow::Error> {
141        let body = Client::builder()
142            .timeout(HTTP_TIMEOUT)
143            .build()?
144            .get(format!("{host}/VERSION?m=text"))
145            .send()?
146            .text()?;
147        body.split('\n')
148            .nth(0)
149            .ok_or_else(|| anyhow!("Getting latest Go version failed"))
150            .map(|v| v.to_owned())
151    }
152    /// list locally installed go version.
153    pub fn list_go_version() -> Result<Vec<Version>, anyhow::Error> {
154        let goup_home = Dir::goup_home()?;
155        // may be .goup not exist
156        if !goup_home.exists() {
157            return Ok(Vec::new());
158        }
159
160        // may be current not exist
161        let current = goup_home.current().read_link();
162        let current = current.as_ref();
163        let dir: Result<Vec<DirEntry>, _> = goup_home.read_dir()?.collect();
164        let mut version_dirs: Vec<_> = dir?
165            .iter()
166            .filter_map(|v| {
167                if !v.path().is_dir() {
168                    return None;
169                }
170
171                let ver = v.file_name().to_string_lossy().to_string();
172                if ver != "gotip" && !goup_home.is_dot_unpacked_success_file_exists(&ver) {
173                    return None;
174                }
175                Some(Version {
176                    version: ver.trim_start_matches("go").into(),
177                    active: current.is_ok_and(|vv| vv == goup_home.version_go(ver).deref()),
178                })
179            })
180            .collect();
181        version_dirs.sort();
182        Ok(version_dirs)
183    }
184
185    /// set active go version
186    pub fn set_go_version(version: &str) -> Result<(), anyhow::Error> {
187        let version = Self::normalize(version);
188        let goup_home = Dir::goup_home()?;
189        let original = goup_home.version_go(&version);
190        if !original.exists() {
191            return Err(anyhow!(
192                "Go version {version} is not installed. Install it with `goup install`."
193            ));
194        }
195        let link = goup_home.current();
196        let _ = fs::remove_dir_all(&link);
197        #[cfg(unix)]
198        {
199            use std::os::unix::fs as unix_fs;
200            unix_fs::symlink(original, &link)?;
201        }
202        #[cfg(windows)]
203        {
204            junction::create(original, &link)?;
205        }
206        log::info!("Default Go is set to '{version}'");
207        Ok(())
208    }
209    /// remove the go version, if it is current active go version, will ignore deletion.
210    pub fn remove_go_version(version: &str) -> Result<(), anyhow::Error> {
211        let version = Self::normalize(version);
212        let cur = Self::current_go_version()?;
213        if Some(&version) == cur.as_ref() {
214            log::warn!("{version} is current active version,  ignore deletion!");
215        } else {
216            let version_dir = Dir::goup_home()?.version(version);
217            if version_dir.exists() {
218                fs::remove_dir_all(&version_dir)?;
219            }
220        }
221        Ok(())
222    }
223
224    /// remove multiple go version, if it is current active go version, will ignore deletion.
225    pub fn remove_go_versions(vers: &[&str]) -> Result<(), anyhow::Error> {
226        if !vers.is_empty() {
227            let goup_home = Dir::goup_home()?;
228            let cur = Self::current_go_version()?;
229            for ver in vers {
230                let version = Self::normalize(ver);
231                if Some(&version) == cur.as_ref() {
232                    log::warn!("{ver} is current active version, ignore deletion!");
233                    continue;
234                }
235                let version_dir = goup_home.version(&version);
236                if version_dir.exists() {
237                    fs::remove_dir_all(&version_dir)?;
238                }
239            }
240        }
241        Ok(())
242    }
243
244    /// current active go version
245    pub fn current_go_version() -> Result<Option<String>, anyhow::Error> {
246        // may be current not exist
247        let current = Dir::goup_home()?.current().read_link().ok().and_then(|p| {
248            p.parent()
249                .and_then(|v| v.file_name().map(|vv| vv.to_string_lossy().to_string()))
250        });
251        Ok(current)
252    }
253
254    /// list `${HOME}/.goup/cache` directory items(only file, ignore directory).
255    pub fn list_cache(contain_sha256: Option<bool>) -> Result<Vec<String>, anyhow::Error> {
256        let goup_home = Dir::goup_home()?;
257        // may be .goup or .goup/cache not exist
258        if !goup_home.exists() || !goup_home.cache().exists() {
259            return Ok(Vec::new());
260        }
261        let contain_sha256 = contain_sha256.unwrap_or_default();
262        let dir: Result<Vec<DirEntry>, _> = goup_home.cache().read_dir()?.collect();
263        let mut archive_files: Vec<_> = dir?
264            .iter()
265            .filter_map(|v| {
266                if v.path().is_dir() {
267                    return None;
268                }
269                let filename = v.file_name();
270                let filename = filename.to_string_lossy();
271                (contain_sha256 || !filename.ends_with(".sha256")).then(|| filename.to_string())
272            })
273            .collect();
274        archive_files.sort();
275        Ok(archive_files)
276    }
277
278    /// remove `${HOME}/.goup/cache` directory.
279    pub fn remove_cache() -> Result<(), anyhow::Error> {
280        let dl_dir = Dir::goup_home()?.cache();
281        if dl_dir.exists() {
282            fs::remove_dir_all(&dl_dir)?;
283        }
284        Ok(())
285    }
286
287    /// remove `${HOME}/.goup` directory.
288    pub fn remove_goup_home() -> Result<(), anyhow::Error> {
289        let goup_home_dir = Dir::goup_home()?;
290        if goup_home_dir.exists() {
291            fs::remove_dir_all(&goup_home_dir)?;
292        }
293        Ok(())
294    }
295
296    /// normalize the version string.
297    /// 1.21.1   -> go1.21.1
298    /// go1.21.1 -> go1.21.1
299    /// tip      -> gotip
300    /// gotip    -> gotip
301    pub fn normalize(ver: &str) -> String {
302        if ver.starts_with("go") {
303            ver.to_string()
304        } else {
305            format!("go{ver}")
306        }
307    }
308    /// semantic go version string.
309    /// 1           -> 1.0.0
310    /// 1.21        -> 1.21.0
311    /// 1.21rc2     -> 1.21.0-rc2
312    /// 1.21.1rc2   -> 1.21.1-rc2
313    /// 1.21-rc2    -> 1.21.0-rc2
314    /// 1.21.1-rc2  -> 1.21.1-rc2
315    /// 1.21.1      -> 1.21.1
316    pub fn semantic(ver: &str) -> Result<SemVersion> {
317        let count_dot = |name: &str| name.chars().filter(|&v| v == '.').count();
318        let name = ver
319            .find("alpha")
320            .or_else(|| ver.find("beta"))
321            .or_else(|| ver.find("rc"))
322            .map_or_else(
323                || match count_dot(ver) {
324                    0 => format!("{ver}.0.0"),
325                    1 => format!("{ver}.0"),
326                    _ => ver.to_string(),
327                },
328                |idx| {
329                    let start = &ver[..idx].trim_end_matches('-');
330                    if count_dot(start) == 2 {
331                        format!("{}-{}", start, &ver[idx..])
332                    } else {
333                        format!("{}.0-{}", start, &ver[idx..])
334                    }
335                },
336            );
337        Ok(SemVersion::parse(&name)?)
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::Version;
344    use semver::Version as SemVersion;
345
346    #[test]
347    fn test_normalize() {
348        assert_eq!(Version::normalize("1.21.1"), "go1.21.1",);
349        assert_eq!(Version::normalize("go1.21.1"), "go1.21.1",);
350        assert_eq!(Version::normalize("tip"), "gotip",);
351        assert_eq!(Version::normalize("gotip"), "gotip",);
352    }
353
354    #[test]
355    fn test_semantic() {
356        assert_eq!(
357            Version::semantic("1").unwrap(),
358            "1.0.0".parse::<SemVersion>().unwrap(),
359        );
360        assert_eq!(
361            Version::semantic("1.21").unwrap(),
362            "1.21.0".parse::<SemVersion>().unwrap(),
363        );
364        assert_eq!(
365            Version::semantic("1.21rc2").unwrap(),
366            "1.21.0-rc2".parse::<SemVersion>().unwrap(),
367        );
368        assert_eq!(
369            Version::semantic("1.21.1rc2").unwrap(),
370            "1.21.1-rc2".parse::<SemVersion>().unwrap(),
371        );
372        assert_eq!(
373            Version::semantic("1.21-rc2").unwrap(),
374            "1.21.0-rc2".parse::<SemVersion>().unwrap(),
375        );
376        assert_eq!(
377            Version::semantic("1.21.1-rc2").unwrap(),
378            "1.21.1-rc2".parse::<SemVersion>().unwrap(),
379        );
380        assert_eq!(
381            Version::semantic("1.21.1").unwrap(),
382            "1.21.1".parse::<SemVersion>().unwrap(),
383        );
384    }
385
386    #[test]
387    fn test_all_go_version_semantic() {
388        let go_versions = [
389            "1",
390            "1.2.2",
391            "1.3rc1",
392            "1.3rc2",
393            "1.3",
394            "1.3.1",
395            "1.3.2",
396            "1.3.3",
397            "1.4beta1",
398            "1.4rc1",
399            "1.4rc2",
400            "1.4",
401            "1.4.1",
402            "1.4.2",
403            "1.4.3",
404            "1.5beta1",
405            "1.5beta2",
406            "1.5beta3",
407            "1.5rc1",
408            "1.5",
409            "1.5.1",
410            "1.5.2",
411            "1.5.3",
412            "1.5.4",
413            "1.6beta1",
414            "1.6beta2",
415            "1.6rc1",
416            "1.6rc2",
417            "1.6",
418            "1.6.1",
419            "1.6.2",
420            "1.6.3",
421            "1.6.4",
422            "1.7beta1",
423            "1.7beta2",
424            "1.7rc1",
425            "1.7rc2",
426            "1.7rc3",
427            "1.7rc4",
428            "1.7rc5",
429            "1.7rc6",
430            "1.7",
431            "1.7.1",
432            "1.7.3",
433            "1.7.4",
434            "1.7.5",
435            "1.7.6",
436            "1.8beta1",
437            "1.8beta2",
438            "1.8rc1",
439            "1.8rc2",
440            "1.8rc3",
441            "1.8",
442            "1.8.1",
443            "1.8.2",
444            "1.8.3",
445            "1.8.4",
446            "1.8.5",
447            "1.8.6",
448            "1.8.7",
449            "1.9beta1",
450            "1.9beta2",
451            "1.9rc1",
452            "1.9rc2",
453            "1.9",
454            "1.9.1",
455            "1.9.2rc2",
456            "1.9.2",
457            "1.9.3",
458            "1.9.4",
459            "1.9.5",
460            "1.9.6",
461            "1.9.7",
462            "1.10beta1",
463            "1.10beta2",
464            "1.10rc1",
465            "1.10rc2",
466            "1.10",
467            "1.10.1",
468            "1.10.2",
469            "1.10.3",
470            "1.10.4",
471            "1.10.5",
472            "1.10.6",
473            "1.10.7",
474            "1.10.8",
475            "1.11beta1",
476            "1.11beta2",
477            "1.11beta3",
478            "1.11rc1",
479            "1.11rc2",
480            "1.11",
481            "1.11.1",
482            "1.11.2",
483            "1.11.3",
484            "1.11.4",
485            "1.11.5",
486            "1.11.6",
487            "1.11.7",
488            "1.11.8",
489            "1.11.9",
490            "1.11.10",
491            "1.11.11",
492            "1.11.12",
493            "1.11.13",
494            "1.12beta1",
495            "1.12beta2",
496            "1.12rc1",
497            "1.12",
498            "1.12.1",
499            "1.12.2",
500            "1.12.3",
501            "1.12.4",
502            "1.12.5",
503            "1.12.6",
504            "1.12.7",
505            "1.12.8",
506            "1.12.9",
507            "1.12.10",
508            "1.12.11",
509            "1.12.12",
510            "1.12.13",
511            "1.12.14",
512            "1.12.15",
513            "1.12.16",
514            "1.12.17",
515            "1.13beta1",
516            "1.13rc1",
517            "1.13rc2",
518            "1.13",
519            "1.13.1",
520            "1.13.2",
521            "1.13.3",
522            "1.13.4",
523            "1.13.5",
524            "1.13.6",
525            "1.13.7",
526            "1.13.8",
527            "1.13.9",
528            "1.13.10",
529            "1.13.11",
530            "1.13.12",
531            "1.13.13",
532            "1.13.14",
533            "1.13.15",
534            "1.14beta1",
535            "1.14rc1",
536            "1.14",
537            "1.14.1",
538            "1.14.2",
539            "1.14.3",
540            "1.14.4",
541            "1.14.5",
542            "1.14.6",
543            "1.14.7",
544            "1.14.8",
545            "1.14.9",
546            "1.14.10",
547            "1.14.11",
548            "1.14.12",
549            "1.14.13",
550            "1.14.14",
551            "1.14.15",
552            "1.15beta1",
553            "1.15rc1",
554            "1.15rc2",
555            "1.15",
556            "1.15.1",
557            "1.15.2",
558            "1.15.3",
559            "1.15.4",
560            "1.15.5",
561            "1.15.6",
562            "1.15.7",
563            "1.15.8",
564            "1.15.9",
565            "1.15.10",
566            "1.15.11",
567            "1.15.12",
568            "1.15.13",
569            "1.15.14",
570            "1.15.15",
571            "1.16beta1",
572            "1.16rc1",
573            "1.16",
574            "1.16.1",
575            "1.16.2",
576            "1.16.3",
577            "1.16.4",
578            "1.16.5",
579            "1.16.6",
580            "1.16.7",
581            "1.16.8",
582            "1.16.9",
583            "1.16.10",
584            "1.16.11",
585            "1.16.12",
586            "1.16.13",
587            "1.16.14",
588            "1.16.15",
589            "1.17beta1",
590            "1.17rc1",
591            "1.17rc2",
592            "1.17",
593            "1.17.1",
594            "1.17.2",
595            "1.17.3",
596            "1.17.4",
597            "1.17.5",
598            "1.17.6",
599            "1.17.7",
600            "1.17.8",
601            "1.17.9",
602            "1.17.10",
603            "1.17.11",
604            "1.17.12",
605            "1.17.13",
606            "1.18beta1",
607            "1.18beta2",
608            "1.18rc1",
609            "1.18",
610            "1.18.1",
611            "1.18.2",
612            "1.18.3",
613            "1.18.4",
614            "1.18.5",
615            "1.18.6",
616            "1.18.7",
617            "1.18.8",
618            "1.18.9",
619            "1.18.10",
620            "1.19beta1",
621            "1.19rc1",
622            "1.19rc2",
623            "1.19",
624            "1.19.1",
625            "1.19.2",
626            "1.19.3",
627            "1.19.4",
628            "1.19.5",
629            "1.19.6",
630            "1.19.7",
631            "1.19.8",
632            "1.19.9",
633            "1.19.10",
634            "1.19.11",
635            "1.19.12",
636            "1.19.13",
637            "1.20rc1",
638            "1.20rc2",
639            "1.20rc3",
640            "1.20",
641            "1.20.1",
642            "1.20.2",
643            "1.20.3",
644            "1.20.4",
645            "1.20.5",
646            "1.20.6",
647            "1.20.7",
648            "1.20.8",
649            "1.20.9",
650            "1.20.10",
651            "1.20.11",
652            "1.20.12",
653            "1.20.13",
654            "1.20.14",
655            "1.21rc2",
656            "1.21rc3",
657            "1.21rc4",
658            "1.21.0",
659            "1.21.1",
660            "1.21.2",
661            "1.21.3",
662            "1.21.4",
663            "1.21.5",
664            "1.21.6",
665            "1.21.7",
666            "1.21.8",
667            "1.21.9",
668            "1.21.10",
669            "1.21.12",
670            "1.21.13",
671            "1.22rc1",
672            "1.22rc2",
673            "1.22.0",
674            "1.22.1",
675            "1.22.2",
676            "1.22.3",
677            "1.22.4",
678            "1.22.5",
679            "1.22.6",
680            "1.22.7",
681            "1.22.8",
682            "1.22.9",
683            "1.22.10",
684            "1.22.11",
685            "1.23rc1",
686            "1.23rc2",
687            "1.23.0",
688            "1.23.1",
689            "1.23.2",
690            "1.23.3",
691            "1.23.4",
692            "1.23.5",
693            "1.23.6",
694            "1.24rc1",
695            "1.24rc2",
696            "1.24rc3",
697            "1.24.0",
698            "1.24.1",
699            "1.24.2",
700            "1.24.3",
701            "1.24.4",
702            "1.25rc1",
703        ];
704        for ver in go_versions {
705            assert!(Version::semantic(ver).is_ok())
706        }
707    }
708}