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 can be pinned anyway
40//!
41//! The `aka.ms` address is a redirect, and where it ends is a storage URL with the build in it that
42//! is not written to again: the channel for 17.14.37710.0 and the installer manifest it names are
43//! served from two such URLs, and the 17.8, 17.10 and 17.12 channels of the long term servicing
44//! branches are still served from theirs a year or more on. So a build's two documents can be held
45//! to the hash of the bytes they are served as, measured here rather than taken from the channel,
46//! and [`PINNED_BUILD`] is that. The installer manifest at its URL hashed to `f0a50ea1...` in
47//! September 2026 on the day it was first measured and again weeks later, from two networks. What
48//! rotates is which build the redirect points at, and that is the part a pin leaves out.
49//!
50//! # What is chosen, and why it is so little
51//!
52//! A C compiler needs headers and import libraries and nothing else. No linker, no assembler, no
53//! debugger, no redistributables, no spectre mitigated variants, no onecore flavour of the desktop
54//! libraries, and no tools of any kind, because the tool is this compiler. That comes to the CRT
55//! headers, two CRT library packages per architecture, and seven of the Windows SDK's installers,
56//! which is ten files for one target out of a manifest with nineteen thousand packages in it. Five
57//! of the seven are the same whatever the architecture, so the three architectures we target come to
58//! seventeen files rather than thirty.
59//!
60//! # Why the store package is one of the two
61//!
62//! Because it is where Microsoft puts the import libraries for the DLL CRT, which is what a program
63//! built the ordinary way links against. Measured by unpacking both packages of Visual C++
64//! 14.44.35207 for x86-64: the desktop package is 38 files, the static CRT and its debug
65//! information, `libcmt.lib` and `libcpmt.lib` and `libvcruntime.lib` and the rest, and `msvcrt.lib`
66//! is not among them. The store package is 96 files and has `msvcrt.lib`, `vcruntime.lib`,
67//! `oldnames.lib` and the CRT's own object fragments such as `chkstk.obj` in it. Both unpack into
68//! the same `lib/<chip>` directory of a Visual Studio installation, so the two together are what
69//! that directory is, and the store package's `store` and `uwp` subdirectories are the part of it
70//! that is actually about store apps and that an unpack leaves behind. `xwin` takes it for the same
71//! reason and says so in the same words, which is a second opinion rather than the source of this
72//! one.
73//!
74//! # The cabinets are named by the installers rather than by the manifest
75//!
76//! The Windows SDK half of the selection is MSIs, and an MSI holds no bytes: it is a small database
77//! saying which cabinet each of its files is in and what that cabinet calls it, and the cabinets are
78//! separate files in the same package, named by a hash. The newest kit publishes 149 of them and
79//! they come to 484 MB, of which one target wants a fraction, so which cabinets to download is a
80//! question only the installers can answer and this module does not guess at it. What it does is
81//! carry them: [`Selection::cab`] takes the name an installer gives and hands back the file the
82//! manifest publishes under it.
83//!
84//! The newest version of each in the manifest is taken, and the manifest is what decides it. The
85//! one the current channel names moves as Microsoft retires old versions, and the one
86//! [`PINNED_BUILD`] names does not, so the same `rucc --fetch` gets the same files on every machine
87//! for as long as Microsoft serves that build.
88
89use std::collections::BTreeMap;
90use std::fmt;
91
92use rucc_tuple::{Arch, TargetTuple};
93
94use crate::json::{JsonError, Reader};
95
96/// One file Microsoft publishes, as the manifest describes it.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct Payload {
99    /// The name the manifest gives it, which for the SDK has a `Installers\` in front of it.
100    pub name: String,
101    /// Where to get it.
102    pub url: String,
103    /// What it must hash to, as sixty four lowercase hex characters.
104    pub sha256: String,
105    /// How many bytes it is, which is what lets a caller say the total before it starts.
106    pub size: u64,
107}
108
109/// The architectures Microsoft ships a CRT for, spelled the way each document spells them.
110///
111/// Two spellings, because the package ids and the SDK installer names disagree about case and
112/// about `arm64`. That is not a thing to normalise away: both are keys into somebody else's
113/// document and a key is what it is.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
115pub enum Chip {
116    /// 32-bit x86.
117    X86,
118    /// x86-64.
119    X64,
120    /// 32-bit ARM.
121    Arm,
122    /// 64-bit ARM.
123    Arm64,
124}
125
126impl Chip {
127    /// Which chip a target is, or [`None`] for a target Microsoft ships nothing for.
128    ///
129    /// ARM64EC has no answer here on purpose. It is tier 4 in
130    /// `spec/cross-compile/04-target-matrix.md`, nothing in this compiler emits code for it, and
131    /// the manifest's ARM64EC packages are a few kilobytes of thunks rather than a C library.
132    #[must_use]
133    pub const fn of(target: TargetTuple) -> Option<Self> {
134        match target.arch() {
135            Arch::X86 => Some(Chip::X86),
136            Arch::X86_64 => Some(Chip::X64),
137            Arch::Arm => Some(Chip::Arm),
138            Arch::Aarch64 => Some(Chip::Arm64),
139            _ => None,
140        }
141    }
142
143    /// How a Visual C++ package id spells it.
144    #[must_use]
145    pub const fn in_package(self) -> &'static str {
146        match self {
147            Chip::X86 => "x86",
148            Chip::X64 => "x64",
149            Chip::Arm => "arm",
150            // The one that is not lower case, which is Microsoft's inconsistency and not ours.
151            Chip::Arm64 => "ARM64",
152        }
153    }
154
155    /// How a Windows SDK installer name spells it.
156    #[must_use]
157    pub const fn in_installer(self) -> &'static str {
158        match self {
159            Chip::X86 => "x86",
160            Chip::X64 => "x64",
161            Chip::Arm => "arm",
162            Chip::Arm64 => "arm64",
163        }
164    }
165
166    /// How the tree `--sysroot` reads spells it in a directory name.
167    ///
168    /// LLVM's names rather than Microsoft's, because that is what `xwin` produces and the tree is
169    /// the one it produces. So a target that was fetched with `xwin` and one that was fetched with
170    /// this both answer to the same `--sysroot`, and the `x64` in Microsoft's own package names
171    /// stays in the packages where a person reading a manifest would go looking for it.
172    #[must_use]
173    pub const fn in_tree(self) -> &'static str {
174        match self {
175            Chip::X86 => "x86",
176            Chip::X64 => "x86_64",
177            Chip::Arm => "aarch",
178            Chip::Arm64 => "aarch64",
179        }
180    }
181}
182
183/// What the channel manifest says, which is the entry point and nothing else.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Channel {
186    /// The release as a person reads it, such as `17.14.41 (September 2026)`.
187    pub release: String,
188    /// The build, such as `17.14.37710.0`, which is what the installer manifest is versioned by.
189    pub build: String,
190    /// The installer manifest, as the channel describes it. See this module's note about the hash.
191    pub manifest: Payload,
192    /// Where Microsoft publishes the licence that permits this download, taken from the build
193    /// tools product in the channel rather than written down here, so that the address a person is
194    /// sent to is the one Microsoft is serving today.
195    pub licence: String,
196}
197
198impl Channel {
199    /// Read a channel manifest.
200    ///
201    /// # Errors
202    ///
203    /// When the document does not parse, when it has no installer manifest in it, and when the
204    /// build tools product it takes the licence from is not there.
205    pub fn parse(text: &str) -> Result<Self, MsvcError> {
206        let mut release = String::new();
207        let mut build = String::new();
208        let mut manifest = None;
209        let mut licence = String::new();
210
211        let mut reader = Reader::new(text);
212        reader.enter_object()?;
213        while let Some(key) = reader.next_key()? {
214            match &*key {
215                "info" => {
216                    reader.enter_object()?;
217                    while let Some(field) = reader.next_key()? {
218                        match &*field {
219                            "productDisplayVersion" => release = reader.string()?.into_owned(),
220                            "buildVersion" => build = reader.string()?.into_owned(),
221                            _ => reader.skip()?,
222                        }
223                    }
224                }
225                "channelItems" => {
226                    reader.enter_array()?;
227                    while reader.next_item()? {
228                        let item = channel_item(&mut reader)?;
229                        if item.kind == "Manifest" {
230                            manifest = item.payload;
231                        } else if item.id == BUILD_TOOLS && !item.licence.is_empty() {
232                            licence = item.licence;
233                        }
234                    }
235                }
236                _ => reader.skip()?,
237            }
238        }
239
240        let manifest = manifest.ok_or(MsvcError::NoManifest)?;
241        if licence.is_empty() {
242            return Err(MsvcError::NoLicence);
243        }
244        Ok(Channel { release, build, manifest, licence })
245    }
246}
247
248/// A Visual Studio build whose channel and installer manifest are held to hashes.
249///
250/// What `rucc --fetch` of an MSVC target starts from, where `rucc --fetch-msvc-sdk` starts from
251/// whatever the `aka.ms` channel names today. The hashes are of the bytes as served, measured
252/// rather than copied out of the channel, since the channel's hash for its own manifest is the
253/// wrong one. Every file after these two is held to the hash the pinned manifest gives for it, so
254/// the whole download is fixed by these two lines.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub struct PinnedBuild {
257    /// The build, which the channel has to say it is.
258    pub build: &'static str,
259    /// Where the channel manifest for this build is served.
260    pub channel: &'static str,
261    /// What the channel manifest hashes to.
262    pub channel_sha256: &'static str,
263    /// Where the installer manifest for this build is served, which is the URL its channel names.
264    pub manifest: &'static str,
265    /// What the installer manifest hashes to.
266    pub manifest_sha256: &'static str,
267}
268
269/// The build this release fetches, which is Visual Studio 2022 17.14.41 of September 2026.
270///
271/// Its installer manifest picks MSVC CRT 14.44 and Windows SDK 10.0.26100. Moving it is a matter of
272/// following `aka.ms/vs/17/release/channel` to where it ends, hashing that file and the manifest it
273/// names, and writing the four values here.
274pub const PINNED_BUILD: PinnedBuild = PinnedBuild {
275    build: "17.14.37710.0",
276    channel: "https://download.visualstudio.microsoft.com/download/pr/\
277              bc92e2cb-33de-4a0c-995d-efa817f16b16/\
278              0dbdfd40c17757e64fc9f72cd9954ec8471c04fd8461a77806e5291e23239ac0/\
279              VisualStudio.17.Release.chman",
280    channel_sha256: "fca418ba94ffbcfb7a2b25f10f16f39dd09660568d21eef4bd3f274cb0b27b8c",
281    manifest: "https://download.visualstudio.microsoft.com/download/pr/\
282               bc92e2cb-33de-4a0c-995d-efa817f16b16/\
283               6e470016e4324c84c255ffd0beb3767d17ec89cc8561e9409ee3e1f6d29400f5/\
284               VisualStudio.vsman",
285    manifest_sha256: "f0a50ea157222c29abd5ea6ff01bfc3c33b04e011c5e45ee2ca38ef0778e5643",
286};
287
288/// The product the licence is taken from, which is the one a person who wants a compiler and no
289/// IDE would install.
290const BUILD_TOOLS: &str = "Microsoft.VisualStudio.Product.BuildTools";
291
292/// One entry of `channelItems`, reduced to the three things [`Channel::parse`] looks at.
293struct ChannelItem {
294    id: String,
295    kind: String,
296    payload: Option<Payload>,
297    licence: String,
298}
299
300/// Read one `channelItems` entry.
301fn channel_item(reader: &mut Reader<'_>) -> Result<ChannelItem, MsvcError> {
302    let mut item = ChannelItem {
303        id: String::new(),
304        kind: String::new(),
305        payload: None,
306        licence: String::new(),
307    };
308    reader.enter_object()?;
309    while let Some(field) = reader.next_key()? {
310        match &*field {
311            "id" => item.id = reader.string()?.into_owned(),
312            "type" => item.kind = reader.string()?.into_owned(),
313            "payloads" => {
314                let mut all = payloads(reader)?;
315                item.payload = (!all.is_empty()).then(|| all.remove(0));
316            }
317            // Every language says the same address, so the first one that has it wins rather than
318            // the document being searched for a locale nothing here is in a position to choose.
319            "localizedResources" => {
320                reader.enter_array()?;
321                while reader.next_item()? {
322                    reader.enter_object()?;
323                    while let Some(inner) = reader.next_key()? {
324                        if inner == "license" && item.licence.is_empty() {
325                            item.licence = reader.string()?.into_owned();
326                        } else {
327                            reader.skip()?;
328                        }
329                    }
330                }
331            }
332            _ => reader.skip()?,
333        }
334    }
335    Ok(item)
336}
337
338/// Read a `payloads` array.
339fn payloads(reader: &mut Reader<'_>) -> Result<Vec<Payload>, MsvcError> {
340    let mut all = Vec::new();
341    reader.enter_array()?;
342    while reader.next_item()? {
343        let mut one =
344            Payload { name: String::new(), url: String::new(), sha256: String::new(), size: 0 };
345        reader.enter_object()?;
346        while let Some(field) = reader.next_key()? {
347            match &*field {
348                "fileName" => one.name = reader.string()?.into_owned(),
349                "url" => one.url = reader.string()?.into_owned(),
350                // Lower cased here rather than wherever it is compared, because the comparison is
351                // against a hash we computed and those come out lower case.
352                "sha256" => one.sha256 = reader.string()?.to_ascii_lowercase(),
353                "size" => one.size = reader.integer()?,
354                _ => reader.skip()?,
355            }
356        }
357        all.push(one);
358    }
359    Ok(all)
360}
361
362/// One file to download, and which package of Microsoft's it came out of.
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct Wanted {
365    /// The package id, which is what a provenance record names as the source.
366    pub package: String,
367    /// The package version, which is the version of that source.
368    pub version: String,
369    /// The file itself.
370    pub payload: Payload,
371}
372
373/// Which files an MSVC sysroot for a set of architectures is made of.
374#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct Selection {
376    /// The Visual C++ release the CRT came from, such as `14.44.35220`.
377    pub crt: String,
378    /// The Windows SDK release, such as `10.0.26100.15`.
379    pub sdk: String,
380    /// Every file, sorted by package and then by name so that two runs agree about the order.
381    pub files: Vec<Wanted>,
382    /// The cabinets the Windows SDK publishes, sorted by name, which is where the bytes the
383    /// installers in [`Selection::files`] describe actually are.
384    ///
385    /// All of them rather than the ones a target needs, because that is not a question this module
386    /// can answer: an installer is a database and the cabinet it wants is a row in it. A caller that
387    /// has read one looks the name up with [`Selection::cab`] and downloads what comes back.
388    pub cabs: Vec<Wanted>,
389}
390
391impl Selection {
392    /// Choose what to download out of an installer manifest.
393    ///
394    /// `chips` is which architectures to get libraries for. The headers are shared, so a selection
395    /// for four architectures is eight library packages and one of everything else.
396    ///
397    /// # Errors
398    ///
399    /// When the document does not parse, when it has no Visual C++ or no Windows SDK in it, and
400    /// when a package or an installer the selection needs is not among its files.
401    pub fn parse(text: &str, chips: &[Chip]) -> Result<Self, MsvcError> {
402        let found = collect(text)?;
403        let crt = newest(found.keys().filter_map(|id| vc_release(id)))
404            .ok_or(MsvcError::NothingFound("a Visual C++ CRT"))?;
405        let sdk_id = newest_sdk(&found).ok_or(MsvcError::NothingFound("a Windows SDK"))?;
406
407        let mut files = Vec::new();
408        let headers = format!("Microsoft.VC.{crt}.CRT.Headers.base");
409        let mut wanted = vec![headers.clone()];
410        for chip in sorted(chips) {
411            // Two packages per architecture, and the store one is not about store apps. This
412            // module's note says what is in each of them and how it was measured.
413            wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Desktop.base", chip.in_package()));
414            wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Store.base", chip.in_package()));
415        }
416        // The headers package's own version is what the CRT is reported as, not the last library
417        // package's. They are two numbers of one release and the headers are the one to name.
418        let mut crt_version = String::new();
419        for id in wanted {
420            let package = found.get(&id).ok_or_else(|| MsvcError::NoPackage(id.clone()))?;
421            if id == headers {
422                crt_version.clone_from(&package.version);
423            }
424            for payload in &package.payloads {
425                files.push(Wanted {
426                    package: id.clone(),
427                    version: package.version.clone(),
428                    payload: payload.clone(),
429                });
430            }
431        }
432
433        let sdk = found.get(&sdk_id).ok_or_else(|| MsvcError::NoPackage(sdk_id.clone()))?;
434        for installer in installers(chips) {
435            let payload = sdk
436                .payloads
437                .iter()
438                .find(|payload| leaf(&payload.name) == installer)
439                .ok_or_else(|| MsvcError::NoInstaller(installer.clone()))?;
440            files.push(Wanted {
441                package: sdk_id.clone(),
442                version: sdk.version.clone(),
443                payload: payload.clone(),
444            });
445        }
446
447        let mut cabs: Vec<Wanted> = sdk
448            .payloads
449            .iter()
450            .filter(|payload| leaf(&payload.name).to_ascii_lowercase().ends_with(".cab"))
451            .map(|payload| Wanted {
452                package: sdk_id.clone(),
453                version: sdk.version.clone(),
454                payload: payload.clone(),
455            })
456            .collect();
457        cabs.sort_by(|a, b| a.payload.name.cmp(&b.payload.name));
458
459        files.sort_by(|a, b| (&a.package, &a.payload.name).cmp(&(&b.package, &b.payload.name)));
460        Ok(Selection { crt: crt_version, sdk: sdk.version.clone(), files, cabs })
461    }
462
463    /// The cabinet an installer named, or [`None`] for a name the Windows SDK does not publish.
464    ///
465    /// The two documents spell it differently and neither spelling is wrong. An installer's own
466    /// table says `d60d1d4a1b5da9e4d41b0bcb0b1dcb14.cab`, because that is what it calls the file it
467    /// wants, and the manifest says `Installers\d60d1d4a1b5da9e4d41b0bcb0b1dcb14.cab`, because that
468    /// is where the Visual Studio installer would put it. So the comparison is on the last component
469    /// and it ignores case, which costs nothing and is what a Windows file name means.
470    #[must_use]
471    pub fn cab(&self, name: &str) -> Option<&Wanted> {
472        self.cabs.iter().find(|cab| leaf(&cab.payload.name).eq_ignore_ascii_case(leaf(name)))
473    }
474
475    /// How many bytes the files are, which is what a person is told before accepting.
476    ///
477    /// The files and not the cabinets. A cabinet is downloaded only once an installer has named it,
478    /// so a total that included all 149 of them would be several times what a run actually moves,
479    /// and one that included none of them would be short by most of the Windows SDK. What is honest
480    /// before anything has been read is the number this gives and a sentence saying the cabinets
481    /// come after, which is what the caller prints.
482    #[must_use]
483    pub fn size(&self) -> u64 {
484        self.files.iter().map(|file| file.payload.size).sum()
485    }
486}
487
488/// The Windows SDK installers a selection needs, in a stable order.
489///
490/// The x86 desktop headers are here whatever was asked for, because that installer is where the
491/// headers that are not per architecture live and it is four times the size of the other two for
492/// that reason. The universal CRT is one installer for every architecture. The store app headers
493/// and libraries are here because a desktop program still includes `windows.h`, and the desktop
494/// installers do not carry all of what that reaches. The OnecoreUap one of the store headers is
495/// where `winapifamily.h`, `sdkddkver.h` and `specstrings.h` are, which `windows.h` includes first.
496fn installers(chips: &[Chip]) -> Vec<String> {
497    let mut all = vec![
498        "Universal CRT Headers Libraries and Sources-x86_en-us.msi".to_owned(),
499        "Windows SDK Desktop Headers x86-x86_en-us.msi".to_owned(),
500        "Windows SDK OnecoreUap Headers x86-x86_en-us.msi".to_owned(),
501        "Windows SDK for Windows Store Apps Headers-x86_en-us.msi".to_owned(),
502        "Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi".to_owned(),
503        "Windows SDK for Windows Store Apps Libs-x86_en-us.msi".to_owned(),
504    ];
505    for chip in sorted(chips) {
506        let arch = chip.in_installer();
507        all.push(format!("Windows SDK Desktop Headers {arch}-x86_en-us.msi"));
508        all.push(format!("Windows SDK Desktop Libs {arch}-x86_en-us.msi"));
509    }
510    all.sort();
511    all.dedup();
512    all
513}
514
515/// The chips asked for, in one order and without repeats, so that a selection does not depend on
516/// how the command line happened to be written.
517fn sorted(chips: &[Chip]) -> Vec<Chip> {
518    let mut all = chips.to_vec();
519    all.sort_unstable();
520    all.dedup();
521    all
522}
523
524/// A package the manifest has and this module might want.
525#[derive(Debug)]
526struct Package {
527    version: String,
528    payloads: Vec<Payload>,
529}
530
531/// Walk the installer manifest and keep the packages whose ids could matter.
532///
533/// The document is eighteen megabytes and nineteen thousand packages, and the way this stays cheap
534/// is that a package whose id is not interesting has its payloads skipped rather than read. That
535/// works because the manifest writes `id` before `payloads`, and a manifest that stopped doing so
536/// would be a manifest this refuses rather than one it quietly misreads.
537fn collect(text: &str) -> Result<BTreeMap<String, Package>, MsvcError> {
538    let mut found: BTreeMap<String, Package> = BTreeMap::new();
539    let mut reader = Reader::new(text);
540    reader.enter_object()?;
541    while let Some(key) = reader.next_key()? {
542        if key != "packages" {
543            reader.skip()?;
544            continue;
545        }
546        reader.enter_array()?;
547        while reader.next_item()? {
548            let mut id = String::new();
549            let mut version = String::new();
550            let mut keep = None;
551            let mut listed = false;
552            let mut read = false;
553            reader.enter_object()?;
554            while let Some(field) = reader.next_key()? {
555                match &*field {
556                    "id" => {
557                        id = reader.string()?.into_owned();
558                        keep = Some(interesting(&id));
559                    }
560                    "version" => version = reader.string()?.into_owned(),
561                    "payloads" => {
562                        listed = true;
563                        read = keep == Some(true);
564                        if read {
565                            let all = payloads(&mut reader)?;
566                            found
567                                .entry(id.clone())
568                                .or_insert(Package { version: version.clone(), payloads: all });
569                        } else {
570                            reader.skip()?;
571                        }
572                    }
573                    _ => reader.skip()?,
574                }
575            }
576            // A package with no files at all is nothing to complain about. A package that had
577            // some, and that turned out to be one of ours only after they had been stepped over,
578            // is a manifest laid out the other way round, and guessing is worse than saying so.
579            if keep == Some(true) && listed && !read && !found.contains_key(&id) {
580                return Err(MsvcError::OutOfOrder(id));
581            }
582            // The version is read after the payloads in no manifest Microsoft has written, but an
583            // entry that ended up without one is a record with a hole in it rather than a package.
584            if let Some(package) = found.get_mut(&id) {
585                if package.version.is_empty() {
586                    package.version.clone_from(&version);
587                }
588            }
589        }
590    }
591    Ok(found)
592}
593
594/// Whether a package id is one of the two shapes this module chooses from.
595///
596/// Deliberately coarse. It is the filter that keeps the walk cheap, and narrowing it down to the
597/// exact ids happens afterwards, where the newest release is already known.
598fn interesting(id: &str) -> bool {
599    (id.starts_with("Microsoft.VC.") && id.contains(".CRT.") && id.ends_with(".base"))
600        || id.starts_with("Win10SDK_10.0.")
601        || id.starts_with("Win11SDK_10.0.")
602}
603
604/// The `14.44.17.14` out of `Microsoft.VC.14.44.17.14.CRT.Headers.base`.
605///
606/// Four numbers, the Visual C++ release and the Visual Studio release it shipped with, and they
607/// are what the newest is chosen by. The package's own `version` field is the fifth number as
608/// well, and it is not what to sort on: two packages of one release can differ in it.
609fn vc_release(id: &str) -> Option<&str> {
610    let rest = id.strip_prefix("Microsoft.VC.")?;
611    let at = rest.find(".CRT.")?;
612    let release = &rest[..at];
613    release
614        .split('.')
615        .all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
616        .then_some(release)
617}
618
619/// The largest of a set of dotted number strings, compared number by number.
620///
621/// Not as text, because `10.0.9.0` is the larger string and the older release, which is the same
622/// trap the Windows SDK search in the driver documents.
623fn newest<'a>(all: impl Iterator<Item = &'a str>) -> Option<String> {
624    all.max_by(|left, right| numbers(left).cmp(&numbers(right))).map(ToOwned::to_owned)
625}
626
627/// A dotted number string as the numbers it is, for comparing.
628fn numbers(text: &str) -> Vec<u64> {
629    text.split('.').map(|part| part.parse().unwrap_or(0)).collect()
630}
631
632/// The newest Windows SDK package id among the ones collected.
633///
634/// Both generations are candidates, because a manifest carries the Windows 10 kits beside the
635/// Windows 11 ones and the newest of all of them is the one to take. They compare by the build in
636/// the id and then by the package version, which is how two revisions of one build are ordered.
637fn newest_sdk(found: &BTreeMap<String, Package>) -> Option<String> {
638    found
639        .iter()
640        .filter(|(id, _)| id.starts_with("Win10SDK_10.0.") || id.starts_with("Win11SDK_10.0."))
641        .max_by_key(|(id, package)| {
642            let build = id.rsplit('.').next().and_then(|last| last.parse::<u64>().ok());
643            (build.unwrap_or(0), numbers(&package.version))
644        })
645        .map(|(id, _)| id.clone())
646}
647
648/// A payload name without the Windows directory Microsoft puts in front of it.
649fn leaf(name: &str) -> &str {
650    name.rsplit('\\').next().unwrap_or(name)
651}
652
653/// Why a manifest could not be read or could not be chosen from.
654#[derive(Debug, Clone, PartialEq, Eq)]
655pub enum MsvcError {
656    /// The document did not parse.
657    ///
658    /// The reader behind this is not part of the interface, so what comes out of it is the two
659    /// things a person needs rather than the reader's own type: what was expected and where.
660    Json {
661        /// The byte offset in the document.
662        at: usize,
663        /// What was expected there, in the words a person would use.
664        wanted: &'static str,
665    },
666    /// The channel manifest has no installer manifest in it, which means it is not one.
667    NoManifest,
668    /// The channel manifest names no licence, and a download that cannot show one is a download
669    /// that does not happen.
670    NoLicence,
671    /// The installer manifest has none of something there has to be one of.
672    NothingFound(&'static str),
673    /// A package the selection needs is not in the manifest.
674    NoPackage(String),
675    /// A Windows SDK installer the selection needs is not among the SDK package's files.
676    NoInstaller(String),
677    /// A package wrote its payloads before its id, which is a manifest laid out in a way this
678    /// reader was written not to guess at.
679    OutOfOrder(String),
680}
681
682impl From<JsonError> for MsvcError {
683    fn from(why: JsonError) -> Self {
684        MsvcError::Json { at: why.at, wanted: why.wanted }
685    }
686}
687
688impl fmt::Display for MsvcError {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        match self {
691            MsvcError::Json { at, wanted } => {
692                write!(f, "this is not the document it was taken for: {wanted} at byte {at} of it")
693            }
694            MsvcError::NoManifest => {
695                write!(f, "this channel names no installer manifest, so it is not a channel")
696            }
697            MsvcError::NoLicence => write!(
698                f,
699                "this channel names no licence for the build tools, and the download it describes \
700                 is one nobody may make without reading one"
701            ),
702            MsvcError::NothingFound(what) => {
703                write!(f, "this manifest has no {what} in it")
704            }
705            MsvcError::NoPackage(id) => {
706                write!(
707                    f,
708                    "this manifest has no {id}, which is a package an MSVC sysroot is made of"
709                )
710            }
711            MsvcError::NoInstaller(name) => write!(
712                f,
713                "the Windows SDK in this manifest has no {name} in it, which is an installer an \
714                 MSVC sysroot is made of"
715            ),
716            MsvcError::OutOfOrder(id) => write!(
717                f,
718                "{id} lists its files before it says what it is, which this reader relies on the \
719                 manifest not doing"
720            ),
721        }
722    }
723}
724
725impl std::error::Error for MsvcError {}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    /// The pin is two storage URLs and two hashes, and the one thing that can be checked about it
732    /// without the network is that it is written the way the code that reads it expects.
733    #[test]
734    fn the_pinned_build_names_two_storage_urls_and_holds_each_to_a_hash() {
735        let pin = PINNED_BUILD;
736        for (url, sha256, file) in [
737            (pin.channel, pin.channel_sha256, "VisualStudio.17.Release.chman"),
738            (pin.manifest, pin.manifest_sha256, "VisualStudio.vsman"),
739        ] {
740            // The line continuations have to have taken the indentation with them.
741            assert!(!url.contains(' '), "{url}");
742            assert!(url.starts_with("https://download.visualstudio.microsoft.com/"), "{url}");
743            assert!(url.ends_with(&format!("/{file}")), "{url}");
744            assert_eq!(sha256.len(), 64, "{sha256}");
745            assert!(sha256.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)));
746        }
747        // The manifest's URL has the channel's wrong hash in it and not the right one, which is
748        // why the right one is written here and not read out of the channel.
749        assert!(!pin.manifest.contains(pin.manifest_sha256));
750        assert!(pin.build.starts_with("17."));
751    }
752
753    /// A channel manifest cut down to the two entries that are read, with the real shape and the
754    /// real addresses of the September 2026 release.
755    const CHANNEL: &str = r#"{
756      "manifestVersion": "1.1",
757      "info": { "buildVersion": "17.14.37710.0", "productDisplayVersion": "17.14.41 (September 2026)" },
758      "channelItems": [
759        {
760          "id": "Microsoft.VisualStudio.Manifests.VisualStudio",
761          "version": "17.14.37710.0",
762          "type": "Manifest",
763          "payloads": [
764            {
765              "fileName": "VisualStudio.vsman",
766              "sha256": "6E470016E4324C84C255FFD0BEB3767D17EC89CC8561E9409EE3E1F6D29400F5",
767              "size": 30443537,
768              "url": "https://download.visualstudio.microsoft.com/download/pr/bc92e2cb/VisualStudio.vsman"
769            }
770          ]
771        },
772        {
773          "id": "Microsoft.VisualStudio.Product.BuildTools",
774          "type": "ChannelProduct",
775          "localizedResources": [
776            { "language": "en-US", "license": "https://go.microsoft.com/fwlink/?LinkId=2179911" }
777          ]
778        }
779      ]
780    }"#;
781
782    /// An installer manifest cut down to what is chosen and a little of what is not: an older
783    /// Visual C++ release, an older kit, a language pack, and a package with no payload this
784    /// wants.
785    const MANIFEST: &str = r#"{
786      "manifestVersion": "1.1",
787      "packages": [
788        { "id": "Microsoft.VC.14.29.16.11.CRT.Headers.base", "version": "14.29.30157", "type": "Vsix",
789          "payloads": [ { "fileName": "old.vsix", "sha256": "aa", "size": 1, "url": "https://example.invalid/old" } ] },
790        { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220", "type": "Vsix",
791          "payloads": [ { "fileName": "headers.vsix", "sha256": "B1", "size": 2128977, "url": "https://example.invalid/headers" } ] },
792        { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.Resources", "language": "de-DE", "version": "14.44.35220", "type": "Vsix",
793          "payloads": [ { "fileName": "de.vsix", "sha256": "cc", "size": 3, "url": "https://example.invalid/de" } ] },
794        { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
795          "payloads": [ { "fileName": "x64.vsix", "sha256": "b2", "size": 51521199, "url": "https://example.invalid/x64" } ] },
796        { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Store.base", "version": "14.44.35226", "type": "Vsix",
797          "payloads": [ { "fileName": "x64-store.vsix", "sha256": "b4", "size": 28032384, "url": "https://example.invalid/x64-store" } ] },
798        { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
799          "payloads": [ { "fileName": "arm64.vsix", "sha256": "b3", "size": 49166761, "url": "https://example.invalid/arm64" } ] },
800        { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Store.base", "version": "14.44.35226", "type": "Vsix",
801          "payloads": [ { "fileName": "arm64-store.vsix", "sha256": "b5", "size": 26214400, "url": "https://example.invalid/arm64-store" } ] },
802        { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base", "version": "14.44.35226", "type": "Vsix",
803          "payloads": [ { "fileName": "spectre.vsix", "sha256": "dd", "size": 4, "url": "https://example.invalid/spectre" } ] },
804        { "id": "Microsoft.VisualStudio.Component.Windows11SDK", "version": "17.14.35", "type": "Component",
805          "payloads": [ { "fileName": "nothing.vsix", "sha256": "ee", "size": 5, "url": "https://example.invalid/nothing" } ] },
806        { "id": "Win10SDK_10.0.19041", "version": "10.0.19041.4", "type": "Exe",
807          "payloads": [ { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "ff", "size": 6, "url": "https://example.invalid/old-sdk" } ] },
808        { "id": "Win11SDK_10.0.26100", "version": "10.0.26100.15", "type": "Exe",
809          "payloads": [
810            { "fileName": "Installers\\Universal CRT Headers Libraries and Sources-x86_en-us.msi", "sha256": "c1", "size": 589824, "url": "https://example.invalid/ucrt" },
811            { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "c2", "size": 790528, "url": "https://example.invalid/hx86" },
812            { "fileName": "Installers\\Windows SDK Desktop Headers x64-x86_en-us.msi", "sha256": "c3", "size": 450560, "url": "https://example.invalid/hx64" },
813            { "fileName": "Installers\\Windows SDK Desktop Headers arm64-x86_en-us.msi", "sha256": "c4", "size": 446464, "url": "https://example.invalid/harm64" },
814            { "fileName": "Installers\\Windows SDK Desktop Libs x86-x86_en-us.msi", "sha256": "c5", "size": 528384, "url": "https://example.invalid/lx86" },
815            { "fileName": "Installers\\Windows SDK Desktop Libs x64-x86_en-us.msi", "sha256": "c6", "size": 528384, "url": "https://example.invalid/lx64" },
816            { "fileName": "Installers\\Windows SDK Desktop Libs arm64-x86_en-us.msi", "sha256": "c7", "size": 528384, "url": "https://example.invalid/larm64" },
817            { "fileName": "Installers\\Windows SDK OnecoreUap Headers x86-x86_en-us.msi", "sha256": "c8", "size": 495616, "url": "https://example.invalid/onecore" },
818            { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers-x86_en-us.msi", "sha256": "c9", "size": 1060864, "url": "https://example.invalid/store-h" },
819            { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi", "sha256": "ce", "size": 471040, "url": "https://example.invalid/store-onecore" },
820            { "fileName": "Installers\\Windows SDK for Windows Store Apps Libs-x86_en-us.msi", "sha256": "ca", "size": 528384, "url": "https://example.invalid/store-l" },
821            { "fileName": "Installers\\Windows SDK Desktop Tools x64-x86_en-us.msi", "sha256": "cb", "size": 475136, "url": "https://example.invalid/tools" },
822            { "fileName": "Installers\\0f1a2b3c.cab", "sha256": "cc", "size": 9999, "url": "https://example.invalid/cab" },
823            { "fileName": "Installers\\7e6d5c4b.cab", "sha256": "cd", "size": 8888, "url": "https://example.invalid/other-cab" }
824          ] }
825      ]
826    }"#;
827
828    fn target(tuple: &str) -> TargetTuple {
829        tuple.parse().expect("a target this understands")
830    }
831
832    #[test]
833    fn a_channel_says_the_release_the_manifest_and_where_the_licence_is() {
834        let channel = Channel::parse(CHANNEL).expect("a channel");
835        assert_eq!(channel.release, "17.14.41 (September 2026)");
836        assert_eq!(channel.build, "17.14.37710.0");
837        assert_eq!(channel.manifest.name, "VisualStudio.vsman");
838        assert_eq!(channel.manifest.size, 30_443_537);
839        // Lower cased on the way in, because it is compared against a hash we computed.
840        assert!(channel.manifest.sha256.starts_with("6e470016"), "{}", channel.manifest.sha256);
841        assert_eq!(channel.licence, "https://go.microsoft.com/fwlink/?LinkId=2179911");
842    }
843
844    #[test]
845    fn a_channel_with_no_manifest_or_no_licence_in_it_says_which() {
846        let without = CHANNEL.replace("\"type\": \"Manifest\"", "\"type\": \"Bootstrapper\"");
847        assert_eq!(Channel::parse(&without).expect_err("no manifest"), MsvcError::NoManifest);
848
849        let without = CHANNEL.replace("Microsoft.VisualStudio.Product.BuildTools", "Other.Product");
850        assert_eq!(Channel::parse(&without).expect_err("no licence"), MsvcError::NoLicence);
851    }
852
853    #[test]
854    fn the_newest_visual_cpp_and_the_newest_kit_are_the_ones_chosen() {
855        let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
856        assert_eq!(chosen.crt, "14.44.35220");
857        assert_eq!(chosen.sdk, "10.0.26100.15");
858        let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
859        assert!(!packages.contains(&"Microsoft.VC.14.29.16.11.CRT.Headers.base"), "{packages:?}");
860        assert!(!packages.contains(&"Win10SDK_10.0.19041"), "{packages:?}");
861    }
862
863    #[test]
864    fn a_selection_is_the_headers_two_library_packages_per_chip_and_the_installers() {
865        let one = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
866        assert_eq!(one.files.len(), 1 + 2 + 8);
867
868        let two = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
869        // Two more library packages and two more installers, and the headers are still one copy.
870        assert_eq!(two.files.len(), one.files.len() + 4);
871        assert!(two.size() > one.size());
872
873        // The order is the same however the command line was written, and nothing appears twice.
874        let again =
875            Selection::parse(MANIFEST, &[Chip::Arm64, Chip::X64, Chip::X64]).expect("a selection");
876        assert_eq!(again, two);
877    }
878
879    #[test]
880    fn nothing_that_is_not_a_header_or_a_library_is_chosen() {
881        let chosen = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
882        let names: Vec<&str> = chosen.files.iter().map(|file| leaf(&file.payload.name)).collect();
883        for unwanted in ["spectre.vsix", "de.vsix", "nothing.vsix"] {
884            assert!(!names.contains(&unwanted), "{unwanted} is in {names:?}");
885        }
886        // The tools are not a compiler's business, and the cabs are not among the files because the
887        // installer is what says which of them it needs.
888        assert!(!names.iter().any(|name| name.contains("Tools")), "{names:?}");
889        assert!(!names.iter().any(|name| name.ends_with(".cab")), "{names:?}");
890    }
891
892    #[test]
893    fn the_store_package_is_taken_for_its_import_libraries_and_the_spectre_one_is_not() {
894        let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
895        let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
896        // `msvcrt.lib` and `oldnames.lib` are in this one and in no other, which is why a compiler
897        // that only took the desktop package could link nothing against the DLL CRT.
898        assert!(packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Store.base"), "{packages:?}");
899        assert!(
900            packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base"),
901            "{packages:?}"
902        );
903        assert!(
904            !packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base"),
905            "{packages:?}"
906        );
907    }
908
909    #[test]
910    fn a_cabinet_is_found_by_the_name_an_installer_gives_it() {
911        let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
912        assert_eq!(chosen.cabs.len(), 2);
913        // The name an MSI's own table carries, which has no directory on it and is the manifest's
914        // name with the Windows path taken off.
915        let cab = chosen.cab("0f1a2b3c.cab").expect("the cabinet");
916        assert_eq!(cab.payload.name, r"Installers\0f1a2b3c.cab");
917        assert_eq!(cab.payload.url, "https://example.invalid/cab");
918        assert_eq!(cab.package, "Win11SDK_10.0.26100");
919        // Case is not a difference between two Windows file names.
920        assert_eq!(chosen.cab("0F1A2B3C.CAB"), Some(cab));
921        assert_eq!(chosen.cab("nothing.cab"), None);
922    }
923
924    #[test]
925    fn a_missing_package_or_installer_says_which_one_by_name() {
926        let without = MANIFEST.replace("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "Other");
927        let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
928        assert_eq!(
929            why,
930            MsvcError::NoPackage("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base".into())
931        );
932
933        let without =
934            MANIFEST.replace("Windows SDK Desktop Libs x64", "Windows SDK Desktop Libs mips");
935        let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
936        assert_eq!(
937            why,
938            MsvcError::NoInstaller("Windows SDK Desktop Libs x64-x86_en-us.msi".into())
939        );
940
941        let empty = r#"{ "packages": [] }"#;
942        assert_eq!(
943            Selection::parse(empty, &[Chip::X64]).expect_err("a refusal"),
944            MsvcError::NothingFound("a Visual C++ CRT")
945        );
946    }
947
948    #[test]
949    fn the_windows_path_in_an_installer_name_survives_being_read() {
950        let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
951        let ucrt = chosen
952            .files
953            .iter()
954            .find(|file| leaf(&file.payload.name).starts_with("Universal CRT"))
955            .expect("the universal CRT");
956        assert_eq!(
957            ucrt.payload.name,
958            r"Installers\Universal CRT Headers Libraries and Sources-x86_en-us.msi"
959        );
960        assert_eq!(ucrt.version, "10.0.26100.15");
961    }
962
963    #[test]
964    fn a_target_maps_to_the_chip_microsoft_spells_two_ways() {
965        assert_eq!(Chip::of(target("x86_64-windows-msvc")), Some(Chip::X64));
966        assert_eq!(Chip::of(target("aarch64-windows-msvc")), Some(Chip::Arm64));
967        assert_eq!(Chip::of(target("i686-windows-msvc")), Some(Chip::X86));
968        // Tier 4 and nothing emits code for it, so there is nothing to fetch a library for.
969        assert_eq!(Chip::of(target("arm64ec-windows-msvc")), None);
970        assert_eq!(Chip::of(target("riscv64-linux-gnu")), None);
971
972        assert_eq!(Chip::Arm64.in_package(), "ARM64");
973        assert_eq!(Chip::Arm64.in_installer(), "arm64");
974    }
975
976    #[test]
977    fn a_dotted_version_is_compared_as_numbers_and_not_as_text() {
978        // The trap the driver's own Windows SDK search documents: the larger string is the older
979        // release.
980        assert_eq!(
981            newest(["10.0.9.0", "10.0.22000.0"].into_iter()).as_deref(),
982            Some("10.0.22000.0")
983        );
984        assert_eq!(vc_release("Microsoft.VC.14.44.17.14.CRT.Headers.base"), Some("14.44.17.14"));
985        assert_eq!(vc_release("Microsoft.VC.Runtimes.x64.base"), None);
986    }
987
988    #[test]
989    fn a_package_that_lists_its_files_before_it_says_what_it_is_is_refused() {
990        let backwards = r#"{ "packages": [
991          { "payloads": [ { "fileName": "a", "sha256": "b", "size": 1, "url": "c" } ],
992            "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220" } ] }"#;
993        let why = Selection::parse(backwards, &[Chip::X64]).expect_err("a refusal");
994        assert_eq!(
995            why,
996            MsvcError::OutOfOrder("Microsoft.VC.14.44.17.14.CRT.Headers.base".to_owned())
997        );
998    }
999}