Skip to main content

rucc_sysroot/
msvc.rs

1//! Microsoft's installer manifest, and the few packages in it an MSVC sysroot is made of.
2//!
3//! Design: `spec/cross-compile/13-distribution.md` section 13.4, which is the Microsoft half of
4//! [`crate::Wall`].
5//!
6//! # What this is for
7//!
8//! The Windows SDK and the MSVC universal CRT are not ours to redistribute, so no release of this
9//! compiler will ever pin an artifact for an MSVC target the way it pins one for mingw-w64. What
10//! Microsoft does publish is an installer manifest that names every file the Visual Studio
11//! installer would download, and a licence that lets a person who accepts it download those files.
12//! That is the mechanism `cargo-xwin` uses and section 13.4 says we copy it. This module is the
13//! reading half of that: given the two documents, which packages does a compiler need and which
14//! files are those packages made of.
15//!
16//! It fetches nothing and it writes nothing. Everything here is a function of text the caller was
17//! handed, which is the same rule the rest of this crate is held to.
18//!
19//! # The chain, and the one link in it that is not a hash
20//!
21//! There are three documents. The channel manifest, at a fixed `aka.ms` address, which names the
22//! installer manifest. The installer manifest, which names every package and gives a sha256 for
23//! every file in every one of them. And the files.
24//!
25//! Every file is verified against the installer manifest, so the interesting question is what
26//! verifies the installer manifest. The channel gives a sha256 for it, and as of this writing that
27//! hash is wrong: `aka.ms/vs/17/release/channel` says the manifest for 17.14.37710.0 is 30443537
28//! bytes long and hashes to `6e470016...`, and the file served at the URL it names in the same
29//! breath is 17954732 bytes and hashes to `f0a50ea1...`, from two different Microsoft regions on
30//! two different days. Microsoft replaced the file and did not update the record.
31//!
32//! So the record is not a pin, it is a note, and treating it as a pin means a command that never
33//! works. [`Channel`] carries what the channel said and leaves the decision to the caller, which is
34//! the honest arrangement: what actually protects the install is the per file hash a level down,
35//! and what the channel adds is only a check that the CDN served the index the channel described.
36//! A caller that reports both hashes gives a person auditing the download the one thing that
37//! matters, which is exactly which bytes they got.
38//!
39//! # What is chosen, and why it is so little
40//!
41//! A C compiler needs headers and import libraries and nothing else. No linker, no assembler, no
42//! debugger, no redistributables, no spectre mitigated variants, no store or onecore flavours of
43//! the desktop libraries, and no tools of any kind, because the tool is this compiler. That comes
44//! to the CRT headers, one CRT library package per architecture, and six of the Windows SDK's
45//! installers, out of a manifest with nineteen thousand packages in it.
46//!
47//! The newest version of each is taken rather than a pinned one. A pinned version would be a
48//! promise about a file on somebody else's server, which section 13.8 already declines to make for
49//! the sysroots we do publish, and Microsoft retires old versions from the manifest.
50
51use std::collections::BTreeMap;
52use std::fmt;
53
54use rucc_tuple::{Arch, TargetTuple};
55
56use crate::json::{JsonError, Reader};
57
58/// One file Microsoft publishes, as the manifest describes it.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Payload {
61    /// The name the manifest gives it, which for the SDK has a `Installers\` in front of it.
62    pub name: String,
63    /// Where to get it.
64    pub url: String,
65    /// What it must hash to, as sixty four lowercase hex characters.
66    pub sha256: String,
67    /// How many bytes it is, which is what lets a caller say the total before it starts.
68    pub size: u64,
69}
70
71/// The architectures Microsoft ships a CRT for, spelled the way each document spells them.
72///
73/// Two spellings, because the package ids and the SDK installer names disagree about case and
74/// about `arm64`. That is not a thing to normalise away: both are keys into somebody else's
75/// document and a key is what it is.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
77pub enum Chip {
78    /// 32-bit x86.
79    X86,
80    /// x86-64.
81    X64,
82    /// 32-bit ARM.
83    Arm,
84    /// 64-bit ARM.
85    Arm64,
86}
87
88impl Chip {
89    /// Which chip a target is, or [`None`] for a target Microsoft ships nothing for.
90    ///
91    /// ARM64EC has no answer here on purpose. It is tier 4 in
92    /// `spec/cross-compile/04-target-matrix.md`, nothing in this compiler emits code for it, and
93    /// the manifest's ARM64EC packages are a few kilobytes of thunks rather than a C library.
94    #[must_use]
95    pub const fn of(target: TargetTuple) -> Option<Self> {
96        match target.arch() {
97            Arch::X86 => Some(Chip::X86),
98            Arch::X86_64 => Some(Chip::X64),
99            Arch::Arm => Some(Chip::Arm),
100            Arch::Aarch64 => Some(Chip::Arm64),
101            _ => None,
102        }
103    }
104
105    /// How a Visual C++ package id spells it.
106    #[must_use]
107    pub const fn in_package(self) -> &'static str {
108        match self {
109            Chip::X86 => "x86",
110            Chip::X64 => "x64",
111            Chip::Arm => "arm",
112            // The one that is not lower case, which is Microsoft's inconsistency and not ours.
113            Chip::Arm64 => "ARM64",
114        }
115    }
116
117    /// How a Windows SDK installer name spells it.
118    #[must_use]
119    pub const fn in_installer(self) -> &'static str {
120        match self {
121            Chip::X86 => "x86",
122            Chip::X64 => "x64",
123            Chip::Arm => "arm",
124            Chip::Arm64 => "arm64",
125        }
126    }
127}
128
129/// What the channel manifest says, which is the entry point and nothing else.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct Channel {
132    /// The release as a person reads it, such as `17.14.41 (September 2026)`.
133    pub release: String,
134    /// The build, such as `17.14.37710.0`, which is what the installer manifest is versioned by.
135    pub build: String,
136    /// The installer manifest, as the channel describes it. See this module's note about the hash.
137    pub manifest: Payload,
138    /// Where Microsoft publishes the licence that permits this download, taken from the build
139    /// tools product in the channel rather than written down here, so that the address a person is
140    /// sent to is the one Microsoft is serving today.
141    pub licence: String,
142}
143
144impl Channel {
145    /// Read a channel manifest.
146    ///
147    /// # Errors
148    ///
149    /// When the document does not parse, when it has no installer manifest in it, and when the
150    /// build tools product it takes the licence from is not there.
151    pub fn parse(text: &str) -> Result<Self, MsvcError> {
152        let mut release = String::new();
153        let mut build = String::new();
154        let mut manifest = None;
155        let mut licence = String::new();
156
157        let mut reader = Reader::new(text);
158        reader.enter_object()?;
159        while let Some(key) = reader.next_key()? {
160            match &*key {
161                "info" => {
162                    reader.enter_object()?;
163                    while let Some(field) = reader.next_key()? {
164                        match &*field {
165                            "productDisplayVersion" => release = reader.string()?.into_owned(),
166                            "buildVersion" => build = reader.string()?.into_owned(),
167                            _ => reader.skip()?,
168                        }
169                    }
170                }
171                "channelItems" => {
172                    reader.enter_array()?;
173                    while reader.next_item()? {
174                        let item = channel_item(&mut reader)?;
175                        if item.kind == "Manifest" {
176                            manifest = item.payload;
177                        } else if item.id == BUILD_TOOLS && !item.licence.is_empty() {
178                            licence = item.licence;
179                        }
180                    }
181                }
182                _ => reader.skip()?,
183            }
184        }
185
186        let manifest = manifest.ok_or(MsvcError::NoManifest)?;
187        if licence.is_empty() {
188            return Err(MsvcError::NoLicence);
189        }
190        Ok(Channel { release, build, manifest, licence })
191    }
192}
193
194/// The product the licence is taken from, which is the one a person who wants a compiler and no
195/// IDE would install.
196const BUILD_TOOLS: &str = "Microsoft.VisualStudio.Product.BuildTools";
197
198/// One entry of `channelItems`, reduced to the three things [`Channel::parse`] looks at.
199struct ChannelItem {
200    id: String,
201    kind: String,
202    payload: Option<Payload>,
203    licence: String,
204}
205
206/// Read one `channelItems` entry.
207fn channel_item(reader: &mut Reader<'_>) -> Result<ChannelItem, MsvcError> {
208    let mut item = ChannelItem {
209        id: String::new(),
210        kind: String::new(),
211        payload: None,
212        licence: String::new(),
213    };
214    reader.enter_object()?;
215    while let Some(field) = reader.next_key()? {
216        match &*field {
217            "id" => item.id = reader.string()?.into_owned(),
218            "type" => item.kind = reader.string()?.into_owned(),
219            "payloads" => {
220                let mut all = payloads(reader)?;
221                item.payload = (!all.is_empty()).then(|| all.remove(0));
222            }
223            // Every language says the same address, so the first one that has it wins rather than
224            // the document being searched for a locale nothing here is in a position to choose.
225            "localizedResources" => {
226                reader.enter_array()?;
227                while reader.next_item()? {
228                    reader.enter_object()?;
229                    while let Some(inner) = reader.next_key()? {
230                        if inner == "license" && item.licence.is_empty() {
231                            item.licence = reader.string()?.into_owned();
232                        } else {
233                            reader.skip()?;
234                        }
235                    }
236                }
237            }
238            _ => reader.skip()?,
239        }
240    }
241    Ok(item)
242}
243
244/// Read a `payloads` array.
245fn payloads(reader: &mut Reader<'_>) -> Result<Vec<Payload>, MsvcError> {
246    let mut all = Vec::new();
247    reader.enter_array()?;
248    while reader.next_item()? {
249        let mut one =
250            Payload { name: String::new(), url: String::new(), sha256: String::new(), size: 0 };
251        reader.enter_object()?;
252        while let Some(field) = reader.next_key()? {
253            match &*field {
254                "fileName" => one.name = reader.string()?.into_owned(),
255                "url" => one.url = reader.string()?.into_owned(),
256                // Lower cased here rather than wherever it is compared, because the comparison is
257                // against a hash we computed and those come out lower case.
258                "sha256" => one.sha256 = reader.string()?.to_ascii_lowercase(),
259                "size" => one.size = reader.integer()?,
260                _ => reader.skip()?,
261            }
262        }
263        all.push(one);
264    }
265    Ok(all)
266}
267
268/// One file to download, and which package of Microsoft's it came out of.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct Wanted {
271    /// The package id, which is what a provenance record names as the source.
272    pub package: String,
273    /// The package version, which is the version of that source.
274    pub version: String,
275    /// The file itself.
276    pub payload: Payload,
277}
278
279/// Which files an MSVC sysroot for a set of architectures is made of.
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct Selection {
282    /// The Visual C++ release the CRT came from, such as `14.44.35220`.
283    pub crt: String,
284    /// The Windows SDK release, such as `10.0.26100.15`.
285    pub sdk: String,
286    /// Every file, sorted by package and then by name so that two runs agree about the order.
287    pub files: Vec<Wanted>,
288}
289
290impl Selection {
291    /// Choose what to download out of an installer manifest.
292    ///
293    /// `chips` is which architectures to get libraries for. The headers are shared, so a selection
294    /// for four architectures is four library packages and one of everything else.
295    ///
296    /// # Errors
297    ///
298    /// When the document does not parse, when it has no Visual C++ or no Windows SDK in it, and
299    /// when a package or an installer the selection needs is not among its files.
300    pub fn parse(text: &str, chips: &[Chip]) -> Result<Self, MsvcError> {
301        let found = collect(text)?;
302        let crt = newest(found.keys().filter_map(|id| vc_release(id)))
303            .ok_or(MsvcError::NothingFound("a Visual C++ CRT"))?;
304        let sdk_id = newest_sdk(&found).ok_or(MsvcError::NothingFound("a Windows SDK"))?;
305
306        let mut files = Vec::new();
307        let headers = format!("Microsoft.VC.{crt}.CRT.Headers.base");
308        let mut wanted = vec![headers.clone()];
309        for chip in sorted(chips) {
310            wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Desktop.base", chip.in_package()));
311        }
312        // The headers package's own version is what the CRT is reported as, not the last library
313        // package's. They are two numbers of one release and the headers are the one to name.
314        let mut crt_version = String::new();
315        for id in wanted {
316            let package = found.get(&id).ok_or_else(|| MsvcError::NoPackage(id.clone()))?;
317            if id == headers {
318                crt_version.clone_from(&package.version);
319            }
320            for payload in &package.payloads {
321                files.push(Wanted {
322                    package: id.clone(),
323                    version: package.version.clone(),
324                    payload: payload.clone(),
325                });
326            }
327        }
328
329        let sdk = found.get(&sdk_id).ok_or_else(|| MsvcError::NoPackage(sdk_id.clone()))?;
330        for installer in installers(chips) {
331            let payload = sdk
332                .payloads
333                .iter()
334                .find(|payload| leaf(&payload.name) == installer)
335                .ok_or_else(|| MsvcError::NoInstaller(installer.clone()))?;
336            files.push(Wanted {
337                package: sdk_id.clone(),
338                version: sdk.version.clone(),
339                payload: payload.clone(),
340            });
341        }
342
343        files.sort_by(|a, b| (&a.package, &a.payload.name).cmp(&(&b.package, &b.payload.name)));
344        Ok(Selection { crt: crt_version, sdk: sdk.version.clone(), files })
345    }
346
347    /// How many bytes the whole selection is, which is what a person is told before accepting.
348    #[must_use]
349    pub fn size(&self) -> u64 {
350        self.files.iter().map(|file| file.payload.size).sum()
351    }
352}
353
354/// The Windows SDK installers a selection needs, in a stable order.
355///
356/// The x86 desktop headers are here whatever was asked for, because that installer is where the
357/// headers that are not per architecture live and it is four times the size of the other two for
358/// that reason. The universal CRT is one installer for every architecture. The store app headers
359/// and libraries are here because a desktop program still includes `windows.h`, and the desktop
360/// installers do not carry all of what that reaches.
361fn installers(chips: &[Chip]) -> Vec<String> {
362    let mut all = vec![
363        "Universal CRT Headers Libraries and Sources-x86_en-us.msi".to_owned(),
364        "Windows SDK Desktop Headers x86-x86_en-us.msi".to_owned(),
365        "Windows SDK OnecoreUap Headers x86-x86_en-us.msi".to_owned(),
366        "Windows SDK for Windows Store Apps Headers-x86_en-us.msi".to_owned(),
367        "Windows SDK for Windows Store Apps Libs-x86_en-us.msi".to_owned(),
368    ];
369    for chip in sorted(chips) {
370        let arch = chip.in_installer();
371        all.push(format!("Windows SDK Desktop Headers {arch}-x86_en-us.msi"));
372        all.push(format!("Windows SDK Desktop Libs {arch}-x86_en-us.msi"));
373    }
374    all.sort();
375    all.dedup();
376    all
377}
378
379/// The chips asked for, in one order and without repeats, so that a selection does not depend on
380/// how the command line happened to be written.
381fn sorted(chips: &[Chip]) -> Vec<Chip> {
382    let mut all = chips.to_vec();
383    all.sort_unstable();
384    all.dedup();
385    all
386}
387
388/// A package the manifest has and this module might want.
389#[derive(Debug)]
390struct Package {
391    version: String,
392    payloads: Vec<Payload>,
393}
394
395/// Walk the installer manifest and keep the packages whose ids could matter.
396///
397/// The document is eighteen megabytes and nineteen thousand packages, and the way this stays cheap
398/// is that a package whose id is not interesting has its payloads skipped rather than read. That
399/// works because the manifest writes `id` before `payloads`, and a manifest that stopped doing so
400/// would be a manifest this refuses rather than one it quietly misreads.
401fn collect(text: &str) -> Result<BTreeMap<String, Package>, MsvcError> {
402    let mut found: BTreeMap<String, Package> = BTreeMap::new();
403    let mut reader = Reader::new(text);
404    reader.enter_object()?;
405    while let Some(key) = reader.next_key()? {
406        if key != "packages" {
407            reader.skip()?;
408            continue;
409        }
410        reader.enter_array()?;
411        while reader.next_item()? {
412            let mut id = String::new();
413            let mut version = String::new();
414            let mut keep = None;
415            let mut listed = false;
416            let mut read = false;
417            reader.enter_object()?;
418            while let Some(field) = reader.next_key()? {
419                match &*field {
420                    "id" => {
421                        id = reader.string()?.into_owned();
422                        keep = Some(interesting(&id));
423                    }
424                    "version" => version = reader.string()?.into_owned(),
425                    "payloads" => {
426                        listed = true;
427                        read = keep == Some(true);
428                        if read {
429                            let all = payloads(&mut reader)?;
430                            found
431                                .entry(id.clone())
432                                .or_insert(Package { version: version.clone(), payloads: all });
433                        } else {
434                            reader.skip()?;
435                        }
436                    }
437                    _ => reader.skip()?,
438                }
439            }
440            // A package with no files at all is nothing to complain about. A package that had
441            // some, and that turned out to be one of ours only after they had been stepped over,
442            // is a manifest laid out the other way round, and guessing is worse than saying so.
443            if keep == Some(true) && listed && !read && !found.contains_key(&id) {
444                return Err(MsvcError::OutOfOrder(id));
445            }
446            // The version is read after the payloads in no manifest Microsoft has written, but an
447            // entry that ended up without one is a record with a hole in it rather than a package.
448            if let Some(package) = found.get_mut(&id) {
449                if package.version.is_empty() {
450                    package.version.clone_from(&version);
451                }
452            }
453        }
454    }
455    Ok(found)
456}
457
458/// Whether a package id is one of the two shapes this module chooses from.
459///
460/// Deliberately coarse. It is the filter that keeps the walk cheap, and narrowing it down to the
461/// exact ids happens afterwards, where the newest release is already known.
462fn interesting(id: &str) -> bool {
463    (id.starts_with("Microsoft.VC.") && id.contains(".CRT.") && id.ends_with(".base"))
464        || id.starts_with("Win10SDK_10.0.")
465        || id.starts_with("Win11SDK_10.0.")
466}
467
468/// The `14.44.17.14` out of `Microsoft.VC.14.44.17.14.CRT.Headers.base`.
469///
470/// Four numbers, the Visual C++ release and the Visual Studio release it shipped with, and they
471/// are what the newest is chosen by. The package's own `version` field is the fifth number as
472/// well, and it is not what to sort on: two packages of one release can differ in it.
473fn vc_release(id: &str) -> Option<&str> {
474    let rest = id.strip_prefix("Microsoft.VC.")?;
475    let at = rest.find(".CRT.")?;
476    let release = &rest[..at];
477    release
478        .split('.')
479        .all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
480        .then_some(release)
481}
482
483/// The largest of a set of dotted number strings, compared number by number.
484///
485/// Not as text, because `10.0.9.0` is the larger string and the older release, which is the same
486/// trap the Windows SDK search in the driver documents.
487fn newest<'a>(all: impl Iterator<Item = &'a str>) -> Option<String> {
488    all.max_by(|left, right| numbers(left).cmp(&numbers(right))).map(ToOwned::to_owned)
489}
490
491/// A dotted number string as the numbers it is, for comparing.
492fn numbers(text: &str) -> Vec<u64> {
493    text.split('.').map(|part| part.parse().unwrap_or(0)).collect()
494}
495
496/// The newest Windows SDK package id among the ones collected.
497///
498/// Both generations are candidates, because a manifest carries the Windows 10 kits beside the
499/// Windows 11 ones and the newest of all of them is the one to take. They compare by the build in
500/// the id and then by the package version, which is how two revisions of one build are ordered.
501fn newest_sdk(found: &BTreeMap<String, Package>) -> Option<String> {
502    found
503        .iter()
504        .filter(|(id, _)| id.starts_with("Win10SDK_10.0.") || id.starts_with("Win11SDK_10.0."))
505        .max_by_key(|(id, package)| {
506            let build = id.rsplit('.').next().and_then(|last| last.parse::<u64>().ok());
507            (build.unwrap_or(0), numbers(&package.version))
508        })
509        .map(|(id, _)| id.clone())
510}
511
512/// A payload name without the Windows directory Microsoft puts in front of it.
513fn leaf(name: &str) -> &str {
514    name.rsplit('\\').next().unwrap_or(name)
515}
516
517/// Why a manifest could not be read or could not be chosen from.
518#[derive(Debug, Clone, PartialEq, Eq)]
519pub enum MsvcError {
520    /// The document did not parse.
521    ///
522    /// The reader behind this is not part of the interface, so what comes out of it is the two
523    /// things a person needs rather than the reader's own type: what was expected and where.
524    Json {
525        /// The byte offset in the document.
526        at: usize,
527        /// What was expected there, in the words a person would use.
528        wanted: &'static str,
529    },
530    /// The channel manifest has no installer manifest in it, which means it is not one.
531    NoManifest,
532    /// The channel manifest names no licence, and a download that cannot show one is a download
533    /// that does not happen.
534    NoLicence,
535    /// The installer manifest has none of something there has to be one of.
536    NothingFound(&'static str),
537    /// A package the selection needs is not in the manifest.
538    NoPackage(String),
539    /// A Windows SDK installer the selection needs is not among the SDK package's files.
540    NoInstaller(String),
541    /// A package wrote its payloads before its id, which is a manifest laid out in a way this
542    /// reader was written not to guess at.
543    OutOfOrder(String),
544}
545
546impl From<JsonError> for MsvcError {
547    fn from(why: JsonError) -> Self {
548        MsvcError::Json { at: why.at, wanted: why.wanted }
549    }
550}
551
552impl fmt::Display for MsvcError {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        match self {
555            MsvcError::Json { at, wanted } => {
556                write!(f, "this is not the document it was taken for: {wanted} at byte {at} of it")
557            }
558            MsvcError::NoManifest => {
559                write!(f, "this channel names no installer manifest, so it is not a channel")
560            }
561            MsvcError::NoLicence => write!(
562                f,
563                "this channel names no licence for the build tools, and the download it describes \
564                 is one nobody may make without reading one"
565            ),
566            MsvcError::NothingFound(what) => {
567                write!(f, "this manifest has no {what} in it")
568            }
569            MsvcError::NoPackage(id) => {
570                write!(
571                    f,
572                    "this manifest has no {id}, which is a package an MSVC sysroot is made of"
573                )
574            }
575            MsvcError::NoInstaller(name) => write!(
576                f,
577                "the Windows SDK in this manifest has no {name} in it, which is an installer an \
578                 MSVC sysroot is made of"
579            ),
580            MsvcError::OutOfOrder(id) => write!(
581                f,
582                "{id} lists its files before it says what it is, which this reader relies on the \
583                 manifest not doing"
584            ),
585        }
586    }
587}
588
589impl std::error::Error for MsvcError {}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    /// A channel manifest cut down to the two entries that are read, with the real shape and the
596    /// real addresses of the September 2026 release.
597    const CHANNEL: &str = r#"{
598      "manifestVersion": "1.1",
599      "info": { "buildVersion": "17.14.37710.0", "productDisplayVersion": "17.14.41 (September 2026)" },
600      "channelItems": [
601        {
602          "id": "Microsoft.VisualStudio.Manifests.VisualStudio",
603          "version": "17.14.37710.0",
604          "type": "Manifest",
605          "payloads": [
606            {
607              "fileName": "VisualStudio.vsman",
608              "sha256": "6E470016E4324C84C255FFD0BEB3767D17EC89CC8561E9409EE3E1F6D29400F5",
609              "size": 30443537,
610              "url": "https://download.visualstudio.microsoft.com/download/pr/bc92e2cb/VisualStudio.vsman"
611            }
612          ]
613        },
614        {
615          "id": "Microsoft.VisualStudio.Product.BuildTools",
616          "type": "ChannelProduct",
617          "localizedResources": [
618            { "language": "en-US", "license": "https://go.microsoft.com/fwlink/?LinkId=2179911" }
619          ]
620        }
621      ]
622    }"#;
623
624    /// An installer manifest cut down to what is chosen and a little of what is not: an older
625    /// Visual C++ release, an older kit, a language pack, and a package with no payload this
626    /// wants.
627    const MANIFEST: &str = r#"{
628      "manifestVersion": "1.1",
629      "packages": [
630        { "id": "Microsoft.VC.14.29.16.11.CRT.Headers.base", "version": "14.29.30157", "type": "Vsix",
631          "payloads": [ { "fileName": "old.vsix", "sha256": "aa", "size": 1, "url": "https://example.invalid/old" } ] },
632        { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220", "type": "Vsix",
633          "payloads": [ { "fileName": "headers.vsix", "sha256": "B1", "size": 2128977, "url": "https://example.invalid/headers" } ] },
634        { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.Resources", "language": "de-DE", "version": "14.44.35220", "type": "Vsix",
635          "payloads": [ { "fileName": "de.vsix", "sha256": "cc", "size": 3, "url": "https://example.invalid/de" } ] },
636        { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
637          "payloads": [ { "fileName": "x64.vsix", "sha256": "b2", "size": 51521199, "url": "https://example.invalid/x64" } ] },
638        { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
639          "payloads": [ { "fileName": "arm64.vsix", "sha256": "b3", "size": 49166761, "url": "https://example.invalid/arm64" } ] },
640        { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base", "version": "14.44.35226", "type": "Vsix",
641          "payloads": [ { "fileName": "spectre.vsix", "sha256": "dd", "size": 4, "url": "https://example.invalid/spectre" } ] },
642        { "id": "Microsoft.VisualStudio.Component.Windows11SDK", "version": "17.14.35", "type": "Component",
643          "payloads": [ { "fileName": "nothing.vsix", "sha256": "ee", "size": 5, "url": "https://example.invalid/nothing" } ] },
644        { "id": "Win10SDK_10.0.19041", "version": "10.0.19041.4", "type": "Exe",
645          "payloads": [ { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "ff", "size": 6, "url": "https://example.invalid/old-sdk" } ] },
646        { "id": "Win11SDK_10.0.26100", "version": "10.0.26100.15", "type": "Exe",
647          "payloads": [
648            { "fileName": "Installers\\Universal CRT Headers Libraries and Sources-x86_en-us.msi", "sha256": "c1", "size": 589824, "url": "https://example.invalid/ucrt" },
649            { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "c2", "size": 790528, "url": "https://example.invalid/hx86" },
650            { "fileName": "Installers\\Windows SDK Desktop Headers x64-x86_en-us.msi", "sha256": "c3", "size": 450560, "url": "https://example.invalid/hx64" },
651            { "fileName": "Installers\\Windows SDK Desktop Headers arm64-x86_en-us.msi", "sha256": "c4", "size": 446464, "url": "https://example.invalid/harm64" },
652            { "fileName": "Installers\\Windows SDK Desktop Libs x86-x86_en-us.msi", "sha256": "c5", "size": 528384, "url": "https://example.invalid/lx86" },
653            { "fileName": "Installers\\Windows SDK Desktop Libs x64-x86_en-us.msi", "sha256": "c6", "size": 528384, "url": "https://example.invalid/lx64" },
654            { "fileName": "Installers\\Windows SDK Desktop Libs arm64-x86_en-us.msi", "sha256": "c7", "size": 528384, "url": "https://example.invalid/larm64" },
655            { "fileName": "Installers\\Windows SDK OnecoreUap Headers x86-x86_en-us.msi", "sha256": "c8", "size": 495616, "url": "https://example.invalid/onecore" },
656            { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers-x86_en-us.msi", "sha256": "c9", "size": 1060864, "url": "https://example.invalid/store-h" },
657            { "fileName": "Installers\\Windows SDK for Windows Store Apps Libs-x86_en-us.msi", "sha256": "ca", "size": 528384, "url": "https://example.invalid/store-l" },
658            { "fileName": "Installers\\Windows SDK Desktop Tools x64-x86_en-us.msi", "sha256": "cb", "size": 475136, "url": "https://example.invalid/tools" },
659            { "fileName": "0f1a2b3c.cab", "sha256": "cc", "size": 9999, "url": "https://example.invalid/cab" }
660          ] }
661      ]
662    }"#;
663
664    fn target(tuple: &str) -> TargetTuple {
665        tuple.parse().expect("a target this understands")
666    }
667
668    #[test]
669    fn a_channel_says_the_release_the_manifest_and_where_the_licence_is() {
670        let channel = Channel::parse(CHANNEL).expect("a channel");
671        assert_eq!(channel.release, "17.14.41 (September 2026)");
672        assert_eq!(channel.build, "17.14.37710.0");
673        assert_eq!(channel.manifest.name, "VisualStudio.vsman");
674        assert_eq!(channel.manifest.size, 30_443_537);
675        // Lower cased on the way in, because it is compared against a hash we computed.
676        assert!(channel.manifest.sha256.starts_with("6e470016"), "{}", channel.manifest.sha256);
677        assert_eq!(channel.licence, "https://go.microsoft.com/fwlink/?LinkId=2179911");
678    }
679
680    #[test]
681    fn a_channel_with_no_manifest_or_no_licence_in_it_says_which() {
682        let without = CHANNEL.replace("\"type\": \"Manifest\"", "\"type\": \"Bootstrapper\"");
683        assert_eq!(Channel::parse(&without).expect_err("no manifest"), MsvcError::NoManifest);
684
685        let without = CHANNEL.replace("Microsoft.VisualStudio.Product.BuildTools", "Other.Product");
686        assert_eq!(Channel::parse(&without).expect_err("no licence"), MsvcError::NoLicence);
687    }
688
689    #[test]
690    fn the_newest_visual_cpp_and_the_newest_kit_are_the_ones_chosen() {
691        let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
692        assert_eq!(chosen.crt, "14.44.35220");
693        assert_eq!(chosen.sdk, "10.0.26100.15");
694        let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
695        assert!(!packages.contains(&"Microsoft.VC.14.29.16.11.CRT.Headers.base"), "{packages:?}");
696        assert!(!packages.contains(&"Win10SDK_10.0.19041"), "{packages:?}");
697    }
698
699    #[test]
700    fn a_selection_is_the_headers_one_library_package_per_chip_and_the_installers() {
701        let one = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
702        assert_eq!(one.files.len(), 1 + 1 + 7);
703
704        let two = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
705        // One more library package and two more installers, and the headers are still one copy.
706        assert_eq!(two.files.len(), one.files.len() + 3);
707        assert!(two.size() > one.size());
708
709        // The order is the same however the command line was written, and nothing appears twice.
710        let again =
711            Selection::parse(MANIFEST, &[Chip::Arm64, Chip::X64, Chip::X64]).expect("a selection");
712        assert_eq!(again, two);
713    }
714
715    #[test]
716    fn nothing_that_is_not_a_header_or_a_library_is_chosen() {
717        let chosen = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
718        let names: Vec<&str> = chosen.files.iter().map(|file| leaf(&file.payload.name)).collect();
719        for unwanted in ["spectre.vsix", "de.vsix", "nothing.vsix"] {
720            assert!(!names.contains(&unwanted), "{unwanted} is in {names:?}");
721        }
722        // The tools are not a compiler's business, and the cabs are not chosen here because the
723        // installer is what says which of them it needs.
724        assert!(!names.iter().any(|name| name.contains("Tools")), "{names:?}");
725        assert!(!names.iter().any(|name| name.ends_with(".cab")), "{names:?}");
726    }
727
728    #[test]
729    fn a_missing_package_or_installer_says_which_one_by_name() {
730        let without = MANIFEST.replace("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "Other");
731        let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
732        assert_eq!(
733            why,
734            MsvcError::NoPackage("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base".into())
735        );
736
737        let without =
738            MANIFEST.replace("Windows SDK Desktop Libs x64", "Windows SDK Desktop Libs mips");
739        let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
740        assert_eq!(
741            why,
742            MsvcError::NoInstaller("Windows SDK Desktop Libs x64-x86_en-us.msi".into())
743        );
744
745        let empty = r#"{ "packages": [] }"#;
746        assert_eq!(
747            Selection::parse(empty, &[Chip::X64]).expect_err("a refusal"),
748            MsvcError::NothingFound("a Visual C++ CRT")
749        );
750    }
751
752    #[test]
753    fn the_windows_path_in_an_installer_name_survives_being_read() {
754        let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
755        let ucrt = chosen
756            .files
757            .iter()
758            .find(|file| leaf(&file.payload.name).starts_with("Universal CRT"))
759            .expect("the universal CRT");
760        assert_eq!(
761            ucrt.payload.name,
762            r"Installers\Universal CRT Headers Libraries and Sources-x86_en-us.msi"
763        );
764        assert_eq!(ucrt.version, "10.0.26100.15");
765    }
766
767    #[test]
768    fn a_target_maps_to_the_chip_microsoft_spells_two_ways() {
769        assert_eq!(Chip::of(target("x86_64-windows-msvc")), Some(Chip::X64));
770        assert_eq!(Chip::of(target("aarch64-windows-msvc")), Some(Chip::Arm64));
771        assert_eq!(Chip::of(target("i686-windows-msvc")), Some(Chip::X86));
772        // Tier 4 and nothing emits code for it, so there is nothing to fetch a library for.
773        assert_eq!(Chip::of(target("arm64ec-windows-msvc")), None);
774        assert_eq!(Chip::of(target("riscv64-linux-gnu")), None);
775
776        assert_eq!(Chip::Arm64.in_package(), "ARM64");
777        assert_eq!(Chip::Arm64.in_installer(), "arm64");
778    }
779
780    #[test]
781    fn a_dotted_version_is_compared_as_numbers_and_not_as_text() {
782        // The trap the driver's own Windows SDK search documents: the larger string is the older
783        // release.
784        assert_eq!(
785            newest(["10.0.9.0", "10.0.22000.0"].into_iter()).as_deref(),
786            Some("10.0.22000.0")
787        );
788        assert_eq!(vc_release("Microsoft.VC.14.44.17.14.CRT.Headers.base"), Some("14.44.17.14"));
789        assert_eq!(vc_release("Microsoft.VC.Runtimes.x64.base"), None);
790    }
791
792    #[test]
793    fn a_package_that_lists_its_files_before_it_says_what_it_is_is_refused() {
794        let backwards = r#"{ "packages": [
795          { "payloads": [ { "fileName": "a", "sha256": "b", "size": 1, "url": "c" } ],
796            "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220" } ] }"#;
797        let why = Selection::parse(backwards, &[Chip::X64]).expect_err("a refusal");
798        assert_eq!(
799            why,
800            MsvcError::OutOfOrder("Microsoft.VC.14.44.17.14.CRT.Headers.base".to_owned())
801        );
802    }
803}