Skip to main content

windows_clang/
scrape.rs

1//! Multi-architecture scrape orchestration for [`Clang::scrape`](crate::Clang::scrape).
2//!
3//! The builder holds the normal parse configuration. [`ScrapePlan`] carries the extra
4//! orchestration state: arches, outputs, seed metadata, and reference winmds.
5
6use crate::{Clang, clang_resource_dir};
7use std::path::{Path, PathBuf};
8use windows_rdl::{ArchInput, merge_arch_rdl, reader};
9
10/// Target architecture settings that differ between scrape passes.
11pub struct Arch {
12    /// Short name (`x64`, `arm64`, `x86`) and throwaway subdirectory name.
13    pub name: String,
14    /// The clang `--target` triple, e.g. `x86_64-pc-windows-msvc`.
15    pub triple: String,
16    /// `SupportedArchitecture` bitmask: 1 = X86, 2 = X64, 4 = Arm64.
17    pub bits: i32,
18    /// Extra `-D` defines for this architecture.
19    pub defines: Vec<String>,
20}
21
22impl Arch {
23    /// Known triple + `SupportedArchitecture` bit for a short arch name.
24    pub fn known(name: &str) -> Option<Self> {
25        let (triple, bits) = match name {
26            "x64" => ("x86_64-pc-windows-msvc", 2),
27            "arm64" => ("aarch64-pc-windows-msvc", 4),
28            "x86" => ("i686-pc-windows-msvc", 1),
29            _ => return None,
30        };
31        Some(Self {
32            name: name.to_string(),
33            triple: triple.to_string(),
34            bits,
35            defines: Vec::new(),
36        })
37    }
38
39    /// Build the canonical-first arch list: `x64`, then non-`x64` extras.
40    pub fn canonical_plus(extra: &[String], build: impl Fn(&str) -> Self) -> Vec<Self> {
41        let mut archs = vec![build("x64")];
42        for name in extra {
43            if name != "x64" {
44                archs.push(build(name));
45            }
46        }
47        archs
48    }
49}
50
51/// Multi-arch and output state layered on top of a configured [`Clang`](crate::Clang).
52pub struct ScrapePlan {
53    /// Root namespace shared by all emitted header partitions.
54    pub root: String,
55    /// Committed per-header RDL directory.
56    pub rdl_dir: PathBuf,
57    /// Scratch directory for per-arch throwaway RDL dirs and winmds.
58    pub out_dir: PathBuf,
59    /// Committed unified winmd output path.
60    pub winmd: PathBuf,
61    /// `archs[0]` writes `rdl_dir`; extras are folded in by arch-merge.
62    pub archs: Vec<Arch>,
63    /// Exclusion/reference winmds needed by both clang and RDL reader passes.
64    pub reference_winmds: Vec<PathBuf>,
65    /// Resolution-only winmds: they qualify external references but never exclude entities.
66    pub resolution_winmds: Vec<PathBuf>,
67    /// Optional hand-authored seed RDL, preserved across generated output clears.
68    pub seed: Option<PathBuf>,
69    /// Scrape architectures concurrently.
70    pub parallel: bool,
71}
72
73/// What [`Clang::scrape`](crate::Clang::scrape) produced, for the caller's summary output.
74pub struct Summary {
75    /// Committed partition (`.rdl`) files in `rdl_dir`, excluding the seed.
76    pub partitions: usize,
77    /// Per-arch scrape wall-clock times, in `archs` order.
78    pub arch_timings: Vec<(String, f32)>,
79    /// Wall-clock time of the (possibly parallel) scrape phase.
80    pub scrape_wall: f32,
81    /// Arch-merge time (0.0 for a single-arch scrape).
82    pub merge_wall: f32,
83    /// Final unified-winmd derivation time (0.0 for a single-arch scrape).
84    pub winmd_wall: f32,
85    /// Whether more than one architecture was scraped and merged.
86    pub multi_arch: bool,
87}
88
89/// Prints timing details; callers add the output paths.
90impl std::fmt::Display for Summary {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        writeln!(f, "Timing:")?;
93        for (name, secs) in &self.arch_timings {
94            writeln!(f, "  scrape {name:<6}         {secs:>8.2}s")?;
95        }
96        writeln!(f, "  scrape (parallel wall) {:>8.2}s", self.scrape_wall)?;
97        if self.multi_arch {
98            let arches: Vec<&str> = self.arch_timings.iter().map(|(n, _)| n.as_str()).collect();
99            writeln!(f, "Arch-merged: {}", arches.join(" + "))?;
100            writeln!(f, "  arch-merge             {:>8.2}s", self.merge_wall)?;
101            writeln!(f, "  final winmd            {:>8.2}s", self.winmd_wall)?;
102        }
103        Ok(())
104    }
105}
106
107/// Find `name` in `dirs`, returning a forward-slashed path.
108pub fn find_in_dirs(name: &str, dirs: &[String]) -> Option<String> {
109    dirs.iter()
110        .map(|dir| Path::new(dir).join(name))
111        .find(|path| path.is_file())
112        .map(|path| path.to_string_lossy().replace('\\', "/"))
113}
114
115struct Job<'a> {
116    arch: &'a Arch,
117    rdl_dir: PathBuf,
118    winmd: PathBuf,
119}
120
121impl Clang {
122    /// Run the plan, replaying this builder once per architecture and merging the results.
123    pub fn scrape(&self, plan: &ScrapePlan) -> Summary {
124        assert!(
125            !plan.archs.is_empty(),
126            "scraper: `plan.archs` must list at least one architecture"
127        );
128
129        std::fs::create_dir_all(&plan.out_dir)
130            .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", plan.out_dir.display()));
131        std::fs::create_dir_all(&plan.rdl_dir)
132            .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", plan.rdl_dir.display()));
133
134        let canonical = &plan.archs[0];
135        let winmd_file = plan
136            .winmd
137            .file_name()
138            .unwrap_or_else(|| panic!("`plan.winmd` has no file name: `{}`", plan.winmd.display()));
139        let stem = plan
140            .winmd
141            .file_stem()
142            .unwrap_or_else(|| panic!("`plan.winmd` has no file stem: `{}`", plan.winmd.display()));
143        let canonical_winmd = plan.out_dir.join(winmd_file);
144
145        // The canonical arch writes committed RDL; extras write throwaway dirs.
146        let mut jobs = vec![Job {
147            arch: canonical,
148            rdl_dir: plan.rdl_dir.clone(),
149            winmd: canonical_winmd.clone(),
150        }];
151        for arch in &plan.archs[1..] {
152            let mut winmd_file = stem.to_os_string();
153            winmd_file.push(format!(".{}.winmd", arch.name));
154            jobs.push(Job {
155                arch,
156                rdl_dir: plan.out_dir.join(&arch.name),
157                winmd: plan.out_dir.join(winmd_file),
158            });
159        }
160        let multi_arch = jobs.len() > 1;
161
162        // Non-canonical arches need version-matched clang resource headers; resolve before
163        // workers start so they do not race the one-time fetch.
164        let resource_dir = multi_arch.then(clang_resource_dir);
165
166        let timings = std::sync::Mutex::new(Vec::<(String, f32)>::new());
167        let scrape_start = std::time::Instant::now();
168        let scrape_one = |job: &Job| {
169            let t = std::time::Instant::now();
170            let resource = (job.arch.bits != canonical.bits).then(|| {
171                resource_dir
172                    .as_deref()
173                    .expect("resource dir resolved for multi-arch")
174            });
175            self.scrape_arch(plan, job.arch, &job.rdl_dir, &job.winmd, resource);
176            timings
177                .lock()
178                .unwrap()
179                .push((job.arch.name.clone(), t.elapsed().as_secs_f32()));
180        };
181        if plan.parallel {
182            windows_threading::for_each(jobs.iter(), scrape_one);
183        } else {
184            jobs.iter().for_each(scrape_one);
185        }
186        let scrape_wall = scrape_start.elapsed().as_secs_f32();
187
188        let mut merge_wall = 0.0;
189        let mut winmd_wall = 0.0;
190        if multi_arch {
191            // Fold per-arch winmds back into defining-header partitions and rebuild the winmd.
192            let arch_inputs: Vec<ArchInput> = jobs
193                .iter()
194                .map(|j| ArchInput {
195                    rdl_dir: j.rdl_dir.clone(),
196                    winmd: j.winmd.clone(),
197                    bits: j.arch.bits,
198                })
199                .collect();
200            let m = std::time::Instant::now();
201            merge_arch_rdl(&arch_inputs, plan.seed.as_deref(), &plan.rdl_dir)
202                .unwrap_or_else(|e| panic!("arch-merge failed: {e}"));
203            merge_wall = m.elapsed().as_secs_f32();
204
205            let w = std::time::Instant::now();
206            let mut reader = reader();
207            reader.input(&plan.rdl_dir);
208            if let Some(seed) = &plan.seed {
209                reader.input(seed);
210            }
211            for reference in &plan.reference_winmds {
212                reader.reference(reference);
213            }
214            for resolution in &plan.resolution_winmds {
215                reader.reference(resolution);
216            }
217            reader.output(&plan.winmd).write().unwrap_or_else(|e| {
218                panic!(
219                    "failed to compile merged winmd `{}`: {e}",
220                    plan.winmd.display()
221                )
222            });
223            winmd_wall = w.elapsed().as_secs_f32();
224        } else {
225            // Single arch: publish the canonical job's winmd.
226            std::fs::copy(&canonical_winmd, &plan.winmd).unwrap_or_else(|e| {
227                panic!("failed to publish winmd to `{}`: {e}", plan.winmd.display())
228            });
229        }
230
231        let mut arch_timings = timings.into_inner().unwrap();
232        arch_timings.sort_by_key(|(name, _)| {
233            plan.archs
234                .iter()
235                .position(|a| a.name == *name)
236                .unwrap_or(usize::MAX)
237        });
238
239        Summary {
240            partitions: count_partitions(&plan.rdl_dir, plan.seed.as_deref()),
241            arch_timings,
242            scrape_wall,
243            merge_wall,
244            winmd_wall,
245            multi_arch,
246        }
247    }
248
249    /// Scrape one architecture into `rdl_dir` and compile those partitions into `winmd`.
250    fn scrape_arch(
251        &self,
252        plan: &ScrapePlan,
253        arch: &Arch,
254        rdl_dir: &Path,
255        winmd: &Path,
256        resource_dir: Option<&str>,
257    ) {
258        clear_rdl_dir(rdl_dir, plan.seed.as_deref());
259
260        let mut clang = self.clone();
261        clang
262            .target(&arch.triple)
263            .args(arch.defines.iter().map(String::as_str));
264        if let Some(dir) = resource_dir {
265            clang.args(["-resource-dir", dir]);
266        }
267        for reference in &plan.reference_winmds {
268            clang.reference(reference);
269        }
270        for resolution in &plan.resolution_winmds {
271            clang.resolution_input(resolution);
272        }
273
274        clang
275            .namespace(&plan.root)
276            .output(rdl_dir)
277            .write_by_header()
278            .unwrap_or_else(|e| {
279                panic!(
280                    "failed to generate partitions in `{}`: {e}",
281                    rdl_dir.display()
282                )
283            });
284
285        let mut rdl_paths = collect_rdl_paths(rdl_dir);
286        if let Some(seed) = &plan.seed
287            && !rdl_paths.iter().any(|p| p == seed)
288        {
289            rdl_paths.push(seed.clone());
290        }
291
292        let mut reader = reader();
293        reader.inputs(&rdl_paths);
294        for reference in &plan.reference_winmds {
295            reader.reference(reference);
296        }
297        for resolution in &plan.resolution_winmds {
298            reader.reference(resolution);
299        }
300        reader.output(winmd).write().unwrap_or_else(|e| {
301            panic!(
302                "failed to compile `{}` into `{}`: {e}",
303                rdl_dir.display(),
304                winmd.display()
305            )
306        });
307    }
308}
309
310/// Remove stale generated `.rdl` partitions, preserving the seed file by name.
311fn clear_rdl_dir(rdl_dir: &Path, seed: Option<&Path>) {
312    std::fs::create_dir_all(rdl_dir)
313        .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", rdl_dir.display()));
314    let seed_name = seed.and_then(Path::file_name);
315    for entry in std::fs::read_dir(rdl_dir)
316        .unwrap_or_else(|e| panic!("failed to read `{}`: {e}", rdl_dir.display()))
317    {
318        let path = entry.unwrap().path();
319        let is_seed = path.file_name() == seed_name;
320        if !is_seed && path.extension().is_some_and(|x| x == "rdl") {
321            std::fs::remove_file(&path)
322                .unwrap_or_else(|e| panic!("failed to remove `{}`: {e}", path.display()));
323        }
324    }
325}
326
327/// Sorted `.rdl` file paths in a directory.
328fn collect_rdl_paths(rdl_dir: &Path) -> Vec<PathBuf> {
329    let mut paths: Vec<PathBuf> = std::fs::read_dir(rdl_dir)
330        .unwrap_or_else(|e| panic!("failed to read `{}`: {e}", rdl_dir.display()))
331        .filter_map(|entry| entry.ok())
332        .map(|entry| entry.path())
333        .filter(|path| path.extension().is_some_and(|x| x == "rdl"))
334        .collect();
335    paths.sort();
336    paths
337}
338
339/// Count committed partition files, excluding the seed.
340fn count_partitions(rdl_dir: &Path, seed: Option<&Path>) -> usize {
341    let seed_name = seed.and_then(Path::file_name);
342    std::fs::read_dir(rdl_dir).map_or(0, |rd| {
343        rd.filter_map(|e| e.ok())
344            .filter(|e| {
345                let path = e.path();
346                path.extension().is_some_and(|x| x == "rdl") && path.file_name() != seed_name
347            })
348            .count()
349    })
350}