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//! What the bundled set cannot give is the things whose *layout* only the
41//! platform knows — `struct stat`, `DIR`, `pthread_mutex_t`, the real
42//! `FILE` — so a program that needs those turns the switch on with
43//! `#pragma cinrs system_include` (or `CINRS_SYSTEM_INCLUDE=1` in the
44//! environment, which is the crate-wide default the pragma overrides). With
45//! [`System::Last`] the bundled headers still win, and only a name they do not
46//! carry reaches the platform; with [`System::First`] the platform's copy of
47//! every header wins, which is what makes `FILE` the real `struct _IO_FILE`.
48//!
49//! The directories searched are [`SYSTEM_PATH_ENV_VAR`] when it is set, and
50//! otherwise [`system_directories`]'s per-target default. **The compiler's own
51//! private directories are never among them**: GCC's and Clang's
52//! `.../include/{limits,stdint,stddef,stdarg}.h` chain to the next header of
53//! the same name with `#include_next` and expect their own compiler's
54//! builtins, and every one of those headers is bundled here anyway.
55//!
56//! Nothing found under step 5 is tracked for rebuilds: a system header is part
57//! of the machine rather than of the crate, and `include_str!`-ing
58//! `/usr/include/stdio.h` into the build would make every unit rebuild when the
59//! libc package is upgraded, which is not what the file identifies.
60
61use std::path::{Path, PathBuf};
62
63use crate::target::{Arch, Env, Os, TargetModel, TargetSource};
64
65/// The bundled standard headers, as `(name, text)` pairs.
66///
67/// They are compiled into the crate rather than installed anywhere, so no part
68/// of a build depends on where `cinrs` itself lives on disk.
69pub const BUNDLED: &[(&str, &str)] = &[
70    ("alloca.h", include_str!("../include/alloca.h")),
71    ("assert.h", include_str!("../include/assert.h")),
72    ("complex.h", include_str!("../include/complex.h")),
73    ("ctype.h", include_str!("../include/ctype.h")),
74    ("errno.h", include_str!("../include/errno.h")),
75    ("fcntl.h", include_str!("../include/fcntl.h")),
76    ("float.h", include_str!("../include/float.h")),
77    ("inttypes.h", include_str!("../include/inttypes.h")),
78    ("iso646.h", include_str!("../include/iso646.h")),
79    ("limits.h", include_str!("../include/limits.h")),
80    ("math.h", include_str!("../include/math.h")),
81    ("setjmp.h", include_str!("../include/setjmp.h")),
82    ("signal.h", include_str!("../include/signal.h")),
83    ("stdalign.h", include_str!("../include/stdalign.h")),
84    ("stdarg.h", include_str!("../include/stdarg.h")),
85    ("stdatomic.h", include_str!("../include/stdatomic.h")),
86    ("stdbool.h", include_str!("../include/stdbool.h")),
87    ("stdckdint.h", include_str!("../include/stdckdint.h")),
88    ("stddef.h", include_str!("../include/stddef.h")),
89    ("stdint.h", include_str!("../include/stdint.h")),
90    ("stdio.h", include_str!("../include/stdio.h")),
91    ("stdlib.h", include_str!("../include/stdlib.h")),
92    ("stdnoreturn.h", include_str!("../include/stdnoreturn.h")),
93    ("string.h", include_str!("../include/string.h")),
94    ("strings.h", include_str!("../include/strings.h")),
95    ("sys/types.h", include_str!("../include/sys/types.h")),
96    ("threads.h", include_str!("../include/threads.h")),
97    ("time.h", include_str!("../include/time.h")),
98    ("uchar.h", include_str!("../include/uchar.h")),
99    ("unistd.h", include_str!("../include/unistd.h")),
100    ("wchar.h", include_str!("../include/wchar.h")),
101    ("wctype.h", include_str!("../include/wctype.h")),
102];
103
104/// The directory the bundled headers appear to live in.
105///
106/// It is not a directory at all — the headers are strings inside this crate —
107/// but a diagnostic has to name the file it is talking about, and
108/// `<cinrs>/stdio.h` says both which header it is and that it is ours. The
109/// angle brackets keep it from being mistaken for a path that exists.
110pub const BUNDLED_DIR: &str = "<cinrs>";
111
112/// The text of a bundled header, by name.
113pub fn bundled(name: &str) -> Option<&'static str> {
114    BUNDLED
115        .iter()
116        .find(|(n, _)| *n == name)
117        .map(|(_, text)| *text)
118}
119
120/// The display name a bundled header is known by: `<cinrs>/stdio.h`.
121pub fn bundled_name(name: &str) -> String {
122    format!("{BUNDLED_DIR}/{name}")
123}
124
125/// How the header name was spelled.
126#[derive(Clone, Copy, PartialEq, Eq, Debug)]
127pub enum Form {
128    /// `#include "name"`, which searches the including file's directory first.
129    Quoted,
130    /// `#include <name>`, which does not.
131    Angled,
132}
133
134/// The directory a file's own `#include "…"` searches first.
135#[derive(Clone, Debug, Default)]
136pub enum Origin {
137    /// A directory on disk. Empty means the current directory, which is what
138    /// the parent of a bare `lib.rs` comes out as.
139    Dir(PathBuf),
140    /// A directory on disk reached through one of the platform's own
141    /// directories — the including file is a *system* header.
142    ///
143    /// It searches that directory first, as any other file does, and its
144    /// `<…>` includes then go to the platform's directories **before** the
145    /// bundled ones whatever the mode is. That is what keeps the platform's
146    /// header set self-consistent: glibc's `<pthread.h>` gets glibc's
147    /// `<time.h>`, and so one `struct timespec` rather than two.
148    SystemDir(PathBuf),
149    /// The bundled set: one bundled header including another finds it there.
150    Bundled,
151    /// Nowhere — the compiler would not say where the including file is.
152    #[default]
153    Unknown,
154}
155
156/// Whether the platform's own include directories are searched, and where in
157/// the order they go.
158///
159/// Set by `#pragma cinrs system_include` in a unit and by
160/// [`SYSTEM_ENV_VAR`] across a crate; see the [module docs](self).
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum System {
163    /// Not searched at all, which is where the switch starts.
164    #[default]
165    Off,
166    /// Searched after the bundled headers: the bundled `<stdio.h>` still wins,
167    /// and only a header cinrs does not carry — `<sys/stat.h>`, `<pthread.h>`
168    /// — comes from the platform. `#pragma cinrs system_include`.
169    Last,
170    /// Searched before them, so that the platform's copy of every header wins.
171    /// `#pragma cinrs system_include first`.
172    First,
173}
174
175impl System {
176    /// The value [`SYSTEM_ENV_VAR`] holds, read the way a build system spells
177    /// a boolean: `1`, `on`, `true` and `yes` for [`System::Last`], `first`
178    /// for [`System::First`], and `0`, `off`, `false`, `no` or nothing at all
179    /// for [`System::Off`]. Anything else is `None`, which the caller reports.
180    pub fn from_env_value(value: &str) -> Option<Self> {
181        match value.trim().to_ascii_lowercase().as_str() {
182            "" | "0" | "off" | "false" | "no" => Some(System::Off),
183            "1" | "on" | "true" | "yes" => Some(System::Last),
184            "first" => Some(System::First),
185            _ => None,
186        }
187    }
188
189    /// Whether the platform's directories are searched at all.
190    pub fn is_on(self) -> bool {
191        self != System::Off
192    }
193}
194
195/// The environment variable that turns the platform's directories on for a
196/// whole crate: `1` for [`System::Last`], `first` for [`System::First`].
197///
198/// A `#pragma cinrs system_include` in a unit overrides it.
199pub const SYSTEM_ENV_VAR: &str = "CINRS_SYSTEM_INCLUDE";
200
201/// The environment variable that *replaces* [`system_directories`]'s
202/// per-target default, split the way the platform splits `PATH`.
203///
204/// This is the only way to name the directories on a target whose default
205/// cinrs does not know — Apple's, whose SDK path is knowable only from
206/// `xcrun --show-sdk-path`, and Windows — and the only way to point a cross
207/// build at a sysroot.
208pub const SYSTEM_PATH_ENV_VAR: &str = "CINRS_SYSTEM_INCLUDE_PATH";
209
210/// One place a search looks, in the order it looks.
211///
212/// A [`Resolved`] records the entry it was found under so that an
213/// `#include_next` written inside it can go on from the entry *after* that
214/// one, which is the whole of GCC's semantics for the directive.
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub enum Entry {
217    /// A configured directory: `#pragma cinrs include_path`,
218    /// [`crate::Options::include_paths`] or [`ENV_VAR`].
219    Dir(PathBuf),
220    /// The working directory, which only a quoted name that is itself a path
221    /// is looked for in; see the [module docs](self).
222    WorkingDir,
223    /// The [bundled headers](BUNDLED).
224    Bundled,
225    /// One of the platform's own directories.
226    System(PathBuf),
227}
228
229/// The include directories a unit searches, in the order it searches them.
230///
231/// The lists are kept apart so that the order is a decision rather than an
232/// accident: what the unit itself asks for wins over what the build asked for,
233/// which wins over what the environment asked for, which wins over what is
234/// bundled — and the platform's own directories are not there at all until
235/// [`SearchPaths::enable_system`] puts them there.
236#[derive(Clone, Debug, Default)]
237pub struct SearchPaths {
238    /// Directories from `#pragma cinrs include_path`, in the order written.
239    pragma: Vec<PathBuf>,
240    /// Directories from [`crate::Options::include_paths`].
241    options: Vec<PathBuf>,
242    /// Directories from the `CINRS_INCLUDE_PATH` environment variable.
243    env: Vec<PathBuf>,
244    /// The platform's own directories, empty while the switch is off.
245    system: Vec<PathBuf>,
246    /// Where those go, and whether they are searched at all.
247    mode: System,
248}
249
250/// The environment variable holding a global list of include directories.
251///
252/// Split the way the platform splits `PATH`: with `:` on Unix and `;` on
253/// Windows, through [`std::env::split_paths`].
254pub const ENV_VAR: &str = "CINRS_INCLUDE_PATH";
255
256/// The environment variable Cargo sets to the package's own directory, which
257/// is what a relative `#pragma cinrs include_path` is resolved against.
258pub const MANIFEST_DIR_VAR: &str = "CARGO_MANIFEST_DIR";
259
260impl SearchPaths {
261    /// The directories configured before preprocessing starts.
262    pub fn new(options: &[PathBuf]) -> Self {
263        Self {
264            pragma: Vec::new(),
265            options: options.to_vec(),
266            env: std::env::var_os(ENV_VAR)
267                .map(|value| std::env::split_paths(&value).collect())
268                .unwrap_or_default(),
269            system: Vec::new(),
270            mode: System::Off,
271        }
272    }
273
274    /// Adds a directory named by `#pragma cinrs include_path`.
275    ///
276    /// A relative path is resolved against `CARGO_MANIFEST_DIR` — the package
277    /// being compiled, which the procedural macro reads from the environment
278    /// of the `rustc` process Cargo started — so that a `c99!` block means the
279    /// same thing however the build was invoked. Without that variable (a unit
280    /// test, a hand-rolled `rustc`) the path is left as it stands, and is then
281    /// relative to the working directory.
282    pub fn add_pragma(&mut self, dir: &str) {
283        let path = Path::new(dir);
284        let resolved = if path.is_absolute() {
285            path.to_path_buf()
286        } else {
287            match std::env::var_os(MANIFEST_DIR_VAR) {
288                Some(root) => Path::new(&root).join(path),
289                None => path.to_path_buf(),
290            }
291        };
292        if !self.pragma.contains(&resolved) {
293            self.pragma.push(resolved);
294        }
295    }
296
297    /// Every configured directory, in search order — the ones a `#embed`
298    /// resource is looked for in, which is everything but the header steps.
299    fn dirs(&self) -> impl Iterator<Item = &PathBuf> {
300        self.pragma
301            .iter()
302            .chain(self.options.iter())
303            .chain(self.env.iter())
304    }
305
306    /// Puts the platform's own directories on the path.
307    ///
308    /// Idempotent in the mode that matters: turning the switch on twice with
309    /// the same directories changes nothing, and asking for `first` after
310    /// asking for the plain form moves them, which is what a unit that writes
311    /// both pragmas means.
312    pub fn enable_system(&mut self, mode: System, dirs: Vec<PathBuf>) {
313        self.mode = mode;
314        self.system = dirs;
315    }
316
317    /// Whether — and where — the platform's own directories are searched.
318    pub fn system_mode(&self) -> System {
319        self.mode
320    }
321
322    /// Every place a header is looked for, in the order it is looked for,
323    /// after the including file's own directory.
324    ///
325    /// This is the list `#include_next` walks: a header found under
326    /// entry *i* continues from entry *i + 1*.
327    ///
328    /// `from_system` says the directive was written *in* one of the platform's
329    /// own headers, which puts the platform's directories ahead of the bundled
330    /// ones however the switch was set; see [`Origin::SystemDir`].
331    pub fn entries(&self, from_system: bool) -> Vec<Entry> {
332        let mut entries: Vec<Entry> = self.dirs().cloned().map(Entry::Dir).collect();
333        entries.push(Entry::WorkingDir);
334        let system = self.system.iter().cloned().map(Entry::System);
335        match self.mode {
336            System::Off => entries.push(Entry::Bundled),
337            System::Last if !from_system => {
338                entries.push(Entry::Bundled);
339                entries.extend(system);
340            }
341            System::Last | System::First => {
342                entries.extend(system);
343                entries.push(Entry::Bundled);
344            }
345        }
346        entries
347    }
348}
349
350/// The platform's own include directories for `target`, when nothing named
351/// them.
352///
353/// [`SYSTEM_PATH_ENV_VAR`] takes priority over this and is checked first;
354/// what is left is the default, and there are only two rules to it.
355///
356/// **A cross build has no default.** The directories below belong to the
357/// machine the compiler is *running* on, and a `<sys/stat.h>` laid out for
358/// another architecture is worse than no `<sys/stat.h>` at all — its
359/// `struct stat` would have the wrong offsets and the program would read the
360/// wrong bytes. So a target that is not the host is an error naming
361/// [`SYSTEM_PATH_ENV_VAR`], which is how a sysroot is pointed at.
362///
363/// **Only the C library's directories, never the compiler's.** On Linux that
364/// is `/usr/local/include`, the multiarch directory
365/// (`/usr/include/x86_64-linux-gnu`, and its like, added only when it exists)
366/// and `/usr/include`. GCC's `/usr/lib/gcc/*/include` and Clang's
367/// `/usr/lib/clang/*/include` are deliberately absent: their `limits.h`,
368/// `stdint.h`, `stddef.h` and `stdarg.h` are that compiler's own, chain onward
369/// with `#include_next`, and are bundled here anyway.
370///
371/// Apple's platforms have no default because the SDK moves with Xcode and is
372/// only knowable from `xcrun --show-sdk-path`; Windows and everything else has
373/// none because there is no such convention to follow.
374pub fn system_directories(
375    target: &TargetModel,
376    source: &TargetSource,
377) -> Result<Vec<PathBuf>, String> {
378    if let Some(value) = std::env::var_os(SYSTEM_PATH_ENV_VAR) {
379        let dirs: Vec<PathBuf> = std::env::split_paths(&value)
380            .filter(|dir| !dir.as_os_str().is_empty())
381            .collect();
382        if !dirs.is_empty() {
383            return Ok(dirs);
384        }
385    }
386    if *target != TargetModel::host() {
387        let named = match source.triple() {
388            Some(triple) => format!("'{triple}'"),
389            None => "another machine".to_owned(),
390        };
391        return Err(format!(
392            "the platform's include directories are the *host*'s, and this unit is being \
393             translated for {named}; a header laid out for another machine is worse than none, \
394             so point {SYSTEM_PATH_ENV_VAR} at the target's sysroot include directories — or \
395             leave the system headers switched off for this target"
396        ));
397    }
398    match target.os {
399        Os::Linux => {
400            let mut dirs = vec![PathBuf::from("/usr/local/include")];
401            for tuple in multiarch_tuples(target) {
402                let dir = PathBuf::from(format!("/usr/include/{tuple}"));
403                if dir.is_dir() {
404                    dirs.push(dir);
405                }
406            }
407            dirs.push(PathBuf::from("/usr/include"));
408            Ok(dirs)
409        }
410        Os::Darwin => Err(format!(
411            "on Apple's platforms the C library's headers live inside the SDK, whose path only \
412             'xcrun --show-sdk-path' knows, so cinrs has no default to offer: set \
413             {SYSTEM_PATH_ENV_VAR} to \"$(xcrun --show-sdk-path)/usr/include\""
414        )),
415        os => Err(format!(
416            "cinrs has no default include directories for {}; set {SYSTEM_PATH_ENV_VAR} to the \
417             directories the platform's headers live in",
418            os.as_str()
419        )),
420    }
421}
422
423/// The multiarch directory names a Linux target's headers may live under,
424/// most specific first.
425///
426/// Debian's layout, which Ubuntu and a good many others follow: the tuple is
427/// `<arch>-linux-<env>`, with the architecture spelled the way `dpkg` spells
428/// it rather than the way the triple does — `i386` for 32-bit x86,
429/// `powerpc64le` for little-endian 64-bit PowerPC. Only a directory that
430/// really exists is used, which is what lets the Arm entries name both the
431/// hard-float and the soft-float spelling without guessing.
432fn multiarch_tuples(target: &TargetModel) -> Vec<String> {
433    let env = match target.env {
434        Env::Musl => "musl",
435        Env::Bionic => "android",
436        Env::Uclibc => "uclibc",
437        // A Linux triple that names no environment is glibc; see `Env`.
438        _ => "gnu",
439    };
440    let archs: &[&str] = match target.arch {
441        Arch::X86_64 => &["x86_64"],
442        Arch::X86 => &["i386"],
443        Arch::Aarch64 => &["aarch64"],
444        Arch::Arm => &["arm"],
445        Arch::Riscv32 => &["riscv32"],
446        Arch::Riscv64 => &["riscv64"],
447        Arch::PowerPc => &["powerpc"],
448        Arch::PowerPc64 => {
449            if target.big_endian {
450                &["powerpc64"]
451            } else {
452                &["powerpc64le"]
453            }
454        }
455        Arch::S390x => &["s390x"],
456        Arch::Mips => {
457            if target.big_endian {
458                &["mips"]
459            } else {
460                &["mipsel"]
461            }
462        }
463        Arch::Mips64 => {
464            if target.big_endian {
465                &["mips64"]
466            } else {
467                &["mips64el"]
468            }
469        }
470        Arch::Sparc => &["sparc"],
471        Arch::Sparc64 => &["sparc64"],
472        Arch::LoongArch64 => &["loongarch64"],
473        // Nothing installs headers under a wasm tuple.
474        Arch::Wasm32 => &[],
475    };
476    // Arm's ABI is part of the tuple and the model does not carry it, so both
477    // spellings are offered and the one that exists is taken.
478    let suffixes: &[&str] = if target.arch == Arch::Arm && env == "gnu" {
479        &["eabihf", "eabi"]
480    } else {
481        &[""]
482    };
483    let mut tuples = Vec::new();
484    for arch in archs {
485        for suffix in suffixes {
486            tuples.push(format!("{arch}-linux-{env}{suffix}"));
487        }
488    }
489    tuples
490}
491
492/// A header that was found.
493#[derive(Clone, Debug)]
494pub struct Resolved {
495    /// The name diagnostics call it: a path for a file on disk, and
496    /// `<cinrs>/stdio.h` for a bundled header.
497    pub name: String,
498    /// Its contents.
499    pub text: String,
500    /// Where its own `#include "…"` looks first.
501    pub origin: Origin,
502    /// What identifies it for `#pragma once` and the include-guard
503    /// optimisation: the canonical path of the file, or the bundled name.
504    pub key: String,
505    /// The absolute path of the file, for rebuild tracking. `None` for a
506    /// bundled header, which cannot change without the crate changing, and for
507    /// a header taken from one of the platform's own directories, which is
508    /// part of the machine rather than of the crate.
509    pub path: Option<PathBuf>,
510    /// The [entry](Entry) it was found under, which is where an
511    /// `#include_next` written inside it goes on *after*. `None` when it was
512    /// not found by a search at all: an absolute name, or the including file's
513    /// own directory.
514    pub found_in: Option<Entry>,
515    /// Whether it came from one of the platform's own directories.
516    pub system: bool,
517}
518
519impl Resolved {
520    /// Marks a header as one of the platform's own.
521    ///
522    /// Two things follow, and they are the whole of what "system header" means
523    /// here: its own `<…>` includes prefer the platform's directories, so that
524    /// the platform's header set stays self-consistent; and it is not tracked
525    /// for rebuilds, because it belongs to the machine rather than to the
526    /// crate.
527    fn make_system(&mut self) {
528        self.system = true;
529        self.path = None;
530        if let Origin::Dir(dir) = std::mem::take(&mut self.origin) {
531            self.origin = Origin::SystemDir(dir);
532        }
533    }
534}
535
536/// Why a header could not be included.
537#[derive(Clone, Debug)]
538pub enum Error {
539    /// Nothing of that name is anywhere the unit searches.
540    NotFound {
541        /// The directories that were looked in, in order, as a reader can
542        /// check them.
543        searched: Vec<String>,
544    },
545    /// A file of that name is there, but could not be read: no permission, not
546    /// UTF-8, gone between the test and the read.
547    Unreadable {
548        /// The file that could not be read.
549        path: String,
550        /// What the operating system said about it.
551        error: String,
552    },
553}
554
555/// Looks a header name up.
556pub fn resolve(
557    name: &str,
558    form: Form,
559    origin: &Origin,
560    paths: &SearchPaths,
561) -> Result<Resolved, Error> {
562    let mut searched: Vec<String> = Vec::new();
563
564    // An absolute name is not searched for: it either is the file or it is
565    // nothing.
566    if Path::new(name).is_absolute() {
567        return absolute(name, origin);
568    }
569
570    if form == Form::Quoted {
571        match origin {
572            Origin::Dir(dir) | Origin::SystemDir(dir) => {
573                if let Some(mut found) = read_file(&dir.join(name))? {
574                    // A header beside a system header is a system header too:
575                    // `bits/types.h` is reached that way, and it must not
576                    // suddenly start preferring the bundled set.
577                    if let Origin::SystemDir(_) = origin {
578                        found.make_system();
579                    }
580                    return Ok(found);
581                }
582                searched.push(display_dir(dir));
583            }
584            Origin::Bundled => {
585                if let Some(found) = read_bundled(name) {
586                    return Ok(found);
587                }
588                searched.push(BUNDLED_DIR.to_owned());
589            }
590            Origin::Unknown => {}
591        }
592    }
593
594    walk(name, form, paths, from_system(origin), 0, searched)
595}
596
597/// Whether the file writing the directive is one of the platform's own.
598fn from_system(origin: &Origin) -> bool {
599    matches!(origin, Origin::SystemDir(_))
600}
601
602/// A header named by an absolute path, which is not searched for: it either is
603/// the file or it is nothing.
604///
605/// One header of the platform's naming another by its full path keeps the set
606/// together, exactly as a relative name found beside it does.
607fn absolute(name: &str, origin: &Origin) -> Result<Resolved, Error> {
608    match read_file(Path::new(name))? {
609        Some(mut found) => {
610            if from_system(origin) {
611                found.make_system();
612            }
613            Ok(found)
614        }
615        None => Err(Error::NotFound {
616            searched: vec![display_path(Path::new(name))],
617        }),
618    }
619}
620
621/// `#include_next <name>`: the same search, taken up again at the entry
622/// *after* the one the file writing the directive was found under.
623///
624/// GCC's semantics exactly, and the reason the directive exists: a platform's
625/// `<limits.h>` finishes with `#include_next <limits.h>` to reach the *next*
626/// `limits.h` on the path rather than itself. `current` is
627/// [`Resolved::found_in`] of the file the directive is written in; a file that
628/// was not found by a search at all — the unit's own text, or a header taken
629/// from the including file's directory — has no entry to go on from, and GCC
630/// starts such a search at the beginning, which is what `None` does here.
631///
632/// The quoted and the angled forms mean the same thing, as they do in GCC: the
633/// including file's own directory is never step one of an `#include_next`.
634pub fn resolve_next(
635    name: &str,
636    origin: &Origin,
637    current: Option<&Entry>,
638    paths: &SearchPaths,
639) -> Result<Resolved, Error> {
640    if Path::new(name).is_absolute() {
641        return absolute(name, origin);
642    }
643    let system = from_system(origin);
644    let start = match current {
645        Some(entry) => paths
646            .entries(system)
647            .iter()
648            .position(|e| e == entry)
649            .map_or(0, |at| at + 1),
650        None => 0,
651    };
652    walk(name, Form::Angled, paths, system, start, Vec::new())
653}
654
655/// The part of the search that walks [`SearchPaths::entries`], from `start`.
656fn walk(
657    name: &str,
658    form: Form,
659    paths: &SearchPaths,
660    from_system: bool,
661    start: usize,
662    mut searched: Vec<String>,
663) -> Result<Resolved, Error> {
664    for entry in paths.entries(from_system).into_iter().skip(start) {
665        let shown = match &entry {
666            Entry::Dir(dir) | Entry::System(dir) => {
667                if let Some(mut found) = read_file(&dir.join(name))? {
668                    if matches!(entry, Entry::System(_)) {
669                        found.make_system();
670                    }
671                    found.found_in = Some(entry);
672                    return Ok(found);
673                }
674                display_dir(dir)
675            }
676            // A name that is itself a path, taken from the working directory.
677            // This is what `#include __FILE__` needs: the name a header is
678            // known by is relative to the working directory, so a header that
679            // includes itself asks for `some/dir/thing.h` from inside
680            // `some/dir`, which neither the origin nor a `-I` will resolve. A
681            // bare header name is not looked for here, so nothing in the
682            // working directory can shadow a bundled header.
683            Entry::WorkingDir => {
684                if form != Form::Quoted || !is_path(name) {
685                    continue;
686                }
687                if let Some(mut found) = read_file(Path::new(name))? {
688                    found.found_in = Some(entry);
689                    return Ok(found);
690                }
691                display_dir(Path::new(""))
692            }
693            Entry::Bundled => {
694                if let Some(mut found) = read_bundled(name) {
695                    found.found_in = Some(entry);
696                    return Ok(found);
697                }
698                BUNDLED_DIR.to_owned()
699            }
700        };
701        if !searched.contains(&shown) {
702            searched.push(shown);
703        }
704    }
705    Err(Error::NotFound { searched })
706}
707
708/// A resource `#embed` found.
709#[derive(Clone, Debug)]
710pub struct Embedded {
711    /// The name diagnostics call it.
712    pub name: String,
713    /// Its contents, byte for byte.
714    pub bytes: Vec<u8>,
715    /// Its absolute path, for rebuild tracking.
716    pub path: PathBuf,
717}
718
719/// Looks an `#embed` resource up (C23 6.10.3).
720///
721/// The search is `#include`'s with the two steps that are about *headers* left
722/// out: there are no bundled resources — a picture is not a declaration — and
723/// nothing is read from the working directory by a bare name. What is left is
724/// the directory of the file the directive is written in, for the quoted form,
725/// and then the include path.
726pub fn resolve_embed(
727    name: &str,
728    form: Form,
729    origin: &Origin,
730    paths: &SearchPaths,
731) -> Result<Embedded, Error> {
732    let mut searched: Vec<String> = Vec::new();
733
734    if Path::new(name).is_absolute() {
735        return match read_bytes(Path::new(name))? {
736            Some(found) => Ok(found),
737            None => Err(Error::NotFound {
738                searched: vec![display_path(Path::new(name))],
739            }),
740        };
741    }
742
743    if form == Form::Quoted {
744        match origin {
745            Origin::Dir(dir) | Origin::SystemDir(dir) => {
746                if let Some(found) = read_bytes(&dir.join(name))? {
747                    return Ok(found);
748                }
749                searched.push(display_dir(dir));
750            }
751            // A bundled header's `#embed` has nowhere of its own to look, and
752            // the platform's directories hold no resources either — `#embed`
753            // is for a picture, not for a declaration.
754            Origin::Bundled | Origin::Unknown => {}
755        }
756    }
757
758    for dir in paths.dirs() {
759        if let Some(found) = read_bytes(&dir.join(name))? {
760            return Ok(found);
761        }
762        let shown = display_dir(dir);
763        if !searched.contains(&shown) {
764            searched.push(shown);
765        }
766    }
767
768    // As for a header, a name that is itself a path is looked for from the
769    // working directory, so that a resource named relative to it can be found
770    // from a directive written elsewhere.
771    if form == Form::Quoted && is_path(name) {
772        if let Some(found) = read_bytes(Path::new(name))? {
773            return Ok(found);
774        }
775        let shown = display_dir(Path::new(""));
776        if !searched.contains(&shown) {
777            searched.push(shown);
778        }
779    }
780
781    Err(Error::NotFound { searched })
782}
783
784/// Reads a candidate resource, with [`read_file`]'s convention: `Ok(None)`
785/// means there is no such file and the search goes on.
786fn read_bytes(path: &Path) -> Result<Option<Embedded>, Error> {
787    match std::fs::metadata(path) {
788        Ok(meta) if meta.is_file() => {}
789        _ => return Ok(None),
790    }
791    let bytes = std::fs::read(path).map_err(|error| Error::Unreadable {
792        path: display_path(path),
793        error: error.to_string(),
794    })?;
795    Ok(Some(Embedded {
796        name: display_path(path),
797        bytes,
798        path: std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf()),
799    }))
800}
801
802/// Whether a header name names a directory as well as a file.
803///
804/// Both separators count on every platform: a `#include "sub/thing.h"` is
805/// written with a forward slash in portable C whatever the host does with
806/// them.
807fn is_path(name: &str) -> bool {
808    name.contains('/') || name.contains('\\')
809}
810
811fn read_bundled(name: &str) -> Option<Resolved> {
812    let text = bundled(name)?;
813    let display = bundled_name(name);
814    Some(Resolved {
815        text: text.to_owned(),
816        origin: Origin::Bundled,
817        key: display.clone(),
818        name: display,
819        path: None,
820        found_in: Some(Entry::Bundled),
821        system: false,
822    })
823}
824
825/// Reads the file an `include_c99!("…")` names.
826///
827/// Not a search: the path was resolved against the directory of the invoking
828/// `.rs` file before it got here, so it either is the translation unit or it is
829/// nothing. What comes back is the same [`Resolved`] a header does — the
830/// display name for diagnostics and for `__FILE__`, the text, the directory its
831/// own `#include "…"` searches first, and the absolute path the expansion
832/// tracks for rebuilds.
833pub fn read_source(path: &Path) -> Result<Resolved, Error> {
834    match read_file(path)? {
835        Some(found) => Ok(found),
836        None => Err(Error::NotFound {
837            searched: vec![display_path(path)],
838        }),
839    }
840}
841
842/// Reads a candidate path: `Ok(None)` when there is no such file, which means
843/// the search goes on, and an error when there is one and it cannot be used,
844/// which means it does not.
845fn read_file(path: &Path) -> Result<Option<Resolved>, Error> {
846    match std::fs::metadata(path) {
847        Ok(meta) if meta.is_file() => {}
848        // A directory of that name, or nothing at all: keep looking.
849        _ => return Ok(None),
850    }
851    let text = std::fs::read_to_string(path).map_err(|error| Error::Unreadable {
852        path: display_path(path),
853        error: error.to_string(),
854    })?;
855    let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
856    let key = std::fs::canonicalize(path)
857        .unwrap_or_else(|_| absolute.clone())
858        .display()
859        .to_string();
860    Ok(Some(Resolved {
861        name: display_path(path),
862        text,
863        origin: Origin::Dir(path.parent().unwrap_or(Path::new("")).to_path_buf()),
864        key,
865        path: Some(absolute),
866        // Filled in by the search that found it; a file read by name — an
867        // absolute `#include`, or `include_c99!` — was found by no search.
868        found_in: None,
869        system: false,
870    }))
871}
872
873/// How a path is written in a diagnostic.
874///
875/// Relative wherever it can be — a message naming
876/// `/home/someone/crate/include/foo.h` is a message that differs between two
877/// machines, which makes it useless in a test and noisy everywhere else — so a
878/// path inside the working directory is shown relative to it.
879pub fn display_path(path: &Path) -> String {
880    let relative = std::env::current_dir()
881        .ok()
882        .and_then(|cwd| path.strip_prefix(&cwd).ok().map(Path::to_path_buf));
883    relative
884        .unwrap_or_else(|| path.to_path_buf())
885        .display()
886        .to_string()
887}
888
889/// How a directory is written in the list a "not found" message shows.
890fn display_dir(dir: &Path) -> String {
891    let shown = display_path(dir);
892    if shown.is_empty() {
893        // `Path::parent` of a bare file name; the working directory.
894        ".".to_owned()
895    } else {
896        shown
897    }
898}
899
900#[cfg(test)]
901mod tests {
902    use super::*;
903
904    #[test]
905    fn every_bundled_header_is_listed_once_and_sorted() {
906        let mut names: Vec<&str> = BUNDLED.iter().map(|(n, _)| *n).collect();
907        let count = names.len();
908        names.sort_unstable();
909        names.dedup();
910        assert_eq!(names.len(), count, "a header is listed twice");
911        let listed: Vec<&str> = BUNDLED.iter().map(|(n, _)| *n).collect();
912        assert_eq!(listed, names, "keep the table sorted by name");
913    }
914
915    #[test]
916    fn a_bundled_header_is_found_without_touching_the_disk() {
917        let found = resolve(
918            "stddef.h",
919            Form::Angled,
920            &Origin::Unknown,
921            &SearchPaths::default(),
922        )
923        .expect("stddef.h is bundled");
924        assert_eq!(found.name, "<cinrs>/stddef.h");
925        assert!(found.path.is_none());
926        assert!(found.text.contains("size_t"));
927    }
928
929    #[test]
930    fn a_quoted_name_that_is_a_path_is_taken_from_the_working_directory() {
931        // What `#include __FILE__` in a header comes to: the name is a path
932        // relative to the working directory, and the directive is written in
933        // the directory that path leads to, so neither the origin nor a `-I`
934        // resolves it.
935        // `cargo test` runs with the package directory as the working
936        // directory, and `include/` is where the bundled headers live as
937        // files.
938        let found = resolve(
939            "include/stddef.h",
940            Form::Quoted,
941            &Origin::Dir(PathBuf::from("include")),
942            &SearchPaths::default(),
943        )
944        .expect("the file is there, relative to the package directory");
945        assert!(found.text.contains("size_t"));
946        assert!(
947            found.path.is_some(),
948            "it is a real file, not the bundled one"
949        );
950    }
951
952    #[test]
953    fn a_bare_name_in_the_working_directory_does_not_shadow_a_bundled_header() {
954        // `Cargo.toml` is in the working directory and is not a header, but
955        // the point is the rule: a name with no separator is never looked for
956        // there, so the step cannot take a `stdio.h` somebody left lying
957        // about in preference to ours.
958        assert!(!is_path("stdio.h"));
959        assert!(is_path("sub/stdio.h"));
960        assert!(is_path("sub\\stdio.h"));
961    }
962
963    #[test]
964    fn a_missing_header_lists_where_it_looked() {
965        let error = resolve(
966            "nowhere.h",
967            Form::Quoted,
968            &Origin::Dir(PathBuf::from("src")),
969            &SearchPaths::default(),
970        )
971        .expect_err("nothing is called nowhere.h");
972        match error {
973            Error::NotFound { searched } => assert_eq!(searched, ["src", "<cinrs>"]),
974            other => panic!("expected a not-found error, got {other:?}"),
975        }
976    }
977
978    // -- the platform's own directories -------------------------------------
979
980    fn with_system(mode: System) -> SearchPaths {
981        let mut paths = SearchPaths::default();
982        paths.enable_system(mode, vec![PathBuf::from("/usr/include")]);
983        paths
984    }
985
986    #[test]
987    fn the_switch_reads_the_spellings_a_build_system_uses() {
988        assert_eq!(System::from_env_value("1"), Some(System::Last));
989        assert_eq!(System::from_env_value(" YES "), Some(System::Last));
990        assert_eq!(System::from_env_value("First"), Some(System::First));
991        assert_eq!(System::from_env_value("0"), Some(System::Off));
992        assert_eq!(System::from_env_value(""), Some(System::Off));
993        assert_eq!(System::from_env_value("maybe"), None);
994        assert!(!System::Off.is_on());
995        assert!(System::Last.is_on() && System::First.is_on());
996    }
997
998    #[test]
999    fn the_mode_decides_where_the_platform_goes() {
1000        let system = Entry::System(PathBuf::from("/usr/include"));
1001        assert_eq!(
1002            SearchPaths::default().entries(false),
1003            [Entry::WorkingDir, Entry::Bundled],
1004            "off by default"
1005        );
1006        assert_eq!(
1007            with_system(System::Last).entries(false),
1008            [Entry::WorkingDir, Entry::Bundled, system.clone()]
1009        );
1010        assert_eq!(
1011            with_system(System::First).entries(false),
1012            [Entry::WorkingDir, system.clone(), Entry::Bundled]
1013        );
1014        // A directive written inside a system header prefers the platform
1015        // whatever the mode, so that glibc's `<pthread.h>` gets glibc's
1016        // `<time.h>` and there is one `struct timespec` rather than two.
1017        assert_eq!(
1018            with_system(System::Last).entries(true),
1019            [Entry::WorkingDir, system.clone(), Entry::Bundled]
1020        );
1021        assert_eq!(
1022            SearchPaths::default().entries(true),
1023            [Entry::WorkingDir, Entry::Bundled],
1024            "and nothing at all while the switch is off"
1025        );
1026    }
1027
1028    #[test]
1029    fn the_platform_is_searched_only_when_the_switch_is_on() {
1030        // Something every Unix has and this crate does not bundle. The test is
1031        // about the *order*, so a platform without it simply checks less.
1032        let name = "sys/stat.h";
1033        let there = Path::new("/usr/include").join(name).is_file();
1034        let off = resolve(
1035            name,
1036            Form::Angled,
1037            &Origin::Unknown,
1038            &SearchPaths::default(),
1039        );
1040        assert!(off.is_err(), "the platform is not searched by default");
1041        if there {
1042            let on = resolve(
1043                name,
1044                Form::Angled,
1045                &Origin::Unknown,
1046                &with_system(System::Last),
1047            )
1048            .expect("the platform has it");
1049            assert!(on.system, "and it is marked as the platform's");
1050            assert!(on.path.is_none(), "so it is not tracked for rebuilds");
1051            assert!(matches!(on.origin, Origin::SystemDir(_)));
1052        }
1053    }
1054
1055    #[test]
1056    fn the_bundled_copy_wins_unless_the_platform_goes_first() {
1057        let name = "stdio.h";
1058        if !Path::new("/usr/include").join(name).is_file() {
1059            return;
1060        }
1061        let last = resolve(
1062            name,
1063            Form::Angled,
1064            &Origin::Unknown,
1065            &with_system(System::Last),
1066        )
1067        .expect("bundled");
1068        assert_eq!(last.name, bundled_name(name));
1069        let first = resolve(
1070            name,
1071            Form::Angled,
1072            &Origin::Unknown,
1073            &with_system(System::First),
1074        )
1075        .expect("the platform's");
1076        assert!(first.system, "the platform's copy came first");
1077    }
1078
1079    #[test]
1080    fn a_cross_target_has_no_default_directories() {
1081        // The one target that is certainly not the host, whichever machine
1082        // this is: the model differs from the host's in at least its
1083        // architecture.
1084        let host = TargetModel::host();
1085        let triple = if host.arch == Arch::S390x {
1086            "sparc64-unknown-linux-gnu"
1087        } else {
1088            "s390x-unknown-linux-gnu"
1089        };
1090        let target = TargetModel::from_triple(triple).expect("a target this crate models");
1091        let source = TargetSource::Env(triple.to_owned());
1092        // Only meaningful when the environment has not named directories
1093        // itself, which is exactly the case the rule is about.
1094        if std::env::var_os(SYSTEM_PATH_ENV_VAR).is_some() {
1095            return;
1096        }
1097        let message = system_directories(&target, &source).expect_err("a cross build has none");
1098        assert!(message.contains(SYSTEM_PATH_ENV_VAR), "{message}");
1099        assert!(message.contains(triple), "{message}");
1100    }
1101
1102    #[test]
1103    fn the_multiarch_tuple_follows_the_model() {
1104        let tuples =
1105            |triple: &str| multiarch_tuples(&TargetModel::from_triple(triple).expect("modelled"));
1106        assert_eq!(tuples("x86_64-unknown-linux-gnu"), ["x86_64-linux-gnu"]);
1107        assert_eq!(tuples("i686-unknown-linux-gnu"), ["i386-linux-gnu"]);
1108        assert_eq!(tuples("aarch64-unknown-linux-musl"), ["aarch64-linux-musl"]);
1109        assert_eq!(
1110            tuples("arm-unknown-linux-gnueabihf"),
1111            ["arm-linux-gnueabihf", "arm-linux-gnueabi"],
1112            "the ABI is not in the model, so both spellings are offered"
1113        );
1114        assert_eq!(
1115            tuples("powerpc64le-unknown-linux-gnu"),
1116            ["powerpc64le-linux-gnu"]
1117        );
1118        assert_eq!(tuples("s390x-unknown-linux-gnu"), ["s390x-linux-gnu"]);
1119    }
1120}