Concept: .dapk packaging, signing, releases and dependencies
| Status | Draft: concept, not implemented |
| Scope | bin/dabt subcommands, the new lib/dapk/ module (section 14), share/ci/, share/release/ |
| Constraint | Pure bash + POSIX utils + awk. No new runtime dependencies |
| Related | lib/apps/tui_sync.sh (three-way sync), lib/apps/tui_install.sh, lib/apps/tui_update.sh, install.sh, lib/markup/tui_cache.sh (progress palette) |
1. Summary
Developers building a D.A.B.T application need a reproducible way to turn their project into one distributable, verifiable file, publish it, and let end users preview what an update brings before applying it.
This concept introduces:
.dapk, the DABT application package: a deterministic gzip tar whose first member is a signedMANIFESTcarrying per-entry checksums, the app descriptor and its dependencies.dabt buildto produce it, plusverify,info,install,news,release,notes,publish,dep,includeandci init.dabt.pkg, a TOML build configuration, including[[include]]and[[dependency]]tables.- Changelog handling (
## [Unreleased]rewrite,### Newsextraction) and release-note templates. - Ed25519 signatures via
ssh-keygen -Y, embedded in the package. - GitHub release publishing as draft, prerelease or full release.
- CI templates (GitHub Actions, GitLab CI) that a project can drop in.
- A consistent CLI experience:
install.sh-style steps, the cache-spinner color palette, CI-aware output, and full logs.
2. Goals and non-goals
Goals
- G1. One command produces a reproducible, signed, self-verifying package.
- G2. The manifest lists a checksum for every top-level file and directory of the package.
- G3. Versions and build numbers are derived, never hand-edited per build, in CI and locally.
- G4. Users can read “what’s new” for an update without running or downloading it.
- G5. Publishing to GitHub is one command, safe by default (draft).
- G6. Dependencies on system packages are declared, checked, and installed only with user consent.
- G7. No new dependencies beyond tools already assumed by DABT, plus
ssh-keygenfor signing.
Non-goals
- Auto-update daemons or background checking.
- Dependency resolution between
.dapkpackages (only system packages; see 11). - Hosting or a package registry. Distribution is via GitHub releases or any HTTPS URL.
- Windows support beyond what bash provides.
- Sandboxing installed apps.
3. Terminology
| Term | Meaning |
|---|---|
Package / .dapk |
The distributable build artifact |
dabt.pkg |
Developer-authored build configuration (TOML) |
| Descriptor | The header section of MANIFEST. There is no separate descriptor file. |
| Top-level entry | A direct child of the package root (file or directory) |
| Full version | <semver>[-<suffix>]-<build>, e.g. 1.4.2-dev-57 |
| News | Bullets from a changelog’s ### News section |
| Signer | The Ed25519 key that signed MANIFEST |
4. Package format
4.1 Identity
- Extension:
.dapk, held in one constant,DABT_PKG_EXT. - Filename:
<name>-<version>-<build>.dapk, e.g.myapp-1.4.2-57.dapk. - Container: gzip-compressed POSIX tar with a single root directory
<name>-<version>-<build>/. - A file is a valid
.dapkonly if its first tar member is<root>/MANIFESTand begins with# DABT-MANIFEST 1. Plain tarballs are rejected.
4.2 Layout
myapp-1.4.2-57/
MANIFEST first member: descriptor + checksums
MANIFEST.sig ssh-keygen signature over MANIFEST
NEWS extracted News bullets (only if a changelog is configured)
CHANGELOG.md with the Unreleased header rewritten
bin/ lib/ config/ ... project content plus [[include]] targets
Because MANIFEST is first, dabt info reads it with tar -xzOf pkg '*/MANIFEST' and stops early, without unpacking the archive.
4.3 MANIFEST specification
Line-oriented UTF-8, \n line endings, parseable with bash and awk.
# DABT-MANIFEST 1
name=myapp
version=1.4.2-dev
build=57
commit=abc1234
built=2026-09-21T12:00:00Z
entry=bin/myapp.sh
requires_dabt=>=0.9
homepage=https://github.com/me/myapp
news_url=https://github.com/me/myapp/releases/latest/download/myapp-news.txt
signer=SHA256:<fingerprint>
dep.1.name=ripgrep
dep.1.optional=false
dep.1.check=command -v rg
dep.1.apt=ripgrep
dep.1.pacman=ripgrep
---
d <sha256> - - bin
d <sha256> - - lib
f <sha256> 1204 644 README.md
- Header:
key=value, first=splits, no quoting. Unknown keys are preserved and ignored by older readers. ---separates header from entries.- Entry line:
<type> <sha256> <size> <mode> <path>, whitespace-separated,pathlast.typeisford. Directories carry-for size and mode. - Entries are the top-level entries only, sorted with
LC_ALL=C.MANIFESTandMANIFEST.sigare excluded because they cannot describe themselves. - Modes are normalized: files
644, or755if any exec bit is set. Directories are not recorded.
4.4 Checksum algorithm
- File hash:
sha256of the content. - Directory hash (Merkle):
sha256of the concatenation, inLC_ALL=Cname order, of one line per direct child:<type> <hash> <mode> <name>\n. A child directory contributes its own directory hash and-for mode. - A change to any file at any depth changes the hash of its top-level ancestor.
- Hash tool:
sha256sum, falling back toshasum -a 256. Hashing is a single pass; the file count is known up front for progress reporting.
4.5 Reproducibility
Building the same tree with the same inputs yields a byte-identical .dapk:
- Members are added from an explicit list:
MANIFEST,MANIFEST.sig, then everything else inLC_ALL=Corder. --owner=0 --group=0 --numeric-owner, mtime fixed toSOURCE_DATE_EPOCH(default: the commit date ofHEAD, else the build date),gzip -n.built=in the header is the only non-deterministic field, and it derives from the same epoch.
4.6 Sidecars (in dist/)
| File | Purpose |
|—|—|
| <pkg>.dapk.sha256 | Download integrity only |
| <name>-news.txt | Preview without downloading the package |
| RELEASE_NOTES.md | Rendered release notes |
| build.log | Full build log (section 12) |
5. Configuration: dabt.pkg
5.1 Format
A strict TOML subset, so every accepted file is valid TOML and works with editors and linters. lib/dapk/toml.sh (awk) supports: comments, bare and quoted keys, strings, booleans, integers, string arrays, [table] and [[array-of-tables]]. Anything else is an error with a line number. The parser emits flat lines (key=value, include.N.field=value, dependency.N.field=value), so the rest of the build code never handles TOML.
Alternatives were considered in section 15.
5.2 Example
name = "myapp"
entry = "bin/myapp.sh"
requires_dabt = ">=0.9"
homepage = "https://github.com/me/myapp"
changelog = "CHANGELOG.md"
suffix = "" # dev | prerelease | rc.N; CLI flag overrides
release_template = ".dabt/release.tpl"
sign_key = "~/.ssh/dabt_ed25519"
publish_repo = "me/myapp" # default: derived from git remote
include_paths = ["bin", "lib", "config", "share"]
exclude = ["*.log", ".git", "tests"]
[[include]]
type = "file"
source = "LICENSE"
target = "docs/LICENSE"
[[include]]
type = "directory"
source = "shared-assets"
target = "share/assets"
recursive = true
exclude = ["*.tmp"]
optional = true
[[dependency]]
name = "ripgrep"
check = "command -v rg"
version_cmd = "rg --version"
min_version = "13.0"
apt = "ripgrep"
pacman = "ripgrep"
Defaults without a dabt.pkg: name = directory name, everything except dotfiles included.
5.3 [[include]]
| Field | Required | Meaning |
|—|—|—|
| type | yes | file or directory (aliases f, d) |
| source | yes | Path relative to the project root |
| target | yes | Path inside the package. A file target ending in / keeps the source basename. |
| recursive | no (directory) | false (default): top-level files only. true: descend. |
| exclude | no | Glob list for this entry |
| optional | no | true: skip a missing source. Default false: error. |
| overwrite | no | Permit replacing a target produced earlier. Default false: collision is an error. |
| follow_symlinks | no | Default false |
Rules:
sourcemust resolve inside the project root...and absolute paths are rejected unless--allow-external, andoptionaldoes not bypass this.targetmust be relative and contain no...- Symlinks are rejected (build error) unless
follow_symlinks = true, in which case they are dereferenced. - A directory include matching no files runs
mkdir -pon the target and emits a warning (include '<src>' matched no files).--strictpromotes it to an error. - Resolution order: base tree, then
[[include]]entries in file order, then CLI entries.
CLI equivalents (entry-scoped flags apply to the most recent -f/-d):
dabt build -f -s LICENSE -t docs/LICENSE -d -s shared-assets -t share/assets -r
dabt include add|list|rm # edits [[include]] tables in dabt.pkg
dabt build --plan # print resolved source -> target list; write nothing
5.4 [[dependency]]
| Field | Meaning |
|—|—|
| name | Label, and default check target |
| check | Shell test; exit 0 = present. Default command -v <name> |
| version_cmd, min_version | Optional version gate, compared with sort -V |
| optional | Reported only; never blocks or fails |
| apt pacman dnf zypper apk brew | Package name per package manager |
| install | Fallback command when no mapping matches (discouraged, see 10.4) |
dabt build validates the schema and warns for a dependency with neither a mapping nor install. Dependencies are copied into the MANIFEST header as dep.N.* and are therefore covered by the signature. dabt dep add|list|rm manages the tables.
6. Versioning
- Version source:
VERSION(semver). In CI, a tagvX.Y.Z[-suffix]overrides it. - Suffix:
--suffix dev|prerelease|rc.Norsuffix=indabt.pkg, yielding1.4.2-dev. Ordering follows semver prerelease rules. - Build number, first match wins:
DABT_BUILD_NUMBER,GITHUB_RUN_NUMBER,CI_PIPELINE_IID,git rev-list --count HEAD, else0. - Full version:
1.4.2-dev-57. Per semver, build metadata does not affect precedence. dabt versionprints the resolved version.dabt version bump patch|minor|majoreditsVERSION.
7. Changelog and News
7.1 Format
Keep-a-Changelog conventions:
## [Unreleased]
### News
- New file explorer pane
- Mouse drag now works in tabs
(indented continuation lines extend the previous bullet)
### Fixed
- ...
## [1.4.1] - 2026-08-30
### News
- ...
- A
### Newssection runs until the next heading of level 1-3. - A bullet is a line starting with
-. Indented lines that follow continue it. Other lines are ignored.
7.2 Build behavior (never mutates the working tree)
In the packaged copy of the changelog, ## [Unreleased] becomes ## [<version>] - <date> and a fresh empty ## [Unreleased] is inserted above it. For suffixed builds the header is ## [<version>-<build>] - <date>.
The section for the built version yields:
NEWSinside the package, oneversion<TAB>textline per bullet.<name>-news.txtindist/, with the same content.
7.3 dabt release [patch|minor|major] (mutates the working tree)
Preconditions: clean tree, non-empty Unreleased section. Steps: bump VERSION, apply the header rewrite on disk, commit Release v<ver>, tag v<ver>. It never pushes unless --push is given. Suffixed builds never consume Unreleased on disk.
7.4 Loader (lib/dapk/news.sh)
dapk.news.load <file|url>fillsDAPK_NEWS_VERSIONS[]andDAPK_NEWS_ITEMS[].dapk.news.since <installed-version>filters to newer versions.dabt news <pkg|file|url>prints the bullets.- The update path fetches only
news_urlfirst, so users see the changes before anything is downloaded or applied. - A
<news src=... since=.../>markup tag renders the list into a pane.
8. Release notes templates
- Location:
release_templateindabt.pkg, default.dabt/release.tpl.share/release/default.tplships as the fallback. -
Engine: awk, three constructs only:
{{var}}substitution{{#if var}} ... {{/if}}{{section:Name}}(any changelog section of the built version)
- Variables:
name,version,build,date,commit,news,compare_url,checksums(top-level manifest entries). - Unknown variables are a build error (typos must not silently produce empty notes). Output goes to
dist/RELEASE_NOTES.md;dabt notesrenders it without building.
# {{name}} {{version}}
{{#if news}}
## What's new
{{news}}
{{/if}}
{{section:Fixed}}
**Full changelog:** {{compare_url}}
9. Publishing to GitHub
dabt publish [--draft | --prerelease | --release]
- Transport:
curlagainst the GitHub REST API withGITHUB_TOKEN. Ifghis present it may be used instead. No hard dependency ongh. - Mode:
- Default is
--draft. - A suffixed version forces
--prerelease, and--releaseis refused for it. dabt publish --releaseon an existing draft promotes it (PATCH draft=false).
- Default is
- Steps:
- Locate an existing release for
v<version>. Draft releases are not returned byGET /releases/tags/{tag}, so list releases and matchtag_name. This makes reruns idempotent. - Create or update it with the rendered
RELEASE_NOTES.mdas body. - Upload the
.dapk,.dapk.sha256and-news.txt. Existing assets of the same name are replaced.
- Locate an existing release for
- The repository is
publish_repo, else derived from thegit remote. - The token is read from the environment only, is never logged, and is masked in any logged command or header.
10. Signing, trust and verification
10.1 Choice
Ed25519 via ssh-keygen -Y sign|verify, namespace dabt-pkg.
Rationale: OpenSSH is near-universal, needs no GPG keyring or web of trust, has a stable signature format, and verifies against a plain allowed_signers file. It adds no dependency beyond OpenSSH.
10.2 What is signed
MANIFEST only, producing MANIFEST.sig inside the package. The manifest pins every top-level hash, so a valid signature covers the whole package, and a .dapk verifies with no external files. The .sha256 sidecar is transport integrity only and is not trusted for authenticity.
10.3 Verification (dabt verify, and the first step of dabt install)
- Structure check: first member is
MANIFESTwith the expected magic. - Signature check of
MANIFESTagainst$TUI_HOME/trusted_signers. - Recompute every top-level hash and compare with the manifest, and reject members not listed.
- Any failure is fatal. There is no “install anyway”.
10.4 Trust and privilege model
- Trust store:
$TUI_HOME/trusted_signers(allowed_signersformat), pinned per app name. - First install of an unknown signer: explicit prompt showing the fingerprint, accepted only with an interactive yes or
--trust-key.--yesnever trusts a key. - A signer change for an already-installed app fails hard.
- CI signing keys come from a secret, are written to a mode-600 temp file, and are removed after the build.
sudois used only for dependency installation, only when not already root, and is always shown in the plan. dabt never handles a password.- Custom
installcommands are the most dangerous surface: printed verbatim, flaggedcustom command, never batched, and always confirmed individually unless--allow-custom-installis passed.--yesdoes not cover them.
11. Installation and dependencies
11.1 dabt install <pkg.dapk|url>
Order of operations (nothing is written before step 5):
- Fetch, if a URL. Check
.sha256if present. verify(section 10.3).- Show descriptor and News.
- Dependency step (11.2).
- Extract to
$TUI_HOME/apps/<name>/(override with--install-path) and copyMANIFESTalongside. - Register the app, and write a log (section 12.3).
dabt install --plan prints the verify result, dependency plan and commands, executing nothing.
11.2 Dependency flags
| Flag | Effect |
|---|---|
| (none) | Check; if required dependencies are missing, show the plan and ask Install missing dependencies? [y/N] |
--install-dependencies |
Same plan; prompt defaults to yes ([Y/n]); without a TTY requires --yes or fails |
--yes |
Answers the dependency prompt with yes only |
--no-deps |
Run checks and warn; no prompt, no install; app is installed and marked deps=unmet |
--require-deps |
Missing required dependencies abort the install (exit 1, nothing written) |
| Invocation | Condition | Result |
|---|---|---|
| default, TTY | answer yes | dependencies installed, then app |
| default, TTY | answer no | warn; app installed; deps=unmet |
| default, no TTY | n/a | behaves as --no-deps |
--require-deps |
yes and install succeeds | dependencies installed, then app |
--require-deps |
no, or an install fails | exit 1, nothing installed |
--require-deps, no TTY, no --yes |
n/a | exit 1 (prompt cannot be answered) |
--no-deps |
n/a | warn; app installed; deps=unmet |
--yes alone |
n/a | default flow with the prompt auto-answered yes |
Rules:
--no-depscombined with--install-dependenciesor--require-depsis a usage error (exit 2).- Optional dependencies appear in the plan and prompt but never cause failure, even under
--require-deps. - One batched command per package manager is shown and run, for example
sudo pacman -S --needed ripgrep fzf. - Package manager detection: first of
pacman,apt-get,dnf,zypper,apk,brewfound withcommand -v, tie-broken by/etc/os-release.--pm <name>overrides. - After installation every
checkis re-run. Failures are summarized and the exit code is non-zero. deps=unmetis written into the installed copy ofMANIFEST, cleared bydabt deps <app>when all checks pass, and reported when an app with unmet dependencies is launched.
12. CLI reference and user experience
12.1 Commands
| Command | Purpose |
|—|—|
| dabt build [dir] | Build dist/<pkg>.dapk and sidecars |
| dabt verify <pkg> | Signature and hash verification |
| dabt info <pkg> | Descriptor, dependencies, News, signer |
| dabt install <pkg\|url> | Install (section 11) |
| dabt deps [app\|pkg] | Check dependencies |
| dabt news <pkg\|file\|url> | Print News bullets |
| dabt version [bump ...] | Show or bump version |
| dabt release [bump] | Cut a release commit and tag |
| dabt notes | Render release notes |
| dabt publish [mode] | GitHub release |
| dabt include ..., dabt dep ... | Edit dabt.pkg tables |
| dabt ci init github\|gitlab | Copy a CI template into the project |
12.2 Presentation
- Layout, colors and helpers match
install.sh:[n/N]step headers (bold blue),✓(green),!(yellow),✗(red), dim key-value lines. They are implemented inlib/dapk/ui.sh(dapk.ui.step,ok,warn,err,kv), which has no dependency ontui.shstate.install.shadopts it later (14.3). - Human output goes to stderr. Machine-readable results (artifact path from
build, data frominfoandnews) go to stdout, sopath=$(dabt build)works.install.shkeeps its current stream behavior. - Exit codes:
0success,1failure,2usage error,3refused action (as ininstall.sh). - Warnings are collected and repeated as a count and list at the end.
--strictmakes any warning exit 1.
12.3 Progress bar
Shown only when stdout is a TTY and no CI environment is detected:
DABT: (Hashing files) ▕██████████████░░░░░░░░░░░░░░░░▏ 47%
- Identical look to the cache spinner bar: 30 cells,
█filled,░empty,▕/▏end caps. If the locale is not UTF-8 it falls back to#,-,[,]. - One line redrawn with
\r\e[K, repainted only when the integer percent changes, no forks per redraw. - Colors reuse the cache spinner palette from
lib/markup/tui_cache.sh(tui.cache.warm_with_spinner):
| Segment | Color | RGB | 256-color |
|---|---|---|---|
| 1 | pink | 255;140;191 |
212 |
| 2 | blue | 168;216;255 |
153 |
| 3 | yellow | 255;243;168 |
229 |
| 4 | red | 255;158;158 |
217 |
Filled cells are colored by position along the bar. Empty cells are 238, brackets 245, label and percent 250. Truecolor when COLORTERM contains truecolor or 24bit, else the 256-color values.
- The four letters of
DABT:take the logo pastels. For steps with no measurable progress (upload, package-manager install, signing) the letters rotate every 400 ms and the bar is omitted. - Work runs in the background with output to the log. The foreground polls with
read -t 0.2on a fifo (the pattern used by the cache spinner), with nosleepforks. A step ends with✓ <step>. - Below roughly 60 columns the bar degrades to the percent only.
- Progress units: files hashed / total,
tarmembers written or read / counted beforehand, packages installed / total, assets uploaded / total. - The palette and the bar renderer live in
lib/dapk/ui.sh(dapk.ui.bar PCT WIDTH).tui_cache.shkeeps its own copy (refactor out of scope, D10).
12.4 CI behavior
- CI is detected when any of
CI,GITHUB_ACTIONS,GITLAB_CI,JENKINS_URL,BUILDKITE,TF_BUILDis set, or stdout is not a TTY. - In CI the bar is replaced by plain line steps (
[3/6] Hashing, then✓ 42 files) with no\r.--progressand--no-progressoverride. - On GitHub Actions, warnings and errors are also emitted as
::warning::and::error::annotations and steps are wrapped in::group::. - Colors: on for a TTY or
FORCE_COLOR, off withNO_COLOR.
12.5 Logging
- A full plain-text log is always written, with timestamps and levels (
INFO,WARN,ERROR,DEBUG):dist/build.logfor builds,$TUI_HOME/logs/install-<timestamp>.logfor installs, overridable with--log FILE. - Console verbosity:
-vadds debug detail (resolved source/target pairs, exact commands),-qprints only warnings and errors. The log is unaffected by-q. - Secrets (signing keys,
GITHUB_TOKEN, passwords) are never logged; commands are logged with tokens masked.
13. CI templates
Shipped in share/ci/ (github-actions.yml, gitlab-ci.yml) and copied by dabt ci init.
| Trigger | Behavior |
|---|---|
| Push to main | dabt build --suffix dev; upload as CI artifact only; no publish |
Tag v*-* |
build, sign, verify, dabt publish --prerelease |
Tag v* |
build, sign, verify, dabt publish --draft |
The version comes from the tag or VERSION, and the build number from the CI run. The signing key comes from a CI secret (10.4).
14. Module structure and implementation plan
All packaging logic lives in its own module directory, lib/dapk/, separate from the existing flat lib/tui_*.sh files. Migrating the rest of lib/ to this structure is a later, separate effort and is out of scope here. This module is built with that end state in mind, so its conventions can be reused as-is.
14.1 Layout
lib/dapk/
dapk.sh loader: sources the modules below in dependency order, defines DABT_PKG_EXT, guards double-sourcing
ui.sh steps, colors, progress bar, logging, CI detection (12.2 - 12.5)
toml.sh strict TOML subset parser (5.1)
config.sh loads dabt.pkg, applies defaults, validates
version.sh version and build-number resolution (6)
changelog.sh Unreleased rewrite, News extraction (7.1 - 7.3)
news.sh News loading and filtering (7.4)
collect.sh base tree and [[include]] resolution, plan output (5.3)
manifest.sh hashing, Merkle directory hashes, MANIFEST read and write (4.3, 4.4)
pack.sh deterministic tar and gzip, member counting (4.5)
sign.sh ssh-keygen sign, trust store (10)
verify.sh structure, signature and hash verification (10.3)
deps.sh dependency check, plan, install (5.4, 11.2)
install.sh install orchestration (11.1)
notes.sh release-note template engine (8)
publish.sh GitHub release client (9)
ci.sh CI template installation (13)
cmd/ one file per subcommand: build, verify, info, install, deps, news, version, release, notes, publish, include, dep, ci
bin/dabt thin dispatcher: sources lib/dapk/dapk.sh, maps `dabt <cmd>` to `dapk.cmd.<cmd>`
tests/dapk/ one bats file per module
14.2 Module rules
- Single responsibility. A module owns one concern from the list above. Cross-cutting behavior (output, logging) goes through
ui.sh, not reimplemented per module. - Namespacing. Public functions are
dapk.<module>.<name>(for exampledapk.manifest.write). Private helpers are_dapk.<module>.<name>. Public globals areDAPK_<MODULE>_*. This follows the existingtui.*/_tui.*convention, and the private-function invariant in CLAUDE.md applies unchanged: nothing outside a module calls its underscore functions or reads its private state. - Explicit interface. Each module starts with a header comment listing its public functions, the globals it reads and writes, and the modules it depends on.
- Acyclic dependencies. The loader order in
dapk.shis the dependency order.ui,tomlandversiondepend on no other module.cmd/*may depend on anything. No module depends oncmd/*. - No process control inside modules. Modules
returnstatus codes and never callexit. Only thecmd/layer andbin/dabttranslate results into exit codes (12.2). - Values out, chatter elsewhere. A function that produces a value returns it through a named variable (nameref) or stdout. Human-facing messages go through
ui.shto stderr. Nothing mixes the two. - Standalone. A module can be sourced by itself for tests, with no dependency on
tui.shstate, matching the standalone rule forterminal_renderer.sh. - Thin commands.
cmd/*files only parse arguments, call module APIs and report results. Logic that a second command needs moves into a module. - Testable seams. External effects (network, package manager,
ssh-keygen,sudo) are isolated behind small functions inpublish.sh,deps.shandsign.sh, so tests replace them by mock executables onPATHor by overriding a function.
14.3 Touch points outside the module
install.shandlib/apps/tui_update.share unchanged in this effort.ui.shexposes a stable interface (dapk.ui.step-style output,dapk.ui.bar) soinstall.shcan adopt it when the wider migration happens. Until then the two share the visual design, not code.- App-facing wrappers (
tui.news.*for apps, a<news>markup tag) are thin delegates todapk.newsand are added on thetuiside only when needed. - New non-code assets:
share/ci/*,share/release/default.tpl,docs/guide/packaging.md.
14.4 Implementation order
Each step is shippable and has its own tests in tests/dapk/:
dapk.sh,ui.sh,toml.sh,config.shmanifest.sh,pack.sh,cmd/build, thensign.sh,verify.shcollect.sh([[include]]),version.shchangelog.sh,news.sh,cmd/releasedeps.sh,install.shnotes.sh,publish.shci.sh, CI templates, docs
15. Alternatives considered
| Decision | Chosen | Rejected and why |
|---|---|---|
| Config format | TOML subset | YAML: indentation and anchors cannot be parsed safely in awk. JSON: no comments, awkward in awk. Key=value: no repeatable structured entries, not a recognized standard. |
| Signing | ssh-keygen -Y |
GPG: keyring and trust-model complexity. minisign: extra dependency. Raw openssl Ed25519: no standard container or trust file. |
| Descriptor | Header of MANIFEST |
Separate descriptor file: second source of truth to keep in sync and to sign. |
| Signature placement | Embedded MANIFEST.sig |
Detached tarball signature: package no longer verifies on its own. |
| GitHub transport | curl REST |
gh only: hard dependency. |
| Directory checksum | Merkle over children | Hash of a tar stream: not stable across tar implementations. |
16. Risks
| Risk | Mitigation |
|---|---|
Non-GNU tar lacks --sort, --mtime |
Explicit member list and fixed ordering; document bsdtar flags; detect and fail with a clear message if reproducibility cannot be guaranteed |
Custom install commands execute arbitrary code |
Signed manifest, verbatim display, never batched, individual confirmation (10.4) |
sudo prompts inside a progress step |
Run dependency installs in the foreground without the bar; the bar resumes afterwards |
| TOML subset diverges from real TOML | Reject unsupported syntax with line numbers, and test fixtures are validated against a real TOML parser during development |
| Signing key exposure in CI | Secret only, mode 600 temp file, removed after use, never logged |
17. Test strategy (bats, tmp dirs only, network mocked)
- Manifest: determinism (same input, same bytes), Merkle hash changes on deep edits, sorting, mode normalization.
- Tamper detection: modified file, added member, removed member, altered
MANIFEST, wrong or unknown signer, missing signature. - TOML parser: accepted subset, error messages with line numbers.
- Includes: path escape (
.., absolute), collisions, recursive vs non-recursive, symlinks, empty-directorymkdir -pand warning,--strict. - Changelog: Unreleased rewrite, suffixed header, bullet continuation, missing News, empty Unreleased for
release. - Templates: substitution, conditionals, section blocks, unknown variable error.
- Dependencies: the full matrix in 11.2 using a mock package manager on
PATH; conflict flags; non-TTY; custom-command gating;min_version;deps=unmetwritten and cleared; nothing written when--require-depsaborts. - Publish: mocked
curl; draft lookup, idempotent rerun, promotion, suffix forcing prerelease. - UI: bar formatter at fixed percents with escapes stripped;
CI=1yields no\ror escapes;NO_COLOR;-qversus log contents; redraw only on percent change. - Signing uses a throwaway keypair generated per test run.
17a. As implemented: deviations from this concept
- Commands:
dabt buildis top-level; the rest live underdabt pkg <cmd>(dabt version,dabt info, … already mean other things). Installing a package isdabt app install PKG.dapk|URL, becausedabt installinstalls DABT itself..dapksupport was added to the existingdabt appinstaller (lib/apps/tui_apps.sh), which keeps using.dabt.metadata;dabt buildkeeps that file in sync or generates it. --trust-keytakes the signer fingerprint (--trust-key SHA256:...), so a key is never trusted blindly. Unsigned packages:dabt build --no-signanddabt app install --allow-unsigned.- Signing key priority:
--key>$DABT_SIGN_KEY(private key text, for CI) >sign_keyindabt.pkg. - The unmet-dependency marker is
$TUI_HOME/apps.deps/<name>(the signed manifest copy is not edited);dabt app runwarns about it,dabt pkg deps APPclears it. --install-path DIRis the exact app folder. Dependencycheck/version_cmdstrings run throughbash -c, only after the package verified.- Build numbers: CI counters (GitHub, GitLab, Buildkite, CircleCI, Jenkins, Azure) are used first;
build_offsetindabt.pkgshifts them (and the git commit count); a shallow clone warns. See docs/guide/packaging.md. dabt pkg publish --suffix Sselects a suffixed build; CI tags with a suffix resolve it from the tag.
18. Decision log and open questions
Decided:
- D1. Package extension is
.dapk(confirmed; single constant). - D2.
dabt.pkgis a separate TOML file. - D3.
MANIFESTheader is the descriptor; no separate descriptor file. - D4. Signing via
ssh-keygen -Y, signature embedded. - D5. Empty directory include:
mkdir -pand warn. - D6. Publish defaults to draft; suffix forces prerelease.
- D7. Default dependency behavior is to prompt;
--no-depswarns and continues;--require-depsfails. - D8. The progress bar matches the cache spinner’s look exactly (
▕█░▏, 30 cells), with an ASCII fallback for non-UTF-8 locales. - D9. Default install location is
$TUI_HOME/apps/<name>/, overridden with--install-path. - D10. Refactoring
tui_cache.shto use the shared bar renderer is out of scope.
Open: none.
