Skip to main content

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}