1pub mod tree;
48
49use std::collections::BTreeMap;
50use std::path::{Path, PathBuf};
51
52use rucc_sysroot::msvc::{Channel, Chip, Selection, Wanted};
53use rucc_sysroot::{Input, Licence, Manifest, Provenance, Sysroot, sha256};
54use rucc_tuple::TargetTuple;
55use rucc_unpack::cab::File as CabFile;
56use rucc_unpack::{Cab, Cfb, Msi, Zip, under};
57
58use crate::fetch;
59use crate::{CliError, err};
60
61pub const CHANNEL: &str = "https://aka.ms/vs/17/release/channel";
67
68fn downloads(cache: &Path, build: &str) -> PathBuf {
74 cache.join("downloads").join("msvc").join(build)
75}
76
77fn stored_as(name: &str) -> &str {
86 name.rsplit(['\\', '/']).next().unwrap_or(name)
87}
88
89fn mb(bytes: u64) -> String {
98 format!("{:.1} MB", bytes as f64 / 1_000_000.0)
99}
100
101pub fn fetch_msvc_sdk(target: TargetTuple, accepted: bool, cache: &Path) -> i32 {
107 let tuple = target.to_canonical_string();
108 match run(target, &tuple, accepted, cache) {
109 Ok(code) => code,
110 Err(why) => crate::complain(why),
111 }
112}
113
114fn run(target: TargetTuple, tuple: &str, accepted: bool, cache: &Path) -> Result<i32, CliError> {
116 let say = |line: &str| println!("rucc: {tuple}: {line}");
117
118 if rucc_sysroot::Wall::of(target) != Some(rucc_sysroot::Wall::Microsoft) {
123 return Err(err(format!(
124 "--fetch-msvc-sdk gets what is behind Microsoft's licence wall, and {tuple} is not \
125 behind it, so there is nothing here to get for it. `rucc --fetch {tuple}` is the \
126 command that gets a sysroot this release pins"
127 )));
128 }
129 let Some(chip) = Chip::of(target) else {
130 return Err(err(format!(
131 "--fetch-msvc-sdk {tuple}: Microsoft publishes the SDK for x86, x86-64, arm and \
132 arm64, and {tuple} is none of those, so there is nothing in the manifest to get for it"
133 )));
134 };
135
136 let dir = cache.join("downloads").join("msvc");
137 let channel = dir.join("channel.json");
138 fetch::trusted(CHANNEL, &channel)?;
141 let text = read(&channel)?;
142 let channel = Channel::parse(&text)
143 .map_err(|why| err(format!("{CHANNEL} is not a channel manifest: {why}")))?;
144 say(&format!("Visual Studio {}, build {}", channel.release, channel.build));
145
146 let dir = downloads(cache, &channel.build);
147 let manifest = dir.join(stored_as(&channel.manifest.name));
148 let mut text = if manifest.exists() { read(&manifest).ok() } else { None };
153 if text.as_deref().and_then(|text| Selection::parse(text, &[chip]).ok()).is_none() {
154 fetch::trusted(&channel.manifest.url, &manifest)?;
155 text = Some(read(&manifest)?);
156 }
157 let text = text.unwrap_or_default();
158 let chosen = Selection::parse(&text, &[chip]).map_err(|why| {
159 err(format!("{} is not an installer manifest: {why}", channel.manifest.url))
160 })?;
161 say(&format!("MSVC CRT {} and Windows SDK {}", chosen.crt, chosen.sdk));
162
163 if !accepted {
164 refuse(&channel.licence, &chosen, tuple);
165 return Ok(1);
166 }
167
168 say(&format!("the licence at {} was accepted on the command line", channel.licence));
169 let mut had = 0;
170 for file in &chosen.files {
171 let at = dir.join(&file.payload.sha256[..12]).join(stored_as(&file.payload.name));
172 match fetch::fetch(&file.payload.url, &file.payload.sha256, &at)? {
173 fetch::Fetched::AlreadyThere => had += 1,
174 fetch::Fetched::Downloaded(by) => {
175 say(&format!(
176 "{} ({}) with {}",
177 stored_as(&file.payload.name),
178 mb(file.payload.size),
179 by.program()
180 ));
181 }
182 }
183 }
184 if had > 0 {
185 say(&format!("{had} of the {} files were already here", chosen.files.len()));
186 }
187 say(&format!(
188 "{} files totalling {} are at {}",
189 chosen.files.len(),
190 mb(chosen.size()),
191 dir.display()
192 ));
193
194 let tree = unpack(target, chip, &chosen, &dir, cache, &say)?;
195 say(&format!("compile for {tuple} with --sysroot={}", tree.display()));
196 Ok(0)
197}
198
199fn unpack(
207 target: TargetTuple,
208 chip: Chip,
209 chosen: &Selection,
210 from: &Path,
211 cache: &Path,
212 say: &dyn Fn(&str),
213) -> Result<PathBuf, CliError> {
214 let version = format!("{}-{}", chosen.crt, chosen.sdk);
215 let root = cache.join("msvc").join(version).join(target.to_canonical_string());
216 let record = Sysroot::at(root.clone(), target).manifest_path();
217 if std::fs::read_to_string(&record).is_ok_and(|text| Manifest::parse(&text).is_ok()) {
218 say(&format!("the tree at {} was laid out already", root.display()));
219 return Ok(root);
220 }
221 if root.exists() {
222 std::fs::remove_dir_all(&root).map_err(|why| err(format!("{}: {why}", root.display())))?;
223 }
224
225 let mut manifest = Manifest::new(target);
226 for file in &chosen.files {
227 let at = from.join(&file.payload.sha256[..12]).join(stored_as(&file.payload.name));
228 let bytes = slurp(&at)?;
229 if stored_as(&file.payload.name).to_ascii_lowercase().ends_with(".msi") {
230 from_msi(&bytes, chip, &root, file, chosen, from, &mut manifest)?;
231 } else {
232 from_vsix(&bytes, chip, &root, file, &mut manifest)?;
233 }
234 }
235
236 let written = manifest.inputs().len();
237 let alike = aliases(&root)?;
238 std::fs::create_dir_all(&root).map_err(|why| err(format!("{}: {why}", root.display())))?;
239 std::fs::write(&record, manifest.render())
240 .map_err(|why| err(format!("{}: {why}", record.display())))?;
241 say(&format!("{written} files and {alike} lowercase names are at {}", root.display()));
242 Ok(root)
243}
244
245fn from_vsix(
247 bytes: &[u8],
248 chip: Chip,
249 root: &Path,
250 file: &Wanted,
251 manifest: &mut Manifest,
252) -> Result<(), CliError> {
253 let name = stored_as(&file.payload.name);
254 let zip = Zip::read(bytes).map_err(|why| err(format!("{name}: {why}")))?;
255 for member in zip.members() {
256 if member.is_dir() {
257 continue;
258 }
259 let Some(at) = tree::crt(&member.name, chip) else {
260 continue;
261 };
262 let body = zip.contents(member).map_err(|why| err(format!("{name}: {why}")))?;
263 put(root, &at, &body, file, &file.payload.url, manifest)?;
264 }
265 Ok(())
266}
267
268fn from_msi(
277 bytes: &[u8],
278 chip: Chip,
279 root: &Path,
280 file: &Wanted,
281 chosen: &Selection,
282 from: &Path,
283 manifest: &mut Manifest,
284) -> Result<(), CliError> {
285 let name = stored_as(&file.payload.name);
286 let compound = Cfb::read(bytes).map_err(|why| err(format!("{name}: {why}")))?;
287 let installer = Msi::read(&compound).map_err(|why| err(format!("{name}: {why}")))?;
288 let describes = installer.payload().map_err(|why| err(format!("{name}: {why}")))?;
289
290 let mut wanted: BTreeMap<&str, Vec<(&str, String)>> = BTreeMap::new();
291 for payload in &describes {
292 let Some(at) = tree::sdk(&payload.directory, &payload.name, chip) else {
293 continue;
294 };
295 if payload.cabinet.starts_with('#') || payload.cabinet.is_empty() {
299 return Err(err(format!(
300 "{name} keeps {} in {}, which is inside the installer rather than in a cabinet \
301 beside it, and this does not read those yet",
302 payload.name,
303 if payload.cabinet.is_empty() { "the media" } else { &payload.cabinet }
304 )));
305 }
306 wanted.entry(&payload.cabinet).or_default().push((&payload.key, at));
307 }
308
309 for (cabinet, files) in wanted {
310 let Some(published) = chosen.cab(cabinet) else {
311 return Err(err(format!(
312 "{name} says its files are in {cabinet}, which is not a file this Windows SDK \
313 publishes, so there is nowhere to get them from"
314 )));
315 };
316 let at =
317 from.join(&published.payload.sha256[..12]).join(stored_as(&published.payload.name));
318 fetch::fetch(&published.payload.url, &published.payload.sha256, &at)?;
319 let bytes = slurp(&at)?;
320 let cab = Cab::read(&bytes).map_err(|why| err(format!("{}: {why}", at.display())))?;
321 spill(&cab, &files, root, file, &published.payload.url, manifest)
322 .map_err(|why| err(format!("{}: {why}", at.display())))?;
323 }
324 Ok(())
325}
326
327fn spill(
333 cab: &Cab<'_>,
334 files: &[(&str, String)],
335 root: &Path,
336 file: &Wanted,
337 url: &str,
338 manifest: &mut Manifest,
339) -> Result<(), CliError> {
340 let places: BTreeMap<&str, &str> = files.iter().map(|(key, at)| (*key, at.as_str())).collect();
341 let mut folders: BTreeMap<usize, Vec<&CabFile>> = BTreeMap::new();
342 for member in cab.files() {
343 if places.contains_key(member.name.as_str()) {
344 folders.entry(member.folder).or_default().push(member);
345 }
346 }
347 for members in folders.into_values() {
348 let folder = cab.folder(members[0]).map_err(|why| err(why.to_string()))?;
349 for member in members {
350 let at = usize::try_from(member.at).unwrap_or(usize::MAX);
351 let size = usize::try_from(member.size).unwrap_or(usize::MAX);
352 let body =
353 at.checked_add(size).and_then(|end| folder.get(at..end)).ok_or_else(|| {
354 err(format!("{} is not where this cabinet's folder says it is", member.name))
355 })?;
356 put(root, places[member.name.as_str()], body, file, url, manifest)?;
357 }
358 }
359 Ok(())
360}
361
362fn put(
369 root: &Path,
370 at: &str,
371 body: &[u8],
372 file: &Wanted,
373 url: &str,
374 manifest: &mut Manifest,
375) -> Result<(), CliError> {
376 let to = under(root, at).ok_or_else(|| {
377 err(format!("{at} is a name out of a Microsoft package that will not be written"))
378 })?;
379 if let Some(parent) = to.parent() {
380 std::fs::create_dir_all(parent)
381 .map_err(|why| err(format!("{}: {why}", parent.display())))?;
382 }
383 std::fs::write(&to, body).map_err(|why| err(format!("{}: {why}", to.display())))?;
384 manifest.push(Input {
385 path: at.to_owned(),
386 source: format!("{} {}", file.package, file.version),
387 url: url.to_owned(),
388 sha256: sha256::hex(body),
389 licence: Licence::MicrosoftSdk,
390 provenance: Provenance::Fetched,
391 });
392 Ok(())
393}
394
395#[cfg(unix)]
405fn aliases(root: &Path) -> Result<usize, CliError> {
406 let mut todo = vec![root.to_path_buf()];
407 let mut made = 0;
408 while let Some(dir) = todo.pop() {
409 let mut here = Vec::new();
410 let listing =
411 std::fs::read_dir(&dir).map_err(|why| err(format!("{}: {why}", dir.display())))?;
412 for entry in listing {
413 let entry = entry.map_err(|why| err(format!("{}: {why}", dir.display())))?;
414 let kind = entry.file_type().map_err(|why| err(format!("{}: {why}", dir.display())))?;
416 if kind.is_dir() {
417 todo.push(entry.path());
418 }
419 here.push(entry.file_name());
420 }
421 for name in here {
422 let Some(name) = name.to_str() else {
423 continue;
424 };
425 let Some(lower) = tree::lowercase(name) else {
426 continue;
427 };
428 let link = dir.join(&lower);
429 match std::os::unix::fs::symlink(name, &link) {
430 Ok(()) => made += 1,
431 Err(why) if why.kind() == std::io::ErrorKind::AlreadyExists => {}
432 Err(why) => return Err(err(format!("{}: {why}", link.display()))),
433 }
434 }
435 }
436 Ok(made)
437}
438
439#[cfg(not(unix))]
444fn aliases(_root: &Path) -> Result<usize, CliError> {
445 Ok(0)
446}
447
448fn slurp(at: &Path) -> Result<Vec<u8>, CliError> {
450 std::fs::read(at).map_err(|why| err(format!("{}: {why}", at.display())))
451}
452
453fn refuse(licence: &str, chosen: &Selection, tuple: &str) {
460 println!(
461 "The Windows SDK and the MSVC CRT are not ours to give you. Microsoft publishes them under\n\
462 the Visual Studio Build Tools licence, which is at\n\
463 \n {licence}\n\
464 \nand which you have to read and accept yourself. This compiler will not accept it for you\n\
465 and will not download anything until you have said that you did.\n"
466 );
467 println!(
468 "What would be downloaded for {tuple}, {} totalling {}:",
469 files(chosen.files.len()),
470 mb(chosen.size())
471 );
472 for file in &chosen.files {
473 println!(" {:>9} {}", mb(file.payload.size), stored_as(&file.payload.name));
474 }
475 println!(
476 "\nThe Windows SDK installers in that list hold no bytes of their own. Each one is a small\n\
477 database naming the cabinets its headers and libraries are in, and those cabinets are\n\
478 separate files that this total does not count, because which of them a target needs is a\n\
479 question only the installers can answer."
480 );
481 println!(
482 "\nIf you accept that licence, run this again with --accept-licence on the command line.\n\
483 If you would rather not, build for the mingw-w64 environment instead, which is fully\n\
484 redistributable and needs nothing installed."
485 );
486}
487
488fn files(count: usize) -> String {
490 if count == 1 { "1 file".to_owned() } else { format!("{count} files") }
491}
492
493fn read(at: &Path) -> Result<String, CliError> {
495 std::fs::read_to_string(at).map_err(|why| err(format!("{}: {why}", at.display())))
496}
497
498#[cfg(test)]
499mod tests {
500 use super::{aliases, downloads, files, mb, put, stored_as};
501 use rucc_sysroot::{Manifest, Provenance};
502 use std::path::{Path, PathBuf};
503
504 fn scratch(name: &str) -> PathBuf {
506 let at = std::env::temp_dir().join(format!("rucc-msvc-{name}-{}", std::process::id()));
507 let _ = std::fs::remove_dir_all(&at);
508 std::fs::create_dir_all(&at).expect("a directory to work in");
509 at
510 }
511
512 fn wanted(package: &str) -> rucc_sysroot::msvc::Wanted {
514 rucc_sysroot::msvc::Wanted {
515 package: package.to_owned(),
516 version: "10.0.26100.15".to_owned(),
517 payload: rucc_sysroot::msvc::Payload {
518 name: format!(r"Installers\{package}.msi"),
519 url: "https://example.invalid/thing".to_owned(),
520 sha256: "ab".repeat(32),
521 size: 4,
522 },
523 }
524 }
525
526 #[test]
527 fn a_file_is_written_where_the_tree_says_and_recorded_as_microsofts() {
528 let root = scratch("put");
529 let mut manifest = Manifest::new("x86_64-windows-msvc".parse().expect("a tuple"));
530 let from = wanted("Win11SDK_10.0.26100");
531 put(
532 &root,
533 "sdk/include/um/windows.h",
534 b"#pragma once\n",
535 &from,
536 "https://ms/cab",
537 &mut manifest,
538 )
539 .expect("a file written");
540 assert_eq!(
541 std::fs::read(root.join("sdk/include/um/windows.h")).expect("what was written"),
542 b"#pragma once\n"
543 );
544 let input = &manifest.inputs()[0];
545 assert_eq!(input.path, "sdk/include/um/windows.h");
546 assert_eq!(input.source, "Win11SDK_10.0.26100 10.0.26100.15");
547 assert_eq!(input.url, "https://ms/cab");
550 assert_eq!(input.sha256, rucc_sysroot::sha256::hex(b"#pragma once\n"));
551 assert!(!input.licence.redistributable());
553 assert_eq!(input.provenance, Provenance::Fetched);
554 let _ = std::fs::remove_dir_all(&root);
555 }
556
557 #[test]
558 fn a_name_out_of_a_package_that_would_leave_the_tree_is_refused() {
559 let root = scratch("escape");
560 let mut manifest = Manifest::new("x86_64-windows-msvc".parse().expect("a tuple"));
561 let from = wanted("Win11SDK_10.0.26100");
562 let escape = put(&root, "../../etc/passwd", b"no", &from, "https://ms/cab", &mut manifest);
565 assert!(escape.is_err(), "a name that climbs out of the tree is not written");
566 assert!(manifest.inputs().is_empty());
567 let _ = std::fs::remove_dir_all(&root);
568 }
569
570 #[cfg(unix)]
571 #[test]
572 fn every_name_with_a_capital_in_it_gets_a_lowercase_one_beside_it() {
573 let root = scratch("aliases");
574 std::fs::create_dir_all(root.join("sdk/include/um")).expect("a directory");
575 std::fs::create_dir_all(root.join("crt/include/CodeAnalysis")).expect("a directory");
576 std::fs::write(root.join("sdk/include/um/Windows.h"), b"h").expect("a header");
577 std::fs::write(root.join("sdk/include/um/winbase.h"), b"h").expect("a header");
578 std::fs::write(root.join("crt/include/CodeAnalysis/warnings.h"), b"h").expect("a header");
579
580 let made = aliases(&root).expect("the links");
581 assert!(made == 2 || made == 0, "{made} links for two names with a capital in them");
585 assert_eq!(std::fs::read(root.join("sdk/include/um/windows.h")).expect("the link"), b"h");
586 assert_eq!(
587 std::fs::read(root.join("crt/include/codeanalysis/warnings.h")).expect("the link"),
588 b"h"
589 );
590 assert_eq!(aliases(&root).expect("the links again"), 0);
593 let _ = std::fs::remove_dir_all(&root);
594 }
595
596 #[test]
597 fn a_payload_keeps_its_name_and_loses_the_directory_the_manifest_put_it_in() {
598 assert_eq!(
601 stored_as(r"Installers\Windows SDK Desktop Headers x64-x86_en-us.msi"),
602 "Windows SDK Desktop Headers x64-x86_en-us.msi"
603 );
604 assert_eq!(
606 stored_as("Microsoft.VC.14.44.17.14.CRT.Headers.base.vsix"),
607 "Microsoft.VC.14.44.17.14.CRT.Headers.base.vsix"
608 );
609 assert_eq!(stored_as("a/b/c.msi"), "c.msi");
611 }
612
613 #[test]
614 fn the_download_directory_is_under_the_build() {
615 assert_eq!(
618 downloads(Path::new("/cache"), "17.14.37710.0"),
619 Path::new("/cache/downloads/msvc/17.14.37710.0")
620 );
621 }
622
623 #[test]
624 fn sizes_are_readable_and_counts_agree_with_themselves() {
625 assert_eq!(mb(2_128_977), "2.1 MB");
626 assert_eq!(mb(197_673_853), "197.7 MB");
627 assert_eq!(files(1), "1 file");
628 assert_eq!(files(14), "14 files");
629 }
630}