pub mod tree;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use rucc_sysroot::msvc::{Channel, Chip, Selection, Wanted};
use rucc_sysroot::{Input, Licence, Manifest, Provenance, Sysroot, sha256};
use rucc_tuple::TargetTuple;
use rucc_unpack::cab::File as CabFile;
use rucc_unpack::{Cab, Cfb, Msi, Zip, under};
use crate::fetch;
use crate::{CliError, err};
pub const CHANNEL: &str = "https://aka.ms/vs/17/release/channel";
fn downloads(cache: &Path, build: &str) -> PathBuf {
cache.join("downloads").join("msvc").join(build)
}
fn stored_as(name: &str) -> &str {
name.rsplit(['\\', '/']).next().unwrap_or(name)
}
fn mb(bytes: u64) -> String {
format!("{:.1} MB", bytes as f64 / 1_000_000.0)
}
pub fn fetch_msvc_sdk(target: TargetTuple, accepted: bool, cache: &Path) -> i32 {
let tuple = target.to_canonical_string();
match run(target, &tuple, accepted, cache) {
Ok(code) => code,
Err(why) => crate::complain(why),
}
}
fn run(target: TargetTuple, tuple: &str, accepted: bool, cache: &Path) -> Result<i32, CliError> {
let say = |line: &str| println!("rucc: {tuple}: {line}");
if rucc_sysroot::Wall::of(target) != Some(rucc_sysroot::Wall::Microsoft) {
return Err(err(format!(
"--fetch-msvc-sdk gets what is behind Microsoft's licence wall, and {tuple} is not \
behind it, so there is nothing here to get for it. `rucc --fetch {tuple}` is the \
command that gets a sysroot this release pins"
)));
}
let Some(chip) = Chip::of(target) else {
return Err(err(format!(
"--fetch-msvc-sdk {tuple}: Microsoft publishes the SDK for x86, x86-64, arm and \
arm64, and {tuple} is none of those, so there is nothing in the manifest to get for it"
)));
};
let dir = cache.join("downloads").join("msvc");
let channel = dir.join("channel.json");
fetch::trusted(CHANNEL, &channel)?;
let text = read(&channel)?;
let channel = Channel::parse(&text)
.map_err(|why| err(format!("{CHANNEL} is not a channel manifest: {why}")))?;
say(&format!("Visual Studio {}, build {}", channel.release, channel.build));
let dir = downloads(cache, &channel.build);
let manifest = dir.join(stored_as(&channel.manifest.name));
let mut text = if manifest.exists() { read(&manifest).ok() } else { None };
if text.as_deref().and_then(|text| Selection::parse(text, &[chip]).ok()).is_none() {
fetch::trusted(&channel.manifest.url, &manifest)?;
text = Some(read(&manifest)?);
}
let text = text.unwrap_or_default();
let chosen = Selection::parse(&text, &[chip]).map_err(|why| {
err(format!("{} is not an installer manifest: {why}", channel.manifest.url))
})?;
say(&format!("MSVC CRT {} and Windows SDK {}", chosen.crt, chosen.sdk));
if !accepted {
refuse(&channel.licence, &chosen, tuple);
return Ok(1);
}
say(&format!("the licence at {} was accepted on the command line", channel.licence));
let mut had = 0;
for file in &chosen.files {
let at = dir.join(&file.payload.sha256[..12]).join(stored_as(&file.payload.name));
match fetch::fetch(&file.payload.url, &file.payload.sha256, &at)? {
fetch::Fetched::AlreadyThere => had += 1,
fetch::Fetched::Downloaded(by) => {
say(&format!(
"{} ({}) with {}",
stored_as(&file.payload.name),
mb(file.payload.size),
by.program()
));
}
}
}
if had > 0 {
say(&format!("{had} of the {} files were already here", chosen.files.len()));
}
say(&format!(
"{} files totalling {} are at {}",
chosen.files.len(),
mb(chosen.size()),
dir.display()
));
let tree = unpack(target, chip, &chosen, &dir, cache, &say)?;
say(&format!("compile for {tuple} with --sysroot={}", tree.display()));
Ok(0)
}
fn unpack(
target: TargetTuple,
chip: Chip,
chosen: &Selection,
from: &Path,
cache: &Path,
say: &dyn Fn(&str),
) -> Result<PathBuf, CliError> {
let version = format!("{}-{}", chosen.crt, chosen.sdk);
let root = cache.join("msvc").join(version).join(target.to_canonical_string());
let record = Sysroot::at(root.clone(), target).manifest_path();
if std::fs::read_to_string(&record).is_ok_and(|text| Manifest::parse(&text).is_ok()) {
say(&format!("the tree at {} was laid out already", root.display()));
return Ok(root);
}
if root.exists() {
std::fs::remove_dir_all(&root).map_err(|why| err(format!("{}: {why}", root.display())))?;
}
let mut manifest = Manifest::new(target);
for file in &chosen.files {
let at = from.join(&file.payload.sha256[..12]).join(stored_as(&file.payload.name));
let bytes = slurp(&at)?;
if stored_as(&file.payload.name).to_ascii_lowercase().ends_with(".msi") {
from_msi(&bytes, chip, &root, file, chosen, from, &mut manifest)?;
} else {
from_vsix(&bytes, chip, &root, file, &mut manifest)?;
}
}
let written = manifest.inputs().len();
let alike = aliases(&root)?;
std::fs::create_dir_all(&root).map_err(|why| err(format!("{}: {why}", root.display())))?;
std::fs::write(&record, manifest.render())
.map_err(|why| err(format!("{}: {why}", record.display())))?;
say(&format!("{written} files and {alike} lowercase names are at {}", root.display()));
Ok(root)
}
fn from_vsix(
bytes: &[u8],
chip: Chip,
root: &Path,
file: &Wanted,
manifest: &mut Manifest,
) -> Result<(), CliError> {
let name = stored_as(&file.payload.name);
let zip = Zip::read(bytes).map_err(|why| err(format!("{name}: {why}")))?;
for member in zip.members() {
if member.is_dir() {
continue;
}
let Some(at) = tree::crt(&member.name, chip) else {
continue;
};
let body = zip.contents(member).map_err(|why| err(format!("{name}: {why}")))?;
put(root, &at, &body, file, &file.payload.url, manifest)?;
}
Ok(())
}
fn from_msi(
bytes: &[u8],
chip: Chip,
root: &Path,
file: &Wanted,
chosen: &Selection,
from: &Path,
manifest: &mut Manifest,
) -> Result<(), CliError> {
let name = stored_as(&file.payload.name);
let compound = Cfb::read(bytes).map_err(|why| err(format!("{name}: {why}")))?;
let installer = Msi::read(&compound).map_err(|why| err(format!("{name}: {why}")))?;
let describes = installer.payload().map_err(|why| err(format!("{name}: {why}")))?;
let mut wanted: BTreeMap<&str, Vec<(&str, String)>> = BTreeMap::new();
for payload in &describes {
let Some(at) = tree::sdk(&payload.directory, &payload.name, chip) else {
continue;
};
if payload.cabinet.starts_with('#') || payload.cabinet.is_empty() {
return Err(err(format!(
"{name} keeps {} in {}, which is inside the installer rather than in a cabinet \
beside it, and this does not read those yet",
payload.name,
if payload.cabinet.is_empty() { "the media" } else { &payload.cabinet }
)));
}
wanted.entry(&payload.cabinet).or_default().push((&payload.key, at));
}
for (cabinet, files) in wanted {
let Some(published) = chosen.cab(cabinet) else {
return Err(err(format!(
"{name} says its files are in {cabinet}, which is not a file this Windows SDK \
publishes, so there is nowhere to get them from"
)));
};
let at =
from.join(&published.payload.sha256[..12]).join(stored_as(&published.payload.name));
fetch::fetch(&published.payload.url, &published.payload.sha256, &at)?;
let bytes = slurp(&at)?;
let cab = Cab::read(&bytes).map_err(|why| err(format!("{}: {why}", at.display())))?;
spill(&cab, &files, root, file, &published.payload.url, manifest)
.map_err(|why| err(format!("{}: {why}", at.display())))?;
}
Ok(())
}
fn spill(
cab: &Cab<'_>,
files: &[(&str, String)],
root: &Path,
file: &Wanted,
url: &str,
manifest: &mut Manifest,
) -> Result<(), CliError> {
let places: BTreeMap<&str, &str> = files.iter().map(|(key, at)| (*key, at.as_str())).collect();
let mut folders: BTreeMap<usize, Vec<&CabFile>> = BTreeMap::new();
for member in cab.files() {
if places.contains_key(member.name.as_str()) {
folders.entry(member.folder).or_default().push(member);
}
}
for members in folders.into_values() {
let folder = cab.folder(members[0]).map_err(|why| err(why.to_string()))?;
for member in members {
let at = usize::try_from(member.at).unwrap_or(usize::MAX);
let size = usize::try_from(member.size).unwrap_or(usize::MAX);
let body =
at.checked_add(size).and_then(|end| folder.get(at..end)).ok_or_else(|| {
err(format!("{} is not where this cabinet's folder says it is", member.name))
})?;
put(root, places[member.name.as_str()], body, file, url, manifest)?;
}
}
Ok(())
}
fn put(
root: &Path,
at: &str,
body: &[u8],
file: &Wanted,
url: &str,
manifest: &mut Manifest,
) -> Result<(), CliError> {
let to = under(root, at).ok_or_else(|| {
err(format!("{at} is a name out of a Microsoft package that will not be written"))
})?;
if let Some(parent) = to.parent() {
std::fs::create_dir_all(parent)
.map_err(|why| err(format!("{}: {why}", parent.display())))?;
}
std::fs::write(&to, body).map_err(|why| err(format!("{}: {why}", to.display())))?;
manifest.push(Input {
path: at.to_owned(),
source: format!("{} {}", file.package, file.version),
url: url.to_owned(),
sha256: sha256::hex(body),
licence: Licence::MicrosoftSdk,
provenance: Provenance::Fetched,
});
Ok(())
}
#[cfg(unix)]
fn aliases(root: &Path) -> Result<usize, CliError> {
let mut todo = vec![root.to_path_buf()];
let mut made = 0;
while let Some(dir) = todo.pop() {
let mut here = Vec::new();
let listing =
std::fs::read_dir(&dir).map_err(|why| err(format!("{}: {why}", dir.display())))?;
for entry in listing {
let entry = entry.map_err(|why| err(format!("{}: {why}", dir.display())))?;
let kind = entry.file_type().map_err(|why| err(format!("{}: {why}", dir.display())))?;
if kind.is_dir() {
todo.push(entry.path());
}
here.push(entry.file_name());
}
for name in here {
let Some(name) = name.to_str() else {
continue;
};
let Some(lower) = tree::lowercase(name) else {
continue;
};
let link = dir.join(&lower);
match std::os::unix::fs::symlink(name, &link) {
Ok(()) => made += 1,
Err(why) if why.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(why) => return Err(err(format!("{}: {why}", link.display()))),
}
}
}
Ok(made)
}
#[cfg(not(unix))]
fn aliases(_root: &Path) -> Result<usize, CliError> {
Ok(0)
}
fn slurp(at: &Path) -> Result<Vec<u8>, CliError> {
std::fs::read(at).map_err(|why| err(format!("{}: {why}", at.display())))
}
fn refuse(licence: &str, chosen: &Selection, tuple: &str) {
println!(
"The Windows SDK and the MSVC CRT are not ours to give you. Microsoft publishes them under\n\
the Visual Studio Build Tools licence, which is at\n\
\n {licence}\n\
\nand which you have to read and accept yourself. This compiler will not accept it for you\n\
and will not download anything until you have said that you did.\n"
);
println!(
"What would be downloaded for {tuple}, {} totalling {}:",
files(chosen.files.len()),
mb(chosen.size())
);
for file in &chosen.files {
println!(" {:>9} {}", mb(file.payload.size), stored_as(&file.payload.name));
}
println!(
"\nThe Windows SDK installers in that list hold no bytes of their own. Each one is a small\n\
database naming the cabinets its headers and libraries are in, and those cabinets are\n\
separate files that this total does not count, because which of them a target needs is a\n\
question only the installers can answer."
);
println!(
"\nIf you accept that licence, run this again with --accept-licence on the command line.\n\
If you would rather not, build for the mingw-w64 environment instead, which is fully\n\
redistributable and needs nothing installed."
);
}
fn files(count: usize) -> String {
if count == 1 { "1 file".to_owned() } else { format!("{count} files") }
}
fn read(at: &Path) -> Result<String, CliError> {
std::fs::read_to_string(at).map_err(|why| err(format!("{}: {why}", at.display())))
}
#[cfg(test)]
mod tests {
use super::{aliases, downloads, files, mb, put, stored_as};
use rucc_sysroot::{Manifest, Provenance};
use std::path::{Path, PathBuf};
fn scratch(name: &str) -> PathBuf {
let at = std::env::temp_dir().join(format!("rucc-msvc-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("a directory to work in");
at
}
fn wanted(package: &str) -> rucc_sysroot::msvc::Wanted {
rucc_sysroot::msvc::Wanted {
package: package.to_owned(),
version: "10.0.26100.15".to_owned(),
payload: rucc_sysroot::msvc::Payload {
name: format!(r"Installers\{package}.msi"),
url: "https://example.invalid/thing".to_owned(),
sha256: "ab".repeat(32),
size: 4,
},
}
}
#[test]
fn a_file_is_written_where_the_tree_says_and_recorded_as_microsofts() {
let root = scratch("put");
let mut manifest = Manifest::new("x86_64-windows-msvc".parse().expect("a tuple"));
let from = wanted("Win11SDK_10.0.26100");
put(
&root,
"sdk/include/um/windows.h",
b"#pragma once\n",
&from,
"https://ms/cab",
&mut manifest,
)
.expect("a file written");
assert_eq!(
std::fs::read(root.join("sdk/include/um/windows.h")).expect("what was written"),
b"#pragma once\n"
);
let input = &manifest.inputs()[0];
assert_eq!(input.path, "sdk/include/um/windows.h");
assert_eq!(input.source, "Win11SDK_10.0.26100 10.0.26100.15");
assert_eq!(input.url, "https://ms/cab");
assert_eq!(input.sha256, rucc_sysroot::sha256::hex(b"#pragma once\n"));
assert!(!input.licence.redistributable());
assert_eq!(input.provenance, Provenance::Fetched);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_name_out_of_a_package_that_would_leave_the_tree_is_refused() {
let root = scratch("escape");
let mut manifest = Manifest::new("x86_64-windows-msvc".parse().expect("a tuple"));
let from = wanted("Win11SDK_10.0.26100");
let escape = put(&root, "../../etc/passwd", b"no", &from, "https://ms/cab", &mut manifest);
assert!(escape.is_err(), "a name that climbs out of the tree is not written");
assert!(manifest.inputs().is_empty());
let _ = std::fs::remove_dir_all(&root);
}
#[cfg(unix)]
#[test]
fn every_name_with_a_capital_in_it_gets_a_lowercase_one_beside_it() {
let root = scratch("aliases");
std::fs::create_dir_all(root.join("sdk/include/um")).expect("a directory");
std::fs::create_dir_all(root.join("crt/include/CodeAnalysis")).expect("a directory");
std::fs::write(root.join("sdk/include/um/Windows.h"), b"h").expect("a header");
std::fs::write(root.join("sdk/include/um/winbase.h"), b"h").expect("a header");
std::fs::write(root.join("crt/include/CodeAnalysis/warnings.h"), b"h").expect("a header");
let made = aliases(&root).expect("the links");
assert!(made == 2 || made == 0, "{made} links for two names with a capital in them");
assert_eq!(std::fs::read(root.join("sdk/include/um/windows.h")).expect("the link"), b"h");
assert_eq!(
std::fs::read(root.join("crt/include/codeanalysis/warnings.h")).expect("the link"),
b"h"
);
assert_eq!(aliases(&root).expect("the links again"), 0);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_payload_keeps_its_name_and_loses_the_directory_the_manifest_put_it_in() {
assert_eq!(
stored_as(r"Installers\Windows SDK Desktop Headers x64-x86_en-us.msi"),
"Windows SDK Desktop Headers x64-x86_en-us.msi"
);
assert_eq!(
stored_as("Microsoft.VC.14.44.17.14.CRT.Headers.base.vsix"),
"Microsoft.VC.14.44.17.14.CRT.Headers.base.vsix"
);
assert_eq!(stored_as("a/b/c.msi"), "c.msi");
}
#[test]
fn the_download_directory_is_under_the_build() {
assert_eq!(
downloads(Path::new("/cache"), "17.14.37710.0"),
Path::new("/cache/downloads/msvc/17.14.37710.0")
);
}
#[test]
fn sizes_are_readable_and_counts_agree_with_themselves() {
assert_eq!(mb(2_128_977), "2.1 MB");
assert_eq!(mb(197_673_853), "197.7 MB");
assert_eq!(files(1), "1 file");
assert_eq!(files(14), "14 files");
}
}