diff --git a/bash-styleguide.md b/bash-styleguide.md index d34860e..c8f2494 100644 --- a/bash-styleguide.md +++ b/bash-styleguide.md @@ -1,8 +1,15 @@ # CE OS Script Style Guide — Bash - - + + + + + + --- @@ -14,16 +21,17 @@ script — installer, utility, tier component, library, or one-off tool — must conform to these rules before it is considered fit for review, testing, or promotion. -A Python style guide will be produced separately once sufficient CE OS Python -use cases exist to ground it in real experience rather than theory. +A Python style guide will be produced as a companion to this document. Python +scripts should follow this Bash guide as closely as the language permits; +deviations are documented in the Python guide with explicit justification. -These rules encode hard-won lessons from real hardware deployment across Alpine -aarch64, Alpine armhf, Alpine x86, Debian amd64, and NixOS. They exist because +These rules encode hard-won lessons from real hardware deployment across Alpine, +Debian, and NixOS on armv6/armhf, arm64, x86, and x86-64. They exist because silent failures, missed cleanups, and untested assumptions have caused real problems on real machines. Follow them without exception. -CE OS scripts are also teaching documents. An MUO-level reader — someone -competent but not a professional developer — should be able to follow any +CE OS scripts are also teaching documents. An enthusiast-level reader — someone +eager to learn but not a professional developer — should be able to follow any script and understand what it does and why. Clarity is not optional. --- @@ -35,13 +43,15 @@ subscript, or library). ### 1.1 Attribution header -Every script — without exception — carries this attribution in its header: +Every script — without exception — carries both attributions in its header: ``` +Built standing on the shoulders of billions of dwarves Created by John A. Hoeven with the ethical assistance of Claude AI ``` -This is non-negotiable. It appears in every file, every time. +The dwarves come first. This is non-negotiable. It appears in every file, +every time. ### 1.2 Version and status @@ -59,27 +69,55 @@ Version tracks follow CE OS conventions: | Testing | `v0.x.x` | Second point on milestones | | Stable | `vX.Y` | Major/minor on scope/compatibility | -### 1.3 Update and upgrade first +### 1.3 Licence + +The default licence for all CE OS scripts is **The Unlicense**. Every script +header must declare its licence: + +```bash +# Licence: The Unlicense — https://unlicense.org + +Commercial use is explicitly permitted. No conditions, no attribution +requirement, no notification needed. Do what you want with it. + +We do ask, however, that you voluntarily attribute the dwarves — the +billions of contributors whose work made yours possible. It costs +nothing and means everything. +``` + +Any deviation from The Unlicense requires an explicit explanation in the +script header: + +```bash +# Licence: +# Licence note: +``` + +A script with no licence declaration is incomplete. + +### 1.4 Update and upgrade first Every script that installs or modifies packages must run the OS package manager's update and upgrade sequence as its very first system-touching action — before any package installation, before any capability test. -The exact command depends on the detected OS (see §2.6 and §3.6). No -exceptions. A script that installs packages without first updating the -package index is defective. +The exact command depends on the detected OS (see §2.6). No exceptions. +A script that installs packages without first updating the package index +is defective. -### 1.4 Architecture awareness +### 1.5 Architecture awareness Every script that installs packages must: 1. Detect the running architecture (`uname -m`) -2. Test that each required package is available for that architecture +2. Consult `ce-index.conf` for known arch-specific availability +3. Test that each required package is available for that architecture before attempting installation -3. Log clearly which packages are unavailable and why -4. Never silently skip a package — always surface the gap to the OHIOD +4. Log clearly which packages are unavailable and why +5. Never silently skip a package — always surface the gap to the + OHIOD[^1]. -### 1.5 Test before, during, and after +### 1.6 Test before, during, and after Testing is not optional and not an afterthought. Every script has three explicit test gates: @@ -97,7 +135,7 @@ explicit test gates: Test results are written to the log file with timestamps. A script with no test gates is incomplete. -### 1.6 Cleanup before exit +### 1.7 Cleanup before exit Every script registers a cleanup handler that runs on all exits — normal, error, and signal. The cleanup handler must: @@ -109,29 +147,34 @@ error, and signal. The cleanup handler must: A script that leaves debris is defective. -### 1.7 No silent failures +### 1.8 No silent failures Every error is logged and surfaced. A script that swallows errors and reports success is worse than one that fails loudly. If an operation cannot be completed, say so clearly, log it, and either hard-fail or warn — never silently skip. -### 1.8 Credentials +### 1.9 Credentials Never write real credentials in any script, config file, or comment. Use `[PLACEHOLDER]` format always. This applies to passwords, tokens, API keys, private keys, and any other secret material. -### 1.9 Never run as root +### 1.10 Never run as root -CE OS scripts run as the OHIOD user and invoke `doas` for privileged -operations. Scripts must not require or assume direct root execution. -If a script is accidentally run as root, it should warn and exit. +CE OS scripts run as the OHIOD user and invoke the +detected privilege tool (`doas` or `sudo`) for privileged operations. +Scripts must not require or assume direct root execution. If a script is +accidentally run as root, it should warn and exit. -### 1.10 Script length limits +The privilege tool is detected by `bootstrap.sh` and written to +`ce_env.conf` as `CE_PRIV`. All scripts source this value — never +hardcode `doas` or `sudo`. + +### 1.11 Script length limits CE OS Bash scripts follow these length limits. Lines are counted as written, -including comments — comments are part of the script, not overhead. +including comments — comments are part of the script, not overhead; empty/blank lines are not included in the line count. | Script role | Soft limit | Hard limit | Notes | |---|---|---|---| @@ -161,7 +204,7 @@ five libraries and calls twelve subscripts in sequence may be 300 lines of clean, readable flow control. That is correct. Logic belongs in libraries and subscripts; the orchestrator reads like a table of contents. -### 1.11 Scripts as teaching documents +### 1.12 Scripts as teaching documents CE OS scripts are read by people learning the system, not only by people maintaining it. Write accordingly. @@ -178,9 +221,9 @@ maintaining it. Write accordingly. should understand the full install flow without opening a single sourced file. Each call to a subscript or library function should be self-evident from its name and any inline comment. -- **Target register: MUO level.** Make It Understandable and Obvious. - A competent but non-professional reader should be able to follow the logic. - If they cannot, the script needs work — not the reader. +- **Target register: enthusiast level.** Make It Understandable and Obvious. + A competent but non-professional reader, eager to learn, should be able + to follow the logic. If they cannot, the script needs work — not the reader. --- @@ -191,8 +234,9 @@ maintaining it. Write accordingly. CE OS uses two Bash script classes. Choose the correct one: **Class A — System scripts** -OpenRC service scripts, `/etc/local.d/` scripts, bootstrap phases, -anything that runs before CE OS layers are confirmed present. +`bootstrap.sh`, OpenRC service scripts, `/etc/local.d/` scripts, and +anything that runs before the CE OS environment is confirmed present. +Class A scripts are POSIX sh / ash throughout — no bash features permitted. ```sh #!/bin/sh @@ -200,47 +244,36 @@ anything that runs before CE OS layers are confirmed present. ``` **Class B — CE OS scripts** -All installer scripts, operational scripts, tier components, user-facing -tools. Everything written for CE OS day-to-day use. +All installer scripts, operational scripts, tier components, libraries, +and user-facing tools. Everything written for CE OS day-to-day use. ```bash #!/usr/bin/env bash -# Bash features permitted. BusyBox tools on PATH. +# Bash features permitted. BusyBox tools on PATH on Alpine. ``` When in doubt, write Class B. Use Class A only when you have a specific reason to require ash compatibility. -**The bootstrap exception:** The opening lines of an installer that runs -before bash is confirmed must be ash-safe, then hand off: - -```sh -#!/bin/sh -# Bootstrap phase — ash only until bash is confirmed -command -v bash >/dev/null 2>&1 || { - echo "Installing bash..." - doas apk add bash -} -exec bash "$0" "$@" -# Everything below this line is normal CE OS bash -``` - ### 2.2 Full header block Every CE OS Bash script carries this header in full: ```bash #!/usr/bin/env bash +# Built standing on the shoulders of billions of dwarves # Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org # --------------------------------------------------------------------------- # .sh # /opt/ceos/scripts/.sh # Version: v0.0.1 | Status: DEVELOPMENT +# Role: # --------------------------------------------------------------------------- # Purpose: One-line description of what this script does. # Target: Which nodes / contexts this runs on (e.g. Alpine aarch64 Pi 5) # Entry: How it is invoked (e.g. doas bash phase1.sh, or ./tool.sh --flag) -# Depends: ce-common.sh, ce-test-lib.sh (list sourced libraries) +# Depends: ce-common-lib.sh, ce-test-lib.sh (list sourced libraries) # --------------------------------------------------------------------------- # Phases: # 0 — Pre-flight (environment checks, no system changes) @@ -320,59 +353,119 @@ Rules: ### 2.6 Package manager abstraction -Detect the OS and set package manager variables at script start: +By the time any Class B script runs, `bootstrap.sh` has already detected +the package manager and written it to `ce_env.conf`. Class B scripts source +this file and load the correct package manager library — they do not detect +the package manager themselves. ```bash -detect_package_manager() { - if command -v apk >/dev/null 2>&1; then - PKG_MANAGER="apk" - PKG_UPDATE="apk update" - PKG_UPGRADE="apk upgrade" - PKG_INSTALL="apk add" - PKG_CACHE_CLEAN="rm -rf /var/cache/apk/*" - PKG_QUERY="apk info -e" - elif command -v apt-get >/dev/null 2>&1; then - PKG_MANAGER="apt" - PKG_UPDATE="apt-get update" - PKG_UPGRADE="apt-get upgrade -y" - PKG_INSTALL="apt-get install -y" - PKG_CACHE_CLEAN="apt-get clean && rm -rf /var/lib/apt/lists/*" - PKG_QUERY="dpkg -l" - elif command -v nix-env >/dev/null 2>&1; then - PKG_MANAGER="nix" - PKG_UPDATE="nix-channel --update" - PKG_UPGRADE="nixos-rebuild switch" - PKG_INSTALL="nix-env -iA" - PKG_CACHE_CLEAN="nix-collect-garbage -d" - PKG_QUERY="nix-env -q" - else - log_error "No supported package manager found" - exit 1 - fi - log_info "Package manager: ${PKG_MANAGER}" +# Source the bootstrap environment — always the first sourcing action +source /tmp/ce_env.conf || { + echo "ERROR: ce_env.conf not found. Was bootstrap.sh run first?" >&2 + exit 1 } -# Run update and upgrade BEFORE any package installs -run_package_update() { - log_info "Running package update..." - doas ${PKG_UPDATE} || { log_error "Package update failed"; exit 1; } - log_info "Running package upgrade..." - doas ${PKG_UPGRADE} || { log_error "Package upgrade failed"; exit 1; } - log_info "Package update and upgrade complete" +# Load the correct package manager library for this distro +source "${LIB_DIR}/pkg/${CE_PKG_LIB}" || { + echo "ERROR: Package manager library not found: ${CE_PKG_LIB}" >&2 + exit 1 } ``` -### 2.7 Architecture-aware package testing +Each package manager library (`lib/pkg/apk.sh`, `lib/pkg/apt.sh`, etc.) +exposes the same interface: -Before installing any package, test availability for the running architecture. -For Alpine, the search order is: +```bash +pkg_update() # Update package index +pkg_upgrade() # Upgrade installed packages +pkg_install() # Install a package +pkg_available() # Test availability — returns 0 if found +pkg_cache_clean() # Clear download cache +``` -1. Alpine stable (main) -2. Alpine community -3. Alpine edge -4. CE APK repo on Forgejo (`CE_APK_REPO` — packages that must be built for CE OS) +This means all higher-level scripts call `pkg_install vim` regardless of +distro — the library handles the distro-specific implementation. -Set the CE repo URL as a constant at the top of any script that may need it: +### 2.7 Privilege tool abstraction + +`bootstrap.sh` detects `doas` or `sudo` and writes `CE_PRIV` to +`ce_env.conf`. Never hardcode either tool: + +```bash +# Correct — uses detected privilege tool +${CE_PRIV} pkg_install vim + +# Wrong — assumes Alpine/CE OS default +doas pkg_install vim + +# Wrong — assumes most other distros +sudo pkg_install vim +``` + +### 2.8 Init system abstraction + +`bootstrap.sh` detects OpenRC or systemd and writes `CE_INIT` to +`ce_env.conf`. Load the correct init library: + +```bash +source "${LIB_DIR}/init/${CE_INIT_LIB}" || { + echo "ERROR: Init library not found: ${CE_INIT_LIB}" >&2 + exit 1 +} +``` + +Each init library exposes the same interface: + +```bash +svc_enable() # Enable service at boot +svc_start() # Start service now +svc_stop() # Stop service +svc_restart() # Restart service +svc_status() # Query service status +``` + +### 2.9 Userland abstraction + +`bootstrap.sh` detects BusyBox or GNU userland and writes `CE_USERLAND` +to `ce_env.conf`. For Class B scripts running on Alpine, prefer +BusyBox-safe patterns regardless — this keeps scripts portable across +both userlands without branching: + +| Avoid (GNU-only) | Use instead (BusyBox-safe) | +|------------------------|-----------------------------------------------------| +| `grep -P` | `grep -E` | +| `sed 's/\w//'` | `sed 's/[[:alnum:]_]//'` | +| `find -printf` | `find ... -exec basename {} \;` or pipe to `awk` | +| `stat -c "%Y" f` | `stat -t f \| awk '{print $12}'` | +| `dd status=progress` | plain `dd`, or `pv` if progress needed | +| `ping` in scripts | `curl --silent --max-time 5 --output /dev/null URL` | + +When a GNU-only feature is genuinely required and has no BusyBox-safe +equivalent, gate it on `CE_USERLAND`: + +```bash +if [[ "${CE_USERLAND}" == "gnu" ]]; then + # GNU-specific implementation +else + # BusyBox-safe fallback +fi +``` + +Document the reason in a comment. Do not gate silently. + +### 2.10 Architecture-aware package testing + +Before installing any package, test availability for the running +architecture. The search order for Alpine is: + +1. Consult `ce-index.conf` for known arch-specific status +2. Alpine stable (main) +3. Alpine community +4. Alpine edge +5. CE APK repo on Forgejo (`CE_APK_REPO`) + +Set the CE repo URL as a constant at the top of any script that may +need it: ```bash # CE APK repository — packages built by Cervello Elettrico @@ -385,9 +478,17 @@ test_package_available() { local pkg="${1:?test_package_available: pkg required}" local arch="${ARCH}" + # Check index for known unavailability before hitting live repos + local index_key="CE_PKG_${pkg}_${arch}" + local index_val="${!index_key:-}" + if [[ "${index_val}" == "unavailable" ]]; then + log_warn " KNOWN UNAVAILABLE (index): ${pkg} on ${arch}" + return 1 + fi + case "${PKG_MANAGER}" in apk) - # Check official Alpine repos (stable, community, edge) first + # Search official Alpine repos (stable, community, edge) if apk search -x "${pkg}" 2>/dev/null | grep -q "^${pkg}-"; then log_info " FOUND (Alpine repo): ${pkg}" return 0 @@ -412,6 +513,9 @@ test_package_available() { esac log_warn "Package '${pkg}' not available for ${arch} on ${PKG_MANAGER}" + log_warn "If you believe this is incorrect, please file a bug report at:" + log_warn " ${CE_BUG_REPORT_URL}" + log_warn "Include: package=${pkg} arch=${ARCH} distro=${CE_DISTRO} scriptset=${CE_SCRIPTSET} date=$(date '+%Y-%m-%d')" return 1 } @@ -425,7 +529,7 @@ install_packages() { unavailable+=( "${pkg}" ) continue fi - if ! doas ${PKG_INSTALL} "${pkg}"; then + if ! ${CE_PRIV} pkg_install "${pkg}"; then log_error "Failed to install: ${pkg}" failed+=( "${pkg}" ) else @@ -447,14 +551,14 @@ install_packages() { } ``` -### 2.8 Logging +### 2.11 Logging -Source `ce-common.sh` for the standard logging functions. If writing a -standalone script that cannot source `ce-common.sh`, implement these -four levels minimally: +Source `ce-common-lib.sh` for the standard logging functions. If writing +a standalone script that cannot source `ce-common-lib.sh`, implement +these four levels minimally: ```bash -# Minimal logging — use ce-common.sh log functions when available +# Minimal logging — use ce-common-lib.sh log functions when available _log() { local level="${1}" local msg="${2}" @@ -471,7 +575,9 @@ log_debug() { [[ "${CE_DEBUG:-0}" == "1" ]] && _log "DEBUG" "${1}"; } Log file location: `~/.local/logs/ceos_installer/` (XDG compliant). Log file name: `-.log`. -### 2.9 Cleanup trap +***Possible new logging scheme: /home/[user]/[subdirectory of choice]/[scriptset directory]/[script log time datestamp].log*** + +### 2.12 Cleanup trap Register the cleanup trap immediately after defining the cleanup function, before any work begins: @@ -493,12 +599,7 @@ cleanup() { fi # Clear package manager cache - case "${PKG_MANAGER:-}" in - apk) doas rm -rf /var/cache/apk/* 2>/dev/null || true ;; - apt) doas apt-get clean 2>/dev/null || true - doas rm -rf /var/lib/apt/lists/* 2>/dev/null || true ;; - nix) : ;; # nix cache managed separately - esac + pkg_cache_clean 2>/dev/null || true log_info "Cleanup complete (exit code: ${exit_code})" } @@ -509,14 +610,13 @@ trap cleanup EXIT INT TERM HUP TEMP_DIR="$(mktemp -d)" ``` -### 2.10 Test gates — structure +### 2.13 Test gates — structure ```bash # ── Pre-flight ───────────────────────────────────────────────────────────── phase_preflight() { log_info "=== PRE-FLIGHT ===" - local pass=0 # Each check: run_test [warn|fail] run_test "bash version >= 4" '[[ "${BASH_VERSINFO[0]}" -ge 4 ]]' fail @@ -537,7 +637,8 @@ phase_preflight() { phase_install_vim() { log_info "=== INSTALLING VIM ===" - doas ${PKG_INSTALL} vim || { log_error "vim install failed"; return 1; } + # vim is the CE OS default editor — required for all tiers + ${CE_PRIV} pkg_install vim || { log_error "vim install failed"; return 1; } # Inline test — verify immediately after install run_test "vim installed" 'command -v vim >/dev/null 2>&1' fail @@ -583,7 +684,7 @@ run_test() { } ``` -### 2.11 User confirmation +### 2.14 User confirmation One confirmation prompt before system modifications begin. No more: @@ -594,25 +695,13 @@ confirm_proceed() { read -r response case "${response}" in [yY]|[yY][eE][sS]) return 0 ;; - *) log_info "Aborted by Organic Humanoid I/O Device unit ID ${USER}"; exit 0 ;; + *) log_info "Aborted by OHIOD Unit ID ${USER}" + exit 0 ;; esac } ``` -### 2.12 POSIX / BusyBox compatibility - -For Class B scripts running on Alpine, prefer BusyBox-safe patterns: - -| Avoid (GNU-only) | Use instead (BusyBox-safe) | -|------------------------|----------------------------------------------------| -| `grep -P` | `grep -E` | -| `sed 's/\w//'` | `sed 's/[[:alnum:]_]//'` | -| `find -printf` | `find ... -exec basename {} \;` or pipe to `awk` | -| `stat -c "%Y" f` | `stat -t f \| awk '{print $12}'` | -| `dd status=progress` | plain `dd`, or `pv` if progress needed | -| `ping` in scripts | `curl --silent --max-time 5 --output /dev/null URL`| - -### 2.13 Script architecture — orchestrators, subscripts, and libraries +### 2.15 Script architecture — orchestrators, subscripts, and libraries CE OS Bash scripts follow a three-layer architecture. Understanding which layer a script belongs to determines its structure, its line limits, and @@ -620,7 +709,7 @@ what it is permitted to contain. #### The three layers -**Libraries (`ce-*-lib.sh`)** +**Libraries (`ce-*-lib.sh` and variant libraries)** A library is a collection of related functions and constants that other scripts source. Libraries contain no executable code at the top level — @@ -634,16 +723,19 @@ Extract a function to a library when: - The function requires a comment block longer than itself to be understood (the comment is telling you it belongs in isolation) -Naming: `ce--lib.sh` — e.g. `ce-pkg-lib.sh`, `ce-net-lib.sh`, -`ce-test-lib.sh`. +Naming: `ce--lib.sh` for shared libraries; variant libraries +live in domain subdirectories (`lib/pkg/apk.sh`, `lib/init/openrc.sh`). ```bash -#!/usr/bin/env bash +#!/bin/sh +# Built standing on the shoulders of billions of dwarves # Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org # --------------------------------------------------------------------------- # ce-pkg-lib.sh # /opt/ceos/lib/ce-pkg-lib.sh # Version: v0.0.1 | Status: DEVELOPMENT +# Role: Library — Package manager detection and operations # --------------------------------------------------------------------------- # Purpose: Package manager detection, update, and availability testing. # Used by: ce-install.sh, ce-base.sh, ce-minimal.sh @@ -660,6 +752,19 @@ test_package_available() { ... } run_package_update() { ... } ``` +**Load-order dependencies in library subdirectories** + +When libraries within a subdirectory depend on each other, numbered +prefixes (`01_`, `02_`) show load order. This is not sufficient +documentation on its own. The dependency must also be declared explicitly +in the `Depends:` header field of the dependent file: + +```bash +# Depends: 01_core.sh — CE_PRIV and CE_USERLAND must be set before this loads +``` + +Numbered prefixes show order. The `Depends:` field explains why. + **Subscripts (`ce-*.sh`)** A subscript owns a single phase or a coherent group of related operations. @@ -678,7 +783,7 @@ consider whether the phase itself should be split. An orchestrator coordinates subscripts and libraries. It contains: -- Sourcing of required libraries +- Sourcing of `ce_env.conf` and required libraries - Calling subscripts in sequence - Inter-phase safety gates (explicit `[y/N]` confirmation before each tier) - The master cleanup trap @@ -695,19 +800,26 @@ libraries and calls ten subscripts in sequence is correct and expected. #### Sourcing libraries ```bash -# Source libraries — paths relative to script location -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly LIB_DIR="${SCRIPT_DIR}/../lib" +# Source the bootstrap environment first — always +source /tmp/ce_env.conf || { + echo "ERROR: ce_env.conf not found. Was bootstrap.sh run first?" >&2 + exit 1 +} -# shellcheck source=../lib/ce-common-lib.sh +# Resolve library directory relative to script location +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly LIB_DIR="${SCRIPT_DIR}/lib" + +# shellcheck source=lib/ce-common-lib.sh source "${LIB_DIR}/ce-common-lib.sh" || { echo "ERROR: ce-common-lib.sh not found at ${LIB_DIR}" >&2 exit 1 } -# shellcheck source=../lib/ce-pkg-lib.sh -source "${LIB_DIR}/ce-pkg-lib.sh" || { - echo "ERROR: ce-pkg-lib.sh not found at ${LIB_DIR}" >&2 +# Load variant libraries resolved by bootstrap +# shellcheck source=lib/pkg/apk.sh +source "${LIB_DIR}/pkg/${CE_PKG_LIB}" || { + echo "ERROR: Package library not found: ${CE_PKG_LIB}" >&2 exit 1 } ``` @@ -718,37 +830,397 @@ library is a hard failure — never silently proceed without it. #### Directory layout ``` -/opt/ceos/ +ceos-alpine-installer/ +├── bootstrap.sh # Class A — POSIX sh throughout +├── ce-install.sh # Orchestrator +├── ce-base.sh # Tier 0 subscript +├── ce-minimal.sh # Tier 1 subscript +├── ce-basic.sh # Tier 2 subscript +├── deploy.sh # Deployment wrapper +├── ce-manifest.conf # Script set target declaration +├── ce-index.conf # Arch/distro package and library index ├── lib/ │ ├── ce-common-lib.sh # Logging, run_test, confirm_proceed -│ ├── ce-pkg-lib.sh # Package manager detection and operations │ ├── ce-test-lib.sh # Test gate framework -│ └── ce-net-lib.sh # Network checks -├── scripts/ -│ ├── ce-install.sh # Orchestrator -│ ├── ce-base.sh # Tier 0 subscript -│ ├── ce-minimal.sh # Tier 1 subscript -│ └── ce-basic.sh # Tier 2 subscript +│ ├── base/ # Tier 0 component libraries +│ │ ├── 01_core.sh +│ │ └── 02_doas.sh +│ ├── minimal/ # Tier 1 component libraries +│ │ ├── 01_shell.sh +│ │ └── 02_terminal.sh +│ ├── basic/ # Tier 2 component libraries +│ │ ├── 01_comms.sh +│ │ └── 02_monitor.sh +│ ├── pkg/ # Package manager variant libraries +│ │ ├── apk.sh # Alpine +│ │ └── apt.sh # Debian +│ ├── init/ # Init system variant libraries +│ │ ├── openrc.sh # Alpine +│ │ └── systemd.sh # Debian +│ └── priv/ # Privilege tool variant libraries +│ ├── doas.sh # CE OS default +│ └── sudo.sh # Debian and others +├── spec/ +│ └── install_spec.md # Human-readable install specification +└── tests/ + ├── test_base.sh + ├── test_minimal.sh + └── test_basic.sh ``` #### Summary table -| Layer | Naming | Contains | Line limit | -|--------------|--------------------|-----------------------------------|---------------------| -| Library | `ce-*-lib.sh` | Functions and constants only | 150 soft / 200 hard | -| Subscript | `ce-*.sh` | Single phase or domain logic | 150 soft / 200 hard | -| Orchestrator | `ce-install.sh` | Flow, gates, sourcing, trap only | None | +| Layer | Naming | Contains | Line limit | +|--------------|---------------------|----------------------------------|---------------------| +| Library | `ce-*-lib.sh` | Functions and constants only | 150 soft / 200 hard | +| Variant lib | `lib//*.sh` | Distro-specific implementations | 150 soft / 200 hard | +| Subscript | `ce-*.sh` | Single phase or domain logic | 150 soft / 200 hard | +| Orchestrator | `ce-install.sh` | Flow, gates, sourcing, trap only | None | + +### 2.16 Bootstrap — `bootstrap.sh` + +`bootstrap.sh` is the sole Class A script in a CE OS installer set. It is +POSIX sh / ash throughout — no bash features, no CE OS libraries, no +sourcing of `ce_env.conf` (it creates it). It has one job per phase: + +**Phase 1 — Ensure bash is present** + +```sh +#!/bin/sh +# Built standing on the shoulders of billions of dwarves +# Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org +# --------------------------------------------------------------------------- +# bootstrap.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Role: Bootstrap — Class A, POSIX sh throughout +# --------------------------------------------------------------------------- +# Purpose: Ensure bash is present, detect environment, write ce_env.conf, +# validate compatibility, then hand off to ce-install.sh. +# --------------------------------------------------------------------------- + +command -v bash >/dev/null 2>&1 || { + echo "bash not found — installing..." + # Use whatever is available — no CE_PRIV yet + if command -v apk >/dev/null 2>&1; then + apk add bash + elif command -v apt-get >/dev/null 2>&1; then + apt-get install -y bash + else + echo "ERROR: Cannot install bash — no supported package manager found" >&2 + exit 1 + fi +} +``` + +**Phase 2 — Read the script set manifest** + +```sh +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "${SCRIPT_DIR}/ce-manifest.conf" || { + echo "ERROR: ce-manifest.conf not found" >&2 + exit 1 +} +``` + +**Phase 3 — Detect environment** + +```sh +# Distro +CE_DISTRO="unknown" +if [ -f /etc/os-release ]; then + . /etc/os-release + case "${ID:-}" in + alpine) CE_DISTRO="alpine" ;; + debian|ubuntu) CE_DISTRO="debian" ;; + nixos) CE_DISTRO="nixos" ;; + esac +fi + +# Init system +CE_INIT="unknown" +command -v rc-service >/dev/null 2>&1 && CE_INIT="openrc" +command -v systemctl >/dev/null 2>&1 && CE_INIT="systemd" + +# Privilege tool +CE_PRIV="unknown" +command -v doas >/dev/null 2>&1 && CE_PRIV="doas" +command -v sudo >/dev/null 2>&1 && CE_PRIV="sudo" + +# Userland +CE_USERLAND="gnu" +ls --version 2>&1 | grep -q BusyBox && CE_USERLAND="busybox" + +# Architecture +CE_ARCH="$(uname -m)" +``` + +**Phase 4 — Validate against manifest and hard fail if unsupported** + +```sh +# Check distro compatibility +distro_supported=0 +for d in ${CE_SUPPORTED_DISTROS}; do + [ "${CE_DISTRO}" = "${d}" ] && distro_supported=1 && break +done + +if [ "${distro_supported}" -eq 0 ]; then + cat <&2 + exit 1 +} + +# Resolve library paths for detected environment +eval "CE_PKG_LIB=\${CE_LIB_pkg_${CE_DISTRO}:-}" +eval "CE_INIT_LIB=\${CE_LIB_init_${CE_DISTRO}:-}" +eval "CE_PRIV_LIB=\${CE_LIB_priv_${CE_DISTRO}:-}" + +[ -n "${CE_PKG_LIB}" ] || { echo "ERROR: No package library for ${CE_DISTRO}" >&2; exit 1; } +``` + +**Phase 6 — Write `ce_env.conf`** + +```sh +cat > /tmp/ce_env.conf <_="available|edge|ce-repo|unavailable" +# Only entries that deviate from 'available' need listing. +# Packages not listed are assumed available — the live search will confirm. +# When a package search fails unexpectedly, file a bug report so this +# index can be updated. +CE_PKG_glow_x86_64="edge" +CE_PKG_glow_aarch64="edge" +CE_PKG_glow_armhf="unavailable" +CE_PKG_glow_x86="unavailable" + +# Library variant index +# Format: CE_LIB__="filename.sh" +CE_LIB_pkg_alpine="apk.sh" +CE_LIB_pkg_debian="apt.sh" +CE_LIB_init_alpine="openrc.sh" +CE_LIB_init_debian="systemd.sh" +CE_LIB_priv_alpine="doas.sh" +CE_LIB_priv_debian="sudo.sh" +``` + +The index is the authoritative record of known deviations from standard +availability. The live package search is the fallback for anything not +listed. When a live search fails on a package that should be available, +that is the signal to file a bug report and update the index — one of +the few bug reports that directly improves the installer for everyone. --- ## Part 3 — Python Style Guide -*The Python style guide will be produced as a separate document once -sufficient CE OS Python use cases exist to ground it in real experience. -Do not apply Bash conventions to Python scripts — wait for the dedicated -guide.* +CE OS Python scripts follow this Bash style guide as closely as the +language permits. The principles are identical: attribution headers, +test gates, cleanup on exit, no silent failures, credentials as +`[PLACEHOLDER]`, and scripts as teaching documents. + +Where Python requires a different approach, the deviation is documented +here with an explicit justification. Do not invent deviations — if +something works the same way in Python as in Bash, do it the same way. + +### 3.1 Attribution and licence header + +Every Python script carries the same attribution as Bash, adapted for +Python comment syntax: + +```python +#!/usr/bin/env python3 +# Built standing on the shoulders of billions of dwarves +# Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org +# --------------------------------------------------------------------------- +# script_name.py +# /opt/ceos/scripts/script_name.py +# Version: v0.0.1 | Status: DEVELOPMENT +# Role: +# --------------------------------------------------------------------------- +# Purpose: One-line description. +# Target: Which nodes / contexts (e.g. Alpine aarch64, Debian amd64) +# Entry: How invoked (e.g. python3 script_name.py --flag) +# Depends: List non-stdlib imports and their install source +# --------------------------------------------------------------------------- +``` + +### 3.2 Where Python deviates from Bash — and why + +| Bash pattern | Python equivalent | Reason for deviation | +|---|---|---| +| `trap cleanup EXIT INT TERM` | `atexit.register()` + `try/finally` | Python has no `trap`; `atexit` is the idiomatic equivalent | +| `set -u` | Type hints + explicit checks | Python raises `NameError` on undefined names; `set -u` has no direct equivalent but type hints and explicit validation serve the same intent | +| `set -o pipefail` | `subprocess.run(check=True)` | Pipe failures are caught per-call in Python, not globally | +| `source library.sh` | `import ce_module` or `from ce_module import fn` | Python modules are the library equivalent; sourcing does not exist | +| `local var` | Variables scoped within functions naturally | Python function scope is implicit; `local` has no equivalent and is not needed | +| Orchestrator shell script | `main()` calling phase functions | Python orchestration lives in `main()` within a single file, or in a `__main__.py` for packages | +| `[[ condition ]]` | `if condition:` | Standard Python conditionals | + +### 3.3 Mandatory patterns + +**`main()` guard — always:** + +```python +def main() -> None: + ... + +if __name__ == "__main__": + main() +``` + +Never put executable code at module level outside `main()` and constant +definitions. This makes the script safe to import for testing — the same +reason Bash libraries have no top-level executable code. + +**Type hints — always:** + +```python +def check_package_available(pkg: str, arch: str) -> bool: + ... +``` + +Type hints serve the same intent as `${VAR:?message}` in Bash — they +document what a function expects and make failures explicit. + +**Specific exception handling — always:** + +```python +# Correct +try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) +except subprocess.CalledProcessError as e: + log.error(f"Command failed: {e.stderr.strip()}") + sys.exit(1) + +# Wrong — never use bare except +try: + ... +except: + pass +``` + +Bare `except` is the Python equivalent of swallowing errors silently. +It is a defect by the same rule as §1.8. + +**Cleanup with `atexit`:** + +```python +import atexit, shutil, tempfile + +_tmp_dir: str | None = None + +def _cleanup() -> None: + if _tmp_dir and os.path.isdir(_tmp_dir): + shutil.rmtree(_tmp_dir, ignore_errors=True) + log.info("Cleanup complete") + +atexit.register(_cleanup) +``` + +Register cleanup before any work begins — same rule as the Bash trap. + +**Line limits:** Python functions consume lines faster than Bash due to +type hints, docstrings, and explicit error handling. Line limits for +Python will be established by the dedicated Python style guide once +sufficient CE OS Python code exists to calibrate them from experience. --- + ## Part 4 — Quick Reference ### 4.1 Mandatory checklist — every Bash script @@ -756,37 +1228,62 @@ guide.* Before any script is considered ready for review, verify all of the following: ``` -[ ] Attribution header present and exact +[ ] Attribution header present: dwarves first, then John A. Hoeven / Claude AI +[ ] Licence declared (The Unlicense or deviation with explanation) [ ] Version and Status in header [ ] Script role declared (orchestrator / subscript / library) [ ] Line limit override comment present if >200 lines (subscripts and libraries) -[ ] Package manager detected, not assumed -[ ] doas ${PKG_UPDATE} && doas ${PKG_UPGRADE} runs first (if installing packages) +[ ] ce_env.conf sourced as first action (Class B scripts) +[ ] Correct variant libraries loaded via ce_env.conf paths +[ ] Package manager used via pkg_* interface, not directly +[ ] Privilege tool used via ${CE_PRIV}, never hardcoded +[ ] Init system used via svc_* interface, not directly +[ ] doas/sudo pkg_update && pkg_upgrade runs first (if installing packages) [ ] Architecture detected (uname -m) before any package operations -[ ] Each package tested: Alpine stable → community → edge → CE repo -[ ] CE_APK_REPO constant present if CE repo check is used +[ ] ce-index.conf consulted before live package search +[ ] Each package tested: index → Alpine stable → community → edge → CE repo +[ ] Package unavailability logs bug report URL with full context [ ] Pre-flight test gate implemented (warn-only, no changes) [ ] Inline tests after each significant operation [ ] Post-install verification gate implemented [ ] Cleanup trap registered before any work begins -[ ] Cleanup removes: temp files, /var/cache/apk/*, lock files +[ ] Cleanup removes: temp files, pkg cache, lock files [ ] Cleanup runs on EXIT INT TERM HUP [ ] No silent failures — every error logged and surfaced [ ] No set -e — errors handled explicitly [ ] No credentials in code — [PLACEHOLDER] only -[ ] No direct root execution — doas for privileged ops +[ ] No direct root execution — ${CE_PRIV} for privileged ops [ ] Single confirmation prompt before system changes begin [ ] Log file written to ~/.local/logs/ceos_installer/ [ ] Comments explain intent, not mechanics [ ] Complex functions extracted to library if they disrupt script flow +[ ] Load-order dependencies declared in Depends: field (not just numbered prefix) ``` ### 4.2 Header templates — copy/paste +**Bootstrap (Class A — POSIX sh throughout):** +```sh +#!/bin/sh +# Built standing on the shoulders of billions of dwarves +# Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org +# --------------------------------------------------------------------------- +# bootstrap.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Role: Bootstrap — Class A, POSIX sh throughout +# --------------------------------------------------------------------------- +# Purpose: Ensure bash present, detect environment, write ce_env.conf, +# validate compatibility, hand off to ce-install.sh. +# --------------------------------------------------------------------------- +``` + **Orchestrator:** ```bash #!/usr/bin/env bash +# Built standing on the shoulders of billions of dwarves # Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org # --------------------------------------------------------------------------- # ce-install.sh # /opt/ceos/scripts/ce-install.sh @@ -796,7 +1293,7 @@ Before any script is considered ready for review, verify all of the following: # Purpose: # Target: # Entry: -# Sources: ce-common-lib.sh, ce-pkg-lib.sh, ce-test-lib.sh +# Sources: ce-common-lib.sh, ce-test-lib.sh # Calls: ce-base.sh, ce-minimal.sh, ce-basic.sh # --------------------------------------------------------------------------- # Phases: @@ -811,7 +1308,9 @@ Before any script is considered ready for review, verify all of the following: **Subscript:** ```bash #!/usr/bin/env bash +# Built standing on the shoulders of billions of dwarves # Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org # --------------------------------------------------------------------------- # ce-base.sh # /opt/ceos/scripts/ce-base.sh @@ -828,7 +1327,9 @@ Before any script is considered ready for review, verify all of the following: **Library:** ```bash #!/usr/bin/env bash +# Built standing on the shoulders of billions of dwarves # Created by John A. Hoeven with the ethical assistance of Claude AI +# Licence: The Unlicense — https://unlicense.org # --------------------------------------------------------------------------- # ce-pkg-lib.sh # /opt/ceos/lib/ce-pkg-lib.sh @@ -849,4 +1350,16 @@ Before any script is considered ready for review, verify all of the following: --- -*Created by John A. Hoeven with the ethical assistance of Claude AI* \ No newline at end of file +[^1]: OHIOD — Organic Humanoid Input/Output Device. The person at the +keyboard. CE OS uses this term throughout to remind both the system and +its developers that the human is the principal, not the machine. + +--- + +*Built standing on the shoulders of billions of dwarves* + +*Created by John A. Hoeven with the ethical assistance of Claude AI* + +*Licence: [The Unlicense](https://unlicense.org) — commercial use explicitly permitted.* + +*We ask that you voluntarily attribute the dwarves.* \ No newline at end of file