tugger_rust_toolchain/
manifest.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! TOML manifests.
6
7use {
8    crate::tar::CompressionFormat,
9    anyhow::{anyhow, Result},
10    std::collections::HashMap,
11    tugger_common::http::RemoteContent,
12};
13
14/// Represents a toolchain manifest file.
15#[derive(Clone, Debug)]
16pub struct Manifest {
17    pub packages: HashMap<String, Package>,
18}
19
20impl Manifest {
21    /// Obtain an instance by parsing TOML bytes.
22    pub fn from_toml_bytes(data: &[u8]) -> Result<Self> {
23        let table = toml::from_slice(data)?;
24
25        Self::from_toml(table)
26    }
27
28    pub fn from_toml(table: toml::value::Table) -> Result<Self> {
29        let manifest_version = match table
30            .get("manifest-version")
31            .ok_or_else(|| anyhow!("manifest TOML doesn't have manifest-version key"))?
32        {
33            toml::Value::String(s) => s,
34            _ => return Err(anyhow!("failed to obtain manifest-version from TOML")),
35        };
36
37        if manifest_version != "2" {
38            return Err(anyhow!(
39                "unhandled manifest-version: {}; only version 2 supported",
40                manifest_version,
41            ));
42        }
43
44        let packages = Self::parse_packages(table)?;
45
46        Ok(Self { packages })
47    }
48
49    pub fn parse_packages(mut table: toml::value::Table) -> Result<HashMap<String, Package>> {
50        let mut result = HashMap::new();
51
52        let pkg_table = match table
53            .remove("pkg")
54            .ok_or_else(|| anyhow!("manifest TOML doesn't have any [pkg]"))?
55        {
56            toml::Value::Table(table) => table,
57            _ => return Err(anyhow!("manifest TOML doesn't have table [pkg]")),
58        };
59
60        for (k, v) in pkg_table {
61            if let toml::Value::Table(v) = v {
62                result.insert(k.clone(), Package::from_table(v)?);
63            }
64        }
65
66        Ok(result)
67    }
68
69    /// Find a package for a target triple in this manifest.
70    pub fn find_package(
71        &self,
72        package: &str,
73        target_triple: &str,
74    ) -> Option<(String, ManifestTargetedPackage)> {
75        match self.packages.get(package) {
76            Some(package) => match &package.target {
77                PackageTarget::Wildcard => None,
78                PackageTarget::Targeted(targets) => targets
79                    .get(target_triple)
80                    .map(|target| (package.version.clone(), target.clone())),
81            },
82            None => None,
83        }
84    }
85}
86
87/// Represents a `[pkg]` entry in a toolchain manifest TOML.
88#[derive(Clone, Debug)]
89pub struct Package {
90    pub version: String,
91    pub target: PackageTarget,
92}
93
94impl Package {
95    pub fn from_table(mut table: toml::value::Table) -> Result<Self> {
96        let version = match table
97            .remove("version")
98            .ok_or_else(|| anyhow!("[pkg] doesn't have version"))?
99        {
100            toml::Value::String(v) => v,
101            _ => return Err(anyhow!("pkg TOML has non-string version")),
102        };
103
104        let mut target_table = match table
105            .remove("target")
106            .ok_or_else(|| anyhow!("[pkg] does not have .target table"))?
107        {
108            toml::Value::Table(t) => t,
109            _ => return Err(anyhow!("[pkg.target] is not a table")),
110        };
111
112        let target = if let Some(toml::Value::Table(_)) = target_table.remove("*") {
113            PackageTarget::Wildcard
114        } else {
115            let mut targets = HashMap::new();
116
117            for (k, v) in target_table {
118                if let toml::Value::Table(mut v) = v {
119                    let available = v
120                        .remove("available")
121                        .ok_or_else(|| anyhow!("available not set"))?
122                        .as_bool()
123                        .ok_or_else(|| anyhow!("available not a bool"))?;
124
125                    let mut urls = vec![];
126
127                    for prefix in &["zst_", "xz_", ""] {
128                        let url = v.remove(&format!("{}url", prefix));
129                        let hash = v.remove(&format!("{}hash", prefix));
130
131                        if let (Some(url), Some(hash)) = (url, hash) {
132                            let url = url
133                                .as_str()
134                                .ok_or_else(|| anyhow!("url not a string"))?
135                                .to_string();
136                            let hash = hash
137                                .as_str()
138                                .ok_or_else(|| anyhow!("hash not a string"))?
139                                .to_string();
140
141                            urls.push((
142                                match *prefix {
143                                    "zst_" => CompressionFormat::Zstd,
144                                    "xz_" => CompressionFormat::Xz,
145                                    "" => CompressionFormat::Gzip,
146                                    _ => panic!("logic error in compression format handling"),
147                                },
148                                url,
149                                hash,
150                            ));
151                        }
152                    }
153
154                    targets.insert(
155                        k.clone(),
156                        ManifestTargetedPackage {
157                            name: k,
158                            available,
159                            urls,
160                        },
161                    );
162                }
163            }
164
165            PackageTarget::Targeted(targets)
166        };
167
168        Ok(Self { version, target })
169    }
170}
171
172#[derive(Clone, Debug)]
173pub enum PackageTarget {
174    Wildcard,
175    Targeted(HashMap<String, ManifestTargetedPackage>),
176}
177
178#[derive(Clone, Debug)]
179pub struct ManifestTargetedPackage {
180    pub name: String,
181    pub available: bool,
182    pub urls: Vec<(CompressionFormat, String, String)>,
183}
184
185impl ManifestTargetedPackage {
186    pub fn download_info(&self) -> Option<(CompressionFormat, RemoteContent)> {
187        let (format, url, digest) = self.urls.get(0)?;
188
189        Some((
190            *format,
191            RemoteContent {
192                name: self.name.clone(),
193                url: url.to_string(),
194                sha256: digest.to_string(),
195            },
196        ))
197    }
198}