Skip to main content

cinrs_core/
include.rs

1//! `#include` resolution: where a header is looked for, and what is bundled.
2//!
3//! # Search order
4//!
5//! `#include "name"` looks in
6//!
7//! 1. the directory of the file the directive is written in — for the macro's
8//!    own text that is the directory of the invoking `.rs` file, and for a
9//!    header it is the directory that header was found in;
10//! 2. the configured include directories, in the order [`SearchPaths`]
11//!    describes;
12//! 3. the working directory, but only when the name *is* a path — when it
13//!    holds a directory separator. That is what makes `#include __FILE__`
14//!    work: [`Resolved::name`] is written relative to the working directory
15//!    wherever it can be (a diagnostic naming an absolute path is a
16//!    diagnostic that differs between two machines), so a header that
17//!    includes itself by `__FILE__` is asking for
18//!    `some/dir/thing.h` from a directive written *in* `some/dir`, which
19//!    neither of the first two steps will find. A bare name is deliberately
20//!    left out of this step, so that a `stdio.h` sitting in the working
21//!    directory never shadows the bundled one;
22//! 4. the [bundled headers](bundled);
23//! 5. the platform's own directories — `/usr/include` and friends — but only
24//!    when [the switch](System) is on.
25//!
26//! `#include <name>` skips steps 1 and 3. A name that is absolute is used as
27//! it stands.
28//!
29//! # The platform's own directories
30//!
31//! Step 5 is **off by default**, and everything above it is enough for a
32//! self-contained, target-model-portable unit. A real `<stdio.h>` is not
33//! plain C: glibc's is a thicket of `__attribute__`, `__extension__`,
34//! `__asm__` renaming and compiler builtins, and its layouts are the host's
35//! rather than the [target model](crate::target)'s. So `cinrs` ships its own
36//! small, plain-C99 declarations of the standard library: they declare exactly
37//! what the platform's real library exports, the linker binds the calls to the
38//! real implementation, and the C that uses them is ordinary C.
39//!
40//! **The bundled set is ISO C, plus what only the compiler can provide.**
41//! Everything in [`BUNDLED`] is a header the C standard describes, with two
42//! groups of exceptions: `<alloca.h>`, because `alloca` is implemented by this
43//! crate rather than by any library, and the Intel intrinsics headers —
44//! `<immintrin.h>`, `<xmmintrin.h>` and the rest — because `__m128i` and
45//! `_mm_add_epi32` are the compiler's too. Neither has a library behind it, so
46//! the platform's copy would have nothing to add and everything to break: a
47//! real `<immintrin.h>` is a thicket of `__attribute__((vector_size))` and
48//! `__builtin_ia32_*`, and this one is prototypes that
49//! [`crate::x86`] maps onto `core::arch`.
50//! POSIX is the platform's: `<unistd.h>`, `<fcntl.h>`, `<sys/types.h>`,
51//! `<pthread.h>`, `<sys/stat.h>` and the rest come from the platform's own
52//! directories, complete and consistent with each other, once the switch below
53//! is on. (Up to 0.1.0 four small POSIX headers were bundled too; they were
54//! incomplete — no `access`, no `fsync`, no `struct flock` — and a program that
55//! turned the platform on still got the bundled ones, which is the opposite of
56//! what it asked for.)
57//!
58//! What the bundled set cannot give is POSIX, and the things whose *layout*
59//! only the platform knows — `struct stat`, `DIR`, `pthread_mutex_t`, the real
60//! `FILE` — so a program that needs those turns the switch on with
61//! `#pragma cinrs system_include` (or `CINRS_SYSTEM_INCLUDE=1` in the
62//! environment, which is the crate-wide default the pragma overrides). With
63//! [`System::Last`] the bundled headers still win, and only a name they do not
64//! carry reaches the platform; with [`System::First`] the platform's copy of
65//! every header wins, which is what makes `FILE` the real `struct _IO_FILE`.
66//! The bundled ISO headers guard the types and macros a platform header would
67//! also define with that platform's own guard macros, so that the two sets can
68//! be mixed in [`System::Last`] mode: see the comments in `include/time.h`.
69//!
70//! The directories searched are [`SYSTEM_PATH_ENV_VAR`] when it is set, and
71//! otherwise [`system_directories`]'s per-target default. **The compiler's own
72//! private directories are never among them**: GCC's and Clang's
73//! `.../include/{limits,stdint,stddef,stdarg}.h` chain to the next header of
74//! the same name with `#include_next` and expect their own compiler's
75//! builtins, and every one of those headers is bundled here anyway.
76//!
77//! Nothing found under step 5 is tracked for rebuilds: a system header is part
78//! of the machine rather than of the crate, and `include_str!`-ing
79//! `/usr/include/stdio.h` into the build would make every unit rebuild when the
80//! libc package is upgraded, which is not what the file identifies.
81
82use std::path::{Path, PathBuf};
83
84use crate::target::{Arch, Env, Os, TargetModel, TargetSource};
85
86/// The bundled standard headers, as `(name, text)` pairs.
87///
88/// They are compiled into the crate rather than installed anywhere, so no part
89/// of a build depends on where `cinrs` itself lives on disk.
90///
91/// Every one of them is a header ISO C describes, with four exceptions:
92/// `<alloca.h>` is here because `alloca` is generated by this crate rather than
93/// called in any library, `<mm_malloc.h>` because it is the compiler's own
94/// (GCC's and Clang's `<xmmintrin.h>` include it, and programs count on the
95/// `<stdlib.h>` it brings), in portable C over `malloc` because GCC's calls
96/// `posix_memalign`, which the Microsoft runtime lacks; the Intel intrinsics
97/// headers are here because
98/// `_mm_add_epi32` is generated too — as a call to `core::arch` — and
99/// `<cpuid.h>` is here because GCC's copy names rbx as an `asm` operand, which
100/// rustc refuses, and this one saves and restores it in the template instead.
101/// In each case the compiler's copy is not one `cinrs` could use.
102/// A POSIX header is *not* here — see the [module docs](self).
103pub const BUNDLED: &[(&str, &str)] = &[
104    ("alloca.h", include_str!("../include/alloca.h")),
105    ("assert.h", include_str!("../include/assert.h")),
106    ("avx2intrin.h", include_str!("../include/avx2intrin.h")),
107    (
108        "avx512bf16intrin.h",
109        include_str!("../include/avx512bf16intrin.h"),
110    ),
111    (
112        "avx512bf16vlintrin.h",
113        include_str!("../include/avx512bf16vlintrin.h"),
114    ),
115    (
116        "avx512bitalgintrin.h",
117        include_str!("../include/avx512bitalgintrin.h"),
118    ),
119    (
120        "avx512bitalgvlintrin.h",
121        include_str!("../include/avx512bitalgvlintrin.h"),
122    ),
123    (
124        "avx512bwintrin.h",
125        include_str!("../include/avx512bwintrin.h"),
126    ),
127    (
128        "avx512cdintrin.h",
129        include_str!("../include/avx512cdintrin.h"),
130    ),
131    (
132        "avx512dqintrin.h",
133        include_str!("../include/avx512dqintrin.h"),
134    ),
135    (
136        "avx512fintrin.h",
137        include_str!("../include/avx512fintrin.h"),
138    ),
139    (
140        "avx512fp16intrin.h",
141        include_str!("../include/avx512fp16intrin.h"),
142    ),
143    (
144        "avx512fp16vlintrin.h",
145        include_str!("../include/avx512fp16vlintrin.h"),
146    ),
147    (
148        "avx512ifmaintrin.h",
149        include_str!("../include/avx512ifmaintrin.h"),
150    ),
151    (
152        "avx512ifmavlintrin.h",
153        include_str!("../include/avx512ifmavlintrin.h"),
154    ),
155    (
156        "avx512vbmi2intrin.h",
157        include_str!("../include/avx512vbmi2intrin.h"),
158    ),
159    (
160        "avx512vbmi2vlintrin.h",
161        include_str!("../include/avx512vbmi2vlintrin.h"),
162    ),
163    (
164        "avx512vbmiintrin.h",
165        include_str!("../include/avx512vbmiintrin.h"),
166    ),
167    (
168        "avx512vbmivlintrin.h",
169        include_str!("../include/avx512vbmivlintrin.h"),
170    ),
171    (
172        "avx512vlbwintrin.h",
173        include_str!("../include/avx512vlbwintrin.h"),
174    ),
175    (
176        "avx512vldqintrin.h",
177        include_str!("../include/avx512vldqintrin.h"),
178    ),
179    (
180        "avx512vlintrin.h",
181        include_str!("../include/avx512vlintrin.h"),
182    ),
183    (
184        "avx512vnniintrin.h",
185        include_str!("../include/avx512vnniintrin.h"),
186    ),
187    (
188        "avx512vnnivlintrin.h",
189        include_str!("../include/avx512vnnivlintrin.h"),
190    ),
191    (
192        "avx512vp2intersectintrin.h",
193        include_str!("../include/avx512vp2intersectintrin.h"),
194    ),
195    (
196        "avx512vpopcntdqintrin.h",
197        include_str!("../include/avx512vpopcntdqintrin.h"),
198    ),
199    (
200        "avx512vpopcntdqvlintrin.h",
201        include_str!("../include/avx512vpopcntdqvlintrin.h"),
202    ),
203    (
204        "avxifmaintrin.h",
205        include_str!("../include/avxifmaintrin.h"),
206    ),
207    ("avxintrin.h", include_str!("../include/avxintrin.h")),
208    (
209        "avxvnniint16intrin.h",
210        include_str!("../include/avxvnniint16intrin.h"),
211    ),
212    (
213        "avxvnniint8intrin.h",
214        include_str!("../include/avxvnniint8intrin.h"),
215    ),
216    (
217        "avxvnniintrin.h",
218        include_str!("../include/avxvnniintrin.h"),
219    ),
220    ("complex.h", include_str!("../include/complex.h")),
221    ("cpuid.h", include_str!("../include/cpuid.h")),
222    ("ctype.h", include_str!("../include/ctype.h")),
223    ("emmintrin.h", include_str!("../include/emmintrin.h")),
224    ("errno.h", include_str!("../include/errno.h")),
225    ("f16cintrin.h", include_str!("../include/f16cintrin.h")),
226    ("float.h", include_str!("../include/float.h")),
227    ("gfniintrin.h", include_str!("../include/gfniintrin.h")),
228    ("immintrin.h", include_str!("../include/immintrin.h")),
229    ("inttypes.h", include_str!("../include/inttypes.h")),
230    ("iso646.h", include_str!("../include/iso646.h")),
231    ("limits.h", include_str!("../include/limits.h")),
232    ("math.h", include_str!("../include/math.h")),
233    ("mm_malloc.h", include_str!("../include/mm_malloc.h")),
234    ("mmintrin.h", include_str!("../include/mmintrin.h")),
235    ("nmmintrin.h", include_str!("../include/nmmintrin.h")),
236    ("pmmintrin.h", include_str!("../include/pmmintrin.h")),
237    ("popcntintrin.h", include_str!("../include/popcntintrin.h")),
238    ("setjmp.h", include_str!("../include/setjmp.h")),
239    ("sha512intrin.h", include_str!("../include/sha512intrin.h")),
240    ("signal.h", include_str!("../include/signal.h")),
241    ("sm3intrin.h", include_str!("../include/sm3intrin.h")),
242    ("sm4intrin.h", include_str!("../include/sm4intrin.h")),
243    ("smmintrin.h", include_str!("../include/smmintrin.h")),
244    ("stdalign.h", include_str!("../include/stdalign.h")),
245    ("stdarg.h", include_str!("../include/stdarg.h")),
246    ("stdatomic.h", include_str!("../include/stdatomic.h")),
247    ("stdbool.h", include_str!("../include/stdbool.h")),
248    ("stdckdint.h", include_str!("../include/stdckdint.h")),
249    ("stddef.h", include_str!("../include/stddef.h")),
250    ("stdint.h", include_str!("../include/stdint.h")),
251    ("stdio.h", include_str!("../include/stdio.h")),
252    ("stdlib.h", include_str!("../include/stdlib.h")),
253    ("stdnoreturn.h", include_str!("../include/stdnoreturn.h")),
254    ("string.h", include_str!("../include/string.h")),
255    ("threads.h", include_str!("../include/threads.h")),
256    ("time.h", include_str!("../include/time.h")),
257    ("tmmintrin.h", include_str!("../include/tmmintrin.h")),
258    ("uchar.h", include_str!("../include/uchar.h")),
259    ("vaesintrin.h", include_str!("../include/vaesintrin.h")),
260    (
261        "vpclmulqdqintrin.h",
262        include_str!("../include/vpclmulqdqintrin.h"),
263    ),
264    ("wchar.h", include_str!("../include/wchar.h")),
265    ("wctype.h", include_str!("../include/wctype.h")),
266    ("wmmintrin.h", include_str!("../include/wmmintrin.h")),
267    ("x86intrin.h", include_str!("../include/x86intrin.h")),
268    ("xmmintrin.h", include_str!("../include/xmmintrin.h")),
269];
270
271/// The directory the bundled headers appear to live in.
272///
273/// It is not a directory at all — the headers are strings inside this crate —
274/// but a diagnostic has to name the file it is talking about, and
275/// `<cinrs>/stdio.h` says both which header it is and that it is ours. The
276/// angle brackets keep it from being mistaken for a path that exists.
277pub const BUNDLED_DIR: &str = "<cinrs>";
278
279/// The text of a bundled header, by name.
280pub fn bundled(name: &str) -> Option<&'static str> {
281    BUNDLED
282        .iter()
283        .find(|(n, _)| *n == name)
284        .map(|(_, text)| *text)
285}
286
287/// The display name a bundled header is known by: `<cinrs>/stdio.h`.
288pub fn bundled_name(name: &str) -> String {
289    format!("{BUNDLED_DIR}/{name}")
290}
291
292/// The POSIX headers a program is likely to reach for, none of which is
293/// bundled.
294///
295/// The list exists for one diagnostic: a `#include <unistd.h>` with the
296/// platform's directories switched off is not a missing file but a missing
297/// *switch*, and saying so is the difference between a puzzle and an
298/// instruction. The first four were bundled up to 0.1.0, which is why they come
299/// first; the rest are the ones a search of any real code base turns up. It is
300/// deliberately not the whole of POSIX — a name that is not here just gets the
301/// ordinary "file not found", which is not wrong, only terser.
302pub const POSIX_HEADERS: &[&str] = &[
303    "unistd.h",
304    "fcntl.h",
305    "strings.h",
306    "sys/types.h",
307    "aio.h",
308    "arpa/inet.h",
309    "dirent.h",
310    "dlfcn.h",
311    "fnmatch.h",
312    "glob.h",
313    "grp.h",
314    "langinfo.h",
315    "libgen.h",
316    "monetary.h",
317    "net/if.h",
318    "netdb.h",
319    "netinet/in.h",
320    "netinet/tcp.h",
321    "nl_types.h",
322    "poll.h",
323    "pthread.h",
324    "pwd.h",
325    "regex.h",
326    "sched.h",
327    "semaphore.h",
328    "spawn.h",
329    "sys/ipc.h",
330    "sys/mman.h",
331    "sys/msg.h",
332    "sys/resource.h",
333    "sys/select.h",
334    "sys/sem.h",
335    "sys/shm.h",
336    "sys/socket.h",
337    "sys/stat.h",
338    "sys/statvfs.h",
339    "sys/time.h",
340    "sys/times.h",
341    "sys/uio.h",
342    "sys/un.h",
343    "sys/utsname.h",
344    "sys/wait.h",
345    "syslog.h",
346    "tar.h",
347    "termios.h",
348    "ulimit.h",
349    "utime.h",
350    "utmpx.h",
351    "wordexp.h",
352];
353
354/// Whether `name` is one of [`POSIX_HEADERS`].
355pub fn is_posix_header(name: &str) -> bool {
356    POSIX_HEADERS.contains(&name)
357}
358
359/// How the header name was spelled.
360#[derive(Clone, Copy, PartialEq, Eq, Debug)]
361pub enum Form {
362    /// `#include "name"`, which searches the including file's directory first.
363    Quoted,
364    /// `#include <name>`, which does not.
365    Angled,
366}
367
368/// The directory a file's own `#include "…"` searches first.
369#[derive(Clone, Debug, Default)]
370pub enum Origin {
371    /// A directory on disk. Empty means the current directory, which is what
372    /// the parent of a bare `lib.rs` comes out as.
373    Dir(PathBuf),
374    /// A directory on disk reached through one of the platform's own
375    /// directories — the including file is a *system* header.
376    ///
377    /// It searches that directory first, as any other file does, and its
378    /// `<…>` includes then go to the platform's directories **before** the
379    /// bundled ones whatever the mode is. That is what keeps the platform's
380    /// header set self-consistent: glibc's `<pthread.h>` gets glibc's
381    /// `<time.h>`, and so one `struct timespec` rather than two.
382    SystemDir(PathBuf),
383    /// The bundled set: one bundled header including another finds it there.
384    Bundled,
385    /// Nowhere — the compiler would not say where the including file is.
386    #[default]
387    Unknown,
388}
389
390/// Whether the platform's own include directories are searched, and where in
391/// the order they go.
392///
393/// Set by `#pragma cinrs system_include` in a unit and by
394/// [`SYSTEM_ENV_VAR`] across a crate; see the [module docs](self).
395#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
396pub enum System {
397    /// Not searched at all, which is where the switch starts.
398    #[default]
399    Off,
400    /// Searched after the bundled headers: the bundled `<stdio.h>` still wins,
401    /// and only a header cinrs does not carry — `<sys/stat.h>`, `<pthread.h>`
402    /// — comes from the platform. `#pragma cinrs system_include`.
403    Last,
404    /// Searched before them, so that the platform's copy of every header wins.
405    /// `#pragma cinrs system_include first`.
406    First,
407}
408
409impl System {
410    /// The value [`SYSTEM_ENV_VAR`] holds, read the way a build system spells
411    /// a boolean: `1`, `on`, `true` and `yes` for [`System::Last`], `first`
412    /// for [`System::First`], and `0`, `off`, `false`, `no` or nothing at all
413    /// for [`System::Off`]. Anything else is `None`, which the caller reports.
414    pub fn from_env_value(value: &str) -> Option<Self> {
415        match value.trim().to_ascii_lowercase().as_str() {
416            "" | "0" | "off" | "false" | "no" => Some(System::Off),
417            "1" | "on" | "true" | "yes" => Some(System::Last),
418            "first" => Some(System::First),
419            _ => None,
420        }
421    }
422
423    /// Whether the platform's directories are searched at all.
424    pub fn is_on(self) -> bool {
425        self != System::Off
426    }
427}
428
429/// The environment variable that turns the platform's directories on for a
430/// whole crate: `1` for [`System::Last`], `first` for [`System::First`].
431///
432/// A `#pragma cinrs system_include` in a unit overrides it.
433pub const SYSTEM_ENV_VAR: &str = "CINRS_SYSTEM_INCLUDE";
434
435/// The environment variable that *replaces* [`system_directories`]'s
436/// per-target default, split the way the platform splits `PATH`.
437///
438/// This is the only way to name the directories on a target whose default
439/// cinrs does not know — Apple's, whose SDK path is knowable only from
440/// `xcrun --show-sdk-path`, and Windows — and the only way to point a cross
441/// build at a sysroot.
442pub const SYSTEM_PATH_ENV_VAR: &str = "CINRS_SYSTEM_INCLUDE_PATH";
443
444/// One place a search looks, in the order it looks.
445///
446/// A [`Resolved`] records the entry it was found under so that an
447/// `#include_next` written inside it can go on from the entry *after* that
448/// one, which is the whole of GCC's semantics for the directive.
449#[derive(Clone, Debug, PartialEq, Eq)]
450pub enum Entry {
451    /// A configured directory: `#pragma cinrs include_path`,
452    /// [`crate::Options::include_paths`] or [`ENV_VAR`].
453    Dir(PathBuf),
454    /// The working directory, which only a quoted name that is itself a path
455    /// is looked for in; see the [module docs](self).
456    WorkingDir,
457    /// The [bundled headers](BUNDLED).
458    Bundled,
459    /// One of the platform's own directories.
460    System(PathBuf),
461}
462
463/// The include directories a unit searches, in the order it searches them.
464///
465/// The lists are kept apart so that the order is a decision rather than an
466/// accident: what the unit itself asks for wins over what the build asked for,
467/// which wins over what the environment asked for, which wins over what is
468/// bundled — and the platform's own directories are not there at all until
469/// [`SearchPaths::enable_system`] puts them there.
470#[derive(Clone, Debug, Default)]
471pub struct SearchPaths {
472    /// Directories from `#pragma cinrs include_path`, in the order written.
473    pragma: Vec<PathBuf>,
474    /// Directories from [`crate::Options::include_paths`].
475    options: Vec<PathBuf>,
476    /// Directories from the `CINRS_INCLUDE_PATH` environment variable.
477    env: Vec<PathBuf>,
478    /// The platform's own directories, empty while the switch is off.
479    system: Vec<PathBuf>,
480    /// Where those go, and whether they are searched at all.
481    mode: System,
482}
483
484/// The environment variable holding a global list of include directories.
485///
486/// Split the way the platform splits `PATH`: with `:` on Unix and `;` on
487/// Windows, through [`std::env::split_paths`].
488pub const ENV_VAR: &str = "CINRS_INCLUDE_PATH";
489
490/// The environment variable Cargo sets to the package's own directory, which
491/// is what a relative `#pragma cinrs include_path` is resolved against.
492pub const MANIFEST_DIR_VAR: &str = "CARGO_MANIFEST_DIR";
493
494impl SearchPaths {
495    /// The directories configured before preprocessing starts.
496    pub fn new(options: &[PathBuf]) -> Self {
497        Self {
498            pragma: Vec::new(),
499            options: options.to_vec(),
500            env: std::env::var_os(ENV_VAR)
501                .map(|value| std::env::split_paths(&value).collect())
502                .unwrap_or_default(),
503            system: Vec::new(),
504            mode: System::Off,
505        }
506    }
507
508    /// Adds a directory named by `#pragma cinrs include_path`.
509    ///
510    /// A relative path is resolved against `CARGO_MANIFEST_DIR` — the package
511    /// being compiled, which the procedural macro reads from the environment
512    /// of the `rustc` process Cargo started — so that a `c99!` block means the
513    /// same thing however the build was invoked. Without that variable (a unit
514    /// test, a hand-rolled `rustc`) the path is left as it stands, and is then
515    /// relative to the working directory.
516    pub fn add_pragma(&mut self, dir: &str) {
517        let path = Path::new(dir);
518        let resolved = if path.is_absolute() {
519            path.to_path_buf()
520        } else {
521            match std::env::var_os(MANIFEST_DIR_VAR) {
522                Some(root) => Path::new(&root).join(path),
523                None => path.to_path_buf(),
524            }
525        };
526        if !self.pragma.contains(&resolved) {
527            self.pragma.push(resolved);
528        }
529    }
530
531    /// Every configured directory, in search order — the ones a `#embed`
532    /// resource is looked for in, which is everything but the header steps.
533    fn dirs(&self) -> impl Iterator<Item = &PathBuf> {
534        self.pragma
535            .iter()
536            .chain(self.options.iter())
537            .chain(self.env.iter())
538    }
539
540    /// Puts the platform's own directories on the path.
541    ///
542    /// Idempotent in the mode that matters: turning the switch on twice with
543    /// the same directories changes nothing, and asking for `first` after
544    /// asking for the plain form moves them, which is what a unit that writes
545    /// both pragmas means.
546    pub fn enable_system(&mut self, mode: System, dirs: Vec<PathBuf>) {
547        self.mode = mode;
548        self.system = dirs;
549    }
550
551    /// Whether — and where — the platform's own directories are searched.
552    pub fn system_mode(&self) -> System {
553        self.mode
554    }
555
556    /// Every place a header is looked for, in the order it is looked for,
557    /// after the including file's own directory.
558    ///
559    /// This is the list `#include_next` walks: a header found under
560    /// entry *i* continues from entry *i + 1*.
561    ///
562    /// `from_system` says the directive was written *in* one of the platform's
563    /// own headers, which puts the platform's directories ahead of the bundled
564    /// ones however the switch was set; see [`Origin::SystemDir`].
565    pub fn entries(&self, from_system: bool) -> Vec<Entry> {
566        let mut entries: Vec<Entry> = self.dirs().cloned().map(Entry::Dir).collect();
567        entries.push(Entry::WorkingDir);
568        let system = self.system.iter().cloned().map(Entry::System);
569        match self.mode {
570            System::Off => entries.push(Entry::Bundled),
571            System::Last if !from_system => {
572                entries.push(Entry::Bundled);
573                entries.extend(system);
574            }
575            System::Last | System::First => {
576                entries.extend(system);
577                entries.push(Entry::Bundled);
578            }
579        }
580        entries
581    }
582}
583
584/// The platform's own include directories for `target`, when nothing named
585/// them.
586///
587/// [`SYSTEM_PATH_ENV_VAR`] takes priority over this and is checked first;
588/// what is left is the default, and there are only two rules to it.
589///
590/// **A cross build has no default.** The directories below belong to the
591/// machine the compiler is *running* on, and a `<sys/stat.h>` laid out for
592/// another architecture is worse than no `<sys/stat.h>` at all — its
593/// `struct stat` would have the wrong offsets and the program would read the
594/// wrong bytes. So a target that is not the host is an error naming
595/// [`SYSTEM_PATH_ENV_VAR`], which is how a sysroot is pointed at.
596///
597/// **Only the C library's directories, never the compiler's.** On Linux that
598/// is `/usr/local/include`, the multiarch directory
599/// (`/usr/include/x86_64-linux-gnu`, and its like, added only when it exists)
600/// and `/usr/include`. GCC's `/usr/lib/gcc/*/include` and Clang's
601/// `/usr/lib/clang/*/include` are deliberately absent: their `limits.h`,
602/// `stdint.h`, `stddef.h` and `stdarg.h` are that compiler's own, chain onward
603/// with `#include_next`, and are bundled here anyway.
604///
605/// **Apple's default is asked of `xcrun`.** The SDK moves with Xcode, so there
606/// is no path to hard-code; `xcrun --show-sdk-path` is the one thing that knows
607/// it, and `apple_sdk_include` below runs it once per process. A macOS user who
608/// writes `#pragma cinrs system_include` therefore gets `<unistd.h>` the way a
609/// Linux user does. When `xcrun` is missing or says nothing — no Xcode, no
610/// command-line tools — the answer is the same error as before, naming
611/// [`SYSTEM_PATH_ENV_VAR`].
612///
613/// Windows and everything else has no default, because there is no such
614/// convention to follow.
615pub fn system_directories(
616    target: &TargetModel,
617    source: &TargetSource,
618) -> Result<Vec<PathBuf>, String> {
619    if let Some(value) = std::env::var_os(SYSTEM_PATH_ENV_VAR) {
620        let dirs: Vec<PathBuf> = std::env::split_paths(&value)
621            .filter(|dir| !dir.as_os_str().is_empty())
622            .collect();
623        if !dirs.is_empty() {
624            return Ok(dirs);
625        }
626    }
627    if *target != TargetModel::host() {
628        let named = match source.triple() {
629            Some(triple) => format!("'{triple}'"),
630            None => "another machine".to_owned(),
631        };
632        return Err(format!(
633            "the platform's include directories are the *host*'s, and this unit is being \
634             translated for {named}; a header laid out for another machine is worse than none, \
635             so point {SYSTEM_PATH_ENV_VAR} at the target's sysroot include directories — or \
636             leave the system headers switched off for this target"
637        ));
638    }
639    match target.os {
640        Os::Linux => {
641            let mut dirs = vec![PathBuf::from("/usr/local/include")];
642            for tuple in multiarch_tuples(target) {
643                let dir = PathBuf::from(format!("/usr/include/{tuple}"));
644                if dir.is_dir() {
645                    dirs.push(dir);
646                }
647            }
648            dirs.push(PathBuf::from("/usr/include"));
649            Ok(dirs)
650        }
651        Os::Darwin => match apple_sdk_include() {
652            Some(dir) => Ok(vec![dir]),
653            None => Err(format!(
654                "on Apple's platforms the C library's headers live inside the SDK, and \
655                 'xcrun --show-sdk-path' — the one thing that knows where it is — is not \
656                 installed or answered nothing, so cinrs has no directory to offer: set \
657                 {SYSTEM_PATH_ENV_VAR} to \"$(xcrun --show-sdk-path)/usr/include\""
658            )),
659        },
660        os => Err(format!(
661            "cinrs has no default include directories for {}; set {SYSTEM_PATH_ENV_VAR} to the \
662             directories the platform's headers live in",
663            os.as_str()
664        )),
665    }
666}
667
668/// `$(xcrun --show-sdk-path)/usr/include`, asked once per process.
669///
670/// Apple's C library headers live inside whichever SDK Xcode is pointing at,
671/// and `xcrun` is the only thing that knows which. A procedural macro is an
672/// ordinary program and may run one, so it does — once, because the answer
673/// cannot change while the process lives and because a unit may ask several
674/// times. `None` when there is no `xcrun`, when it fails, when its output is
675/// not a directory, or when the `usr/include` inside it is not there: every one
676/// of those means "the caller should say to set the environment variable"
677/// rather than "use a path that is not there".
678///
679/// The whole of the Darwin behaviour is in this one function so that the part
680/// a Linux machine cannot test is as small as it can be; [`system_directories`]
681/// is the part unit tests reach.
682fn apple_sdk_include() -> Option<PathBuf> {
683    static SDK: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
684    SDK.get_or_init(|| {
685        let output = std::process::Command::new("xcrun")
686            .arg("--show-sdk-path")
687            .output()
688            .ok()?;
689        if !output.status.success() {
690            return None;
691        }
692        let path = String::from_utf8(output.stdout).ok()?;
693        let path = path.trim();
694        if path.is_empty() {
695            return None;
696        }
697        let include = Path::new(path).join("usr").join("include");
698        include.is_dir().then_some(include)
699    })
700    .clone()
701}
702
703/// The multiarch directory names a Linux target's headers may live under,
704/// most specific first.
705///
706/// Debian's layout, which Ubuntu and a good many others follow: the tuple is
707/// `<arch>-linux-<env>`, with the architecture spelled the way `dpkg` spells
708/// it rather than the way the triple does — `i386` for 32-bit x86,
709/// `powerpc64le` for little-endian 64-bit PowerPC. Only a directory that
710/// really exists is used, which is what lets the Arm entries name both the
711/// hard-float and the soft-float spelling without guessing.
712fn multiarch_tuples(target: &TargetModel) -> Vec<String> {
713    let env = match target.env {
714        Env::Musl => "musl",
715        Env::Bionic => "android",
716        Env::Uclibc => "uclibc",
717        // A Linux triple that names no environment is glibc; see `Env`.
718        _ => "gnu",
719    };
720    let archs: &[&str] = match target.arch {
721        Arch::X86_64 => &["x86_64"],
722        Arch::X86 => &["i386"],
723        Arch::Aarch64 => &["aarch64"],
724        Arch::Arm => &["arm"],
725        Arch::Riscv32 => &["riscv32"],
726        Arch::Riscv64 => &["riscv64"],
727        Arch::PowerPc => &["powerpc"],
728        Arch::PowerPc64 => {
729            if target.big_endian {
730                &["powerpc64"]
731            } else {
732                &["powerpc64le"]
733            }
734        }
735        Arch::S390x => &["s390x"],
736        Arch::Mips => {
737            if target.big_endian {
738                &["mips"]
739            } else {
740                &["mipsel"]
741            }
742        }
743        Arch::Mips64 => {
744            if target.big_endian {
745                &["mips64"]
746            } else {
747                &["mips64el"]
748            }
749        }
750        Arch::Sparc => &["sparc"],
751        Arch::Sparc64 => &["sparc64"],
752        Arch::LoongArch64 => &["loongarch64"],
753        // Nothing installs headers under a wasm tuple.
754        Arch::Wasm32 => &[],
755    };
756    // Arm's ABI is part of the tuple and the model does not carry it, so both
757    // spellings are offered and the one that exists is taken.
758    let suffixes: &[&str] = if target.arch == Arch::Arm && env == "gnu" {
759        &["eabihf", "eabi"]
760    } else {
761        &[""]
762    };
763    let mut tuples = Vec::new();
764    for arch in archs {
765        for suffix in suffixes {
766            tuples.push(format!("{arch}-linux-{env}{suffix}"));
767        }
768    }
769    tuples
770}
771
772/// A header that was found.
773#[derive(Clone, Debug)]
774pub struct Resolved {
775    /// The name diagnostics call it: a path for a file on disk, and
776    /// `<cinrs>/stdio.h` for a bundled header.
777    pub name: String,
778    /// Its contents.
779    pub text: String,
780    /// Where its own `#include "…"` looks first.
781    pub origin: Origin,
782    /// What identifies it for `#pragma once` and the include-guard
783    /// optimisation: the canonical path of the file, or the bundled name.
784    pub key: String,
785    /// The absolute path of the file, for rebuild tracking. `None` for a
786    /// bundled header, which cannot change without the crate changing, and for
787    /// a header taken from one of the platform's own directories, which is
788    /// part of the machine rather than of the crate.
789    pub path: Option<PathBuf>,
790    /// The [entry](Entry) it was found under, which is where an
791    /// `#include_next` written inside it goes on *after*. `None` when it was
792    /// not found by a search at all: an absolute name, or the including file's
793    /// own directory.
794    pub found_in: Option<Entry>,
795    /// Whether it came from one of the platform's own directories.
796    pub system: bool,
797}
798
799impl Resolved {
800    /// Marks a header as one of the platform's own.
801    ///
802    /// Two things follow, and they are the whole of what "system header" means
803    /// here: its own `<…>` includes prefer the platform's directories, so that
804    /// the platform's header set stays self-consistent; and it is not tracked
805    /// for rebuilds, because it belongs to the machine rather than to the
806    /// crate.
807    fn make_system(&mut self) {
808        self.system = true;
809        self.path = None;
810        if let Origin::Dir(dir) = std::mem::take(&mut self.origin) {
811            self.origin = Origin::SystemDir(dir);
812        }
813    }
814}
815
816/// Why a header could not be included.
817#[derive(Clone, Debug)]
818pub enum Error {
819    /// Nothing of that name is anywhere the unit searches.
820    NotFound {
821        /// The directories that were looked in, in order, as a reader can
822        /// check them.
823        searched: Vec<String>,
824    },
825    /// A file of that name is there, but could not be read: no permission, not
826    /// UTF-8, gone between the test and the read.
827    Unreadable {
828        /// The file that could not be read.
829        path: String,
830        /// What the operating system said about it.
831        error: String,
832    },
833}
834
835/// Looks a header name up.
836pub fn resolve(
837    name: &str,
838    form: Form,
839    origin: &Origin,
840    paths: &SearchPaths,
841) -> Result<Resolved, Error> {
842    let mut searched: Vec<String> = Vec::new();
843
844    // An absolute name is not searched for: it either is the file or it is
845    // nothing.
846    if Path::new(name).is_absolute() {
847        return absolute(name, origin);
848    }
849
850    if form == Form::Quoted {
851        match origin {
852            Origin::Dir(dir) | Origin::SystemDir(dir) => {
853                if let Some(mut found) = read_file(&dir.join(name))? {
854                    // A header beside a system header is a system header too:
855                    // `bits/types.h` is reached that way, and it must not
856                    // suddenly start preferring the bundled set.
857                    if let Origin::SystemDir(_) = origin {
858                        found.make_system();
859                    }
860                    return Ok(found);
861                }
862                searched.push(display_dir(dir));
863            }
864            Origin::Bundled => {
865                if let Some(found) = read_bundled(name) {
866                    return Ok(found);
867                }
868                searched.push(BUNDLED_DIR.to_owned());
869            }
870            Origin::Unknown => {}
871        }
872    }
873
874    walk(name, form, paths, from_system(origin), 0, searched)
875}
876
877/// Whether the file writing the directive is one of the platform's own.
878fn from_system(origin: &Origin) -> bool {
879    matches!(origin, Origin::SystemDir(_))
880}
881
882/// A header named by an absolute path, which is not searched for: it either is
883/// the file or it is nothing.
884///
885/// One header of the platform's naming another by its full path keeps the set
886/// together, exactly as a relative name found beside it does.
887fn absolute(name: &str, origin: &Origin) -> Result<Resolved, Error> {
888    match read_file(Path::new(name))? {
889        Some(mut found) => {
890            if from_system(origin) {
891                found.make_system();
892            }
893            Ok(found)
894        }
895        None => Err(Error::NotFound {
896            searched: vec![display_path(Path::new(name))],
897        }),
898    }
899}
900
901/// `#include_next <name>`: the same search, taken up again at the entry
902/// *after* the one the file writing the directive was found under.
903///
904/// GCC's semantics exactly, and the reason the directive exists: a platform's
905/// `<limits.h>` finishes with `#include_next <limits.h>` to reach the *next*
906/// `limits.h` on the path rather than itself. `current` is
907/// [`Resolved::found_in`] of the file the directive is written in; a file that
908/// was not found by a search at all — the unit's own text, or a header taken
909/// from the including file's directory — has no entry to go on from, and GCC
910/// starts such a search at the beginning, which is what `None` does here.
911///
912/// The quoted and the angled forms mean the same thing, as they do in GCC: the
913/// including file's own directory is never step one of an `#include_next`.
914pub fn resolve_next(
915    name: &str,
916    origin: &Origin,
917    current: Option<&Entry>,
918    paths: &SearchPaths,
919) -> Result<Resolved, Error> {
920    if Path::new(name).is_absolute() {
921        return absolute(name, origin);
922    }
923    let system = from_system(origin);
924    let start = match current {
925        Some(entry) => paths
926            .entries(system)
927            .iter()
928            .position(|e| e == entry)
929            .map_or(0, |at| at + 1),
930        None => 0,
931    };
932    walk(name, Form::Angled, paths, system, start, Vec::new())
933}
934
935/// The part of the search that walks [`SearchPaths::entries`], from `start`.
936fn walk(
937    name: &str,
938    form: Form,
939    paths: &SearchPaths,
940    from_system: bool,
941    start: usize,
942    mut searched: Vec<String>,
943) -> Result<Resolved, Error> {
944    for entry in paths.entries(from_system).into_iter().skip(start) {
945        let shown = match &entry {
946            Entry::Dir(dir) | Entry::System(dir) => {
947                if let Some(mut found) = read_file(&dir.join(name))? {
948                    if matches!(entry, Entry::System(_)) {
949                        found.make_system();
950                    }
951                    found.found_in = Some(entry);
952                    return Ok(found);
953                }
954                display_dir(dir)
955            }
956            // A name that is itself a path, taken from the working directory.
957            // This is what `#include __FILE__` needs: the name a header is
958            // known by is relative to the working directory, so a header that
959            // includes itself asks for `some/dir/thing.h` from inside
960            // `some/dir`, which neither the origin nor a `-I` will resolve. A
961            // bare header name is not looked for here, so nothing in the
962            // working directory can shadow a bundled header.
963            Entry::WorkingDir => {
964                if form != Form::Quoted || !is_path(name) {
965                    continue;
966                }
967                if let Some(mut found) = read_file(Path::new(name))? {
968                    found.found_in = Some(entry);
969                    return Ok(found);
970                }
971                display_dir(Path::new(""))
972            }
973            Entry::Bundled => {
974                if let Some(mut found) = read_bundled(name) {
975                    found.found_in = Some(entry);
976                    return Ok(found);
977                }
978                BUNDLED_DIR.to_owned()
979            }
980        };
981        if !searched.contains(&shown) {
982            searched.push(shown);
983        }
984    }
985    Err(Error::NotFound { searched })
986}
987
988/// A resource `#embed` found.
989#[derive(Clone, Debug)]
990pub struct Embedded {
991    /// The name diagnostics call it.
992    pub name: String,
993    /// Its contents, byte for byte.
994    pub bytes: Vec<u8>,
995    /// Its absolute path, for rebuild tracking.
996    pub path: PathBuf,
997}
998
999/// Looks an `#embed` resource up (C23 6.10.3).
1000///
1001/// The search is `#include`'s with the two steps that are about *headers* left
1002/// out: there are no bundled resources — a picture is not a declaration — and
1003/// nothing is read from the working directory by a bare name. What is left is
1004/// the directory of the file the directive is written in, for the quoted form,
1005/// and then the include path.
1006pub fn resolve_embed(
1007    name: &str,
1008    form: Form,
1009    origin: &Origin,
1010    paths: &SearchPaths,
1011) -> Result<Embedded, Error> {
1012    let mut searched: Vec<String> = Vec::new();
1013
1014    if Path::new(name).is_absolute() {
1015        return match read_bytes(Path::new(name))? {
1016            Some(found) => Ok(found),
1017            None => Err(Error::NotFound {
1018                searched: vec![display_path(Path::new(name))],
1019            }),
1020        };
1021    }
1022
1023    if form == Form::Quoted {
1024        match origin {
1025            Origin::Dir(dir) | Origin::SystemDir(dir) => {
1026                if let Some(found) = read_bytes(&dir.join(name))? {
1027                    return Ok(found);
1028                }
1029                searched.push(display_dir(dir));
1030            }
1031            // A bundled header's `#embed` has nowhere of its own to look, and
1032            // the platform's directories hold no resources either — `#embed`
1033            // is for a picture, not for a declaration.
1034            Origin::Bundled | Origin::Unknown => {}
1035        }
1036    }
1037
1038    for dir in paths.dirs() {
1039        if let Some(found) = read_bytes(&dir.join(name))? {
1040            return Ok(found);
1041        }
1042        let shown = display_dir(dir);
1043        if !searched.contains(&shown) {
1044            searched.push(shown);
1045        }
1046    }
1047
1048    // As for a header, a name that is itself a path is looked for from the
1049    // working directory, so that a resource named relative to it can be found
1050    // from a directive written elsewhere.
1051    if form == Form::Quoted && is_path(name) {
1052        if let Some(found) = read_bytes(Path::new(name))? {
1053            return Ok(found);
1054        }
1055        let shown = display_dir(Path::new(""));
1056        if !searched.contains(&shown) {
1057            searched.push(shown);
1058        }
1059    }
1060
1061    Err(Error::NotFound { searched })
1062}
1063
1064/// Reads a candidate resource, with [`read_file`]'s convention: `Ok(None)`
1065/// means there is no such file and the search goes on.
1066fn read_bytes(path: &Path) -> Result<Option<Embedded>, Error> {
1067    match std::fs::metadata(path) {
1068        Ok(meta) if meta.is_file() => {}
1069        _ => return Ok(None),
1070    }
1071    let bytes = std::fs::read(path).map_err(|error| Error::Unreadable {
1072        path: display_path(path),
1073        error: error.to_string(),
1074    })?;
1075    Ok(Some(Embedded {
1076        name: display_path(path),
1077        bytes,
1078        path: std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf()),
1079    }))
1080}
1081
1082/// Whether a header name names a directory as well as a file.
1083///
1084/// Both separators count on every platform: a `#include "sub/thing.h"` is
1085/// written with a forward slash in portable C whatever the host does with
1086/// them.
1087fn is_path(name: &str) -> bool {
1088    name.contains('/') || name.contains('\\')
1089}
1090
1091fn read_bundled(name: &str) -> Option<Resolved> {
1092    let text = bundled(name)?;
1093    let display = bundled_name(name);
1094    Some(Resolved {
1095        text: text.to_owned(),
1096        origin: Origin::Bundled,
1097        key: display.clone(),
1098        name: display,
1099        path: None,
1100        found_in: Some(Entry::Bundled),
1101        system: false,
1102    })
1103}
1104
1105/// Reads the file an `include_c99!("…")` names.
1106///
1107/// Not a search: the path was resolved against the directory of the invoking
1108/// `.rs` file before it got here, so it either is the translation unit or it is
1109/// nothing. What comes back is the same [`Resolved`] a header does — the
1110/// display name for diagnostics and for `__FILE__`, the text, the directory its
1111/// own `#include "…"` searches first, and the absolute path the expansion
1112/// tracks for rebuilds.
1113pub fn read_source(path: &Path) -> Result<Resolved, Error> {
1114    match read_file(path)? {
1115        Some(found) => Ok(found),
1116        None => Err(Error::NotFound {
1117            searched: vec![display_path(path)],
1118        }),
1119    }
1120}
1121
1122/// Reads a candidate path: `Ok(None)` when there is no such file, which means
1123/// the search goes on, and an error when there is one and it cannot be used,
1124/// which means it does not.
1125fn read_file(path: &Path) -> Result<Option<Resolved>, Error> {
1126    match std::fs::metadata(path) {
1127        Ok(meta) if meta.is_file() => {}
1128        // A directory of that name, or nothing at all: keep looking.
1129        _ => return Ok(None),
1130    }
1131    let text = std::fs::read_to_string(path).map_err(|error| Error::Unreadable {
1132        path: display_path(path),
1133        error: error.to_string(),
1134    })?;
1135    let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
1136    let key = std::fs::canonicalize(path)
1137        .unwrap_or_else(|_| absolute.clone())
1138        .display()
1139        .to_string();
1140    Ok(Some(Resolved {
1141        name: display_path(path),
1142        text,
1143        origin: Origin::Dir(path.parent().unwrap_or(Path::new("")).to_path_buf()),
1144        key,
1145        path: Some(absolute),
1146        // Filled in by the search that found it; a file read by name — an
1147        // absolute `#include`, or `include_c99!` — was found by no search.
1148        found_in: None,
1149        system: false,
1150    }))
1151}
1152
1153/// How a path is written in a diagnostic.
1154///
1155/// Relative wherever it can be — a message naming
1156/// `/home/someone/crate/include/foo.h` is a message that differs between two
1157/// machines, which makes it useless in a test and noisy everywhere else — so a
1158/// path inside the working directory is shown relative to it.
1159pub fn display_path(path: &Path) -> String {
1160    let relative = std::env::current_dir()
1161        .ok()
1162        .and_then(|cwd| path.strip_prefix(&cwd).ok().map(Path::to_path_buf));
1163    relative
1164        .unwrap_or_else(|| path.to_path_buf())
1165        .display()
1166        .to_string()
1167}
1168
1169/// How a directory is written in the list a "not found" message shows.
1170fn display_dir(dir: &Path) -> String {
1171    let shown = display_path(dir);
1172    if shown.is_empty() {
1173        // `Path::parent` of a bare file name; the working directory.
1174        ".".to_owned()
1175    } else {
1176        shown
1177    }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182    use super::*;
1183
1184    #[test]
1185    fn every_bundled_header_is_listed_once_and_sorted() {
1186        let mut names: Vec<&str> = BUNDLED.iter().map(|(n, _)| *n).collect();
1187        let count = names.len();
1188        names.sort_unstable();
1189        names.dedup();
1190        assert_eq!(names.len(), count, "a header is listed twice");
1191        let listed: Vec<&str> = BUNDLED.iter().map(|(n, _)| *n).collect();
1192        assert_eq!(listed, names, "keep the table sorted by name");
1193    }
1194
1195    #[test]
1196    fn a_bundled_header_is_found_without_touching_the_disk() {
1197        let found = resolve(
1198            "stddef.h",
1199            Form::Angled,
1200            &Origin::Unknown,
1201            &SearchPaths::default(),
1202        )
1203        .expect("stddef.h is bundled");
1204        assert_eq!(found.name, "<cinrs>/stddef.h");
1205        assert!(found.path.is_none());
1206        assert!(found.text.contains("size_t"));
1207    }
1208
1209    #[test]
1210    fn a_quoted_name_that_is_a_path_is_taken_from_the_working_directory() {
1211        // What `#include __FILE__` in a header comes to: the name is a path
1212        // relative to the working directory, and the directive is written in
1213        // the directory that path leads to, so neither the origin nor a `-I`
1214        // resolves it.
1215        // `cargo test` runs with the package directory as the working
1216        // directory, and `include/` is where the bundled headers live as
1217        // files.
1218        let found = resolve(
1219            "include/stddef.h",
1220            Form::Quoted,
1221            &Origin::Dir(PathBuf::from("include")),
1222            &SearchPaths::default(),
1223        )
1224        .expect("the file is there, relative to the package directory");
1225        assert!(found.text.contains("size_t"));
1226        assert!(
1227            found.path.is_some(),
1228            "it is a real file, not the bundled one"
1229        );
1230    }
1231
1232    #[test]
1233    fn a_bare_name_in_the_working_directory_does_not_shadow_a_bundled_header() {
1234        // `Cargo.toml` is in the working directory and is not a header, but
1235        // the point is the rule: a name with no separator is never looked for
1236        // there, so the step cannot take a `stdio.h` somebody left lying
1237        // about in preference to ours.
1238        assert!(!is_path("stdio.h"));
1239        assert!(is_path("sub/stdio.h"));
1240        assert!(is_path("sub\\stdio.h"));
1241    }
1242
1243    #[test]
1244    fn a_missing_header_lists_where_it_looked() {
1245        let error = resolve(
1246            "nowhere.h",
1247            Form::Quoted,
1248            &Origin::Dir(PathBuf::from("src")),
1249            &SearchPaths::default(),
1250        )
1251        .expect_err("nothing is called nowhere.h");
1252        match error {
1253            Error::NotFound { searched } => assert_eq!(searched, ["src", "<cinrs>"]),
1254            other => panic!("expected a not-found error, got {other:?}"),
1255        }
1256    }
1257
1258    // -- the platform's own directories -------------------------------------
1259
1260    fn with_system(mode: System) -> SearchPaths {
1261        let mut paths = SearchPaths::default();
1262        paths.enable_system(mode, vec![PathBuf::from("/usr/include")]);
1263        paths
1264    }
1265
1266    #[test]
1267    fn the_switch_reads_the_spellings_a_build_system_uses() {
1268        assert_eq!(System::from_env_value("1"), Some(System::Last));
1269        assert_eq!(System::from_env_value(" YES "), Some(System::Last));
1270        assert_eq!(System::from_env_value("First"), Some(System::First));
1271        assert_eq!(System::from_env_value("0"), Some(System::Off));
1272        assert_eq!(System::from_env_value(""), Some(System::Off));
1273        assert_eq!(System::from_env_value("maybe"), None);
1274        assert!(!System::Off.is_on());
1275        assert!(System::Last.is_on() && System::First.is_on());
1276    }
1277
1278    #[test]
1279    fn the_mode_decides_where_the_platform_goes() {
1280        let system = Entry::System(PathBuf::from("/usr/include"));
1281        assert_eq!(
1282            SearchPaths::default().entries(false),
1283            [Entry::WorkingDir, Entry::Bundled],
1284            "off by default"
1285        );
1286        assert_eq!(
1287            with_system(System::Last).entries(false),
1288            [Entry::WorkingDir, Entry::Bundled, system.clone()]
1289        );
1290        assert_eq!(
1291            with_system(System::First).entries(false),
1292            [Entry::WorkingDir, system.clone(), Entry::Bundled]
1293        );
1294        // A directive written inside a system header prefers the platform
1295        // whatever the mode, so that glibc's `<pthread.h>` gets glibc's
1296        // `<time.h>` and there is one `struct timespec` rather than two.
1297        assert_eq!(
1298            with_system(System::Last).entries(true),
1299            [Entry::WorkingDir, system.clone(), Entry::Bundled]
1300        );
1301        assert_eq!(
1302            SearchPaths::default().entries(true),
1303            [Entry::WorkingDir, Entry::Bundled],
1304            "and nothing at all while the switch is off"
1305        );
1306    }
1307
1308    #[test]
1309    fn the_platform_is_searched_only_when_the_switch_is_on() {
1310        // Something every Unix has and this crate does not bundle. The test is
1311        // about the *order*, so a platform without it simply checks less.
1312        let name = "sys/stat.h";
1313        let there = Path::new("/usr/include").join(name).is_file();
1314        let off = resolve(
1315            name,
1316            Form::Angled,
1317            &Origin::Unknown,
1318            &SearchPaths::default(),
1319        );
1320        assert!(off.is_err(), "the platform is not searched by default");
1321        if there {
1322            let on = resolve(
1323                name,
1324                Form::Angled,
1325                &Origin::Unknown,
1326                &with_system(System::Last),
1327            )
1328            .expect("the platform has it");
1329            assert!(on.system, "and it is marked as the platform's");
1330            assert!(on.path.is_none(), "so it is not tracked for rebuilds");
1331            assert!(matches!(on.origin, Origin::SystemDir(_)));
1332        }
1333    }
1334
1335    #[test]
1336    fn the_bundled_copy_wins_unless_the_platform_goes_first() {
1337        let name = "stdio.h";
1338        if !Path::new("/usr/include").join(name).is_file() {
1339            return;
1340        }
1341        let last = resolve(
1342            name,
1343            Form::Angled,
1344            &Origin::Unknown,
1345            &with_system(System::Last),
1346        )
1347        .expect("bundled");
1348        assert_eq!(last.name, bundled_name(name));
1349        let first = resolve(
1350            name,
1351            Form::Angled,
1352            &Origin::Unknown,
1353            &with_system(System::First),
1354        )
1355        .expect("the platform's");
1356        assert!(first.system, "the platform's copy came first");
1357    }
1358
1359    #[test]
1360    fn a_cross_target_has_no_default_directories() {
1361        // The one target that is certainly not the host, whichever machine
1362        // this is: the model differs from the host's in at least its
1363        // architecture.
1364        let host = TargetModel::host();
1365        let triple = if host.arch == Arch::S390x {
1366            "sparc64-unknown-linux-gnu"
1367        } else {
1368            "s390x-unknown-linux-gnu"
1369        };
1370        let target = TargetModel::from_triple(triple).expect("a target this crate models");
1371        let source = TargetSource::Env(triple.to_owned());
1372        // Only meaningful when the environment has not named directories
1373        // itself, which is exactly the case the rule is about.
1374        if std::env::var_os(SYSTEM_PATH_ENV_VAR).is_some() {
1375            return;
1376        }
1377        let message = system_directories(&target, &source).expect_err("a cross build has none");
1378        assert!(message.contains(SYSTEM_PATH_ENV_VAR), "{message}");
1379        assert!(message.contains(triple), "{message}");
1380    }
1381
1382    #[test]
1383    fn the_multiarch_tuple_follows_the_model() {
1384        let tuples =
1385            |triple: &str| multiarch_tuples(&TargetModel::from_triple(triple).expect("modelled"));
1386        assert_eq!(tuples("x86_64-unknown-linux-gnu"), ["x86_64-linux-gnu"]);
1387        assert_eq!(tuples("i686-unknown-linux-gnu"), ["i386-linux-gnu"]);
1388        assert_eq!(tuples("aarch64-unknown-linux-musl"), ["aarch64-linux-musl"]);
1389        assert_eq!(
1390            tuples("arm-unknown-linux-gnueabihf"),
1391            ["arm-linux-gnueabihf", "arm-linux-gnueabi"],
1392            "the ABI is not in the model, so both spellings are offered"
1393        );
1394        assert_eq!(
1395            tuples("powerpc64le-unknown-linux-gnu"),
1396            ["powerpc64le-linux-gnu"]
1397        );
1398        assert_eq!(tuples("s390x-unknown-linux-gnu"), ["s390x-linux-gnu"]);
1399    }
1400}