dpkg defers SIGINT until it reaches a safe state, so it can still be
writing into the chroot when the watchdog's rm -rf starts racing
through it, failing with "directory not empty" and leaving the tree
half-removed. Retry the removal for a few seconds while the
interrupted children finish dying off; a genuinely stuck tree still
ends in the pkh prune message.
build_binary_package stages the parent of the requested cwd, then
re-derived the package directory inside the staging area from
package/version name patterns plus the calling process's working
directory. That only works by accident for interactive users sitting
in the package directory: an embedded caller whose tree lives at
<job>/tree matches no pattern, and the process cwd means nothing to
a library consumer — the bc build above failed here even though the
tree was staged correctly.
The pointed-at tree is authoritative anyway: its changelog defined
the package, version and series for this build. Resolve its staged
copy outright when it carries a debian/ tree, keep the pattern
search (with the quirks overrides) as a fallback, and hand the
resolved directory to local::build instead of searching again.
The fallback listing of find_package_directory called Path::is_dir
on entries returned by list_files — a host-side stat. For an unshare
context (every local build) those paths are rooted inside the chroot
and do not exist at the same host path, so every entry came out a
non-directory, the 'Found directories' list silently stayed empty
and the failure degraded to the list-less 'Could not find package
directory' variant, hiding the actual layout (seen building bc from
an ubuntu/devel checkout whose staged tree was named 'tree').
Classify entries through the new ContextDriver::is_dir, and log
every entry instead of only those matching the broken host stat.
list_files returns context-relative paths (rooted inside the chroot
for an unshare context, on the remote for ssh): whether an entry is
a directory can only be decided through the context, never with a
host-side stat. Give every driver a proper is_dir rather than
approximating it with exists, so callers can keep directories and
files apart — the deb package-directory search lists directories
only.
An Ubuntu upload of a package sitting at X-2build1 produced
X-2build1ubuntu1: the blind append misrepresents the lineage and,
sorting below the proper X-2ubuntu1, could never supersede it. A real
change on top of a rebuild replaces the marker instead, so the
trailing buildN is now stripped before the ubuntu counter is appended
or incremented: X-2build1 becomes X-2ubuntu1, X-2ubuntu1build1
becomes X-2ubuntu2.
The SIGINT handler only records the interruption and wakes a watchdog
through a self-pipe (async-signal-safe); the watchdog runs the whole
shutdown in thread context — the live view's reporter first, then the
notice and the log hint, then the cleanup hooks, then exit 130. Flows
park in wait_for_shutdown instead of racing it with their own exit,
and an end-to-end test drives the sequence by re-spawning the test
binary and raising SIGINT at itself.
The ephemeral guard registers its chroot removal as an interrupt
cleanup hook and, once interrupted, stands down from its own teardown
so the two cannot race umount/rm; bootstrap bails out of tarball
extraction and the lockfile wait, keeping the hook registered on the
bootstrap error path so the watchdog can remove the partial tree. The
live view registers an interrupt reporter that suspends the widget and
returns the log-file hint, silences the tty rendering of the ^C
keypress (the echoed "^C" can wrap near the right edge and shift the
teardown erase by a row, leaving the first widget line on screen) and
kills the shared draw target so late log records cannot repaint the
cleared bars. Failure summaries stay quiet when interrupted: the
captured errors are just the killed children's death throes, and the
dose-builddebcheck diagnosis is skipped for a dependency failure the
user interrupted themselves.
The library holds only the state its own types need when a Ctrl+C
arrives: the interrupted flag flows check to stand down, the cleanup
hook registry for resources that must not outlive the process (the
ephemeral build chroot), and the live view's reporter slot. The
signal handling itself is CLI wiring and lands separately: nothing
here installs handlers, prints or exits, so a library consumer
embedding these types keeps its own signal disposition.
DebUi seeded both widgets with a "(starting...)" placeholder,
replaced as soon as real content arrived during a build. pkh put
reports through the same view but runs no subprocess, so nothing
ever fed the pane: its seed line stayed on screen for the whole
upload, stacked under the per-file byte progress.
Drop the seeds and add the pane bar to the terminal only when the
first classified line arrives; a phase change (and the final
suspend) takes it off again. Flows without subprocess output now
render the status bar alone.
pkh put only spoke SFTP to the PPA queue, so a failure of the SSH
transport itself (TCP, banner exchange) failed the upload even though
dput happily pushes the same files: its plain ppa: profile goes over
the anonymous FTP queue of ppa.launchpad.net, the same destination
over another port.
Classify the SSH connection failures: Transport (the connection never
came up: resolution, TCP, banner or key exchange) degrades to that FTP
queue — the upload order (payload first, .changes last), the
reverse-order DELE cleanup of a failed upload and the per-chunk
progress reporting all mirror the SFTP path, sharing cleanup_list.
Refused failures (host key not accepted, no matching authentication)
stay errors: silently switching transport would bypass the refusal.
The FTP client is suppaftp's blocking stream, with the time bounds it
does not carry by itself: the control channel's reads and writes, the
data channel's writes and connect (through a custom passive stream
builder), and the NAT workaround for PASV replies announcing an
unroutable address. The queue endpoints (host, port) join
data/launchpad.yml next to the SFTP ones, and the FTP transport is
covered by unit tests against an in-process fake queue plus a live
control-channel handshake with the real server (ignored, network).
The chlog series selector only appeared when the changelog's current
distribution resolved to a known series; a Debian package targeting
'unstable' (or 'stable', 'testing', ...) fell through to keeping the
current series, silently, with no menu.
Resolve the changelog distribution through the suite aliases first
(unstable identifies the same series as sid, which resolves to the
Debian series list). The selector offers an aliased series as
'<suite> (<series>)' — 'unstable (sid)' — preselected, but selects
the suite name: what a changelog distribution field expects, instead
of the codename. Every other label and free-typed input selects
itself, unchanged.
Reflect the selector in the README roadmap checklist.
Debian packages conventionally target 'unstable' in their
debian/changelog distribution field, but the series data (the
distro-info CSVs) only knows codenames: the suite is the alias
'unstable' of the series 'sid', a mapping the debian-distro-info tool
resolves internally without exposing it in its data.
Add a per-dist suite_aliases reference-data key (debian: unstable ->
sid), with two helpers on top: resolve_suite_alias, identifying a
changelog suite name with its series codename and the dist that
codename belongs to, and series_suite_alias, the inverse direction.
The two names identify the same series.
Sources listed with "3.0 (quilt)" extra components (node-jest, php-*,
...) carry one tarball per bundled module next to the main orig, named
<package>_<uver>.orig-<component>.tar.<ext>. fetch_orig_tarball picked
the single file matching ".orig.tar." — which cannot even match the
component naming — so a git pull only fetched the main orig. The later
dpkg-source -b quilt verification then failed with "can't find file to
patch" on the first patch touching a component directory.
Select the files with the existing build::changes::is_orig_tarball
helper (mirroring dpkg's \.orig(-.+)?\.tar\. strip pattern) and fetch
all of them, pristine-tar checkout first with a checksummed archive
download fallback, per tarball.
The end-to-end test now asserts every stanza-listed orig lands in the
package dir instead of just any *.orig.tar.* file, and gains a
node-jest (trixie, 24 components) regression case.
Verified live: pkh pull node-jest -d debian fetches all 15 origs of the
sid ds7 repack, and dpkg-source -b builds the debian.tar.xz and dsc
without touching the series.
The flag was disabled in the initial commit, leaving the binary with
no way to report its number — wrong for a release. clap scopes the
automatic version flag to the root command (-V/--version), so the
per-subcommand -v target-version options of pull and chlog are
unaffected.
The context management interface never worked reliably, and keeping
it exposed presents a feature that is not ready. Contexts remain in
the library as the execution backend for pkh deb (unshare chroots
and friends); only the CLI surface goes.
create_temp_dir probed for a free pkh-<seconds> name and then created
the directory, so two contexts arriving together could both observe a
free name and unpack into the same directory — observed as two e2e
tests started within the same second failing on 'File exists when
hard linking' during the chroot tarball unpack.
Name with sub-second precision and create atomically: a losing race
gets AlreadyExists and falls through to the next attempt, which
removes the probe window instead of narrowing it. The schroot and
ssh drivers already use mktemp -d and need no change.
One deb block per package cannot express two series needing different
rules. Make pull and deb lists of entries, each with its own series
scope: every matching entry applies, in file order, so a stonking
entry can carry its own dependency rules next to the resolute one.
Thread the series through find_package_directory for the
package_directory lookup, which keeps its deb-then-pull fallback.
The resolute linux and linux-riscv controls declare llvm-21-dev
unqualified while their other llvm pieces are :native, so the dpkg
cross rules resolve it against the host architecture — whose
dependency closure needs python3:riscv64, conflicting with the
python3 the control itself declares :native. No resolver can install
that set; sbuild fails on it identically.
Give the deb quirks a dependencies rule set rather than a one-off
native-qualification knob: replace rewrites a declared dependency by
name with a full dependency string (qualifier, version and
restrictions included), inject adds dependencies resolved as if
declared, drop ignores declared ones. Entries are scoped by series so
they can be dropped when the upstream packaging catches up, and rule
names that match nothing are warned about so stale quirks surface.
Ship the llvm-21-dev entry for the resolute kernels: resolve it as
llvm-21-dev:native, keeping the declared <!stage1> restriction.
Subprocesses inherit the session environment, so a translated host
locale leaked into builds: perl-based packaging tools
(dpkg-architecture, dpkg-parsechangelog, quilt, ...) warned about
missing locale settings, change their output with the environment,
and dpkg-buildpackage treats some of that output as data.
Default every command to LANG=C and LC_ALL=C; a caller can still
override explicitly through envs().
The e2e pulled linux-riscv from questing, which went EOL in July
2026 and has since been removed from the mirrors entirely — not even
old-releases carries a Release file for it, so the chroot bootstrap
cannot resolve the series and the e2e can no longer run.
Point it at resolute (26.04), the current LTS: linux-riscv is
packaged there like in every supported series, and an LTS stays
pullable for years where an interim series turns the test stale
within months.
Keep a noble fixture next to it, also not run by default: noble-era
controls declare their build tools unqualified (the :native idiom
landed in later series), so exact dpkg semantics demand
host-architecture instances of them (python3:riscv64, gcc-13:riscv64,
...), and the two-architecture install set is unsolvable — t64
libraries conflict with their own foreign-arch variant, and the
riscv64 toolchain instances drag depends chains that do not resolve
from the chroot sources.
A fresh chroot download failed with mmdebstrap unable to copy the
keyrings: its unshare-mode hooks run under an identity that cannot
read the invoking user's private directories, so the 0700 cache
directory the module created broke the trusted.gpg.d setup hook with
'Permission denied' (builds reusing an already-cached tarball never
hit the path).
Create the cache 0755 and chmod the keyrings 0644 instead. The
planting guard does not weaken: cached keyrings are trusted as-is,
and validate_keyring_dir keeps refusing directories that are not
owned by the current user or are writable by group or others — read
access for the bootstrap tool is not a planting vector. Legacy 0700
cache directories are validated as before, then widened.
Replace the apt-get build-dep passes with a native resolver: the
Build-* clauses are evaluated against the context's package state by
crate::debian::deps (the dpkg-checkbuilddeps equivalent), and the
unsatisfied ones install through explicitly architecture-qualified
names.
apt's --host-architecture build-dep resolution is coarser than the
checker it feeds: unqualified Multi-Arch: same libraries only ever
get the host-architecture variant, and there is no way to also
satisfy the build-architecture needs of a cross build without a
native re-resolution pass — which installs co-install partners that
break unpacking for -dev packages whose variants conflict on
arch-differing files (curl-config; the libcurl4-gnutls-dev
regression). The resolver now applies the same dpkg rules as the
checker, preferring the runnable build-architecture variant of
Multi-Arch: foreign tools, and passes virtual names through to apt.
Build-Conflicts are now checked before anything installs, which the
build-dep passes never did, and a failed dose3 diagnosis no longer
masks the resolver error (a latent flaw of the passes: binary-only
builds have no .dsc for dose-builddebcheck to read).
An end-to-end test pins the expected failure: a cross build whose
build-dependencies cannot satisfy dpkg's cross semantics aborts,
naming the dependency.
The Facts lookup already implemented Dpkg::Deps::KnownFacts
_find_package, but nothing pinned its cross-compilation behaviour:
with host != build, an unqualified dependency resolves against the
HOST architecture instance (or any instance of a Multi-Arch: foreign
package, or an Architecture: all one) and never against the
build-architecture instance of a Multi-Arch: no/same package.
Cover the matrix with unit tests and, like the source-build checker,
with a differential sweep against real dpkg-checkbuilddeps on a
synthetic admindir with -a <host>: the checker and the real tool
agree on every fixture, including the quirks (:native aborting on
Multi-Arch: foreign instances, first-match version binding).
get_commits_since_version silently returned an empty change list when
the previous version carried no tag. Walk the history back to the last
commit that modified the changelog itself and use it as the boundary
instead, so entries stay correct in repositories that commit their
changelogs without tagging them.
Tag detection remains the preferred path. Before falling back to the
changelog commit, the version recorded by the committed changelog is
probed for a tag: an uncommitted newer entry on top (e.g. UNRELEASED
from a previous run) does not hide the previous version's tag.
Overflowing log lines in the pkh deb / pkh build rolling pane used to
wrap onto a second line, corrupting the pane layout. Truncate them to
the terminal width (minus the pane prefix) by display width and append
an ellipsis instead; lines are left whole when the terminal size is
unknown.
pkh covered the package lifecycle but never validated the packaging
itself: broken control stanzas, unparsable changelog versions or
uncommitted debian/ edits only surfaced at build or upload time. pkh
lint lints a source tree with day-one lintian parity plus a native Rust
engine for the checks lintian cannot have.
The wrapper reuses the pkh build output next to the tree when it matches
the current changelog entry and no tree content is newer (mtime walk,
skipping .git/.pc), else packs fresh with dpkg-source -b using weak gzip
compression (the artifact is ephemeral; xz dominated the run at 9.8 s
versus 2.7 s on a 111 MB tree) and symlinks quilt orig tarballs from the
tree's parent, which dpkg-source searches in cwd. Findings are parsed
from the installed lintian into a unified report, deduplicated by tag
name against the native engine, and rendered lintian-shaped
(<L>: <pkg> <type>: <tag> <details>) as text or JSON, colorized at
render time (--color auto/always/never). Exit codes follow lintian's
contract (0 clean, 1 findings at/above --fail-on, 2 runtime error);
lintian's own exit code is ignored because it uses 2 both for findings
and for runtime errors. -d/--dist maps to lintian --profile so the
target distro's rules apply even on a foreign host.
The native engine hosts the first workflow check lintian cannot know:
pkh-debian-changes-not-committed flags debian/ content that is not
committed to git, since the pkh flow builds and uploads the tree as-is.
Checks register in a static registry validated by a unit test, and the
wrapper's parser is pinned by golden tests captured from lintian 2.129
output. Strategies for lintian's Ubuntu blind spots (its vendor data
there is one file plus 14 disabled tags) are specced in
plans/pkh-lint.md, deliberately not implemented yet.
--backport was Debian-only: ~bpo is backports.debian.org's scheme and
its number the Debian release, so Ubuntu targets were rejected outright.
Ubuntu backports have their own documented scheme (Ubuntu version-
strings): the development release's version with a per-release ~YY.MM.1
appended, sorting before it (3.1-1ubuntu2 backported to 22.04 becomes
3.1-1ubuntu2~22.04.1; native 3.1 becomes 3.1~22.04.1) and independent of
the version the target release carries. The .N increments for
subsequent per-release SRU uploads.
backport_series_number becomes backport_suffix_for_series: the release
number comes from the new generic get_series_release_number (version
column of the target series' own distro-info data, leading token kept —
"12" for bookworm, "26.04" out of resolute's "26.04 LTS"; empty column
as on sid/experimental means None), and the suffix is picked per vendor:
~bpoNN+ for Debian (plain integer releases only), ~YY.MM. for Ubuntu.
Unnumbered series still error before anything is written.
Also serialize the changelog tests that mutate the process-global
DEBFULLNAME/DEBEMAIL variables behind a tokio Mutex: run in parallel
they raced each other's identity reads, which started failing
intermittently as generate_entry tests accumulated.
A flagless entry (no --backport/--nmu/--rebuild) targeting an Ubuntu
series was numbered the Debian way: 1.0-1 became 1.0-2 with distribution
noble, and getting the conventional 1.0-1ubuntu1 required hand-editing
the version. The bump now derives from the vendor of the target series
(distro-info): Ubuntu series get the ubuntu suffix convention (1.0-1
becomes 1.0-1ubuntu1, and re-bumping an already-Ubuntu changelog
increments the counter instead of the revision), Debian series keep the
plain revision bump, and series that cannot be resolved to a vendor
(UNRELEASED, unknown) fall back to it too. There is no reverse sync, so
the Ubuntu-to-Debian direction needs no special casing.
Runs with an explicit --series now consult distro-info once, where they
previously queried it not at all; the interactive flow already did for
the series selector. EntryKind::Ubuntu remains the library-level way to
force the numbering regardless of the series.
The version flags were never reachable: --backport was declared but not
read, and compute_new_version's NMU/rebuild numbering sat behind a TODO
asking for CLI wiring (the old positional-bool signature always received
false). generate_entry now takes an EntryKind selected by three mutually
exclusive flags, and is async because the backport numbering derives the
Debian release number of the target series from distro-info:
- --backport: 1.0-1 becomes 1.0-1~bpo12+1 (12 = release number of the
target series, backport suite names accepted too). Re-running on an
already-numbered version bumps the counter; series without a numeric
Debian release (sid, Ubuntu series, UNRELEASED) are rejected before
anything is written.
- --nmu: 1.0-1 becomes 1.0-1.1 (native 1.0 becomes 1.0+nmu1).
- --rebuild: 1.0-1 becomes 1.0-1build1.
An explicit --version overrides all three. compute_new_version went
from four positional bools to a private Bump enum; backport numbering
reuses increment_suffix with a '~bpoNN+' suffix. The CLI prints the
computed new version before opening the editor.
In-tree tools locate their libraries with the *host* pkg-config during
cross builds (the kernel's tools/build feature checks derive their
cflags/ldflags from 'pkg-config --cflags/--libs'), whose search path
only covers the build architecture's pkgconfig dirs. Cross builds of
linux-riscv died in rtla's Makefile.config: libtraceevent/libtracefs
were reported missing although the riscv64 -dev packages were
installed, because pkg-config never saw their .pc files.
The host-arch -dev packages that used to make those checks pass came
from the unscoped arch-indep build-dep pass, whose native
re-resolution b34e86d correctly scoped to the host arch — removing the
accidental co-install along with the bug it papered over. Export
PKG_CONFIG_LIBDIR for the target multiarch instead, so the checks
resolve target-arch libraries directly: no native build-dep bloat and
no wrong-arch linking.
Instead of carrying raw UI in core, the ports now represent everything
the CLI used to do inline:
- Prompter::present shows context outside of a question (the wizard
summary screen, the vendoring notice spacing); TerminalPrompter
prints it on stdout exactly like the println!s it replaces, server
embeds forward it as a display event.
- generate_entry returns the generated entry (package, versions,
series, path) instead of printing; the CLI renders the same lines.
- BuildTarget carries a flow-composed display line and a tee_log flag:
the terminal adapter renders it verbatim ("Building source package
...", "Building ... for series/arch", "Uploading ... to ...") and
uploads open no build log.
- The unmet build-dependency diagnostics are rendered by the CLI from
the typed error, in the original order (details, then summary).
- --verbose constructs no live view at all (an idle widget used to
linger), and the re-vendor offer only logs when it is actually
asked, so headless runs print the error exactly once.
The location embedded in the log record was styled with crossterm: a
remote consumer of pkh's log records would receive ANSI codes inside
the message text. Plain text is the logger's business to style.
The chlog target-series resolution becomes changelog::series_candidates
(UNRELEASED pinning, development-series default and fallbacks modeled
by SeriesCandidates), PPA references get package_info::split_ppa
(shared by pull and deb, now also rejecting empty parts), and SSH
endpoints get context::ContextConfig::from_endpoint — so a library
consumer can resolve series, validate PPAs and build context
configurations without reimplementing the CLI's rules. All three carry
unit tests; main.rs keeps only parsing of flags and error handling.
generate_entry's status messages become log lines, and the unmet
build-dependency diagnostics travel inside the UnmetBuildDependencies
error (its Display carries the full report) instead of being printed
to stderr by the library: the caller renders both like any other
outcome.
put() loses its MultiProgress parameter: the summary, pre-flight and
connection spinners become view messages, the per-file SFTP transfer
reports determinate progress through view.progress (upload_file takes
a byte-count callback instead of an indicatif bar), and the display is
released through view.suspend on every exit path. The hardcoded
trust-on-first-use prompt in the SSH host-key verification becomes the
Prompter::accept_host_key port (fail-closed by default; the terminal
prompter prints the authenticity banner and confirms), so a remote
frontend can surface its own host-key dialog.
The interactive half of pkh new no longer touches the terminal prompt
module directly: run() takes a Prompter, picks the wizard or the plain
resolve path through interactive(), and every select/text/confirm
question (including the verification offers) goes through the port.
Cancellations propagate as Err, preserving Ctrl+C-aborts; the summary
and vendoring-notice prints become log lines. A builder-server embed
can now drive the whole scaffold wizard over its own wire format by
implementing Prompter.
Prompter gains interactive(), select() and text() (with the Validator
type), and confirm() now propagates cancellation as Err so flows abort
instead of silently taking a default when the user hits Ctrl+C. The
terminal prompter implements the full port; the port also re-exports
the path display helper, which is pure presentation formatting used by
events and messages rather than terminal code.
The binary build joins the source build on the reporting ports:
build_binary_package takes a DebBuildOptions struct (replacing eleven
positional arguments), reports target, phases, progress and the
outcome through the environment-agnostic BuildView, and the Phase enum
with its default classifiers moves from the terminal widget into the
deb module (announced through the enter_phase helper). DebUi loses its
inherent event methods and only implements the port; tee logging and
the SIGINT behavior are unchanged.
No behavior change for the CLI; headless consumers pass report::Quiet.
Core flows no longer reach into the terminal UI: build_source_package
takes a BuildSourceOptions struct (source tree, domain options, view,
prompter) and reports phases, messages and outcomes through the
environment-agnostic ports in the new report module. The classifiers
move from ui/logfmt to the core logfmt module, DebUi becomes a
BuildView adapter, the re-vendor retry asks the prompter instead of
checking for a TTY, and artifact/success printing moves to the CLI.
Headless consumers pass report::Quiet; an embedding (e.g. a builder
server forwarding events to a web frontend) implements BuildView and
maps the plain-data events onto its own wire format.
Move the four templates' static file bodies into .tpl files under
data/templates/<id>/, referenced by their manifests' files: lists —
the shell skeleton script (executable, {command}-named) with its
skeleton-only debian/install mapping, the empty template's stub README,
the makefile hello.c/Makefile skeleton with its skeleton-only install
mapping, and go's go.mod/main.go skeleton (the go directive of go.mod
stays a literal: nothing about it is answer-derived).
The empty template ends up hookless — zero Rust, its registry entry
points at no hooks — and src/new/templates/empty.rs is deleted. The
shell and go hooks shrink to their probes (plus go's {go_import_path}
context value); the makefile hooks keep only the existing-tree hint
probing the packaged Makefile for a phony install: target, since that
heuristic reads the tree and cannot be data.
Split the Template trait into a data half and a logic half. Every
template is now declared by a manifest under data/templates/<id>/
(CLI id, wizard label, detection markers, Build-Depends, architecture,
rules dh line, rules-extra body, control source fields, gitignore
entries and static file bodies with {placeholder} substitution),
embedded through the TEMPLATE_SOURCES index and parsed once into the
registry; the order of the index is the wizard menu order and the
detection priority at once. The logic half is the slim TemplateHooks
trait (probe, post_write, file-body overrides merged over the manifest
bodies by path shadowing, Build-Depends/architecture amendments and
extra context values), registered per template as a HOOKS static: a
template without hooks needs zero Rust.
- TemplateId becomes a Copy wrapper of the stable CLI string; the
enum, its all/as_str/display_name/from_label matches and the old
statics array collapse into the registry accessors.
- rust's rules overrides move to data/templates/rust/rules.extra.tpl
with {locked}/{artifact} hook context; python's backend table,
meson/cmake's pkg-config opt-in, autotools' gettext and python's
C-extension hints become hook amendments over the manifest baseline.
- detect.rs drops its hardcoded marker cascade: the manifests'
detect.files drive detection in registry order, with the shell
single-script heuristic and the never-detected empty template kept
as the code special cases they are. License sniffing is untouched.
- The template tests port to manifest validation: registry coverage
and stable order, placeholder presence in the rendering context,
rules composition, the Build-Depends/architecture/dh-line table now
asserted against the manifest data, and the hook shadowing merge.
The static skeleton bodies of the shell/empty/makefile/go templates
stay in their Rust hooks for now; the next commit moves them into
their manifests.