1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// This file is part of Gear.

// Copyright (C) 2023-2024 Gear Technologies Inc.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use crate::builder_error::BuilderError;
use anyhow::{Context, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{borrow::Cow, process::Command};

// The channel patterns we support (borrowed from the rustup code)
static TOOLCHAIN_CHANNELS: &[&str] = &[
    "nightly",
    "beta",
    "stable",
    // Allow from 1.0.0 through to 9.999.99 with optional patch version
    r"\d{1}\.\d{1,3}(?:\.\d{1,2})?",
];

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Toolchain(String);

impl Toolchain {
    /// This is a version of nightly toolchain, tested on our CI.
    const PINNED_NIGHTLY_TOOLCHAIN: &'static str = "nightly-2024-01-25";

    /// Returns `Toolchain` representing the recommended nightly version.
    pub fn recommended_nightly() -> Self {
        Self(Self::PINNED_NIGHTLY_TOOLCHAIN.into())
    }

    /// Fetches `Toolchain` via rustup.
    pub fn try_from_rustup() -> Result<Self> {
        let output = Command::new("rustup")
            .args(["show", "active-toolchain"])
            .output()
            .context("`rustup` command failed")?;

        anyhow::ensure!(
            output.status.success(),
            "`rustup` exit code is not successful"
        );

        let toolchain_desc =
            std::str::from_utf8(&output.stdout).expect("unexpected `rustup` output");

        // TODO #3499: replace it with `std::sync::LazyLock` when it becomes stable
        static TOOLCHAIN_CHANNEL_RE: Lazy<Regex> = Lazy::new(|| {
            let channels = TOOLCHAIN_CHANNELS.join("|");
            let pattern = format!(r"(?:{channels})(?:-\d{{4}}-\d{{2}}-\d{{2}})?");
            // Note this regex gives you a guaranteed match of the channel[-date] as group 0,
            // for example: `nightly-2024-01-25`
            Regex::new(&pattern).unwrap()
        });

        let toolchain = TOOLCHAIN_CHANNEL_RE
            .captures(toolchain_desc)
            .ok_or_else(|| BuilderError::CargoToolchainInvalid(toolchain_desc.into()))?
            .get(0)
            .unwrap() // It is safe to use unwrap here because we know the regex matches
            .as_str()
            .to_owned();

        Ok(Self(toolchain))
    }

    /// Returns toolchain string specification without target triple
    /// as it was passed during initialization.
    ///
    /// `<channel>[-<date>]`
    ///
    /// `<channel> = stable|beta|nightly|<major.minor>|<major.minor.patch>`
    ///
    /// `<date>    = YYYY-MM-DD`
    pub fn raw_toolchain_str(&'_ self) -> Cow<'_, str> {
        self.0.as_str().into()
    }

    /// Checks whether the toolchain is recommended.
    pub fn check_recommended_toolchain(&self) -> Result<()> {
        let toolchain = Self::PINNED_NIGHTLY_TOOLCHAIN;
        anyhow::ensure!(
            self.raw_toolchain_str() == toolchain,
            BuilderError::RecommendedToolchainNotFound(toolchain.into()),
        );
        Ok(())
    }
}