Upac is under active rewrite. New work happens on lib-rs, not main — please branch off and
target PRs against lib-rs unless you're told otherwise. main only receives merges once a slice
of lib-rs is ready to be the new baseline.
The design and architecture live in the per-chapter design notes under
doc/eng/ (canonical) / doc/rus/ — read them before touching the
orchestrator/stage pipeline, the composefs layer, or the FFI boundary; they explain the why behind
a lot of decisions that aren't obvious from the code alone.
For current work status, check ROADMAP.md (bigger phases) and TODO.md
(near-term, concrete items) before picking something up — they're kept up to date, unlike design
docs which describe intent rather than progress.
See the README for prerequisites and build commands.
Tracked hooks live in .githooks/:
pre-commitrunscargo fmt --allacross the whole workspace before each commit and re-stages whatever it reformats, so formatting never drifts andcargo fmt --all -- --checkin CI never fails on something that should've been caught locally.commit-msglowercases the first letter after afix:/new:prefix, matching this repo's commit message convention.
Git doesn't run tracked hooks automatically (by design — cloning a repo shouldn't execute arbitrary code), so opt in once per clone:
git config core.hooksPath .githooksThe directory tree embedded in each design chapter (between <!-- tree:start -->/<!-- tree:end --> markers) is generated, not hand-edited. Run cargo xtask gen-tree after a structural change
(new crate, moved directory) to refresh it, or cargo xtask gen-tree --check to verify it's still
current before opening a PR.
This repo is REUSE-compliant and multi-licensed by directory:
lib/*(upac-abi,upac-macro,upac-lib,upac-pki) — LGPL-3.0-or-lateruser/*(upac-cli,upac-sign-cli) — GPL-3.0-onlydoc/*— CC-BY-SA-4.0
Every new source file needs an SPDX header matching its directory's license, e.g.:
// SPDX-FileCopyrightText: 2026 <your name>
//
// SPDX-License-Identifier: LGPL-3.0-or-laterFor files where a comment header doesn't make sense (e.g. Markdown, TOML), add an annotation to
REUSE.toml instead. Run reuse lint before submitting — it must be clean.
- No
unwrap()/expect()in library/binary code — everything fallible returnsResultand propagates. Default toimpl From<Source> for TargetError+ bare?. Reach formap_err()instead only when a single unconditionalFromcan't express the conversion: the same source error type needs to become a different local variant depending on the call site (no context to pick from in aFromimpl), the targetResult'sErrisn't a plain error type (e.g. a(StateId, Error)tuple), you're chaining through twoFromhops with no direct one, or you need to attach context (a message, an original input string) thatFromcan't carry. - Imports (
use) always at the top of the file — no inline or fully-qualified paths. Within a command file that defines its ownpub struct Args,clap::Args(the derive macro) collides with it by name — resolve that withuse clap::Args as ClapArgs;+#[derive(ClapArgs)], not an inline#[derive(clap::Args)]. The one real exception left:main.rs'sCommandenum references multiple different modules'Argsstructs by the same name (generate_root::Args,sign_hook::Args, ...) — importing all of them under one local name isn't possible, so those call sites use the full path (commands::sign_hook::Args) instead. Don't reach for a fully-qualified path to avoid an otherwise ordinary import outside that one case. - Same rule for error types used in
impl From<...>: import the source error type at the top viause some_crate::Error as SomeCrateError;(or the type's real name if it isn't already calledError) and reference the alias in the impl — never writeimpl From<some_crate::Error>with the path inline. This applies even when nothing else in the file collides with the bare name; the alias makes it obvious which crate's error is being converted without hunting through the file. The one exception: amacro_rules!macro whose body references another crate's type via a crate-qualified path (e.g.regex::Errorinsideregex_error_from!inerrors.rs) — such a path resolves through the extern prelude at every call site with zero extra imports needed; replacing it with a locally-aliased bare name would instead force every file that invokes the macro to add its own redundantusejust to satisfy it. Don't do that swap inside a multi-call-site macro body. - No comments unless they explain a non-obvious why (a hidden constraint, a workaround, something that would surprise a reader). Don't restate what the code already says.
- Long, descriptive names over abbreviations, in both variables and functions.
- When a file grows past one logical unit, split it into a folder with a
mod.rs, not a single giant file. - Command families under
mutated/unmutated(e.g.diff/diff_packages/diff_prefix/diff_config, orsearch_meta/search_files) stay flat, one folder per command — don't nest them under a shared group folder. The common name prefix already groups them in any directory listing; nesting would only add import/path churn without a real benefit at the current command count.
[package]:nameis always the very first field.description(when the crate has one) goes right aftername, not down with the rest of the block — then every.workspace = truefield, then any crate-specific field (license override, etc.).[dependencies]: bracketed entries with multiple keys first, ordered by descending key count; then, after a blank line, single-line/bare-version entries. Internalupac-*crates get their own subgroup at the very top, ahead of everything else — even other{ workspace = true }entries.- Within any of those groups, non-optional entries come before
optional = trueones.
- Flat
key = valueTOML configs (rustfmt.tomland similar — notCargo.toml): bool fields, then string fields, then number fields, each group separated by a blank line. - No
const X: &Type = value;for a fixed value (a path, a protocol string, a GUID, ...) directly in a.rsfile — ever. It goes in a.tomlfile (lib.toml, or a crate-local equivalent likebooter.toml) with a comment explaining what the value is and why it's fixed, read in via abuild.rscodegen step. The only thing allowed to live as a literal in Rust source is a genuine cross-language C-ABI contract expressed as a real type (an enum discriminant, a#[repr(C)]field) — never a bare string/scalar constant standing in for one.
- Top-level ordering inside a Rust file:
use→macro_rules!blocks → type aliases (simple ones first, then function-pointer types, with a blank line between each fn-pointer type) → enums → (trait→struct→impl), repeating the trait/struct/impl group per logical unit in the file. - In a file that exports
extern "C"symbols: the exportedextern "C" fns go at the top of the file (right after imports and anyinclude!-generated constants), otherpub fns next, privatefns last — the C-ABI surface is what a reader of that file needs to find first. - Within the
useblock:pub use(if any) first, then plain unconditionaluse(grouped std/external/crate::as usual), then every#[cfg(...)]-gateduselast, each as its own block separated by blank lines — a reader should see what's always compiled before what's conditional. - For every other item in the file (not
use), the split is the other way round: right after themoddeclarations, every#[cfg(...)]-gated item (including#[cfg(not(...))]variants) comes first, grouped together; the unconditional functions/structs/impls that follow the normal top-level ordering above come after that.
macro_rules!only for a concrete, present need — never a macro whose only job is calling other macros, and never one written for uniformity that only holds while the code it abstracts over is still unfinished (todo!()).- Each
macro_rules!block is immediately followed by its own visibility line (e.g.pub(crate) use macro_name;) — don't write the macro, move on, and group all the visibility lines together separately later.
- Dispatcher-style functions (e.g.
field_path_validate): the verb always goes last, with modifiers (path,ptr, ...) before it.
- Prefer methods/associated consts on a type over free functions/module-level consts whenever
there's a natural owning type for them — e.g.
Uki::probes()/Self::BOOT_NEXT_VAR, not a bareprobes()/BOOT_NEXT_VARfloating in the module. Free functions are still fine when there's genuinely no owning type (a generic FFI-symbol-loading helper shared across unrelated callers). - Prefer a newtype +
impl Displayover a freeformat_x(&T) -> Stringfunction or a raw unsafe accessor; reuse an existing safeTryFromimpl instead of calling the unsafe conversion directly. - When moving a type into its own crate breaks a foreign-trait impl (orphan rule), wrap it in a
#[repr(transparent)]newtype and cast viafrom_ref()— don't clone to route around it.
- Ask before extracting a one-off call into its own function — don't do it silently. A helper earns its existence only once it does real work for 2+ call sites; a thin pass-through gets duplicated instead of extracted.
- Never
pub use self::module::Itemat a crate or module root — always reach a type through its full module path.
- When two or more otherwise-independent crates need the exact same constant, share the data —
one
.tomlfile each crate's ownbuild.rsreads independently — rather than introducing a compiled shared crate as a dependency between them. Keeps the crates independent while still giving the value a single source of truth. - A
lib.toml-stylebuild.rs+ TOML pair only earns a dedicatedlayoutmodule wrapper when there's more than one section to namespace; for a single-section config, plaininclude!(concat!(env!("OUT_DIR"), "/layout.rs"))at the crate root is simpler.
- Anything with a real
pubsurface gets atests/integration test file in that crate. - Inline
#[cfg(test)]is only for genuinely private modules with no public surface to test through. - Plain
#[test], no test frameworks (norstest, etc).
Keep PRs scoped to one change. Make sure cargo build --workspace, cargo test --workspace --lib --tests, cargo clippy, and reuse lint all pass before opening — see the PR template's checklist.