xwin/
splat.rs

1use crate::{Arch, Ctx, Error, Path, PathBuf, PayloadKind, SectionKind, Variant, symlink};
2use anyhow::Context as _;
3use rayon::prelude::*;
4use std::collections::BTreeMap;
5
6#[derive(Clone)]
7pub struct SplatConfig {
8    pub include_debug_libs: bool,
9    pub include_debug_symbols: bool,
10    pub enable_symlinks: bool,
11    pub preserve_ms_arch_notation: bool,
12    pub use_winsysroot_style: bool,
13    pub output: PathBuf,
14    pub map: Option<PathBuf>,
15    pub copy: bool,
16    //pub isolated: bool,
17}
18
19/// There is a massive amount of duplication between SDK headers for the Desktop
20/// and Store variants, so we keep track of them so we only splat one unique file
21pub(crate) struct SdkHeaders {
22    pub(crate) inner: BTreeMap<u64, PathBuf>,
23    pub(crate) root: PathBuf,
24}
25
26impl SdkHeaders {
27    fn new(root: PathBuf) -> Self {
28        Self {
29            inner: BTreeMap::new(),
30            root,
31        }
32    }
33
34    #[inline]
35    fn get_relative_path<'path>(&self, path: &'path Path) -> anyhow::Result<&'path Path> {
36        let mut rel = path.strip_prefix(&self.root)?;
37
38        // Skip the first directory, which directly follows the "include", as it
39        // is the one that includes are actually relative to
40        if let Some(first) = rel.iter().next() {
41            rel = rel.strip_prefix(first)?;
42        }
43
44        Ok(rel)
45    }
46}
47
48pub(crate) struct SplatRoots {
49    pub root: PathBuf,
50    pub crt: PathBuf,
51    pub sdk: PathBuf,
52    pub vcrd: PathBuf,
53    src: PathBuf,
54}
55
56pub(crate) fn prep_splat(
57    ctx: std::sync::Arc<Ctx>,
58    root: &Path,
59    winroot: Option<&str>,
60) -> Result<SplatRoots, Error> {
61    // Ensure we create the path first, you can't canonicalize a non-existant path
62    if !root.exists() {
63        std::fs::create_dir_all(root)
64            .with_context(|| format!("unable to create splat directory {root}"))?;
65    }
66
67    let root = crate::util::canonicalize(root)?;
68
69    let (crt_root, sdk_root, vcrd_root) = if let Some(crt_version) = winroot {
70        let mut crt = root.join("VC/Tools/MSVC");
71        crt.push(crt_version);
72
73        let mut sdk = root.join("Windows Kits");
74        sdk.push("10");
75
76        let vcrd = root.join("VCR");
77
78        (crt, sdk, vcrd)
79    } else {
80        (root.join("crt"), root.join("sdk"), root.join("vcrd"))
81    };
82
83    if crt_root.exists() {
84        std::fs::remove_dir_all(&crt_root)
85            .with_context(|| format!("unable to delete existing CRT directory {crt_root}"))?;
86    }
87
88    if sdk_root.exists() {
89        std::fs::remove_dir_all(&sdk_root)
90            .with_context(|| format!("unable to delete existing SDK directory {sdk_root}"))?;
91    }
92
93    if vcrd_root.exists() {
94        std::fs::remove_dir_all(&vcrd_root)
95            .with_context(|| format!("unable to delete existing VCR directory {vcrd_root}"))?;
96    }
97
98    std::fs::create_dir_all(&crt_root)
99        .with_context(|| format!("unable to create CRT directory {crt_root}"))?;
100    std::fs::create_dir_all(&sdk_root)
101        .with_context(|| format!("unable to create SDK directory {sdk_root}"))?;
102
103    let src_root = ctx.work_dir.join("unpack");
104
105    Ok(SplatRoots {
106        root,
107        crt: crt_root,
108        sdk: sdk_root,
109        vcrd: vcrd_root,
110        src: src_root,
111    })
112}
113
114#[allow(clippy::too_many_arguments)]
115pub(crate) fn splat(
116    config: &SplatConfig,
117    roots: &SplatRoots,
118    item: &crate::WorkItem,
119    tree: &crate::unpack::FileTree,
120    map: Option<&crate::Map>,
121    sdk_version: &str,
122    vcrd_version: Option<String>,
123    arches: u32,
124    variants: u32,
125) -> Result<Option<SdkHeaders>, Error> {
126    struct Mapping<'ft> {
127        src: PathBuf,
128        target: PathBuf,
129        tree: &'ft crate::unpack::FileTree,
130        kind: PayloadKind,
131        variant: Option<Variant>,
132        section: SectionKind,
133    }
134
135    let mut src = roots.src.join(&item.payload.filename);
136
137    // If we're moving files from the unpack directory, invalidate it immediately
138    // so it is recreated in a future run if anything goes wrong
139    if !config.copy {
140        src.push(".unpack");
141        if let Err(e) = std::fs::remove_file(&src) {
142            tracing::warn!("Failed to remove {src}: {e}");
143        }
144        src.pop();
145    }
146
147    let get_tree = |src_path: &Path| -> Result<&crate::unpack::FileTree, Error> {
148        let src_path = src_path
149            .strip_prefix(&roots.src)
150            .context("incorrect src root")?;
151        let src_path = src_path
152            .strip_prefix(&item.payload.filename)
153            .context("incorrect src subdir")?;
154
155        tree.subtree(src_path)
156            .with_context(|| format!("missing expected subtree '{src_path}'"))
157    };
158
159    let push_arch = |src: &mut PathBuf, target: &mut PathBuf, arch: Arch| {
160        src.push(arch.as_ms_str());
161        target.push(if config.preserve_ms_arch_notation {
162            arch.as_ms_str()
163        } else {
164            arch.as_str()
165        });
166    };
167
168    let variant = item.payload.variant;
169    let kind = item.payload.kind;
170
171    let mappings = match kind {
172        PayloadKind::CrtHeaders | PayloadKind::AtlHeaders => {
173            src.push("include");
174            let tree = get_tree(&src)?;
175
176            vec![Mapping {
177                src,
178                target: roots.crt.join("include"),
179                tree,
180                kind,
181                variant,
182                section: SectionKind::CrtHeader,
183            }]
184        }
185        PayloadKind::AtlLibs => {
186            src.push("lib");
187            let mut target = roots.crt.join("lib");
188
189            let spectre = (variants & Variant::Spectre as u32) != 0;
190            if spectre {
191                src.push("spectre");
192                target.push("spectre");
193            }
194
195            push_arch(
196                &mut src,
197                &mut target,
198                item.payload
199                    .target_arch
200                    .context("ATL libs didn't specify an architecture")?,
201            );
202
203            let tree = get_tree(&src)?;
204
205            vec![Mapping {
206                src,
207                target,
208                tree,
209                kind,
210                variant,
211                section: SectionKind::CrtLib,
212            }]
213        }
214
215        PayloadKind::CrtLibs => {
216            src.push("lib");
217            let mut target = roots.crt.join("lib");
218
219            let spectre = (variants & Variant::Spectre as u32) != 0;
220
221            match item
222                .payload
223                .variant
224                .context("CRT libs didn't specify a variant")?
225            {
226                Variant::Desktop => {
227                    if spectre {
228                        src.push("spectre");
229                        target.push("spectre");
230                    }
231                }
232                Variant::OneCore => {
233                    if spectre {
234                        src.push("spectre");
235                        target.push("spectre");
236                    }
237
238                    src.push("onecore");
239                    target.push("onecore");
240                }
241                Variant::Store => {}
242                Variant::Spectre => unreachable!(),
243            }
244
245            push_arch(
246                &mut src,
247                &mut target,
248                item.payload
249                    .target_arch
250                    .context("CRT libs didn't specify an architecture")?,
251            );
252
253            let tree = get_tree(&src)?;
254
255            vec![Mapping {
256                src,
257                target,
258                tree,
259                kind,
260                variant,
261                section: SectionKind::CrtLib,
262            }]
263        }
264        PayloadKind::SdkHeaders => {
265            src.push("include");
266            let tree = get_tree(&src)?;
267
268            let target = if map.is_some() {
269                let mut inc = roots.sdk.clone();
270                inc.push("Include");
271                inc.push(sdk_version);
272                inc
273            } else {
274                let mut target = roots.sdk.join("include");
275
276                if config.use_winsysroot_style {
277                    target.push(sdk_version);
278                }
279
280                target
281            };
282
283            vec![Mapping {
284                src,
285                target,
286                tree,
287                kind,
288                variant,
289                section: SectionKind::SdkHeader,
290            }]
291        }
292        PayloadKind::SdkLibs => {
293            src.push("lib/um");
294
295            let mut target = roots.sdk.join("lib");
296
297            if config.use_winsysroot_style {
298                target.push(sdk_version);
299            }
300
301            target.push("um");
302
303            push_arch(
304                &mut src,
305                &mut target,
306                item.payload
307                    .target_arch
308                    .context("SDK libs didn't specify an architecture")?,
309            );
310
311            let tree = get_tree(&src)?;
312
313            vec![Mapping {
314                src,
315                target,
316                tree,
317                kind,
318                variant,
319                section: SectionKind::SdkLib,
320            }]
321        }
322        PayloadKind::SdkStoreLibs => {
323            src.push("lib/um");
324
325            let mut target = roots.sdk.join("lib");
326
327            if config.use_winsysroot_style {
328                target.push(sdk_version);
329            }
330
331            target.push("um");
332
333            Arch::iter(arches)
334                .map(|arch| -> Result<Mapping<'_>, Error> {
335                    let mut src = src.clone();
336                    let mut target = target.clone();
337
338                    push_arch(&mut src, &mut target, arch);
339
340                    let tree = get_tree(&src)?;
341
342                    Ok(Mapping {
343                        src,
344                        target,
345                        tree,
346                        kind,
347                        variant,
348                        section: SectionKind::SdkLib,
349                    })
350                })
351                .collect::<Result<Vec<_>, _>>()?
352        }
353        PayloadKind::Ucrt => {
354            let inc_src = src.join("include/ucrt");
355            let tree = get_tree(&inc_src)?;
356
357            let mut target = if map.is_some() {
358                let mut inc = roots.sdk.join("Include");
359                inc.push(sdk_version);
360                inc
361            } else {
362                let mut target = roots.sdk.join("include");
363                if config.use_winsysroot_style {
364                    target.push(sdk_version);
365                }
366                target
367            };
368
369            target.push("ucrt");
370
371            let mut mappings = vec![Mapping {
372                src: inc_src,
373                target,
374                tree,
375                kind,
376                variant,
377                section: SectionKind::SdkHeader,
378            }];
379
380            src.push("lib/ucrt");
381
382            let mut target = roots.sdk.join("lib");
383
384            if config.use_winsysroot_style {
385                target.push(sdk_version);
386            }
387
388            target.push("ucrt");
389
390            for arch in Arch::iter(arches) {
391                let mut src = src.clone();
392                let mut target = target.clone();
393
394                push_arch(&mut src, &mut target, arch);
395
396                let tree = get_tree(&src)?;
397
398                mappings.push(Mapping {
399                    src,
400                    target,
401                    tree,
402                    kind,
403                    variant,
404                    section: SectionKind::SdkLib,
405                });
406            }
407
408            mappings
409        }
410        PayloadKind::VcrDebug => {
411            if let Some(version) = vcrd_version {
412                let mut src = src.clone();
413                let mut target = roots.vcrd.clone();
414
415                src.push("SourceDir");
416                if !item.payload.filename.to_string().contains("UCRT") {
417                    src.push(match item.payload.target_arch.unwrap() {
418                        Arch::Aarch | Arch::X86 => "System",
419                        Arch::Aarch64 | Arch::X86_64 => "System64",
420                    });
421                };
422
423                target.push(version);
424                target.push("bin");
425                target.push(item.payload.target_arch.unwrap().as_str());
426
427                let tree = get_tree(&src)?;
428
429                vec![Mapping {
430                    src,
431                    target,
432                    tree,
433                    kind,
434                    variant,
435                    section: SectionKind::VcrDebug,
436                }]
437            } else {
438                vec![]
439            }
440        }
441    };
442
443    let mut results = Vec::new();
444
445    item.progress.reset();
446    item.progress
447        .set_length(mappings.iter().map(|map| map.tree.stats().1).sum());
448    item.progress.set_message("📦 splatting");
449
450    struct Dir<'ft> {
451        src: PathBuf,
452        tar: PathBuf,
453        tree: &'ft crate::unpack::FileTree,
454    }
455
456    if let Some(map) = map {
457        mappings
458            .into_par_iter()
459            .map(|mapping| -> Result<Option<SdkHeaders>, Error> {
460                let (prefix, section) = match mapping.section {
461                    SectionKind::SdkHeader => {
462                        // All ucrt headers are in the ucrt subdir, but we have a flat
463                        // list in the mapping file, so we need to drop that from the prefix
464                        // so they match like all the other paths
465
466                        (
467                            if matches!(mapping.kind, PayloadKind::Ucrt) {
468                                mapping.target.parent().unwrap().to_owned()
469                            } else {
470                                mapping.target.clone()
471                            },
472                            &map.sdk.headers,
473                        )
474                    }
475                    SectionKind::SdkLib => (roots.sdk.join("lib"), &map.sdk.libs),
476                    SectionKind::CrtHeader => (mapping.target.clone(), &map.crt.headers),
477                    SectionKind::CrtLib => {
478                        (
479                            // Pop the arch directory, it's part of the prefix in
480                            // the filter
481                            mapping.target.parent().unwrap().to_owned(),
482                            &map.crt.libs,
483                        )
484                    }
485                    SectionKind::VcrDebug => (roots.vcrd.clone(), &map.vcrd.libs),
486                };
487
488                let mut dir_stack = vec![Dir {
489                    src: mapping.src,
490                    tar: mapping.target,
491                    tree: mapping.tree,
492                }];
493
494                while let Some(Dir { src, mut tar, tree }) = dir_stack.pop() {
495                    let mut created_dir = false;
496
497                    for (fname, size) in &tree.files {
498                        // Even if we don't splat 100% of the source files, we still
499                        // want to show that we processed them all
500                        item.progress.inc(*size);
501
502                        tar.push(fname);
503
504                        let unprefixed = tar.strip_prefix(&prefix).with_context(|| {
505                            format!("invalid path {tar}: doesn't begin with prefix {prefix}")
506                        })?;
507
508                        if !section.filter.contains(unprefixed.as_str()) {
509                            tar.pop();
510                            continue;
511                        }
512
513                        let src_path = src.join(fname);
514
515                        if !created_dir {
516                            std::fs::create_dir_all(tar.parent().unwrap())
517                                .with_context(|| format!("unable to create {tar}"))?;
518                            created_dir = true;
519                        }
520
521                        if config.copy {
522                            std::fs::copy(&src_path, &tar)
523                                .with_context(|| format!("failed to copy {src_path} to {tar}"))?;
524                        } else {
525                            std::fs::rename(&src_path, &tar)
526                                .with_context(|| format!("failed to move {src_path} to {tar}"))?;
527                        }
528
529                        // Create any associated symlinks, these are always going to be symlinks
530                        // in the same target directory
531                        if let Some(symlinks) = section.symlinks.get(unprefixed.as_str()) {
532                            for sl in symlinks {
533                                tar.pop();
534                                tar.push(sl);
535                                symlink(fname.as_str(), &tar)?;
536                            }
537                        }
538
539                        tar.pop();
540                    }
541
542                    for (dir, dtree) in &tree.dirs {
543                        dir_stack.push(Dir {
544                            src: src.join(dir),
545                            tar: tar.join(dir),
546                            tree: dtree,
547                        });
548                    }
549                }
550
551                // This is only if we are outputting symlinks, which we don't do when the user
552                // has specified an exact mapping
553                Ok(None)
554            })
555            .collect_into_vec(&mut results);
556    } else {
557        let include_debug_libs = config.include_debug_libs;
558        let include_debug_symbols = config.include_debug_symbols;
559        let filter_store = variants & Variant::Store as u32 == 0;
560
561        mappings
562            .into_par_iter()
563            .map(|mapping| -> Result<Option<SdkHeaders>, Error> {
564                let mut sdk_headers = (mapping.kind == PayloadKind::SdkHeaders)
565                    .then(|| SdkHeaders::new(mapping.target.clone()));
566
567                let mut dir_stack = vec![Dir {
568                    src: mapping.src,
569                    tar: mapping.target,
570                    tree: mapping.tree,
571                }];
572
573                while let Some(Dir { src, mut tar, tree }) = dir_stack.pop() {
574                    std::fs::create_dir_all(&tar)
575                        .with_context(|| format!("unable to create {tar}"))?;
576
577                    for (fname, size) in &tree.files {
578                        // Even if we don't splat 100% of the source files, we still
579                        // want to show that we processed them all
580                        item.progress.inc(*size);
581
582                        if !include_debug_symbols && fname.extension() == Some("pdb") {
583                            tracing::debug!("skipping {fname}");
584                            continue;
585                        }
586
587                        let fname_str = fname.as_str();
588                        if !include_debug_libs
589                            && (mapping.kind == PayloadKind::CrtLibs
590                                || mapping.kind == PayloadKind::Ucrt)
591                            && let Some(stripped) = fname_str.strip_suffix(".lib")
592                            && (stripped.ends_with('d')
593                                || stripped.ends_with("d_netcore")
594                                || stripped
595                                    .strip_suffix(|c: char| c.is_ascii_digit())
596                                    .is_some_and(|fname| fname.ends_with('d')))
597                        {
598                            tracing::debug!("skipping {fname}");
599                            continue;
600                        }
601
602                        tar.push(fname);
603
604                        let src_path = src.join(fname);
605
606                        if config.copy {
607                            std::fs::copy(&src_path, &tar)
608                                .with_context(|| format!("failed to copy {src_path} to {tar}"))?;
609                        } else {
610                            std::fs::rename(&src_path, &tar)
611                                .with_context(|| format!("failed to move {src_path} to {tar}"))?;
612                        }
613
614                        let kind = mapping.kind;
615
616                        let mut add_symlinks = || -> Result<(), Error> {
617                            match kind {
618                                // These are all internally consistent and lowercased, so if
619                                // a library is including them with different casing that is
620                                // kind of on them
621                                //
622                                // The SDK headers are also all over the place with casing
623                                // as well as being internally inconsistent, so we scan
624                                // them all for includes and add those that are referenced
625                                // incorrectly, but we wait until after all the of headers
626                                // have been unpacked before fixing them
627                                PayloadKind::CrtHeaders
628                                | PayloadKind::AtlHeaders
629                                | PayloadKind::Ucrt
630                                | PayloadKind::AtlLibs
631                                | PayloadKind::VcrDebug => {}
632
633                                PayloadKind::SdkHeaders => {
634                                    if let Some(sdk_headers) = &mut sdk_headers {
635                                        let rel_target_path =
636                                            sdk_headers.get_relative_path(&tar)?;
637
638                                        let rel_hash = calc_lower_hash(rel_target_path.as_str());
639
640                                        if sdk_headers.inner.insert(rel_hash, tar.clone()).is_some()
641                                        {
642                                            anyhow::bail!(
643                                                "found duplicate relative path when hashed"
644                                            );
645                                        }
646
647                                        if let Some(additional_name) = match fname_str {
648                                            // https://github.com/zeromq/libzmq/blob/3070a4b2461ec64129062907d915ed665d2ac126/src/precompiled.hpp#L73
649                                            "mstcpip.h" => Some("Mstcpip.h"),
650                                            // https://github.com/ponylang/ponyc/blob/8d41d6650b48b9733cd675df199588e6fccc6346/src/common/platform.h#L191
651                                            "basetsd.h" => Some("BaseTsd.h"),
652                                            _ => None,
653                                        } {
654                                            tar.pop();
655                                            tar.push(additional_name);
656
657                                            symlink(fname_str, &tar)?;
658                                        }
659                                    }
660                                }
661                                PayloadKind::CrtLibs => {
662                                    // While _most_ of the libs *stares at Microsoft.VisualC.STLCLR.dll* are lower case,
663                                    // sometimes when they are specified as linker arguments, crates will link with
664                                    // SCREAMING as if they are angry at the linker, so fix this in the few "common" cases.
665                                    // This list is probably not complete, but that's what PRs are for
666                                    if let Some(angry_lib) = match fname_str.strip_suffix(".lib") {
667                                        Some("libcmt") => Some("LIBCMT.lib"),
668                                        Some("msvcrt") => Some("MSVCRT.lib"),
669                                        Some("oldnames") => Some("OLDNAMES.lib"),
670                                        _ => None,
671                                    } {
672                                        tar.pop();
673                                        tar.push(angry_lib);
674
675                                        symlink(fname_str, &tar)?;
676                                    }
677                                }
678                                PayloadKind::SdkLibs | PayloadKind::SdkStoreLibs => {
679                                    // The SDK libraries are just completely inconsistent, but
680                                    // all usage I have ever seen just links them with lowercase
681                                    // names, so we just fix all of them to be lowercase.
682                                    // Note that we need to not only fix the name but also the
683                                    // extension, as for some inexplicable reason about half of
684                                    // them use an uppercase L for the extension. WTF. This also
685                                    // applies to the tlb files, so at least they are consistently
686                                    // inconsistent
687                                    if fname_str.contains(|c: char| c.is_ascii_uppercase()) {
688                                        tar.pop();
689                                        tar.push(fname_str.to_ascii_lowercase());
690
691                                        symlink(fname_str, &tar)?;
692                                    }
693
694                                    // There is also this: https://github.com/time-rs/time/blob/v0.3.2/src/utc_offset.rs#L454
695                                    // And this: https://github.com/webrtc-rs/util/blob/main/src/ifaces/ffi/windows/mod.rs#L33
696                                    if let Some(additional_name) = match fname_str {
697                                        "kernel32.Lib" => Some("Kernel32.lib"),
698                                        "iphlpapi.lib" => Some("Iphlpapi.lib"),
699                                        _ => None,
700                                    } {
701                                        tar.pop();
702                                        tar.push(additional_name);
703
704                                        symlink(fname_str, &tar)?;
705                                    }
706
707                                    // We also need to support SCREAMING case for the library names
708                                    // due to...reasons https://github.com/microsoft/windows-rs/blob/a27a74784ccf304ab362bf2416f5f44e98e5eecd/src/bindings.rs#L3772
709                                    if tar.extension() == Some("lib") {
710                                        tar.pop();
711                                        tar.push(fname_str.to_ascii_uppercase());
712                                        tar.set_extension("lib");
713
714                                        symlink(fname_str, &tar)?;
715                                    }
716                                }
717                            }
718
719                            Ok(())
720                        };
721
722                        if config.enable_symlinks {
723                            add_symlinks()?;
724                        }
725
726                        tar.pop();
727                    }
728
729                    // Due to some libs from the CRT Store libs variant being needed
730                    // by the regular Desktop variant, if we are not actually
731                    // targeting the Store we can avoid adding the additional
732                    // uwp and store subdirectories
733                    if mapping.variant == Some(Variant::Store)
734                        && filter_store
735                        && mapping.kind == PayloadKind::CrtLibs
736                    {
737                        tracing::debug!("skipping CRT subdirs");
738
739                        item.progress
740                            .inc(tree.dirs.iter().map(|(_, ft)| ft.stats().1).sum());
741                        continue;
742                    }
743
744                    for (dir, dtree) in &tree.dirs {
745                        dir_stack.push(Dir {
746                            src: src.join(dir),
747                            tar: tar.join(dir),
748                            tree: dtree,
749                        });
750                    }
751                }
752
753                Ok(sdk_headers)
754            })
755            .collect_into_vec(&mut results);
756
757        if !config.use_winsysroot_style {
758            match kind {
759                PayloadKind::SdkLibs => {
760                    // Symlink sdk/lib/{sdkversion} -> sdk/lib, regardless of filesystem case sensitivity.
761                    let mut versioned_linkname = roots.sdk.clone();
762                    versioned_linkname.push("lib");
763                    versioned_linkname.push(sdk_version);
764
765                    // Multiple architectures both have a lib dir,
766                    // but we only need to create this symlink once.
767                    if !versioned_linkname.exists() {
768                        crate::symlink_on_windows_too(".", &versioned_linkname)?;
769                    }
770
771                    // https://github.com/llvm/llvm-project/blob/release/14.x/clang/lib/Driver/ToolChains/MSVC.cpp#L1102
772                    if config.enable_symlinks {
773                        let mut title_case = roots.sdk.clone();
774                        title_case.push("Lib");
775                        if !title_case.exists() {
776                            symlink("lib", &title_case)?;
777                        }
778                    }
779                }
780                PayloadKind::SdkHeaders => {
781                    // Symlink sdk/include/{sdkversion} -> sdk/include, regardless of filesystem case sensitivity.
782                    let mut versioned_linkname = roots.sdk.clone();
783                    versioned_linkname.push("include");
784                    versioned_linkname.push(sdk_version);
785
786                    // Desktop and Store variants both have an include dir,
787                    // but we only need to create this symlink once.
788                    if !versioned_linkname.exists() {
789                        crate::symlink_on_windows_too(".", &versioned_linkname)?;
790                    }
791
792                    // https://github.com/llvm/llvm-project/blob/release/14.x/clang/lib/Driver/ToolChains/MSVC.cpp#L1340-L1346
793                    if config.enable_symlinks {
794                        let mut title_case = roots.sdk.clone();
795                        title_case.push("Include");
796                        if !title_case.exists() {
797                            symlink("include", &title_case)?;
798                        }
799                    }
800                }
801                _ => (),
802            };
803        }
804    }
805
806    item.progress.finish_with_message("📦 splatted");
807
808    let headers = results.into_iter().collect::<Result<Vec<_>, _>>()?;
809
810    Ok(headers.into_iter().find_map(|headers| headers))
811}
812
813pub(crate) fn finalize_splat(
814    ctx: &Ctx,
815    sdk_version: Option<&str>,
816    roots: &SplatRoots,
817    sdk_headers: Vec<SdkHeaders>,
818    crt_headers: Option<crate::unpack::FileTree>,
819    atl_headers: Option<crate::unpack::FileTree>,
820) -> Result<(), Error> {
821    let mut files: std::collections::HashMap<
822        _,
823        Header<'_>,
824        std::hash::BuildHasherDefault<twox_hash::XxHash64>,
825    > = Default::default();
826
827    struct Header<'root> {
828        root: &'root SdkHeaders,
829        path: PathBuf,
830    }
831
832    fn compare_hashes(existing: &Path, new: &PathBuf) -> anyhow::Result<()> {
833        use crate::util::Sha256;
834
835        let existing_hash = Sha256::digest(&std::fs::read(existing)?);
836        let new_hash = Sha256::digest(&std::fs::read(new)?);
837
838        anyhow::ensure!(
839            existing_hash == new_hash,
840            "2 files with same relative path were not equal: '{existing}' != '{new}'"
841        );
842
843        Ok(())
844    }
845
846    for hdrs in &sdk_headers {
847        for (k, v) in &hdrs.inner {
848            if let Some(existing) = files.get(k) {
849                // We already have a file with the same path, if they're the same
850                // as each other it's fine, but if they differ we have an issue
851                compare_hashes(&existing.path, v)?;
852                tracing::debug!("skipped {v}, a matching path already exists");
853            } else {
854                files.insert(
855                    k,
856                    Header {
857                        root: hdrs,
858                        path: v.clone(),
859                    },
860                );
861            }
862        }
863    }
864
865    let mut includes: std::collections::HashMap<
866        _,
867        _,
868        std::hash::BuildHasherDefault<twox_hash::XxHash64>,
869    > = Default::default();
870
871    // Many headers won't necessarily be referenced internally by an all
872    // lower case filename, even when that is common from outside the sdk
873    // for basically all files (eg windows.h, psapi.h etc)
874    includes.extend(files.values().filter_map(|fpath| {
875        fpath
876            .root
877            .get_relative_path(&fpath.path)
878            .ok()
879            .and_then(|rel_path| {
880                let rp = rel_path.as_str();
881
882                // Ignore the 2 opengl includes, since they are the one exception
883                // that all subdirectories are lowercased
884                if rel_path.starts_with("gl/") {
885                    return None;
886                }
887
888                rp.contains(|c: char| c.is_ascii_uppercase())
889                    .then(|| (PathBuf::from(rp.to_ascii_lowercase()), true))
890            })
891    }));
892
893    let regex = regex::bytes::Regex::new(r#"#include\s+(?:"|<)([^">]+)(?:"|>)?"#).unwrap();
894
895    let pb =
896        indicatif::ProgressBar::with_draw_target(Some(files.len() as u64), ctx.draw_target.into())
897            .with_style(
898                indicatif::ProgressStyle::default_bar()
899                    .template(
900                        "{spinner:.green} {prefix:.bold} [{elapsed}] {wide_bar:.green} {pos}/{len}",
901                    )?
902                    .progress_chars("█▇▆▅▄▃▂▁  "),
903            );
904
905    pb.set_prefix("symlinks");
906    pb.set_message("🔍 SDK includes");
907
908    // Scan all of the files in the include directory for includes so that
909    // we can add symlinks to at least make the SDK headers internally consistent
910    for file in files.values() {
911        // Of course, there are files with non-utf8 encoding :p
912        let contents =
913            std::fs::read(&file.path).with_context(|| format!("unable to read {}", file.path))?;
914
915        for caps in regex.captures_iter(&contents) {
916            let rel_path = std::str::from_utf8(&caps[1]).with_context(|| {
917                format!(
918                    "{} contained an include with non-utf8 characters",
919                    file.path
920                )
921            })?;
922
923            // TODO: Some includes, particularly in [wrl](https://docs.microsoft.com/en-us/cpp/cppcx/wrl/windows-runtime-cpp-template-library-wrl?view=msvc-170)
924            // use incorrect `\` path separators, this is hopefully not an issue
925            // since no one cares about that target? But if it is a problem
926            // we'll need to actually modify the include to fix the path. :-/
927            if !includes.contains_key(Path::new(rel_path)) {
928                includes.insert(PathBuf::from(rel_path), true);
929            }
930        }
931
932        pb.inc(1);
933    }
934
935    if let Some(crt) = crt_headers
936        .as_ref()
937        .and_then(|crt| crt.subtree(Path::new("include")))
938    {
939        pb.set_message("🔍 CRT includes");
940        let cr = roots.crt.join("include");
941
942        for (path, _) in &crt.files {
943            // Of course, there are files with non-utf8 encoding :p
944            let path = cr.join(path);
945            let contents =
946                std::fs::read(&path).with_context(|| format!("unable to read CRT {path}"))?;
947
948            for caps in regex.captures_iter(&contents) {
949                let rel_path = std::str::from_utf8(&caps[1]).with_context(|| {
950                    format!("{path} contained an include with non-utf8 characters")
951                })?;
952
953                if !includes.contains_key(Path::new(rel_path)) {
954                    includes.insert(PathBuf::from(rel_path), false);
955                }
956            }
957
958            pb.inc(1);
959        }
960    }
961
962    if let Some(atl) = atl_headers
963        .as_ref()
964        .and_then(|atl| atl.subtree(Path::new("include")))
965    {
966        pb.set_message("🔍 ATL includes");
967        let cr = roots.crt.join("include");
968
969        for (path, _) in &atl.files {
970            // Of course, there are files with non-utf8 encoding :p
971            let path = cr.join(path);
972            let contents =
973                std::fs::read(&path).with_context(|| format!("unable to read ATL {path}"))?;
974
975            for caps in regex.captures_iter(&contents) {
976                let rel_path = std::str::from_utf8(&caps[1]).with_context(|| {
977                    format!("{path} contained an include with non-utf8 characters")
978                })?;
979
980                if !includes.contains_key(Path::new(rel_path)) {
981                    includes.insert(PathBuf::from(rel_path), false);
982                }
983            }
984
985            pb.inc(1);
986        }
987    }
988
989    pb.finish();
990
991    for (include, is_sdk) in includes {
992        let lower_hash = calc_lower_hash(include.as_str());
993
994        match files.get(&lower_hash) {
995            Some(disk_file) => match (disk_file.path.file_name(), include.file_name()) {
996                (Some(disk_name), Some(include_name)) if disk_name != include_name => {
997                    let mut link = disk_file.path.clone();
998                    link.pop();
999                    link.push(include_name);
1000                    symlink(disk_name, &link)?;
1001                }
1002                _ => {}
1003            },
1004            None => {
1005                if is_sdk {
1006                    tracing::debug!("SDK include for '{include}' was not found in the SDK headers");
1007                }
1008            }
1009        }
1010    }
1011
1012    // There is a um/gl directory, but of course there is an include for GL/
1013    // instead, so fix that as well :p
1014    if let Some(_sdk_version) = sdk_version {
1015        // let mut target = roots.sdk.join("Include");
1016        // target.push(sdk_version);
1017        // target.push("um/GL");
1018        // symlink("gl", &target)?;
1019    } else {
1020        symlink("gl", &roots.sdk.join("include/um/GL"))?;
1021    }
1022
1023    Ok(())
1024}
1025
1026use std::hash::Hasher;
1027
1028#[inline]
1029fn calc_lower_hash(path: &str) -> u64 {
1030    let mut hasher = twox_hash::XxHash64::with_seed(0);
1031
1032    for c in path.chars().map(|c| c.to_ascii_lowercase() as u8) {
1033        hasher.write_u8(c);
1034    }
1035
1036    hasher.finish()
1037}