# CE OS Script Style Guide — Bash --- ## Purpose This document defines the mandatory style and structural standards for all Bash scripts produced under the CE OS / Cervello Elettrico umbrella. Every 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 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, 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 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. --- ## Part 1 — Universal Rules These rules apply to every Bash script regardless of its role (orchestrator, subscript, or library). ### 1.1 Attribution 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 ``` The dwarves come first. This is non-negotiable. It appears in every file, every time. ### 1.2 Version and status Every script header includes version and status: ``` Version: v0.0.1 | Status: DEVELOPMENT ``` Version tracks follow CE OS conventions: | Stage | Format | Increment rule | |-------------|----------|---------------------------------------| | Development | `v0.0.x` | Third point on every meaningful edit | | Testing | `v0.x.x` | Second point on milestones | | Stable | `vX.Y` | Major/minor on scope/compatibility | ### 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). No exceptions. A script that installs packages without first updating the package index is defective. ### 1.5 Architecture awareness Every script that installs packages must: 1. Detect the running architecture (`uname -m`) 2. Consult `ce-index.conf` for known arch-specific availability 3. Test that each required package is available for that architecture before attempting installation 4. Log clearly which packages are unavailable and why 5. Never silently skip a package — always surface the gap to the OHIOD[^1]. ### 1.6 Test before, during, and after Testing is not optional and not an afterthought. Every script has three explicit test gates: - **Pre-flight** — before any system changes. Checks environment, dependencies, architecture, disk space, connectivity. All warn-only. No changes made at this stage. - **Inline** — after each significant operation. Verifies the operation succeeded before proceeding. Hard-fail on critical operations; warn-only on optional components. - **Post-install** — after all operations complete. Verifies installed tools, written config files, service state. Hard-fail if required components are absent. Test results are written to the log file with timestamps. A script with no test gates is incomplete. ### 1.7 Cleanup before exit Every script registers a cleanup handler that runs on all exits — normal, error, and signal. The cleanup handler must: - Remove all temporary files and directories created by the script - Clear the package manager's download cache - Remove any lock files created by the script - Log that cleanup completed A script that leaves debris is defective. ### 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.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.10 Never run as root 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. 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; empty/blank lines are not included in the line count. | Script role | Soft limit | Hard limit | Notes | |---|---|---|---| | Subscript | 150 lines | 200 lines | Split or extract at hard limit | | Library | 150 lines | 200 lines | Split by function group if needed | | Orchestrator | None | None | Coordinators by nature; no logic to limit | **Soft limit (150):** Review the script. Can a function be extracted to a library? Can a phase be split into a subscript? If yes, do it. If the script is genuinely cohesive at this length, proceed — but document why in the header. **Hard limit (200):** Split or extract. This is the default action. No argument is needed — just do it. **Hard limit override:** In rare cases, keeping a script intact beyond 200 lines is the correct decision. This is permitted, but must be declared explicitly in the script header: ```bash # Line limit override: ``` A silent override is not an override — it is a defect. Orchestrators are excluded from all limits. An orchestrator that sources 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.12 Scripts as teaching documents CE OS scripts are read by people learning the system, not only by people maintaining it. Write accordingly. - **Comments explain intent, not mechanics.** `# Install vim` is noise. `# vim is the CE OS default editor — required for all tiers` is information. - **Complex functions belong in libraries.** A 50-line function embedded in a subscript forces the reader to context-switch mid-flow. Extracted to a library with a clear header comment, it can be read and understood in isolation. - **If a function needs a comment block longer than itself, extract it.** The comment is telling you the function is too complex to live inline. - **The orchestrator is the lesson plan.** A reader following an orchestrator 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: 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. --- ## Part 2 — Bash Style Guide ### 2.1 Shebang and script class CE OS uses two Bash script classes. Choose the correct one: **Class A — System scripts** `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 # POSIX sh / ash only. No bash features. ``` **Class B — CE OS scripts** 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 on Alpine. ``` When in doubt, write Class B. Use Class A only when you have a specific reason to require ash compatibility. ### 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-lib.sh, ce-test-lib.sh (list sourced libraries) # --------------------------------------------------------------------------- # Phases: # 0 — Pre-flight (environment checks, no system changes) # 1 — # 2 — # N — Cleanup (always runs via trap) # --------------------------------------------------------------------------- ``` All fields are mandatory. Leave none blank. If a field does not apply, write `N/A` — do not delete the line. ### 2.3 Strict mode — selectively applied Do **not** use `set -e` in CE OS scripts. It causes unpredictable exits on operations that are allowed to fail (package availability checks, optional installs, test assertions). CE OS scripts handle errors explicitly. Use these instead: ```bash set -u # Treat unset variables as errors set -o pipefail # Propagate pipe failures ``` Apply `set -u` and `set -o pipefail` after sourcing libraries. Handle each potential failure explicitly with conditional checks. ### 2.4 Variables and naming ```bash # Constants — UPPER_SNAKE_CASE readonly SCRIPT_VERSION="v0.0.1" readonly SCRIPT_NAME="$(basename "$0")" readonly LOG_DIR="${HOME}/.local/logs/ceos_installer" readonly ARCH="$(uname -m)" # Local variables — lower_snake_case local pkg_name="" local install_result=0 # Loop variables — short, descriptive for pkg in "${packages[@]}"; do ... # Arrays — UPPER for constants, lower for mutable readonly REQUIRED_PKGS=( bash vim tmux ) available_pkgs=() ``` Always quote variable expansions: `"${VAR}"` not `$VAR`. Always. ### 2.5 Functions ```bash # Function declaration style — no function keyword check_architecture() { local expected_arch="${1:?check_architecture: expected_arch required}" local actual_arch actual_arch="$(uname -m)" if [[ "${actual_arch}" != "${expected_arch}" ]]; then log_warn "Architecture mismatch: expected ${expected_arch}, got ${actual_arch}" return 1 fi log_info "Architecture confirmed: ${actual_arch}" return 0 } ``` Rules: - No `function` keyword — use `name() {` style - Every function documents its arguments with `${VAR:?message}` or explicit checks - All functions `return 0` on success, non-zero on failure - Local variables declared with `local` — never pollute global scope - Functions are grouped: library sourcing, then utilities, then phases, then main ### 2.6 Package manager abstraction 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 # 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 } # 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 } ``` Each package manager library (`lib/pkg/apk.sh`, `lib/pkg/apt.sh`, etc.) exposes the same interface: ```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 ``` This means all higher-level scripts call `pkg_install vim` regardless of distro — the library handles the distro-specific implementation. ### 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 # Replace [PLACEHOLDER] with the live Forgejo APK repo URL when available readonly CE_APK_REPO="[PLACEHOLDER]" ``` ```bash 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) # 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 fi # Fall back to CE APK repo on Forgejo if [[ -n "${CE_APK_REPO:-}" ]] && \ curl --silent --max-time 5 \ "${CE_APK_REPO}/${arch}/APKINDEX.tar.gz" \ | tar -xzO 2>/dev/null \ | grep -q "^P:${pkg}$"; then log_info " FOUND (CE repo): ${pkg}" return 0 fi ;; apt) apt-cache show "${pkg}" >/dev/null 2>&1 && return 0 ;; nix) nix-env -qaP "${pkg}" >/dev/null 2>&1 && return 0 ;; 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 } install_packages() { local -n pkg_list="${1:?install_packages: pkg_list required}" local failed=() local unavailable=() for pkg in "${pkg_list[@]}"; do if ! test_package_available "${pkg}"; then unavailable+=( "${pkg}" ) continue fi if ! ${CE_PRIV} pkg_install "${pkg}"; then log_error "Failed to install: ${pkg}" failed+=( "${pkg}" ) else log_info "Installed: ${pkg}" fi done if [[ ${#unavailable[@]} -gt 0 ]]; then log_warn "Unavailable for ${ARCH}: ${unavailable[*]}" log_warn "These packages were skipped. Review CE repo for alternatives." fi if [[ ${#failed[@]} -gt 0 ]]; then log_error "Installation failures: ${failed[*]}" return 1 fi return 0 } ``` ### 2.11 Logging 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-lib.sh log functions when available _log() { local level="${1}" local msg="${2}" local ts ts="$(date '+%Y-%m-%d %H:%M:%S')" printf '[%s] [%s] %s\n' "${ts}" "${level}" "${msg}" | tee -a "${LOG_FILE}" } log_info() { _log "INFO " "${1}"; } log_warn() { _log "WARN " "${1}" >&2; } log_error() { _log "ERROR" "${1}" >&2; } log_debug() { [[ "${CE_DEBUG:-0}" == "1" ]] && _log "DEBUG" "${1}"; } ``` Log file location: `~/.local/logs/ceos_installer/` (XDG compliant). Log file name: `-.log`. ***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: ```bash # ── Cleanup ──────────────────────────────────────────────────────────────── TEMP_DIR="" cleanup() { local exit_code="${?}" log_info "Running cleanup..." # Remove temp files if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then rm -rf "${TEMP_DIR}" log_info "Removed temp dir: ${TEMP_DIR}" fi # Clear package manager cache pkg_cache_clean 2>/dev/null || true log_info "Cleanup complete (exit code: ${exit_code})" } trap cleanup EXIT INT TERM HUP # Create temp dir only after trap is registered TEMP_DIR="$(mktemp -d)" ``` ### 2.13 Test gates — structure ```bash # ── Pre-flight ───────────────────────────────────────────────────────────── phase_preflight() { log_info "=== PRE-FLIGHT ===" # Each check: run_test [warn|fail] run_test "bash version >= 4" '[[ "${BASH_VERSINFO[0]}" -ge 4 ]]' fail run_test "running as non-root" '[[ "${EUID}" -ne 0 ]]' fail run_test "internet reachable" \ 'curl --silent --max-time 5 --output /dev/null https://cervelloelettrico.it' \ warn run_test "architecture detected" '[[ -n "${ARCH}" ]]' fail run_test "disk space >= 500MB" \ '[[ $(df / --output=avail | tail -1) -ge 512000 ]]' \ warn log_info "Pre-flight complete" } # ── Inline test (example) ────────────────────────────────────────────────── phase_install_vim() { log_info "=== INSTALLING VIM ===" # 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 log_info "vim install verified" } # ── Post-install ─────────────────────────────────────────────────────────── phase_verify() { log_info "=== POST-INSTALL VERIFICATION ===" local required_tools=( vim tmux starship bash ) for tool in "${required_tools[@]}"; do run_test "${tool} available" "command -v ${tool} >/dev/null 2>&1" fail done log_info "All required tools verified" } ``` The `run_test` function lives in `ce-test-lib.sh`. When writing a standalone script, implement a minimal version: ```bash run_test() { local desc="${1}" local cmd="${2}" local mode="${3:-fail}" # fail | warn if eval "${cmd}" >/dev/null 2>&1; then log_info " PASS: ${desc}" return 0 else if [[ "${mode}" == "fail" ]]; then log_error " FAIL: ${desc}" exit 1 else log_warn " WARN: ${desc}" return 1 fi fi } ``` ### 2.14 User confirmation One confirmation prompt before system modifications begin. No more: ```bash confirm_proceed() { local prompt="${1:-Proceed with installation?}" printf '\n%s [y/N] ' "${prompt}" read -r response case "${response}" in [yY]|[yY][eE][sS]) return 0 ;; *) log_info "Aborted by OHIOD Unit ID ${USER}" exit 0 ;; esac } ``` ### 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 what it is permitted to contain. #### The three layers **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 — they are sourced, not run. Running a library directly should produce no output and no side effects. Extract a function to a library when: - More than one script needs it (reuse) - The function is complex enough that embedding it disrupts the flow of the script it lives in (clarity) - 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` for shared libraries; variant libraries live in domain subdirectories (`lib/pkg/apk.sh`, `lib/init/openrc.sh`). ```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 # --------------------------------------------------------------------------- # This file is a library. Source it — do not run it directly. # --------------------------------------------------------------------------- # Guard against double-sourcing [[ -n "${CE_PKG_LIB_LOADED:-}" ]] && return 0 readonly CE_PKG_LIB_LOADED=1 detect_package_manager() { ... } 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. It accepts arguments, performs its work, and returns a meaningful exit code. Subscripts may be run directly for testing and debugging — this is a feature, not a side effect. Naming: `ce-.sh` or `ce-.sh` — e.g. `ce-base.sh`, `ce-minimal.sh`, `ce-audio.sh`. Subject to the 150-line soft / 200-line hard limit. If a subscript is growing beyond this, extract complex functions to a library first, then consider whether the phase itself should be split. **Orchestrators (`ce-install.sh` or similar)** An orchestrator coordinates subscripts and libraries. It contains: - 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 - Flow control based on subscript exit codes An orchestrator contains **no complex logic of its own.** If something complex needs to happen, it lives in a subscript or library. The orchestrator calls it. A reader following the orchestrator should understand the complete flow without opening any sourced file. Orchestrators have no line limit. A 300-line orchestrator that sources six libraries and calls ten subscripts in sequence is correct and expected. #### Sourcing libraries ```bash # 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 } # 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 } # 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 } ``` Always validate that sourced files exist and loaded correctly. A missing library is a hard failure — never silently proceed without it. #### Directory layout ``` 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-test-lib.sh # Test gate framework │ ├── 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 | | 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 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 Before any script is considered ready for review, verify all of the following: ``` [ ] 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) [ ] 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 [ ] 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, 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 — ${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 # Version: v0.0.1 | Status: DEVELOPMENT # Role: Orchestrator # --------------------------------------------------------------------------- # Purpose: # Target: # Entry: # Sources: ce-common-lib.sh, ce-test-lib.sh # Calls: ce-base.sh, ce-minimal.sh, ce-basic.sh # --------------------------------------------------------------------------- # Phases: # 0 — Pre-flight (environment checks, no system changes) # 1 — Base (Tier 0) # 2 — Minimal (Tier 1) # 3 — Basic (Tier 2) # N — Cleanup (always runs via trap) # --------------------------------------------------------------------------- ``` **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 # Version: v0.0.1 | Status: DEVELOPMENT # Role: Subscript — Tier 0 Base installation # --------------------------------------------------------------------------- # Purpose: # Target: # Entry: # Depends: ce-common-lib.sh, ce-pkg-lib.sh # --------------------------------------------------------------------------- ``` **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 # Version: v0.0.1 | Status: DEVELOPMENT # Role: Library — Package manager detection and operations # --------------------------------------------------------------------------- # Purpose: # Used by: # --------------------------------------------------------------------------- # This file is a library. Source it — do not run it directly. # --------------------------------------------------------------------------- ``` **Line limit override (add to header when applicable):** ```bash # Line limit override: ``` --- [^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.*