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}