fix: comprehensive bakery package manager audit and repair
Critical fixes: - gen-index.sh: emit services, config, optional_system_deps from bakery.toml; parse product list from registry TOML instead of hardcoded array; fail loudly when bakery.toml is missing (was silently producing empty metadata in prod) - install.rs: download service units and example configs from dl server at install time (were never fetched); check systemctl exit codes (were swallowed); save state before file cleanup in remove_package (was inconsistent on error) - doctor.rs: rewrite dep detection to use `pacman -Q` as primary (no more dependency on `which` or pkg-config name mismatches); add optional_system_deps support returning (missing, warnings) — warnings print but never block install - get.sh: fix GitHub fallback URL (was 404 for both latest and versioned releases); add SHA-256 checksum verification using published .sha256 file High priority fixes: - bakery doctor <unknown-pkg>: exit non-zero (was silently passing) - bakery update: add --all flag (documented in README but missing from CLI); add doctor gate before update (was bypassing dep check) - bread_deps: now resolved recursively with cycle detection (was ignored) - manifest.rs: add artifact_urls() helper and optional_system_deps field - state.rs: atomic save via tmp+rename; cmd_info shows optional_system_deps Tests: 17 new unit tests across doctor, download, install, state modules; scripts/test-gen-index.sh fixture test for full pipeline
This commit is contained in:
parent
0b38e8cce3
commit
694829c50f
13 changed files with 971 additions and 148 deletions
110
scripts/gen-index.sh
Normal file → Executable file
110
scripts/gen-index.sh
Normal file → Executable file
|
|
@ -1,28 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# Generate dl.breadway.dev/index.json from:
|
||||
# - registry/bread-ecosystem.toml (product list)
|
||||
# - <repo>/bakery.toml (per-product metadata)
|
||||
# - /srv/breadway-dl/ (built binaries + sha256 files)
|
||||
# - <DL_DIR>/<name>/bakery.toml (per-product metadata, uploaded by release.yml)
|
||||
# - <DL_DIR>/ (built binaries + sha256 files)
|
||||
#
|
||||
# Fallback for local dev: looks for ../name/bakery.toml (sibling repo checkout).
|
||||
# Run on hestia after each product build, before the dl server is refreshed.
|
||||
# Requires: jq, python3 (for toml parsing via tomllib), sha256sum
|
||||
# Requires: jq, python3 (tomllib, stdlib since 3.11), sha256sum
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
DL_DIR="${DL_DIR:-/srv/breadway-dl}"
|
||||
DL_BASE="${DL_BASE:-https://dl.breadway.dev}"
|
||||
GH_BASE="https://github.com"
|
||||
OUT="${DL_DIR}/index.json"
|
||||
|
||||
# Products are read from the registry. Each line is "name repo".
|
||||
products=(
|
||||
"bakery Breadway/bread-ecosystem"
|
||||
"bread Breadway/bread"
|
||||
"breadbar Breadway/breadbar"
|
||||
"breadbox Breadway/breadbox"
|
||||
"breadcrumbs Breadway/breadcrumbs"
|
||||
"breadpad Breadway/breadpad"
|
||||
)
|
||||
# Read the product list from the registry TOML instead of a hardcoded array.
|
||||
mapfile -t products < <(python3 -c "
|
||||
import tomllib, sys
|
||||
with open('${SCRIPT_DIR}/registry/bread-ecosystem.toml', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
for p in d['products']:
|
||||
print(p['name'], p['repo'])
|
||||
")
|
||||
|
||||
# Build a JSON package entry for one product.
|
||||
# $1 = product name, $2 = github repo slug
|
||||
|
|
@ -34,14 +34,14 @@ build_package_json() {
|
|||
local pkg_dir="${DL_DIR}/${name}"
|
||||
if [[ ! -d "${pkg_dir}" ]]; then
|
||||
echo " warning: no release dir for ${name} at ${pkg_dir}" >&2
|
||||
return
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The latest symlink must point to the current version dir.
|
||||
local latest_link="${pkg_dir}/latest"
|
||||
if [[ ! -L "${latest_link}" ]]; then
|
||||
echo " warning: no 'latest' symlink for ${name}" >&2
|
||||
return
|
||||
return 1
|
||||
fi
|
||||
local version_dir
|
||||
version_dir="$(readlink -f "${latest_link}")"
|
||||
|
|
@ -51,11 +51,11 @@ build_package_json() {
|
|||
# Collect all binaries in the version dir (executables only; skip metadata files).
|
||||
local binaries_json="[]"
|
||||
for bin_path in "${version_dir}"/*; do
|
||||
[[ "${bin_path}" == *.sha256 ]] && continue
|
||||
[[ "${bin_path}" == *.toml ]] && continue
|
||||
[[ "${bin_path}" == *.sha256 ]] && continue
|
||||
[[ "${bin_path}" == *.toml ]] && continue
|
||||
[[ "${bin_path}" == *.service ]] && continue
|
||||
[[ "${bin_path}" == *.css ]] && continue
|
||||
[[ "${bin_path}" == *.txt ]] && continue
|
||||
[[ "${bin_path}" == *.css ]] && continue
|
||||
[[ "${bin_path}" == *.txt ]] && continue
|
||||
[[ -f "${bin_path}" ]] || continue
|
||||
local bin_name
|
||||
bin_name="$(basename "${bin_path}")"
|
||||
|
|
@ -77,45 +77,77 @@ build_package_json() {
|
|||
binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')"
|
||||
done
|
||||
|
||||
# Read bakery.toml: the release workflow copies it to DL_DIR alongside the
|
||||
# binaries; fall back to a sibling checkout for local dev use.
|
||||
# Locate bakery.toml: the release workflow copies it to DL_DIR alongside the
|
||||
# binaries. Fall back to a sibling repo checkout for local dev use.
|
||||
local bakery_toml="${DL_DIR}/${name}/bakery.toml"
|
||||
if [[ ! -f "${bakery_toml}" ]]; then
|
||||
bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml"
|
||||
fi
|
||||
local description=""
|
||||
local system_deps="[]"
|
||||
local bread_deps="[]"
|
||||
local services="[]"
|
||||
local config="null"
|
||||
local post_install="[]"
|
||||
if [[ ! -f "${bakery_toml}" ]]; then
|
||||
echo "ERROR: bakery.toml not found for ${name} — release.yml must upload it to ${DL_DIR}/${name}/bakery.toml" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f "${bakery_toml}" ]]; then
|
||||
description="$(python3 -c "
|
||||
import tomllib, sys
|
||||
local description system_deps optional_system_deps bread_deps services config post_install
|
||||
|
||||
description="$(python3 -c "
|
||||
import tomllib
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(d.get('description', ''))
|
||||
" 2>/dev/null || true)"
|
||||
system_deps="$(python3 -c "
|
||||
import tomllib, json, sys
|
||||
|
||||
system_deps="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('system_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
bread_deps="$(python3 -c "
|
||||
import tomllib, json, sys
|
||||
|
||||
optional_system_deps="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('optional_system_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
bread_deps="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('bread_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
post_install="$(python3 -c "
|
||||
import tomllib, json, sys
|
||||
|
||||
# [[service]] entries → [{unit, enable}]
|
||||
services="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
svcs = d.get('service', [])
|
||||
print(json.dumps([{'unit': s['unit'], 'enable': s.get('enable', False)} for s in svcs]))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
# [config] → {dir, example?} or null
|
||||
config="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
cfg = d.get('config')
|
||||
if cfg:
|
||||
obj = {'dir': cfg['dir']}
|
||||
if 'example' in cfg:
|
||||
obj['example'] = cfg['example']
|
||||
print(json.dumps(obj))
|
||||
else:
|
||||
print('null')
|
||||
" 2>/dev/null || echo "null")"
|
||||
|
||||
post_install="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('install', {}).get('post_install', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--arg name "${name}" \
|
||||
|
|
@ -123,8 +155,10 @@ print(json.dumps(d.get('install', {}).get('post_install', [])))
|
|||
--arg version "${version}" \
|
||||
--argjson binaries "${binaries_json}" \
|
||||
--argjson system_deps "${system_deps}" \
|
||||
--argjson optional_system_deps "${optional_system_deps}" \
|
||||
--argjson bread_deps "${bread_deps}" \
|
||||
--argjson services "${services}" \
|
||||
--argjson config "${config}" \
|
||||
--argjson post_install "${post_install}" \
|
||||
'{
|
||||
name: $name,
|
||||
|
|
@ -132,8 +166,10 @@ print(json.dumps(d.get('install', {}).get('post_install', [])))
|
|||
version: $version,
|
||||
binaries: $binaries,
|
||||
system_deps: $system_deps,
|
||||
optional_system_deps: $optional_system_deps,
|
||||
bread_deps: $bread_deps,
|
||||
services: $services,
|
||||
config: $config,
|
||||
post_install: $post_install
|
||||
}'
|
||||
}
|
||||
|
|
|
|||
33
scripts/get.sh
Normal file → Executable file
33
scripts/get.sh
Normal file → Executable file
|
|
@ -1,12 +1,10 @@
|
|||
#!/bin/sh
|
||||
# Bootstrap script: installs the `bakery` binary.
|
||||
# Bootstrap script: downloads and installs the `bakery` binary.
|
||||
# Usage: curl https://breadway.dev/get | sh
|
||||
# Or: curl -sSfL https://breadway.dev/get | sh
|
||||
set -eu
|
||||
|
||||
BAKERY_VERSION="${BAKERY_VERSION:-latest}"
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/${BAKERY_VERSION}/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/${BAKERY_VERSION}/bakery-x86_64"
|
||||
BIN_DIR="${BAKERY_BIN_DIR:-$HOME/.local/bin}"
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
|
@ -15,6 +13,20 @@ die() { echo "error: $*" >&2; exit 1; }
|
|||
uname -m | grep -q x86_64 || die "bakery only supports x86_64 (got $(uname -m))"
|
||||
uname -s | grep -q Linux || die "bakery only supports Linux (got $(uname -s))"
|
||||
|
||||
# Build download URLs. GitHub's "latest" redirect lives at a different path from
|
||||
# versioned releases, so we handle them separately and always prefix tags with 'v'.
|
||||
if [ "${BAKERY_VERSION}" = "latest" ]; then
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/latest/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/latest/download/bakery-x86_64"
|
||||
SHA256_URL="https://dl.breadway.dev/bakery/latest/bakery-x86_64.sha256"
|
||||
else
|
||||
# Strip a leading 'v' if the caller included it, then add it back consistently.
|
||||
ver="${BAKERY_VERSION#v}"
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/v${ver}/bakery-x86_64"
|
||||
SHA256_URL="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64.sha256"
|
||||
fi
|
||||
|
||||
# Pick a download tool.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
fetch() { curl -fsSL "$1" -o "$2"; }
|
||||
|
|
@ -26,13 +38,26 @@ fi
|
|||
|
||||
mkdir -p "${BIN_DIR}"
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "${TMP}"' EXIT
|
||||
trap 'rm -f "${TMP}" "${TMP}.sha256"' EXIT
|
||||
|
||||
echo "downloading bakery…"
|
||||
if fetch "${DL_PRIMARY}" "${TMP}" 2>/dev/null; then
|
||||
echo " from dl.breadway.dev"
|
||||
# Verify checksum when available from primary.
|
||||
if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
||||
expected="$(awk '{print $1}' "${TMP}.sha256")"
|
||||
actual="$(sha256sum "${TMP}" | awk '{print $1}')"
|
||||
if [ "${expected}" != "${actual}" ]; then
|
||||
die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})"
|
||||
fi
|
||||
echo " checksum verified"
|
||||
else
|
||||
echo " warning: could not fetch checksum — skipping verification"
|
||||
fi
|
||||
elif fetch "${DL_FALLBACK}" "${TMP}" 2>/dev/null; then
|
||||
echo " from GitHub (fallback)"
|
||||
# No .sha256 on the GitHub fallback path; proceed without verification.
|
||||
echo " warning: checksum not verified for GitHub fallback download"
|
||||
else
|
||||
die "failed to download bakery from both primary and fallback URLs"
|
||||
fi
|
||||
|
|
|
|||
113
scripts/test-gen-index.sh
Executable file
113
scripts/test-gen-index.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env bash
|
||||
# Smoke-test gen-index.sh against a minimal fixture DL_DIR tree.
|
||||
# Verifies that services, config, system_deps, optional_system_deps,
|
||||
# description, and post_install are all populated correctly.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FIXTURE="$(mktemp -d)"
|
||||
FAKE_REGISTRY="$(mktemp -d)"
|
||||
trap 'rm -rf "${FIXTURE}" "${FAKE_REGISTRY}"' EXIT
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
# ── Build a minimal release tree for "fakepkg" ───────────────────────────────
|
||||
PKG_VER_DIR="${FIXTURE}/fakepkg/0.1.0"
|
||||
mkdir -p "${PKG_VER_DIR}"
|
||||
|
||||
printf 'fake-binary-content' > "${PKG_VER_DIR}/fakepkg-x86_64"
|
||||
sha256sum "${PKG_VER_DIR}/fakepkg-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_VER_DIR}/fakepkg-x86_64.sha256"
|
||||
printf '[Unit]\nDescription=fakepkg\n' > "${PKG_VER_DIR}/fakepkg.service"
|
||||
printf '# example config\n' > "${PKG_VER_DIR}/fakepkg.example.toml"
|
||||
|
||||
cat > "${PKG_VER_DIR}/bakery.toml" <<'TOML'
|
||||
name = "fakepkg"
|
||||
description = "A fake package for testing"
|
||||
binaries = ["fakepkg"]
|
||||
system_deps = ["gtk4"]
|
||||
optional_system_deps = ["hyprland"]
|
||||
bread_deps = []
|
||||
|
||||
[[service]]
|
||||
unit = "fakepkg.service"
|
||||
enable = true
|
||||
|
||||
[config]
|
||||
dir = "~/.config/fakepkg"
|
||||
example = "fakepkg.example.toml"
|
||||
|
||||
[install]
|
||||
post_install = ["echo installed"]
|
||||
TOML
|
||||
|
||||
# gen-index looks for bakery.toml at ${DL_DIR}/<name>/bakery.toml (no version)
|
||||
cp "${PKG_VER_DIR}/bakery.toml" "${FIXTURE}/fakepkg/bakery.toml"
|
||||
ln -s "${PKG_VER_DIR}" "${FIXTURE}/fakepkg/latest"
|
||||
|
||||
# ── Minimal registry pointing only at fakepkg ────────────────────────────────
|
||||
mkdir -p "${FAKE_REGISTRY}/registry"
|
||||
cat > "${FAKE_REGISTRY}/registry/bread-ecosystem.toml" <<'TOML'
|
||||
[ecosystem]
|
||||
name = "test"
|
||||
|
||||
[[products]]
|
||||
name = "fakepkg"
|
||||
repo = "Test/fakepkg"
|
||||
description = "A fake package"
|
||||
TOML
|
||||
|
||||
# ── Run gen-index with overridden SCRIPT_DIR and DL_DIR ──────────────────────
|
||||
OUT="${FIXTURE}/index.json"
|
||||
SCRIPT_DIR="${FAKE_REGISTRY}" DL_DIR="${FIXTURE}" DL_BASE="https://dl.test" \
|
||||
bash "${REPO_ROOT}/scripts/gen-index.sh" 2>&1 | sed 's/^/ [gen-index] /'
|
||||
|
||||
[[ -f "${OUT}" ]] || fail "index.json was not produced"
|
||||
|
||||
# ── Assertions ────────────────────────────────────────────────────────────────
|
||||
jq -e '.packages.fakepkg' "${OUT}" > /dev/null \
|
||||
|| fail "fakepkg missing from index"
|
||||
|
||||
check() {
|
||||
local label="$1" expected="$2" actual="$3"
|
||||
[[ "${actual}" == "${expected}" ]] \
|
||||
|| fail "${label}: expected '${expected}', got '${actual}'"
|
||||
}
|
||||
|
||||
check "description" \
|
||||
"A fake package for testing" \
|
||||
"$(jq -r '.packages.fakepkg.description' "${OUT}")"
|
||||
|
||||
check "system_deps" \
|
||||
"gtk4" \
|
||||
"$(jq -r '.packages.fakepkg.system_deps | join(",")' "${OUT}")"
|
||||
|
||||
check "optional_system_deps" \
|
||||
"hyprland" \
|
||||
"$(jq -r '.packages.fakepkg.optional_system_deps | join(",")' "${OUT}")"
|
||||
|
||||
check "services[0].unit" \
|
||||
"fakepkg.service" \
|
||||
"$(jq -r '.packages.fakepkg.services[0].unit' "${OUT}")"
|
||||
|
||||
check "services[0].enable" \
|
||||
"true" \
|
||||
"$(jq -r '.packages.fakepkg.services[0].enable' "${OUT}")"
|
||||
|
||||
check "config.dir" \
|
||||
"~/.config/fakepkg" \
|
||||
"$(jq -r '.packages.fakepkg.config.dir' "${OUT}")"
|
||||
|
||||
check "config.example" \
|
||||
"fakepkg.example.toml" \
|
||||
"$(jq -r '.packages.fakepkg.config.example' "${OUT}")"
|
||||
|
||||
check "binaries[0].name" \
|
||||
"fakepkg-x86_64" \
|
||||
"$(jq -r '.packages.fakepkg.binaries[0].name' "${OUT}")"
|
||||
|
||||
check "post_install[0]" \
|
||||
"echo installed" \
|
||||
"$(jq -r '.packages.fakepkg.post_install[0]' "${OUT}")"
|
||||
|
||||
echo "OK: all gen-index assertions passed"
|
||||
Loading…
Add table
Add a link
Reference in a new issue