elfpak_core/diagnostics.rs
1//! Every diagnostic code `elfpak` can print, in one place.
2//!
3//! The CLI renders a failure as `error[E2001]` and a warning as
4//! `warning[E2005]`. Scripts match on those codes, so they are stable, and a
5//! code means exactly one thing. Errors and warnings share one namespace, so
6//! they are declared together; a single list is the only thing that can prove
7//! there are no duplicates.
8//!
9//! The family says what a code is about: `E1xxx` reads an object, `E2xxx`
10//! resolves a dependency, `E3xxx` touches a path, `E4xxx` is configuration,
11//! `E5xxx` is verification.
12
13/// Declare the codes and the list [`ALL`] the uniqueness check reads, so that a
14/// code cannot be added to one without appearing in the other.
15macro_rules! codes {
16 (
17 errors { $($(#[$error_doc:meta])* $error:ident = $error_code:literal;)* }
18 warnings { $($(#[$warning_doc:meta])* $warning:ident = $warning_code:literal;)* }
19 ) => {
20 /// Codes for [`crate::Error`], one per variant. See [`crate::Error::code`].
21 pub mod error {
22 $($(#[$error_doc])* pub const $error: &str = $error_code;)*
23 }
24
25 /// Codes for [`crate::plan::Warning`]. A warning never fails a build; it
26 /// reports something the analysis found that the bundle cannot express.
27 pub mod warning {
28 $($(#[$warning_doc])* pub const $warning: &str = $warning_code;)*
29 }
30
31 /// Every code `elfpak` can print, errors first.
32 ///
33 /// The uniqueness check reads this, as can anything else that
34 /// enumerates the namespace. A caller that wants one code names it
35 /// directly.
36 pub const ALL: &[&str] = &[$($error_code,)* $($warning_code,)*];
37 };
38}
39
40codes! {
41 errors {
42 /// A read or write of a named path failed, carrying the OS error.
43 /// Probing a directory that does not exist is not an error; a missing
44 /// candidate is an ordinary answer during a lookup.
45 IO = "E1000";
46 /// A file begins with the ELF magic but does not parse: truncated, or
47 /// with a header whose offsets do not describe its own contents. Such a
48 /// file is skipped while probing search directories, and reported only
49 /// for a file `elfpak` was told to read.
50 ELF = "E1001";
51 /// The input does not start with `\x7fELF`: a shell script, a wrapper,
52 /// or the wrong file.
53 NOT_ELF = "E1002";
54 /// The executable targets a machine `elfpak` does not package for. The
55 /// raw `e_machine` is named too, because the supported set is smaller
56 /// than the set the parser can identify.
57 UNSUPPORTED_ARCHITECTURE = "E1003";
58 /// A named bound was reached: nodes or edges in the closure, or
59 /// directories in one lookup. Every bound sits far above what a real
60 /// program produces, so this means synthetic or malformed input rather
61 /// than a large application.
62 LIMIT_EXCEEDED = "E1005";
63 /// A source file's digest or size no longer matched the plan when its
64 /// bytes were copied. Something wrote to the source root during the
65 /// run, and the output would no longer match its own manifest.
66 SOURCE_CHANGED = "E1006";
67 /// A `DT_NEEDED` name or `PT_INTERP` matched nothing. The directories
68 /// searched are listed in the order the loader would have tried them.
69 UNRESOLVED_LIBRARY = "E2001";
70 /// The closure needs a library `--allow-library` does not name. The
71 /// allow-list is a contract: a new native dependency fails the build
72 /// instead of growing the image.
73 DISALLOWED_LIBRARY = "E2002";
74 /// A candidate with the right name has the wrong machine, ELF class or
75 /// endianness. It is reported in preference to plain absence, because
76 /// it names what was found; the usual cause is a host library reached
77 /// while packaging from a sysroot for another architecture.
78 INCOMPATIBLE_ARCHITECTURE = "E2003";
79 /// A runtime policy feature found nothing to contribute:
80 /// `--ca-certificates` with no trust store in the source root,
81 /// `--tzdata` with no zoneinfo. A bundle that silently shipped neither
82 /// would fail at its first HTTPS request.
83 MISSING_RUNTIME_FILE = "E2004";
84 /// A path resolved outside the root it belongs to, or through a
85 /// symlinked parent leading out of the output directory.
86 PATH_ESCAPE = "E3001";
87 /// An `--include` names a path the source root does not have. `E2004`
88 /// covers the same situation for runtime policy, which probes several
89 /// candidate locations; an `--include` names exactly one.
90 MISSING_SOURCE_PATH = "E3002";
91 /// Resolving a logical path took more symlink hops than glibc's
92 /// `SYMLOOP_MAX`, so it is a path the loader would not resolve either.
93 SYMLINK_LOOP = "E3003";
94 /// The caller asked for something that does not hold together: an
95 /// unparseable `--user`, no output, an `--install` landing on a library
96 /// the closure needs at that exact path, or a plan grown past
97 /// [`crate::plan::PLAN_ENTRIES_MAX`].
98 CONFIG = "E4001";
99 /// A manifest could not be read, or does not parse as one.
100 MANIFEST = "E4002";
101 /// `elfpak verify` found at least one problem. This carries the
102 /// counts; the problems themselves are printed as they are found.
103 VERIFY_FAILED = "E5001";
104 }
105 warnings {
106 /// An object has an undefined reference to a `dlopen`-family function,
107 /// so it may load libraries no `DT_NEEDED` names. Nothing is known to
108 /// be missing; `--include` covers anything that is.
109 DLOPEN = "E1004";
110 /// A library was found through the build host's `ld.so.cache`, its
111 /// `ld.so.conf`, or `--library-path`. None of those travel with the
112 /// bundle, so the packaged loader will not look where the library sits.
113 LIBRARY_UNREACHABLE = "E2005";
114 /// `--install` moves an executable that declares `$ORIGIN`-relative
115 /// search paths, so those paths no longer point where they did.
116 EXECUTABLE_RELOCATED = "E2006";
117 /// Two objects in the bundle share a soname but differ, and one
118 /// generated `/etc/ld.so.cache` can name only one of them. An
119 /// application that reaches the cache — rather than finding its own
120 /// copy through `DT_RPATH`/`DT_RUNPATH` first — may load the other.
121 LOADER_CACHE_AMBIGUOUS = "E2007";
122 /// `--user` records an identity in `passwd`/`group`, and neither file
123 /// was asked for, so an application that looks its own uid up finds
124 /// nothing.
125 USER_WITHOUT_PASSWD_GROUP = "E4003";
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 /// An `error[E1006]` and a `warning[E1006]` that mean different things
134 /// make the codes useless to match on. This is why they share one list.
135 #[test]
136 fn no_two_diagnostics_share_a_code() {
137 let mut sorted = ALL.to_vec();
138 sorted.sort_unstable();
139 let mut unique = sorted.clone();
140 unique.dedup();
141 assert_eq!(sorted, unique, "a diagnostic code is used twice");
142 }
143
144 #[test]
145 fn codes_are_well_formed() {
146 for code in ALL {
147 let digits = code.strip_prefix('E').expect("codes start with `E`");
148 assert_eq!(digits.len(), 4, "{code}");
149 assert!(digits.bytes().all(|b| b.is_ascii_digit()), "{code}");
150 }
151 }
152}