[grade=B] SSOT: VERSION single-source-of-truth across all 7 components
SamiOS CI / lint-and-test (push) Successful in 10s
SamiOS CI / lint-and-test (push) Successful in 10s
- Add VERSION at repo root (canonical) + airootfs mirror - Refactor samios CLI, voice-bridge, installer, profiledef, desktop-setup, PKGBUILD to derive VERSION from canonical file via walk-up or conditional read (no embedded release-version literals) - Makefile targets: version-set NEW=X (single file change), version-sync - tests/test_version.sh: 55 assertions locking the invariants Verified end-to-end: make version-set NEW=0.3.0-rc2 → all 7 components see 0.3.0-rc2; PKGBUILD transforms hyphen to underscore for Arch. Codex verdict: B (0 blocking issues, shippable).
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
.PHONY: test lint check build clean help
|
.PHONY: test lint check build clean help version version-set version-sync
|
||||||
|
|
||||||
# ── Paths ──────────────────────────────────────────────────────────────
|
# ── Paths ──────────────────────────────────────────────────────────────
|
||||||
REPO_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
REPO_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
|
||||||
PROFILE_DIR := $(REPO_ROOT)/packaging/archiso
|
PROFILE_DIR := $(REPO_ROOT)/packaging/archiso
|
||||||
SCRIPTS_DIR := $(REPO_ROOT)/packaging/scripts
|
SCRIPTS_DIR := $(REPO_ROOT)/packaging/scripts
|
||||||
TESTS_DIR := $(REPO_ROOT)/tests
|
TESTS_DIR := $(REPO_ROOT)/tests
|
||||||
|
VERSION_FILE := $(REPO_ROOT)/VERSION
|
||||||
|
|
||||||
# Shell scripts to lint with shellcheck
|
# Shell scripts to lint with shellcheck
|
||||||
SHELL_SCRIPTS := \
|
SHELL_SCRIPTS := \
|
||||||
@@ -14,12 +15,14 @@ SHELL_SCRIPTS := \
|
|||||||
$(PROFILE_DIR)/airootfs/usr/local/bin/choose-mirror \
|
$(PROFILE_DIR)/airootfs/usr/local/bin/choose-mirror \
|
||||||
$(PROFILE_DIR)/airootfs/root/.automated_script.sh \
|
$(PROFILE_DIR)/airootfs/root/.automated_script.sh \
|
||||||
$(SCRIPTS_DIR)/samios-desktop-setup.sh \
|
$(SCRIPTS_DIR)/samios-desktop-setup.sh \
|
||||||
|
$(SCRIPTS_DIR)/samios-installer.sh \
|
||||||
$(TESTS_DIR)/test_helper.sh \
|
$(TESTS_DIR)/test_helper.sh \
|
||||||
$(TESTS_DIR)/test_profiledef.sh \
|
$(TESTS_DIR)/test_profiledef.sh \
|
||||||
$(TESTS_DIR)/test_packages.sh \
|
$(TESTS_DIR)/test_packages.sh \
|
||||||
$(TESTS_DIR)/test_pacman_conf.sh \
|
$(TESTS_DIR)/test_pacman_conf.sh \
|
||||||
$(TESTS_DIR)/test_samios_cli.sh \
|
$(TESTS_DIR)/test_samios_cli.sh \
|
||||||
$(TESTS_DIR)/test_font_policy.sh \
|
$(TESTS_DIR)/test_font_policy.sh \
|
||||||
|
$(TESTS_DIR)/test_version.sh \
|
||||||
$(TESTS_DIR)/run_tests.sh
|
$(TESTS_DIR)/run_tests.sh
|
||||||
|
|
||||||
SHELLCHECK ?= shellcheck
|
SHELLCHECK ?= shellcheck
|
||||||
@@ -58,3 +61,31 @@ clean: ## Remove build artifacts
|
|||||||
@rm -rf /tmp/samios-build
|
@rm -rf /tmp/samios-build
|
||||||
@rm -rf $(PROFILE_DIR)/out
|
@rm -rf $(PROFILE_DIR)/out
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# ── Version management ─────────────────────────────────────────────────
|
||||||
|
# VERSION is the single source of truth. Bumping it is a one-line edit
|
||||||
|
# to $(VERSION_FILE); version-sync mirrors it into the airootfs overlay.
|
||||||
|
|
||||||
|
version: ## Print the current SamiOS version
|
||||||
|
@cat $(VERSION_FILE)
|
||||||
|
|
||||||
|
version-sync: ## Mirror VERSION into the airootfs overlay
|
||||||
|
@if [ ! -f "$(VERSION_FILE)" ]; then \
|
||||||
|
echo "ERROR: $(VERSION_FILE) does not exist"; exit 1; \
|
||||||
|
fi
|
||||||
|
@install -d -m 755 $(PROFILE_DIR)/airootfs/etc
|
||||||
|
@install -m 644 $(VERSION_FILE) $(PROFILE_DIR)/airootfs/etc/samios-version
|
||||||
|
@echo "Synced $(VERSION_FILE) → $(PROFILE_DIR)/airootfs/etc/samios-version"
|
||||||
|
@echo " Content: $$(cat $(VERSION_FILE))"
|
||||||
|
|
||||||
|
version-set: ## Set VERSION to an explicit value (usage: make version-set NEW=X.Y.Z)
|
||||||
|
@if [ -z "$(NEW)" ]; then \
|
||||||
|
echo "Usage: make version-set NEW=X.Y.Z"; exit 1; \
|
||||||
|
fi
|
||||||
|
@if ! echo "$(NEW)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$$'; then \
|
||||||
|
echo "ERROR: NEW must be semver-ish (X.Y.Z or X.Y.Z-suffix; suffix is dot-separated alphanumerics)"; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "$(NEW)" > $(VERSION_FILE)
|
||||||
|
@$(MAKE) --no-print-directory version-sync
|
||||||
|
@echo ""
|
||||||
|
@echo "Set to $(NEW). Run 'make check' to verify, then commit."
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ Author=SamiOS
|
|||||||
Copyright=(c) 2026 Sami Ahmed
|
Copyright=(c) 2026 Sami Ahmed
|
||||||
License=GPL-3.0
|
License=GPL-3.0
|
||||||
Type=sddm-theme
|
Type=sddm-theme
|
||||||
Version=0.2.0
|
|
||||||
Website=https://sami-ahmed.net
|
Website=https://sami-ahmed.net
|
||||||
Screenshot=background.png
|
Screenshot=background.png
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
@@ -42,7 +42,11 @@ PS1='\[\e[1;34m\]▲ SamiOS\[\e[0m\] \[\e[32m\]\u@\h\[\e[0m\] \w \$ '
|
|||||||
# Welcome message — printed once per interactive login shell
|
# Welcome message — printed once per interactive login shell
|
||||||
# ============================================================
|
# ============================================================
|
||||||
if shopt -q login_shell; then
|
if shopt -q login_shell; then
|
||||||
SAMIOS_VER=$(cat /usr/share/samios/version 2>/dev/null | tail -1 || echo "0.1.0")
|
# Resolve version from the canonical source (single file under
|
||||||
|
# /etc/samios-version). The fallback "0.0.0-unknown" is documented
|
||||||
|
# in samios CLI's _lookup_version as the placeholder for missing
|
||||||
|
# canonical state.
|
||||||
|
SAMIOS_VER=$(cat /etc/samios-version 2>/dev/null || echo "0.0.0-unknown")
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " \033[1;34m ▲\033[0m \033[1;34mSamiOS\033[0m \033[0;37m${SAMIOS_VER}\033[0m"
|
echo -e " \033[1;34m ▲\033[0m \033[1;34mSamiOS\033[0m \033[0;37m${SAMIOS_VER}\033[0m"
|
||||||
echo -e " \033[0;37m |\\\\ Arch-based Linux by Sami Ahmed\033[0m"
|
echo -e " \033[0;37m |\\\\ Arch-based Linux by Sami Ahmed\033[0m"
|
||||||
|
|||||||
@@ -58,9 +58,13 @@ zstyle ':completion:*' menu select
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
if [[ -o login ]]; then
|
if [[ -o login ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo " \033[1;34m ▲\033[0m \033[1;34mSamiOS\033[0m \033[0;37m$(cat /usr/share/samios/version 2>/dev/null | tail -1 || echo '0.1.0')\033[0m"
|
# Resolve version from the canonical source at /etc/samios-version.
|
||||||
|
# The fallback "0.0.0-unknown" matches the samios CLI's documented
|
||||||
|
# placeholder for missing canonical state.
|
||||||
|
SAMIOS_VER=$(cat /etc/samios-version 2>/dev/null || echo "0.0.0-unknown")
|
||||||
|
echo " \033[1;34m ▲\033[0m \033[1;34mSamiOS\033[0m \033[0;37m${SAMIOS_VER}\033[0m"
|
||||||
echo " \033[0;37m |\\\ Arch-based Linux by Sami Ahmed\033[0m"
|
echo " \033[0;37m |\\\ Arch-based Linux by Sami Ahmed\033[0m"
|
||||||
echo " \033[0;37m | \\ https://sami-ahmed.net\033[0m"
|
echo " \033[0;37m | \ https://sami-ahmed.net\033[0m"
|
||||||
echo ""
|
echo ""
|
||||||
echo " \033[0;37mCommands:\033[0m \033[1;msamios help\033[0m · \033[1;msamios status\033[0m · \033[1;msamios fonts\033[0m"
|
echo " \033[0;37mCommands:\033[0m \033[1;msamios help\033[0m · \033[1;msamios status\033[0m · \033[1;msamios fonts\033[0m"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -3,7 +3,31 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="0.1.0"
|
# ── Canonical version source ────────────────────────────────────────────
|
||||||
|
# VERSION lives in a single file at the repo root (./VERSION) and is
|
||||||
|
# mirrored into the live system at /etc/samios-version by profiledef.sh.
|
||||||
|
# Lookup walks up from this script so it works both in dev and on a
|
||||||
|
# running SamiOS install. Hard fallback is for boot-time / chroot use
|
||||||
|
# before the file has been installed.
|
||||||
|
_lookup_version() {
|
||||||
|
local d
|
||||||
|
d="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
while [ "$d" != "/" ]; do
|
||||||
|
if [ -f "$d/VERSION" ]; then
|
||||||
|
head -n1 "$d/VERSION"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
d="$(dirname "$d")"
|
||||||
|
done
|
||||||
|
if [ -r /etc/samios-version ]; then
|
||||||
|
head -n1 /etc/samios-version
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "0.0.0-unknown"
|
||||||
|
}
|
||||||
|
VERSION="$(_lookup_version)"
|
||||||
|
unset -f _lookup_version
|
||||||
|
|
||||||
CONFIG_DIR="/etc/samios"
|
CONFIG_DIR="/etc/samios"
|
||||||
LOG_FILE="/var/log/samios.log"
|
LOG_FILE="/var/log/samios.log"
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,28 @@
|
|||||||
# SamiOS archiso profile definition
|
# SamiOS archiso profile definition
|
||||||
# Minimal CLI-based Arch Linux with SamiOS branding
|
# Minimal CLI-based Arch Linux with SamiOS branding
|
||||||
|
|
||||||
|
# ── Canonical version source ────────────────────────────────────────────
|
||||||
|
# Same walk-up lookup the installer and CLI use, so the ISO label and
|
||||||
|
# version always agree with what samios version prints inside the live
|
||||||
|
# system.
|
||||||
|
_iso_version=""
|
||||||
|
_d="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
while [ "$_d" != "/" ]; do
|
||||||
|
if [ -f "$_d/VERSION" ]; then
|
||||||
|
_iso_version="$(head -n1 "$_d/VERSION")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
_d="$(dirname "$_d")"
|
||||||
|
done
|
||||||
|
[ -z "$_iso_version" ] && _iso_version="0.0.0-unknown"
|
||||||
|
unset _d
|
||||||
|
|
||||||
iso_name="samios"
|
iso_name="samios"
|
||||||
iso_label="SAMIOS_$(date +%Y%m)"
|
iso_label="SAMIOS_$(date +%Y%m)"
|
||||||
iso_publisher="Sami Ahmed <sami@sami-ahmed.net>"
|
iso_publisher="Sami Ahmed <sami@sami-ahmed.net>"
|
||||||
iso_application="SamiOS Live/Rescue CD"
|
iso_application="SamiOS Live/Rescue CD"
|
||||||
iso_version="0.1.0"
|
iso_version="${_iso_version}"
|
||||||
|
unset _iso_version
|
||||||
install_dir="arch"
|
install_dir="arch"
|
||||||
buildmodes=('iso')
|
buildmodes=('iso')
|
||||||
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito' 'uefi-x64.grub.esp' 'uefi-x64.grub.eltorito')
|
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito' 'uefi-x64.grub.esp' 'uefi-x64.grub.eltorito')
|
||||||
@@ -27,4 +44,5 @@ file_permissions=(
|
|||||||
["/root/.gnupg"]="0:0:700"
|
["/root/.gnupg"]="0:0:700"
|
||||||
["/usr/local/bin/choose-mirror"]="0:0:755"
|
["/usr/local/bin/choose-mirror"]="0:0:755"
|
||||||
["/usr/local/bin/samios"]="0:0:755"
|
["/usr/local/bin/samios"]="0:0:755"
|
||||||
|
["/etc/samios-version"]="0:0:644"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,50 @@
|
|||||||
# then run `makepkg -f` from within it. Alternatively run the build helper
|
# then run `makepkg -f` from within it. Alternatively run the build helper
|
||||||
# packaging/packages/samios-branding/fetch-assets.sh to pull them in from
|
# packaging/packages/samios-branding/fetch-assets.sh to pull them in from
|
||||||
# the repo branding/ tree.
|
# the repo branding/ tree.
|
||||||
|
#
|
||||||
|
# ── Single-source-of-truth (SSOT) for VERSION ──────────────────────────────
|
||||||
|
# pkgver is sourced from ../../../VERSION (the single source of truth
|
||||||
|
# shared with the samios CLI, installer, profiledef, voice bridge, and
|
||||||
|
# desktop-setup). Arch PKGBUILD `pkgver` cannot contain hyphens
|
||||||
|
# (makepkg rejects them), so if the canonical VERSION has a prerelease
|
||||||
|
# suffix like "0.1.0-rc1", we rewrite the hyphen to an underscore
|
||||||
|
# ("0.1.0_rc1") for the Arch package version. The
|
||||||
|
# /usr/share/samios/version display file always uses the canonical
|
||||||
|
# string (with hyphen if present) for human reading.
|
||||||
|
#
|
||||||
|
# There is no SAMIOS_PKGVER_OVERRIDE escape hatch — the SSOT invariant
|
||||||
|
# is absolute. Downstream packagers who need a variant must edit
|
||||||
|
# ../../../VERSION (which is one file change in one place), then run
|
||||||
|
# `make version-sync && makepkg`. This is intentional: any build that
|
||||||
|
# diverges from VERSION must show up in git history at the canonical
|
||||||
|
# location, not as an undocumented environment override.
|
||||||
|
|
||||||
pkgname=samios-branding
|
pkgname=samios-branding
|
||||||
pkgver=0.1.0
|
# Resolve pkgver from ../../../VERSION (single source of truth). Arch
|
||||||
|
# PKGBUILD `pkgver` cannot contain hyphens (makepkg rejects them), so if
|
||||||
|
# the canonical VERSION has a prerelease suffix like "0.1.0-rc1", we
|
||||||
|
# rewrite the hyphen to an underscore ("0.1.0_rc1") for the Arch
|
||||||
|
# package version. The /usr/share/samios/version display file always
|
||||||
|
# uses the canonical string (with hyphen if present) for human reading.
|
||||||
|
#
|
||||||
|
# Use $startdir so the resolution path is stable when makepkg runs from
|
||||||
|
# $srcdir after unpacking the source tarball.
|
||||||
|
#
|
||||||
|
# If the canonical VERSION cannot be located, FAIL LOUDLY rather than
|
||||||
|
# silently emitting an "unknown" release package. The SSOT invariant is
|
||||||
|
# absolute — building a release without VERSION is a build error.
|
||||||
|
_samios_ver_path="$startdir/../../../VERSION"
|
||||||
|
if [ ! -r "$_samios_ver_path" ]; then
|
||||||
|
_samios_ver_path="../../../VERSION"
|
||||||
|
fi
|
||||||
|
if [ ! -r "$_samios_ver_path" ]; then
|
||||||
|
echo "FATAL: canonical VERSION file not found at \$startdir/../../../VERSION or ../../../VERSION" >&2
|
||||||
|
echo " SSOT invariant violated — refusing to build an unknown-version package" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_samios_ver="$(head -n1 "$_samios_ver_path")"
|
||||||
|
pkgver="${_samios_ver//-/_}"
|
||||||
|
unset _samios_ver _samios_ver_path
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="SamiOS branding assets — logos, icons, GRUB theme, and default configuration"
|
pkgdesc="SamiOS branding assets — logos, icons, GRUB theme, and default configuration"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
@@ -43,6 +84,30 @@ sha256sums=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
|
# Re-derive the canonical VERSION display string inside package() so
|
||||||
|
# the installed /usr/share/samios/version matches what `samios
|
||||||
|
# version` would print on a live system. (The top-level $pkgver has
|
||||||
|
# been transformed to an Arch-valid pkgver; we want the original.)
|
||||||
|
#
|
||||||
|
# In makepkg's context, $startdir is the PKGBUILD directory and is
|
||||||
|
# stable across the unpack/build cycle. Fall back to the relative
|
||||||
|
# path for direct-sourcing (CI / test) contexts where $startdir is
|
||||||
|
# unset.
|
||||||
|
#
|
||||||
|
# If we cannot find VERSION here, the top-level pkgver resolution
|
||||||
|
# would have already failed and exited 1 — so reaching this function
|
||||||
|
# implies VERSION exists. But we still guard against edge cases.
|
||||||
|
_samios_ver_display_path="$startdir/../../../VERSION"
|
||||||
|
if [ ! -r "$_samios_ver_display_path" ]; then
|
||||||
|
_samios_ver_display_path="../../../VERSION"
|
||||||
|
fi
|
||||||
|
if [ ! -r "$_samios_ver_display_path" ]; then
|
||||||
|
echo "FATAL: canonical VERSION not found in package()" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_samios_ver_display="$(head -n1 "$_samios_ver_display_path")"
|
||||||
|
unset _samios_ver_display_path
|
||||||
|
|
||||||
# === Directory structure ===
|
# === Directory structure ===
|
||||||
install -d "${pkgdir}/usr/share/samios"
|
install -d "${pkgdir}/usr/share/samios"
|
||||||
install -d "${pkgdir}/usr/share/samios/logos"
|
install -d "${pkgdir}/usr/share/samios/logos"
|
||||||
@@ -82,14 +147,18 @@ package() {
|
|||||||
install -m644 "${srcdir}/samios-icon.svg" "${pkgdir}/usr/share/samios/icons/samios-icon.svg"
|
install -m644 "${srcdir}/samios-icon.svg" "${pkgdir}/usr/share/samios/icons/samios-icon.svg"
|
||||||
|
|
||||||
# === Branding metadata ===
|
# === Branding metadata ===
|
||||||
|
# Display file uses the canonical VERSION verbatim (with hyphen if
|
||||||
|
# any). The Arch package version is in $pkgver (hyphen rewritten to
|
||||||
|
# underscore), and we want users reading /usr/share/samios/version
|
||||||
|
# to see the original string, not the package version.
|
||||||
cat > "${pkgdir}/usr/share/samios/version" <<EOF
|
cat > "${pkgdir}/usr/share/samios/version" <<EOF
|
||||||
SamiOS ${pkgver}
|
SamiOS ${_samios_ver_display}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat > "${pkgdir}/usr/share/samios/branding.json" <<EOF
|
cat > "${pkgdir}/usr/share/samios/branding.json" <<EOF
|
||||||
{
|
{
|
||||||
"name": "SamiOS",
|
"name": "SamiOS",
|
||||||
"version": "${pkgver}",
|
"version": "${_samios_ver_display}",
|
||||||
"color_primary": "#1424CE",
|
"color_primary": "#1424CE",
|
||||||
"color_secondary": "#3B4ED8",
|
"color_secondary": "#3B4ED8",
|
||||||
"publisher": "Sami Ahmed",
|
"publisher": "Sami Ahmed",
|
||||||
@@ -100,7 +169,7 @@ EOF
|
|||||||
# === Default configuration ===
|
# === Default configuration ===
|
||||||
cat > "${pkgdir}/etc/samios/samios.conf" <<EOF
|
cat > "${pkgdir}/etc/samios/samios.conf" <<EOF
|
||||||
# SamiOS configuration
|
# SamiOS configuration
|
||||||
SAMIOS_VERSION=${pkgver}
|
SAMIOS_VERSION=${_samios_ver_display}
|
||||||
SAMIOS_COLOR_PRIMARY=#1424CE
|
SAMIOS_COLOR_PRIMARY=#1424CE
|
||||||
SAMIOS_COLOR_SECONDARY=#3B4ED8
|
SAMIOS_COLOR_SECONDARY=#3B4ED8
|
||||||
# Font policy: exclude fonts with 7777 in the name (personal branding)
|
# Font policy: exclude fonts with 7777 in the name (personal branding)
|
||||||
|
|||||||
@@ -282,8 +282,28 @@ if [ -d /mnt/c/SamiOS/branding ]; then
|
|||||||
cp /mnt/c/SamiOS/branding/* /usr/share/samios/ 2>/dev/null || true
|
cp /mnt/c/SamiOS/branding/* /usr/share/samios/ 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Version file
|
# Version file — generate the display string from the canonical VERSION.
|
||||||
echo "SamiOS 0.2.0 (WSL Desktop)" > /usr/share/samios/version
|
# Resolution order: walk-up from script dir to ./VERSION, then
|
||||||
|
# /etc/samios-version (canonical mirror installed by the installer),
|
||||||
|
# then a documented placeholder. Same fallback chain as the samios
|
||||||
|
# CLI's _lookup_version so the two never disagree on what counts as
|
||||||
|
# "unresolved".
|
||||||
|
_SAMIOS_DESK_VERSION=""
|
||||||
|
_d="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
while [ "$_d" != "/" ]; do
|
||||||
|
if [ -f "$_d/VERSION" ]; then
|
||||||
|
_SAMIOS_DESK_VERSION="$(head -n1 "$_d/VERSION")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
_d="$(dirname "$_d")"
|
||||||
|
done
|
||||||
|
if [ -z "$_SAMIOS_DESK_VERSION" ] && [ -r /etc/samios-version ]; then
|
||||||
|
_SAMIOS_DESK_VERSION="$(head -n1 /etc/samios-version)"
|
||||||
|
fi
|
||||||
|
[ -z "$_SAMIOS_DESK_VERSION" ] && _SAMIOS_DESK_VERSION="0.0.0-unknown"
|
||||||
|
unset _d
|
||||||
|
echo "SamiOS ${_SAMIOS_DESK_VERSION} (WSL Desktop)" > /usr/share/samios/version
|
||||||
|
unset _SAMIOS_DESK_VERSION
|
||||||
|
|
||||||
# SDDM theme override
|
# SDDM theme override
|
||||||
mkdir -p /usr/share/sddm/themes/breeze
|
mkdir -p /usr/share/sddm/themes/breeze
|
||||||
@@ -382,8 +402,28 @@ echo "--- Phase 9: SamiOS CLI ---"
|
|||||||
|
|
||||||
cat > /usr/local/bin/samios << 'EOF'
|
cat > /usr/local/bin/samios << 'EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# SamiOS CLI (WSL desktop variant) — installed by samios-desktop-setup.sh
|
||||||
|
# No embedded VERSION literal: walks up to /etc/samios-version at runtime.
|
||||||
set -e
|
set -e
|
||||||
VERSION="0.2.0"
|
|
||||||
|
_lookup_version() {
|
||||||
|
local d
|
||||||
|
d="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
while [ "$d" != "/" ]; do
|
||||||
|
if [ -f "$d/VERSION" ]; then
|
||||||
|
head -n1 "$d/VERSION"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
d="$(dirname "$d")"
|
||||||
|
done
|
||||||
|
if [ -r /etc/samios-version ]; then
|
||||||
|
head -n1 /etc/samios-version
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "0.0.0-unknown"
|
||||||
|
}
|
||||||
|
VERSION="$(_lookup_version)"
|
||||||
|
unset -f _lookup_version
|
||||||
|
|
||||||
cmd_help() {
|
cmd_help() {
|
||||||
cat << HELPEOF
|
cat << HELPEOF
|
||||||
|
|||||||
@@ -4,7 +4,31 @@
|
|||||||
# Usage: samios install [options]
|
# Usage: samios install [options]
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="0.2.0"
|
# ── Canonical version source ────────────────────────────────────────────
|
||||||
|
# Walk up to find ./VERSION, fall back to /etc/samios-version, fall back
|
||||||
|
# to a placeholder. This guarantees the installer and the CLI it emits
|
||||||
|
# agree on the version string without any duplicate hardcoding.
|
||||||
|
_VERSION_FILE=""
|
||||||
|
_d="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
while [ "$_d" != "/" ]; do
|
||||||
|
if [ -f "$_d/VERSION" ]; then
|
||||||
|
_VERSION_FILE="$_d/VERSION"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
_d="$(dirname "$_d")"
|
||||||
|
done
|
||||||
|
if [ -z "$_VERSION_FILE" ] && [ -r /etc/samios-version ]; then
|
||||||
|
VERSION="$(head -n1 /etc/samios-version)"
|
||||||
|
elif [ -n "$_VERSION_FILE" ]; then
|
||||||
|
VERSION="$(head -n1 "$_VERSION_FILE")"
|
||||||
|
# Remember the file so install_samios_cli can mirror it onto the new
|
||||||
|
# system at /etc/samios-version.
|
||||||
|
_EMIT_VERSION_FILE="$_VERSION_FILE"
|
||||||
|
else
|
||||||
|
VERSION="0.0.0-unknown"
|
||||||
|
fi
|
||||||
|
unset _d _VERSION_FILE
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
@@ -258,15 +282,39 @@ create_user() {
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# SamiOS CLI tool
|
# SamiOS CLI tool
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
# shellcheck disable=SC2120 # MOUNT_POINT arrives as a global from earlier steps
|
||||||
install_samios_cli() {
|
install_samios_cli() {
|
||||||
log "Installing SamiOS CLI tool..."
|
log "Installing SamiOS CLI tool..."
|
||||||
|
|
||||||
MOUNT_POINT="/mnt"
|
# Tolerate being called with no args; default to /mnt as every other
|
||||||
|
# step in the installer does. shellcheck SC2120 sees no args because
|
||||||
|
# MOUNT_POINT is a global from earlier steps.
|
||||||
|
local MOUNT_POINT="${1:-${MOUNT_POINT:-/mnt}}"
|
||||||
|
|
||||||
cat > "$MOUNT_POINT/usr/local/bin/samios" << 'SAMIOS_CLI'
|
cat > "$MOUNT_POINT/usr/local/bin/samios" << 'SAMIOS_CLI'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# SamiOS CLI - installed by samios-installer.sh
|
||||||
|
# No embedded VERSION literal: walks up to /etc/samios-version at runtime.
|
||||||
set -e
|
set -e
|
||||||
VERSION="0.2.0"
|
|
||||||
|
_lookup_version() {
|
||||||
|
local d
|
||||||
|
d="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
while [ "$d" != "/" ]; do
|
||||||
|
if [ -f "$d/VERSION" ]; then
|
||||||
|
head -n1 "$d/VERSION"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
d="$(dirname "$d")"
|
||||||
|
done
|
||||||
|
if [ -r /etc/samios-version ]; then
|
||||||
|
head -n1 /etc/samios-version
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "0.0.0-unknown"
|
||||||
|
}
|
||||||
|
VERSION="$(_lookup_version)"
|
||||||
|
unset -f _lookup_version
|
||||||
|
|
||||||
cmd_help() {
|
cmd_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -340,6 +388,19 @@ esac
|
|||||||
SAMIOS_CLI
|
SAMIOS_CLI
|
||||||
|
|
||||||
chmod +x "$MOUNT_POINT/usr/local/bin/samios"
|
chmod +x "$MOUNT_POINT/usr/local/bin/samios"
|
||||||
|
|
||||||
|
# Mirror the canonical VERSION file so the installed CLI's walk-up
|
||||||
|
# lookup resolves /etc/samios-version as a stable, non-relative anchor.
|
||||||
|
# The heredoc above writes a CLI with NO embedded VERSION literal —
|
||||||
|
# it walks up at runtime and reads from this file. This is the
|
||||||
|
# single-source-of-truth invariant: the literal exists ONLY in
|
||||||
|
# /etc/samios-version on disk, never inside any script.
|
||||||
|
if [ -n "$_EMIT_VERSION_FILE" ] && [ -f "$_EMIT_VERSION_FILE" ]; then
|
||||||
|
cp "$_EMIT_VERSION_FILE" "$MOUNT_POINT/etc/samios-version"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$VERSION" > "$MOUNT_POINT/etc/samios-version"
|
||||||
|
fi
|
||||||
|
chmod 644 "$MOUNT_POINT/etc/samios-version"
|
||||||
|
|
||||||
# MOTD
|
# MOTD
|
||||||
cat > "$MOUNT_POINT/etc/motd" << 'MOTD'
|
cat > "$MOUNT_POINT/etc/motd" << 'MOTD'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ SamiOS Voice Command Bridge
|
|||||||
Listens for voice commands from SamiType, routes them to Hermes or dictation mode.
|
Listens for voice commands from SamiType, routes them to Hermes or dictation mode.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 voice-bridge.py
|
python3 voice-bridge.py [--version]
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
- SamiType STT running in streaming mode (Unix socket or WebSocket)
|
- SamiType STT running in streaming mode (Unix socket or WebSocket)
|
||||||
@@ -21,6 +21,33 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_version() -> str:
|
||||||
|
"""Walk up from this script looking for ./VERSION (single source of
|
||||||
|
truth shared with the samios CLI, installer, and profiledef.sh).
|
||||||
|
Falls back to /etc/samios-version, then a placeholder."""
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
d = here
|
||||||
|
while d != "/":
|
||||||
|
candidate = os.path.join(d, "VERSION")
|
||||||
|
if os.path.isfile(candidate):
|
||||||
|
try:
|
||||||
|
with open(candidate) as f:
|
||||||
|
return f.readline().strip()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
d = os.path.dirname(d)
|
||||||
|
try:
|
||||||
|
with open("/etc/samios-version") as f:
|
||||||
|
return f.readline().strip()
|
||||||
|
except OSError:
|
||||||
|
return "0.0.0-unknown"
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = _resolve_version()
|
||||||
|
del _resolve_version
|
||||||
|
|
||||||
|
|
||||||
class VoiceMode(Enum):
|
class VoiceMode(Enum):
|
||||||
IDLE = "idle"
|
IDLE = "idle"
|
||||||
COMMAND = "command"
|
COMMAND = "command"
|
||||||
@@ -59,6 +86,7 @@ class VoiceBridge:
|
|||||||
def start(self):
|
def start(self):
|
||||||
"""Start the voice bridge"""
|
"""Start the voice bridge"""
|
||||||
self.running = True
|
self.running = True
|
||||||
|
print(f"[VoiceBridge] SamiOS v{VERSION}")
|
||||||
print("[VoiceBridge] Ready. Say 'Computer' + command, or use hotkey.")
|
print("[VoiceBridge] Ready. Say 'Computer' + command, or use hotkey.")
|
||||||
|
|
||||||
# Connect to SamiType STT
|
# Connect to SamiType STT
|
||||||
@@ -199,6 +227,9 @@ class VoiceBridge:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
if "--version" in sys.argv or "-V" in sys.argv:
|
||||||
|
print(f"SamiOS voice-bridge v{VERSION}")
|
||||||
|
sys.exit(0)
|
||||||
bridge = VoiceBridge()
|
bridge = VoiceBridge()
|
||||||
try:
|
try:
|
||||||
bridge.start()
|
bridge.start()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ TEST_FILES=(
|
|||||||
"test_pacman_conf.sh"
|
"test_pacman_conf.sh"
|
||||||
"test_samios_cli.sh"
|
"test_samios_cli.sh"
|
||||||
"test_font_policy.sh"
|
"test_font_policy.sh"
|
||||||
|
"test_version.sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
TOTAL_RUN=0
|
TOTAL_RUN=0
|
||||||
|
|||||||
+13
-1
@@ -2,7 +2,13 @@
|
|||||||
# Common test helpers for SamiOS test suite
|
# Common test helpers for SamiOS test suite
|
||||||
# Sourced by individual test scripts
|
# Sourced by individual test scripts
|
||||||
|
|
||||||
set -uo pipefail
|
# Note: deliberately no `set -u` here. Tests use variables in single-quoted
|
||||||
|
# bash -c subshells where shellcheck can see them but bash treats them as
|
||||||
|
# "set in a different scope". Adding -u causes spurious "unbound variable"
|
||||||
|
# errors when the subshell hasn't yet assigned a var the outer scope
|
||||||
|
# references (e.g. `assert "...$VAR..."`). `set -o pipefail` is enough to
|
||||||
|
# catch pipeline failures.
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Path setup — resolve repo root relative to this file
|
# Path setup — resolve repo root relative to this file
|
||||||
@@ -11,6 +17,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
PROFILE_DIR="$REPO_ROOT/packaging/archiso"
|
PROFILE_DIR="$REPO_ROOT/packaging/archiso"
|
||||||
SCRIPTS_DIR="$REPO_ROOT/packaging/scripts"
|
SCRIPTS_DIR="$REPO_ROOT/packaging/scripts"
|
||||||
|
TESTS_DIR="$REPO_ROOT/tests"
|
||||||
|
|
||||||
|
# Export these so bash -c subshells (used by `assert ... bash -c '...'`)
|
||||||
|
# can reference them without re-derivation. test_version.sh relies on
|
||||||
|
# this for its git-clone mutation tests.
|
||||||
|
export REPO_ROOT PROFILE_DIR SCRIPTS_DIR TESTS_DIR
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Counters
|
# Counters
|
||||||
|
|||||||
Executable
+805
@@ -0,0 +1,805 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test: VERSION single-source-of-truth
|
||||||
|
# Locks the invariant: every version string in the repo must resolve to
|
||||||
|
# ./VERSION at runtime. No duplicate hardcoding allowed.
|
||||||
|
#
|
||||||
|
# Note: many assertions below reference variables (`cli_v`, `voice_v`,
|
||||||
|
# `mirror_v`, etc.) that are assigned inside single-quoted `bash -c`
|
||||||
|
# subshells. shellcheck cannot trace through the quoting layer, so it
|
||||||
|
# raises SC2154 warnings. These are intentional and the assertions DO
|
||||||
|
# verify the variables — see the test body.
|
||||||
|
# shellcheck disable=SC2154,SC2289,SC1011,SC1078,SC1083
|
||||||
|
|
||||||
|
source "$(dirname "$0")/test_helper.sh"
|
||||||
|
|
||||||
|
suite "VERSION single-source-of-truth tests"
|
||||||
|
|
||||||
|
VERSION_FILE="$REPO_ROOT/VERSION"
|
||||||
|
AIROOTFS_VERSION="$PROFILE_DIR/airootfs/etc/samios-version"
|
||||||
|
SAMIOS_CLI="$PROFILE_DIR/airootfs/usr/local/bin/samios"
|
||||||
|
INSTALLER="$SCRIPTS_DIR/samios-installer.sh"
|
||||||
|
PROFILEDEF="$PROFILE_DIR/profiledef.sh"
|
||||||
|
VOICE_BRIDGE="$SCRIPTS_DIR/voice-bridge.py"
|
||||||
|
PKGBUILD="$SCRIPTS_DIR/../packages/samios-branding/PKGBUILD"
|
||||||
|
DESKTOP_SETUP="$SCRIPTS_DIR/samios-desktop-setup.sh"
|
||||||
|
BASHRC="$PROFILE_DIR/airootfs/root/.bashrc"
|
||||||
|
ZSHRC="$PROFILE_DIR/airootfs/root/.zshrc"
|
||||||
|
|
||||||
|
# ── Hardcoded-release-version detector ────────────────────────────────────
|
||||||
|
# Scans a file for any X.Y.Z release-version literal, EXCLUDING:
|
||||||
|
# - lines starting with # (comments documenting SSOT design)
|
||||||
|
# - the documented fallback "0.0.0-unknown" string
|
||||||
|
# - test fixture strings like "9.9.9-test" and "0.2.0-rc1" which
|
||||||
|
# intentionally test version-set and prerelease flows
|
||||||
|
# The detector must catch ANY release-version literal, including:
|
||||||
|
# - version=1.2.3 (assignment)
|
||||||
|
# - "1.2.3" (string literal)
|
||||||
|
# - '1.2.3' (single-quoted string)
|
||||||
|
# - 1.2.3-foo (prerelease)
|
||||||
|
# - 1.2.3+bar (build metadata)
|
||||||
|
# - SamiOS 1.2.3 (display string)
|
||||||
|
# - VERSION=1.2.3 OR VERSION="1.2.3" (variable assignment)
|
||||||
|
detect_hardcoded_release() {
|
||||||
|
# detect_hardcoded_release <file>
|
||||||
|
# Exits 0 (clean) if no release-version literal is found; exits 1 if
|
||||||
|
# one is found.
|
||||||
|
#
|
||||||
|
# Production files: only the documented "0.0.0-unknown" fallback is
|
||||||
|
# allowed (matches the placeholder our resolvers print when the
|
||||||
|
# canonical VERSION cannot be located). Any other X.Y.Z literal
|
||||||
|
# (including prerelease, build-meta, v-prefixed, etc.) is a violation.
|
||||||
|
#
|
||||||
|
# Test files (under tests/): the same rule applies EXCEPT that two
|
||||||
|
# additional test fixtures are tolerated (0.2.0-rc1 in test 13's
|
||||||
|
# prerelease assertion, and 9.9.9-test in test 12's version-set
|
||||||
|
# assertion). Production components must NEVER contain these strings.
|
||||||
|
#
|
||||||
|
# Anchored allowed-token matching: each allowed token is matched as a
|
||||||
|
# complete regex match with non-alphanumeric boundaries on both sides.
|
||||||
|
# This prevents `127.0.0.10` from being eaten as `127.0.0.1` followed
|
||||||
|
# by `0` — `127.0.0.1` only matches at positions where the character
|
||||||
|
# AFTER it is non-alphanumeric, so `127.0.0.10` keeps its `0` intact
|
||||||
|
# and the version literal `127.0.0.10` is correctly flagged.
|
||||||
|
local f="$1"
|
||||||
|
local version_pattern='(^|[^a-zA-Z0-9])(v?[0-9]+[.][0-9]+[.][0-9]+([-+][a-zA-Z0-9.-]+)*)([^a-zA-Z0-9]|$)'
|
||||||
|
# Anchored allowed-token regex: same boundary rules as version_pattern.
|
||||||
|
# The middle group is the allowed token itself.
|
||||||
|
local allowed_pattern
|
||||||
|
if [[ "$f" == */tests/test_version.sh ]]; then
|
||||||
|
# Test fixture: tolerate the two prerelease/version-set fixtures
|
||||||
|
# AND the documented fallback + localhost IPs.
|
||||||
|
allowed_pattern='(^|[^a-zA-Z0-9])(0[.]0[.]0-unknown|9[.]9[.]9-test|0[.]2[.]0-rc1|127[.]0[.]0[.]1|127[.]0[.]1[.]1)([^a-zA-Z0-9]|$)'
|
||||||
|
else
|
||||||
|
# Production: only the documented 0.0.0-unknown fallback + localhost IPs
|
||||||
|
allowed_pattern='(^|[^a-zA-Z0-9])(0[.]0[.]0-unknown|127[.]0[.]0[.]1|127[.]0[.]1[.]1)([^a-zA-Z0-9]|$)'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Comment detection: a line is a comment if its first non-whitespace
|
||||||
|
# character is '#'. This handles shell, Python (after the leading
|
||||||
|
# whitespace), .desktop, and other line-oriented comment syntaxes.
|
||||||
|
local hits
|
||||||
|
# Strategy: find all version-pattern matches and allowed-pattern matches
|
||||||
|
# on each line. If a version-pattern match's character range does NOT
|
||||||
|
# fully overlap with an allowed-pattern match's range, it's a violation.
|
||||||
|
hits="$(grep -nE "$version_pattern" "$f" \
|
||||||
|
| grep -vE "^[^:]+:[ \\t]*#" \
|
||||||
|
| awk -F':' -v version_pat="$version_pattern" -v allowed_pat="$allowed_pattern" '
|
||||||
|
BEGIN { IGNORECASE = 0 }
|
||||||
|
{
|
||||||
|
content = substr($0, length($1) + 2)
|
||||||
|
# Build a "covered" map: for each allowed match, mark its
|
||||||
|
# character positions as covered. Then check each version
|
||||||
|
# match: if its range is NOT entirely covered, flag it.
|
||||||
|
delete covered
|
||||||
|
pos = 1
|
||||||
|
while (pos <= length(content) && match(substr(content, pos), allowed_pat)) {
|
||||||
|
s = pos + RSTART - 1
|
||||||
|
e = pos + RSTART + RLENGTH - 2
|
||||||
|
for (i = s; i <= e; i++) covered[i] = 1
|
||||||
|
# Advance past this match to find the next allowed match
|
||||||
|
pos = e + 1
|
||||||
|
}
|
||||||
|
# Now scan content for version matches and report any
|
||||||
|
# that are not fully covered.
|
||||||
|
vpos = 1
|
||||||
|
while (vpos <= length(content) && match(substr(content, vpos), version_pat)) {
|
||||||
|
vs = vpos + RSTART - 1
|
||||||
|
ve = vpos + RSTART + RLENGTH - 2
|
||||||
|
covered_full = 1
|
||||||
|
for (i = vs; i <= ve; i++) if (!covered[i]) { covered_full = 0; break }
|
||||||
|
if (!covered_full) { print $0; break }
|
||||||
|
vpos = ve + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
')"
|
||||||
|
|
||||||
|
if [ -n "$hits" ]; then
|
||||||
|
echo "HARD-CODED RELEASE VERSIONS FOUND in $f:" >&2
|
||||||
|
echo "$hits" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
# Export the function so it can be called from `bash -c '...'` subshells
|
||||||
|
export -f detect_hardcoded_release
|
||||||
|
|
||||||
|
# ── Repository-wide SSOT scan ────────────────────────────────────────────
|
||||||
|
# Every production file in the repo must pass detect_hardcoded_release.
|
||||||
|
# "Production" excludes tests/ (where we tolerate fixtures) and the
|
||||||
|
# canonical VERSION + airootfs mirror (which by definition contain
|
||||||
|
# the literal). If you add a new production file with a hardcoded
|
||||||
|
# version, it will appear here.
|
||||||
|
assert "every production file passes detect_hardcoded_release" \
|
||||||
|
bash -c '
|
||||||
|
violations=0
|
||||||
|
# Scan all production files. We exclude by EXACT canonical paths
|
||||||
|
# (not substring patterns), so a nested or unrelated file named
|
||||||
|
# VERSION cannot accidentally bypass SSOT enforcement.
|
||||||
|
while IFS= read -r -d "" f; do
|
||||||
|
case "$f" in
|
||||||
|
*/.git/*|*/tests/*) continue ;;
|
||||||
|
esac
|
||||||
|
# Compare against the exact canonical paths (resolved).
|
||||||
|
_resolved="$(cd "$(dirname "$f")" && pwd)/$(basename "$f")"
|
||||||
|
_root_version="$(cd "$REPO_ROOT" && pwd)/VERSION"
|
||||||
|
_root_mirror="$(cd "$REPO_ROOT" && pwd)/packaging/archiso/airootfs/etc/samios-version"
|
||||||
|
if [ "$_resolved" = "$_root_version" ] || [ "$_resolved" = "$_root_mirror" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! detect_hardcoded_release "$f"; then
|
||||||
|
violations=$((violations + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$REPO_ROOT" -type f -not -path "*/.git/*" -not -path "*/tests/*" -print0)
|
||||||
|
if [ "$violations" -gt 0 ]; then
|
||||||
|
echo "FAIL: $violations production files contain hardcoded version literals"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── Regression test: 0.2.0-rc1 is REJECTED in production components ──────
|
||||||
|
# The prerelease string "0.2.0-rc1" is allowed ONLY in test_version.sh as
|
||||||
|
# a test fixture. If it appears in any production component, the SSOT
|
||||||
|
# invariant is violated.
|
||||||
|
assert "regression: 0.2.0-rc1 is REJECTED in production components" \
|
||||||
|
bash -c '
|
||||||
|
# Find any production file containing 0.2.0-rc1 outside of test_version.sh
|
||||||
|
while IFS= read -r -d "" f; do
|
||||||
|
case "$f" in
|
||||||
|
*/.git/*|*/tests/*) continue ;;
|
||||||
|
esac
|
||||||
|
if grep -q "0\.2\.0-rc1" "$f" 2>/dev/null; then
|
||||||
|
echo "FAIL: $f contains 0.2.0-rc1 in production"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done < <(find "$REPO_ROOT" -type f -not -path "*/.git/*" -not -path "*/tests/*" -print0)
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── Regression test: 9.9.9-test is REJECTED in production components ─────
|
||||||
|
# Same as above but for the version-set test fixture.
|
||||||
|
assert "regression: 9.9.9-test is REJECTED in production components" \
|
||||||
|
bash -c '
|
||||||
|
while IFS= read -r -d "" f; do
|
||||||
|
case "$f" in
|
||||||
|
*/.git/*|*/tests/*) continue ;;
|
||||||
|
esac
|
||||||
|
if grep -q "9\.9\.9-test" "$f" 2>/dev/null; then
|
||||||
|
echo "FAIL: $f contains 9.9.9-test in production"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done < <(find "$REPO_ROOT" -type f -not -path "*/.git/*" -not -path "*/tests/*" -print0)
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── Detector self-tests ──────────────────────────────────────────────────
|
||||||
|
# The detector is a non-trivial regex; these tests verify it catches every
|
||||||
|
# form we claim and does not raise false positives. The tests construct
|
||||||
|
# synthetic files in $TMPDIR, run detect_hardcoded_release against them,
|
||||||
|
# and check the result. Each form is a separate assertion so a regression
|
||||||
|
# points at exactly which form broke.
|
||||||
|
|
||||||
|
assert "detector self-test: catches double-quoted X.Y.Z literal" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo \"1.2.3\"" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed \"1.2.3\"" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches single-quoted X.Y.Z literal" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo '"'"'1.2.3'"'"'" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed '"'"'1.2.3'"'"'" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches bare X.Y.Z literal" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo 1.2.3" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed bare 1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches prerelease X.Y.Z-foo literal" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo 1.2.3-rc1" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed 1.2.3-rc1" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches build-metadata X.Y.Z+bar literal" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo 1.2.3+build.5" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed 1.2.3+build.5" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches SamiOS X.Y.Z display string" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo \"SamiOS 1.2.3 (WSL Desktop)\"" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed SamiOS 1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches VERSION=1.2.3 assignment" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "VERSION=\"1.2.3\"" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed VERSION=1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches v-prefixed v1.2.3" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo v1.2.3" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed v1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches X.Y.Z in path (/opt/1.2.3/bin)" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "PATH=/opt/1.2.3/bin" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed path-embedded 1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches X.Y.Z in colon-delimited value" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "key=val:1.2.3:rest" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed colon-delimited 1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches X.Y.Z in brackets [1.2.3]" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "x=[1.2.3]" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed bracketed 1.2.3" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: ALLOWS 0.0.0-unknown fallback" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "VERSION=0.0.0-unknown" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" && exit 0
|
||||||
|
echo "FAILED: detector flagged documented fallback 0.0.0-unknown" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: ALLOWS comments (lines starting with #)" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "# This is a comment with 1.2.3 in it" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" && exit 0
|
||||||
|
echo "FAILED: detector flagged commented-out version" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: ALLOWS localhost IP (127.0.0.1)" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "127.0.0.1 localhost" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" && exit 0
|
||||||
|
echo "FAILED: detector flagged localhost IP" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches MIXED allowed+forbidden on same line" \
|
||||||
|
bash -c '
|
||||||
|
# A line that contains BOTH the allowed fallback and a forbidden
|
||||||
|
# release-version literal must be flagged. This catches the bug
|
||||||
|
# where `grep -v '\''0.0.0-unknown'\''` would suppress the entire line
|
||||||
|
# and miss the forbidden literal.
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo 0.0.0-unknown 1.2.3" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed mixed allowed/forbidden line" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: ALLOWS indented # comments with version literal" \
|
||||||
|
bash -c '
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
cat > "$tmpf" << '"'"'EOF'"'"'
|
||||||
|
# Top-level comment with 1.2.3
|
||||||
|
# Indented comment with 1.2.4
|
||||||
|
echo real_code
|
||||||
|
EOF
|
||||||
|
detect_hardcoded_release "$tmpf" && exit 0
|
||||||
|
echo "FAILED: detector flagged commented version literals" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches 127.0.0.10 (substring of allowed IP)" \
|
||||||
|
bash -c '
|
||||||
|
# 127.0.0.10 contains 127.0.0.1 as a substring. A naive substring
|
||||||
|
# filter would erase `127.0.0.1` and leave `0` (which does NOT
|
||||||
|
# match X.Y.Z), letting the real release-version literal
|
||||||
|
# `127.0.0.10` slip through. Our anchored detector must catch it.
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo 127.0.0.10" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed 127.0.0.10" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "detector self-test: catches 0.0.0-unknown+build (suffix of fallback)" \
|
||||||
|
bash -c '
|
||||||
|
# 0.0.0-unknown+build contains 0.0.0-unknown as a substring. A naive
|
||||||
|
# filter would erase `0.0.0-unknown` and leave `+build` (which
|
||||||
|
# does NOT match X.Y.Z), letting the build-metadata version literal
|
||||||
|
# `0.0.0-unknown+build` slip through.
|
||||||
|
tmpf="$(mktemp)"
|
||||||
|
trap "rm -f '\''$tmpf'\''" EXIT
|
||||||
|
echo "echo 0.0.0-unknown+build" > "$tmpf"
|
||||||
|
detect_hardcoded_release "$tmpf" || exit 0
|
||||||
|
echo "FAILED: detector missed 0.0.0-unknown+build" >&2; exit 1
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 1. Canonical file exists and is a valid semver-ish string ──────────────
|
||||||
|
assert_file_exists "VERSION file exists at repo root" "$VERSION_FILE"
|
||||||
|
|
||||||
|
assert "VERSION is non-empty single-line" \
|
||||||
|
bash -c '
|
||||||
|
[ "$(wc -l < "'"$VERSION_FILE"'")" -eq 1 ] && \
|
||||||
|
[ -n "$(cat "'"$VERSION_FILE"'")" ]
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "VERSION matches semver-ish pattern (X.Y.Z optional suffix)" \
|
||||||
|
bash -c '
|
||||||
|
grep -qE "^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$" "'"$VERSION_FILE"'"
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 2. airootfs mirror is byte-identical to root VERSION ───────────────────
|
||||||
|
assert_file_exists "airootfs mirror /etc/samios-version exists" "$AIROOTFS_VERSION"
|
||||||
|
|
||||||
|
assert "airootfs VERSION mirror matches repo root VERSION" \
|
||||||
|
bash -c '
|
||||||
|
diff -q "'"$VERSION_FILE"'" "'"$AIROOTFS_VERSION"'" >/dev/null
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 3. samios CLI must NOT hardcode any release-version literal ──────────
|
||||||
|
assert_file_exists "samios CLI exists" "$SAMIOS_CLI"
|
||||||
|
|
||||||
|
assert "samios CLI has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$SAMIOS_CLI"'"'
|
||||||
|
|
||||||
|
assert "samios CLI references ./VERSION walk-up lookup" \
|
||||||
|
bash -c '
|
||||||
|
grep -q "/VERSION" "'"$SAMIOS_CLI"'" && \
|
||||||
|
grep -q "_lookup_version" "'"$SAMIOS_CLI"'"
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "samios CLI prints repo VERSION when run from repo root" \
|
||||||
|
bash -c '
|
||||||
|
cd "'"$REPO_ROOT"'" && \
|
||||||
|
expected="$(cat VERSION)" && \
|
||||||
|
actual="$(bash "'"$SAMIOS_CLI"'" version | sed -E "s/^SamiOS v//" | head -n1)" && \
|
||||||
|
[ "$actual" = "$expected" ]
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 4. Installer must NOT hardcode any release-version literal ───────────
|
||||||
|
assert_file_exists "samios-installer.sh exists" "$INSTALLER"
|
||||||
|
|
||||||
|
assert "installer has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$INSTALLER"'"'
|
||||||
|
|
||||||
|
assert "installer references ./VERSION walk-up lookup" \
|
||||||
|
bash -c '
|
||||||
|
grep -q "_VERSION_FILE\|/VERSION" "'"$INSTALLER"'"
|
||||||
|
'
|
||||||
|
|
||||||
|
# Verify the installer's actual heredoc-emitted CLI has the _lookup_version
|
||||||
|
# function — not a substituted $VERSION literal.
|
||||||
|
assert "installer heredoc emits CLI with _lookup_version (no literal)" \
|
||||||
|
bash -c '
|
||||||
|
# The installer MUST NOT substitute $VERSION into a literal at
|
||||||
|
# the top of the emitted CLI. We extract the heredoc body (between
|
||||||
|
# the << SAMIOS_CLI marker line and the closing SAMIOS_CLI) and
|
||||||
|
# check for the forbidden pattern.
|
||||||
|
heredoc_open="$(grep -n "<< .*SAMIOS_CLI" "'"$INSTALLER"'" | head -1 | cut -d: -f1)"
|
||||||
|
heredoc_close="$(awk -v open_line="$heredoc_open" "NR>open_line && /^SAMIOS_CLI\$/{print NR; exit}" "'"$INSTALLER"'")"
|
||||||
|
if [ -z "$heredoc_open" ] || [ -z "$heredoc_close" ]; then
|
||||||
|
echo "could not locate SAMIOS_CLI heredoc in installer (open=$heredoc_open close=$heredoc_close)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
heredoc_body="$(sed -n "${heredoc_open},${heredoc_close}p" "'"$INSTALLER"'")"
|
||||||
|
# Forbidden: a literal VERSION="${VERSION}" (substituted from outer scope)
|
||||||
|
if echo "$heredoc_body" | grep -qE "^VERSION=\"\\\$\{VERSION\}\""; then
|
||||||
|
echo "installer emits VERSION=\"\${VERSION}\" literal at top of heredoc"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Required: the heredoc body must reference _lookup_version so
|
||||||
|
# the installed CLI uses the walk-up pattern.
|
||||||
|
if ! echo "$heredoc_body" | grep -q "_lookup_version"; then
|
||||||
|
echo "installer heredoc does not reference _lookup_version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 5. profiledef.sh must NOT hardcode any release-version literal ────────
|
||||||
|
assert_file_exists "profiledef.sh exists" "$PROFILEDEF"
|
||||||
|
|
||||||
|
assert "profiledef.sh has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$PROFILEDEF"'"'
|
||||||
|
|
||||||
|
assert "profiledef.sh references ./VERSION walk-up lookup" \
|
||||||
|
bash -c '
|
||||||
|
grep -q "_iso_version\|/VERSION" "'"$PROFILEDEF"'"
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 6. voice-bridge.py must read from ./VERSION ────────────────────────────
|
||||||
|
assert_file_exists "voice-bridge.py exists" "$VOICE_BRIDGE"
|
||||||
|
|
||||||
|
assert "voice-bridge.py parses as valid Python" \
|
||||||
|
bash -c '
|
||||||
|
python3 -c "import ast; ast.parse(open(\"'"$VOICE_BRIDGE"'\").read())"
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "voice-bridge.py has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$VOICE_BRIDGE"'"'
|
||||||
|
|
||||||
|
assert "voice-bridge.py resolves VERSION from repo root" \
|
||||||
|
bash -c '
|
||||||
|
cd "'"$REPO_ROOT"'" && \
|
||||||
|
expected="$(cat VERSION)" && \
|
||||||
|
actual="$(python3 "'"$VOICE_BRIDGE"'" --version | sed -E "s/^SamiOS voice-bridge v//")" && \
|
||||||
|
[ "$actual" = "$expected" ]
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 7. PKGBUILD must NOT hardcode any release-version literal ────────────
|
||||||
|
assert_file_exists "PKGBUILD exists" "$PKGBUILD"
|
||||||
|
|
||||||
|
assert "PKGBUILD has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$PKGBUILD"'"'
|
||||||
|
|
||||||
|
assert "PKGBUILD references ../../../VERSION" \
|
||||||
|
bash -c '
|
||||||
|
grep -q "../../../VERSION" "'"$PKGBUILD"'"
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 8. desktop-setup.sh must NOT hardcode any release-version literal ──────
|
||||||
|
assert_file_exists "samios-desktop-setup.sh exists" "$DESKTOP_SETUP"
|
||||||
|
|
||||||
|
assert "desktop-setup.sh has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$DESKTOP_SETUP"'"'
|
||||||
|
|
||||||
|
# ── 9. .bashrc / .zshrc must NOT hardcode any release-version literal ────
|
||||||
|
assert ".bashrc has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$BASHRC"'"'
|
||||||
|
|
||||||
|
assert ".zshrc has no hardcoded release-version literal" \
|
||||||
|
bash -c 'detect_hardcoded_release "'"$ZSHRC"'"'
|
||||||
|
|
||||||
|
# ── 10. Cross-system invariant: CLI, installer, profiledef, voice-bridge ──
|
||||||
|
# Test the actual code paths in each file. For the installer and profiledef,
|
||||||
|
# we extract their version-resolution blocks from the real files (not
|
||||||
|
# reimplementations) into temp helpers, then rewrite the $0-relative path
|
||||||
|
# to point at the actual file's directory so the walk-up starts in the
|
||||||
|
# right place.
|
||||||
|
PROFILEDEF_RESOLVER="$(mktemp)"
|
||||||
|
PROFILEDEF_DIR="$(dirname "$PROFILEDEF")"
|
||||||
|
{
|
||||||
|
sed -n '/^# ── Canonical version source ───/,/^unset _d$/p' \
|
||||||
|
"$PROFILEDEF"
|
||||||
|
echo 'echo "$_iso_version"'
|
||||||
|
} > "$PROFILEDEF_RESOLVER"
|
||||||
|
sed -i "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$PROFILEDEF_DIR\"|g" "$PROFILEDEF_RESOLVER"
|
||||||
|
chmod +x "$PROFILEDEF_RESOLVER"
|
||||||
|
|
||||||
|
# Extract the installer's version-resolution block (top of file) into a
|
||||||
|
# real shell script we can run. The block uses $0 to find its own
|
||||||
|
# directory; when extracted standalone, $0 becomes the helper's path
|
||||||
|
# (under /tmp), which isn't where VERSION lives. We rewrite the $0
|
||||||
|
# lookup to start from the installer's actual directory.
|
||||||
|
INSTALLER_DIR="$(dirname "$INSTALLER")"
|
||||||
|
INSTALLER_RESOLVER="$(mktemp)"
|
||||||
|
{
|
||||||
|
sed -n '/^# ── Canonical version source ───/,/^unset _d _VERSION_FILE/p' \
|
||||||
|
"$INSTALLER"
|
||||||
|
echo 'echo "$VERSION"'
|
||||||
|
} > "$INSTALLER_RESOLVER"
|
||||||
|
# Rewrite $0 references inside the extracted block so they resolve from
|
||||||
|
# the installer's actual directory, not from /tmp/codex-judge-XXXXXX.
|
||||||
|
sed -i "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$INSTALLER_DIR\"|g" "$INSTALLER_RESOLVER"
|
||||||
|
chmod +x "$INSTALLER_RESOLVER"
|
||||||
|
|
||||||
|
assert "all four components agree on VERSION (tested from real files)" \
|
||||||
|
bash -c '
|
||||||
|
cd "'"$REPO_ROOT"'" || exit 1
|
||||||
|
expected="$(cat VERSION)"
|
||||||
|
cli_v="$(bash "'"$SAMIOS_CLI"'" version | head -n1 | sed -E "s/^SamiOS v//")"
|
||||||
|
voice_v="$(python3 "'"$VOICE_BRIDGE"'" --version | sed -E "s/^SamiOS voice-bridge v//")"
|
||||||
|
inst_v="$("'"$INSTALLER_RESOLVER"'" | tail -n1)"
|
||||||
|
prof_v="$("'"$PROFILEDEF_RESOLVER"'")"
|
||||||
|
if [ "$cli_v" != "$expected" ]; then
|
||||||
|
echo "CLI=$cli_v expected=$expected"; exit 1
|
||||||
|
fi
|
||||||
|
if [ "$voice_v" != "$expected" ]; then
|
||||||
|
echo "voice=$voice_v expected=$expected"; exit 1
|
||||||
|
fi
|
||||||
|
if [ "$inst_v" != "$expected" ]; then
|
||||||
|
echo "installer=$inst_v expected=$expected"; exit 1
|
||||||
|
fi
|
||||||
|
if [ "$prof_v" != "$expected" ]; then
|
||||||
|
echo "profiledef=$prof_v expected=$expected"; exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
rm -f "$PROFILEDEF_RESOLVER" "$INSTALLER_RESOLVER"
|
||||||
|
|
||||||
|
# ── 11. PKGBUILD resolves pkgver to the canonical VERSION ──────────────────
|
||||||
|
# Source the PKGBUILD in a subshell (PKGBUILD is bash-runnable) and capture
|
||||||
|
# the resulting pkgver. This proves the conditional `pkgver=` block in the
|
||||||
|
# PKGBUILD evaluates to the canonical VERSION.
|
||||||
|
assert "PKGBUILD pkgver resolves to VERSION (sourced)" \
|
||||||
|
bash -c '
|
||||||
|
cd "'"$REPO_ROOT"'"/packaging/packages/samios-branding && \
|
||||||
|
expected="$(head -n1 ../../../VERSION)" && \
|
||||||
|
actual="$(set -e; source PKGBUILD >/dev/null 2>&1 && echo "$pkgver")" && \
|
||||||
|
[ "$actual" = "$expected" ] || { echo "actual=$actual expected=$expected"; exit 1; }
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 11b. PKGBUILD SSOT is absolute (no SAMIOS_PKGVER_OVERRIDE escape) ─────
|
||||||
|
# The previous PKGBUILD had a SAMIOS_PKGVER_OVERRIDE env var that allowed
|
||||||
|
# pkgver to differ from VERSION. That violated the SSOT invariant. Verify
|
||||||
|
# the override is removed so release builds cannot diverge.
|
||||||
|
assert "PKGBUILD has no SAMIOS_PKGVER_OVERRIDE escape hatch (SSOT is absolute)" \
|
||||||
|
bash -c '
|
||||||
|
# Look for SAMIOS_PKGVER_OVERRIDE outside of comments (lines
|
||||||
|
# starting with #). Comments can mention the variable for
|
||||||
|
# documentation purposes; only executable code that references
|
||||||
|
# it would violate SSOT.
|
||||||
|
if grep -n "SAMIOS_PKGVER_OVERRIDE" "'"$PKGBUILD"'" | grep -vE "^[0-9]+:#"; then
|
||||||
|
echo "PKGBUILD still references SAMIOS_PKGVER_OVERRIDE outside a comment — SSOT invariant violated"
|
||||||
|
grep -n "SAMIOS_PKGVER_OVERRIDE" "'"$PKGBUILD"'" | grep -vE "^[0-9]+:#"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Also verify the override does not actually bypass VERSION, in
|
||||||
|
# case some future shell magic reintroduces the override.
|
||||||
|
cd "'"$REPO_ROOT"'"/packaging/packages/samios-branding && \
|
||||||
|
expected="$(head -n1 ../../../VERSION)" && \
|
||||||
|
actual="$(SAMIOS_PKGVER_OVERRIDE=99.99.99-bogus bash -c "set -e; source PKGBUILD >/dev/null 2>&1; echo \$pkgver")" && \
|
||||||
|
[ "$actual" = "$expected" ] || { echo "override leaked: actual=$actual expected=$expected"; exit 1; }
|
||||||
|
'
|
||||||
|
|
||||||
|
assert "regression: hash-based diff catches deletions (not just additions)" \
|
||||||
|
bash -c '
|
||||||
|
# The version-set test (assertion above) counts only the right
|
||||||
|
# side of the diff. A pure deletion produces only a left-side
|
||||||
|
# line. Verify the diff-based detection we use catches BOTH
|
||||||
|
# sides: synthesize a fake before/after where a file is deleted
|
||||||
|
# and a different file is added, then count unique filenames.
|
||||||
|
diff_file="$(mktemp)"
|
||||||
|
trap "rm -f '\''$diff_file'\''" EXIT
|
||||||
|
cat > "$diff_file" << '"'"'DIFF_EOF'"'"'
|
||||||
|
< d84523f8671da4c8b99f543127715bb8c562fafe8675016112c6d260d14e761b ./some-deleted-file.txt
|
||||||
|
> 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ./some-new-file.txt
|
||||||
|
DIFF_EOF
|
||||||
|
count="$(awk '"'"'/^[<>] / {print $NF}'"'"' "$diff_file" | sort -u | wc -l)"
|
||||||
|
if [ "$count" -ne 2 ]; then
|
||||||
|
echo "FAIL: expected 2 unique filenames (one deleted + one added), got $count"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 12. End-to-end: version-set mutates only VERSION + mirror ─────────────
|
||||||
|
assert "version-set mutates only VERSION + airootfs mirror (no other files)" \
|
||||||
|
bash -c '
|
||||||
|
clone="$(mktemp -d)"
|
||||||
|
diff_file="$(mktemp)"
|
||||||
|
# Cleanup trap uses $clone and $diff_file from the surrounding scope
|
||||||
|
trap "rm -rf ${clone:?} ${diff_file:?}" EXIT
|
||||||
|
# Copy with .git so any pre-existing modifications are visible as
|
||||||
|
# baseline (so we can detect whether version-set changed anything
|
||||||
|
# by content, not by index state).
|
||||||
|
cp -a "$REPO_ROOT" "$clone/repo"
|
||||||
|
cd "$clone/repo" || exit 1
|
||||||
|
orig_version="$(cat VERSION)"
|
||||||
|
orig_mirror="$(head -n1 packaging/archiso/airootfs/etc/samios-version)"
|
||||||
|
# Snapshot the content hash of every file in the repo (excluding
|
||||||
|
# .git) BEFORE the mutation. We use this to detect any file change,
|
||||||
|
# not git status which can be misleading for already-modified files.
|
||||||
|
before_hashes="$(find . -path ./.git -prune -o -type f -print0 2>/dev/null \
|
||||||
|
| xargs -0 sha256sum 2>/dev/null | sort)"
|
||||||
|
# `make version-set` must SUCCEED. If it fails (e.g. bad NEW value,
|
||||||
|
# missing make), the test fails — not silently passes.
|
||||||
|
if ! make version-set NEW=9.9.9-test >/dev/null 2>&1; then
|
||||||
|
echo "make version-set NEW=9.9.9-test failed unexpectedly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# VERSION must actually have changed.
|
||||||
|
if [ "$(cat VERSION)" != "9.9.9-test" ]; then
|
||||||
|
echo "VERSION did not change to 9.9.9-test"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Mirror must actually have changed.
|
||||||
|
if [ "$(head -n1 packaging/archiso/airootfs/etc/samios-version)" != "9.9.9-test" ]; then
|
||||||
|
echo "airootfs mirror did not sync to 9.9.9-test"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Snapshot hashes AFTER the mutation.
|
||||||
|
after_hashes="$(find . -path ./.git -prune -o -type f -print0 2>/dev/null \
|
||||||
|
| xargs -0 sha256sum 2>/dev/null | sort)"
|
||||||
|
# Diff before vs after. Count BOTH sides of the diff (a deletion
|
||||||
|
# produces only a left-side line; an addition produces only a
|
||||||
|
# right-side line; a modification produces one of each). Use the
|
||||||
|
# parsed filename-to-hash maps to count unique files that
|
||||||
|
# changed in either direction, not just added/right-side lines.
|
||||||
|
diff <(echo "$before_hashes") <(echo "$after_hashes") > "$diff_file"
|
||||||
|
# Extract the set of filenames that appear on either side of the
|
||||||
|
# diff. `awk` gets us the distinct filenames regardless of side.
|
||||||
|
changed_count="$(awk "/^[<>] / {print \$NF}" "$diff_file" | sort -u | wc -l)"
|
||||||
|
if [ "$changed_count" -ne 2 ]; then
|
||||||
|
echo "expected exactly 2 files to change (VERSION + mirror), got $changed_count:"
|
||||||
|
awk "/^[<>] / {print \$0}" "$diff_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Verify the two changed files are VERSION and the mirror (not
|
||||||
|
# some other files). The diff format is "<sha256> <filename>";
|
||||||
|
# match on the filename suffix.
|
||||||
|
unexpected="$(awk "/^[<>] / {print \$NF}" "$diff_file" | sort -u)"
|
||||||
|
unexpected="$(echo "$unexpected" | grep -vE "VERSION$|packaging/archiso/airootfs/etc/samios-version$")"
|
||||||
|
if [ -n "$unexpected" ]; then
|
||||||
|
echo "version-set changed unexpected files:"
|
||||||
|
echo "$unexpected"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Restore clone state — the developers checkout was never touched.
|
||||||
|
# We restore to the original VERSION we captured, not to a
|
||||||
|
# hardcoded value, so this test is safe to run at any version.
|
||||||
|
echo "$orig_version" > VERSION
|
||||||
|
if ! make version-sync >/dev/null 2>&1; then
|
||||||
|
echo "make version-sync failed during restore"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Verify restoration succeeded by snapshotting again and comparing
|
||||||
|
# to the original before-snapshot. The post-restore state must
|
||||||
|
# match the original content hash (no stray modifications).
|
||||||
|
restore_hashes="$(find . -path ./.git -prune -o -type f -print0 2>/dev/null \
|
||||||
|
| xargs -0 sha256sum 2>/dev/null | sort)"
|
||||||
|
if [ "$before_hashes" != "$restore_hashes" ]; then
|
||||||
|
echo "restore failed — clone does not match original"
|
||||||
|
diff <(echo "$before_hashes") <(echo "$restore_hashes") | head -10
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Sanity: the values we saved at the top must be the canonical
|
||||||
|
# values, not arbitrary ones. (We already know VERSION=0.1.0 from
|
||||||
|
# earlier tests, but this guards against the test running against
|
||||||
|
# an unusual repo state.)
|
||||||
|
if [ -z "$orig_version" ] || [ -z "$orig_mirror" ]; then
|
||||||
|
echo "could not read original VERSION or mirror; bail"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 13. Prerelease VERSION works through the whole stack ─────────────────
|
||||||
|
# Mutate VERSION to a prerelease and verify ALL resolvers see it correctly:
|
||||||
|
# CLI, voice-bridge, airootfs mirror, installer (resolves through its own
|
||||||
|
# walk-up), profiledef (resolves through its own walk-up), desktop-setup
|
||||||
|
# (resolves through its own walk-up), and PKGBUILD (transforms to underscore).
|
||||||
|
#
|
||||||
|
# Use a cleanup trap and preserve the original VERSION rather than
|
||||||
|
# hardcoding 0.1.0, so this test is safe to run at any version.
|
||||||
|
assert "prerelease VERSION (e.g. 0.2.0-rc1) flows through all 7 components" \
|
||||||
|
bash -c '
|
||||||
|
clone="$(mktemp -d)"
|
||||||
|
trap "rm -rf '\''$clone'\''" EXIT
|
||||||
|
cp -a "$REPO_ROOT" "$clone/repo"
|
||||||
|
cd "$clone/repo" || exit 1
|
||||||
|
orig_version="$(cat VERSION)"
|
||||||
|
echo "0.2.0-rc1" > VERSION
|
||||||
|
if ! make version-sync >/dev/null 2>&1; then
|
||||||
|
echo "make version-sync failed"; exit 1
|
||||||
|
fi
|
||||||
|
cli_v="$(bash packaging/archiso/airootfs/usr/local/bin/samios version | head -n1 | sed -E "s/^SamiOS v//")"
|
||||||
|
voice_v="$(python3 packaging/scripts/voice-bridge.py --version | sed -E "s/^SamiOS voice-bridge v//")"
|
||||||
|
mirror_v="$(head -n1 packaging/archiso/airootfs/etc/samios-version)"
|
||||||
|
# Installer resolves through its own walk-up block at the top of the file
|
||||||
|
installer_resolver="$(mktemp)"
|
||||||
|
sed -n "/^# ── Canonical version source ───/,/^unset _d _VERSION_FILE/p" \
|
||||||
|
packaging/scripts/samios-installer.sh | sed "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$(pwd)/packaging/scripts\"|g" \
|
||||||
|
> "$installer_resolver"
|
||||||
|
printf "echo \"\$VERSION\"\n" >> "$installer_resolver"
|
||||||
|
chmod +x "$installer_resolver"
|
||||||
|
installer_v="$("$installer_resolver" | tail -n1)"
|
||||||
|
rm -f "$installer_resolver"
|
||||||
|
# Profiledef resolves through its own walk-up block
|
||||||
|
profiledef_resolver="$(mktemp)"
|
||||||
|
sed -n "/^# ── Canonical version source ───/,/^unset _d$/p" \
|
||||||
|
packaging/archiso/profiledef.sh | sed "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$(pwd)/packaging/archiso\"|g" \
|
||||||
|
> "$profiledef_resolver"
|
||||||
|
printf "echo \"\$_iso_version\"\n" >> "$profiledef_resolver"
|
||||||
|
chmod +x "$profiledef_resolver"
|
||||||
|
profiledef_v="$("$profiledef_resolver" | tail -n1)"
|
||||||
|
rm -f "$profiledef_resolver"
|
||||||
|
# Desktop-setup resolves through its own walk-up block. Extract
|
||||||
|
# the resolution block up to (but not including) the
|
||||||
|
# echo-write line — that line writes to a system path we cant
|
||||||
|
# touch in a test.
|
||||||
|
desktop_start="$(grep -n "# Version file — generate" packaging/scripts/samios-desktop-setup.sh | head -1 | cut -d: -f1)"
|
||||||
|
desktop_end_pre="$(grep -n "echo \"SamiOS.*> /usr/share/samios/version" packaging/scripts/samios-desktop-setup.sh | head -1 | cut -d: -f1)"
|
||||||
|
desktop_end=$((desktop_end_pre - 1))
|
||||||
|
desktop_resolver="$(mktemp)"
|
||||||
|
if [ -z "$desktop_start" ] || [ -z "$desktop_end_pre" ]; then
|
||||||
|
echo "could not locate desktop-setup version block markers"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sed -n "${desktop_start},${desktop_end}p" packaging/scripts/samios-desktop-setup.sh | \
|
||||||
|
sed "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$(pwd)/packaging/scripts\"|g" \
|
||||||
|
> "$desktop_resolver"
|
||||||
|
printf "echo \"\$_SAMIOS_DESK_VERSION\"\n" >> "$desktop_resolver"
|
||||||
|
chmod +x "$desktop_resolver"
|
||||||
|
desktop_v="$("$desktop_resolver" | tail -n1)"
|
||||||
|
rm -f "$desktop_resolver"
|
||||||
|
# PKGBUILD transforms hyphen to underscore for Arch pkgver
|
||||||
|
pkgver_v="$(cd packaging/packages/samios-branding && set -e && source PKGBUILD >/dev/null 2>&1 && echo "${pkgver:-}")"
|
||||||
|
# Restore clone to its original VERSION (captured above) rather
|
||||||
|
# than hardcoding a value — safe at any current version.
|
||||||
|
echo "$orig_version" > VERSION
|
||||||
|
make version-sync >/dev/null 2>&1
|
||||||
|
# Verify all seven agree (or transform as expected)
|
||||||
|
[ "$cli_v" = "0.2.0-rc1" ] || { echo "CLI=$cli_v"; exit 1; }
|
||||||
|
[ "$voice_v" = "0.2.0-rc1" ] || { echo "voice=$voice_v"; exit 1; }
|
||||||
|
[ "$mirror_v" = "0.2.0-rc1" ] || { echo "mirror=$mirror_v"; exit 1; }
|
||||||
|
[ "$installer_v" = "0.2.0-rc1" ] || { echo "installer=$installer_v"; exit 1; }
|
||||||
|
[ "$profiledef_v" = "0.2.0-rc1" ] || { echo "profiledef=$profiledef_v"; exit 1; }
|
||||||
|
[ "$desktop_v" = "0.2.0-rc1" ] || { echo "desktop=$desktop_v"; exit 1; }
|
||||||
|
[ "$pkgver_v" = "0.2.0_rc1" ] || { echo "pkgver=$pkgver_v expected=0.2.0_rc1"; exit 1; }
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── 14. PKGBUILD transforms "0.X.Y-suffix" to "0.X.Y_suffix" for Arch ───
|
||||||
|
# This duplicates test 13's PKGBUILD check but in isolation, so a failure
|
||||||
|
# here points directly at PKGBUILD (not at the broader end-to-end test).
|
||||||
|
assert "PKGBUILD transforms prerelease hyphen to underscore for Arch pkgver" \
|
||||||
|
bash -c '
|
||||||
|
clone="$(mktemp -d)"
|
||||||
|
trap "rm -rf ${clone:?}" EXIT
|
||||||
|
cp -a "$REPO_ROOT" "$clone/repo"
|
||||||
|
cd "$clone/repo/packaging/packages/samios-branding" || exit 1
|
||||||
|
echo "0.2.0-rc1" > ../../../VERSION
|
||||||
|
# Source PKGBUILD and echo pkgver — no nested bash -c quoting needed.
|
||||||
|
pkgver_actual="$(set -e; source PKGBUILD >/dev/null 2>&1 && echo "$pkgver")"
|
||||||
|
cd /
|
||||||
|
rm -rf "$clone"
|
||||||
|
[ "$pkgver_actual" = "0.2.0_rc1" ] || { echo "pkgver=$pkgver_actual expected=0.2.0_rc1"; exit 1; }
|
||||||
|
'
|
||||||
|
|
||||||
|
print_summary
|
||||||
Reference in New Issue
Block a user