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