From 9a9ca3458e17436d03ae64e72f26517e76a347b8 Mon Sep 17 00:00:00 2001 From: "David Kariuki (DK)" <14153276+david-kariuki@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:02:31 +0300 Subject: [PATCH 1/2] fix: resolve shellcheck findings --- .shellcheckrc | 11 +++++++++++ bin/linux-workstation-bootstrap | 3 ++- config/packages/debian.sh | 16 ++++++++-------- config/packages/kali.sh | 16 ++++++++-------- config/packages/ubuntu.sh | 16 ++++++++-------- tests/test_core.sh | 4 ++++ tests/test_distro.sh | 4 ++++ tests/test_execution.sh | 2 ++ tests/testlib.sh | 20 ++++++++++++++------ 9 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 .shellcheckrc diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..3745e10 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,11 @@ +# Linux Workstation Bootstrap ShellCheck configuration. +# +# The application is intentionally split across sourced Bash files. Allow +# ShellCheck to follow those source relationships relative to each script. +shell=bash +external-sources=true +source-path=SCRIPTDIR + +# SC2034 reports shared state as unused when individual sourced files are +# analyzed independently. These globals are consumed across bootstrap layers. +disable=SC2034 \ No newline at end of file diff --git a/bin/linux-workstation-bootstrap b/bin/linux-workstation-bootstrap index 74f5da2..cf398c8 100755 --- a/bin/linux-workstation-bootstrap +++ b/bin/linux-workstation-bootstrap @@ -15,7 +15,8 @@ set -Eeuo pipefail # Resolve the repository root regardless of where the command is executed from. # BASH_SOURCE[0] points to this script inside the bin/ directory. -readonly LWBS_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +LWBS_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +readonly LWBS_ROOT # Load the main bootstrap application logic. # shellcheck source=../lib/bootstrap.sh diff --git a/config/packages/debian.sh b/config/packages/debian.sh index c7479fb..f9a759e 100644 --- a/config/packages/debian.sh +++ b/config/packages/debian.sh @@ -11,12 +11,12 @@ # A capability may map to multiple packages by separating package names # with newlines. -LWBS_CAPABILITY_PACKAGES=( - [curl]="curl" - [dos2unix]="dos2unix" - [figlet]="figlet" - [nano]="nano" - [sed]="sed" - [wget]="wget" - [xz]="xz-utils" +declare -gA LWBS_CAPABILITY_PACKAGES=( + ["curl"]="curl" + ["dos2unix"]="dos2unix" + ["figlet"]="figlet" + ["nano"]="nano" + ["sed"]="sed" + ["wget"]="wget" + ["xz"]="xz-utils" ) diff --git a/config/packages/kali.sh b/config/packages/kali.sh index 33ce1af..e2bac21 100644 --- a/config/packages/kali.sh +++ b/config/packages/kali.sh @@ -11,12 +11,12 @@ # A capability may map to multiple packages by separating package names # with newlines. -LWBS_CAPABILITY_PACKAGES=( - [curl]="curl" - [dos2unix]="dos2unix" - [figlet]="figlet" - [nano]="nano" - [sed]="sed" - [wget]="wget" - [xz]="xz-utils" +declare -gA LWBS_CAPABILITY_PACKAGES=( + ["curl"]="curl" + ["dos2unix"]="dos2unix" + ["figlet"]="figlet" + ["nano"]="nano" + ["sed"]="sed" + ["wget"]="wget" + ["xz"]="xz-utils" ) diff --git a/config/packages/ubuntu.sh b/config/packages/ubuntu.sh index 2a362af..891eff7 100644 --- a/config/packages/ubuntu.sh +++ b/config/packages/ubuntu.sh @@ -11,12 +11,12 @@ # A capability may map to multiple packages by separating package names # with newlines. -LWBS_CAPABILITY_PACKAGES=( - [curl]="curl" - [dos2unix]="dos2unix" - [figlet]="figlet" - [nano]="nano" - [sed]="sed" - [wget]="wget" - [xz]="xz-utils" +declare -gA LWBS_CAPABILITY_PACKAGES=( + ["curl"]="curl" + ["dos2unix"]="dos2unix" + ["figlet"]="figlet" + ["nano"]="nano" + ["sed"]="sed" + ["wget"]="wget" + ["xz"]="xz-utils" ) diff --git a/tests/test_core.sh b/tests/test_core.sh index 2741a35..4aebfc2 100644 --- a/tests/test_core.sh +++ b/tests/test_core.sh @@ -57,6 +57,8 @@ test_require_command_rejects_missing_command() { } test_runtime_accepts_linux() { + # Mocked command invoked indirectly by validate_runtime(). + # shellcheck disable=SC2329 uname() { printf '%s\n' "Linux" } @@ -65,6 +67,8 @@ test_runtime_accepts_linux() { } test_runtime_rejects_non_linux() { + # Mocked command invoked indirectly by validate_runtime(). + # shellcheck disable=SC2329 uname() { printf '%s\n' "Darwin" } diff --git a/tests/test_distro.sh b/tests/test_distro.sh index 55ca678..01bcb1e 100644 --- a/tests/test_distro.sh +++ b/tests/test_distro.sh @@ -126,6 +126,8 @@ test_adapter_environment_validation_without_host_changes() { setup_test_logging "$temp_dir" + # Mocked command invoked indirectly by distro_validate_environment(). + # shellcheck disable=SC2329 require_command() { return 0 } @@ -151,6 +153,8 @@ test_adapter_rejects_incompatible_family() { setup_test_logging "$temp_dir" + # Mocked command invoked indirectly by distro_validate_environment(). + # shellcheck disable=SC2329 require_command() { return 0 } diff --git a/tests/test_execution.sh b/tests/test_execution.sh index 464381b..3b05efb 100644 --- a/tests/test_execution.sh +++ b/tests/test_execution.sh @@ -58,6 +58,8 @@ test_command_exit_code_is_preserved() { LWBS_DRY_RUN=false + # Command passed by name into the shared execution wrapper. + # shellcheck disable=SC2329 controlled_failure() { return 7 } diff --git a/tests/testlib.sh b/tests/testlib.sh index a6d9afa..fb3136b 100644 --- a/tests/testlib.sh +++ b/tests/testlib.sh @@ -11,6 +11,8 @@ LWBS_TEST_COUNT=0 LWBS_TEST_FAILURE_COUNT=0 +declare -a LWBS_TEST_CLEANUP_PATHS=() + # Execute one test function in an isolated subshell. run_test() { local test_name="${1:-}" @@ -94,22 +96,28 @@ make_test_temp_dir() { mktemp -d "${TMPDIR:-/tmp}/lwbs-test.XXXXXX" } +# Remove all temporary paths registered by the current isolated test. +cleanup_test_paths() { + local path + + for path in "${LWBS_TEST_CLEANUP_PATHS[@]}"; do + [[ -z "$path" ]] && continue + rm -rf -- "$path" + done +} + # Register one temporary path for removal when the isolated test exits. -# -# The path is shell-escaped when the trap is registered so cleanup does not -# depend on a local variable that may no longer exist when the EXIT trap runs. register_test_cleanup() { local path="${1:-}" - local escaped_path if [[ -z "$path" ]]; then printf 'Error: register_test_cleanup requires a path.\n' >&2 return 2 fi - printf -v escaped_path '%q' "$path" + LWBS_TEST_CLEANUP_PATHS+=("$path") - trap "rm -rf -- $escaped_path" EXIT + trap 'cleanup_test_paths' EXIT } # Initialize logging beneath an isolated test directory. From 03ab33b229dd74ae07b462c689829b26f8fcaea1 Mon Sep 17 00:00:00 2001 From: "David Kariuki (DK)" <14153276+david-kariuki@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:34:23 +0300 Subject: [PATCH 2/2] feat: add common workstation utilities module Closes #30 --- README.md | 1359 +++++--------------------------- modules/common-utilities.sh | 73 ++ tests/run.sh | 1 + tests/test_common_utilities.sh | 308 ++++++++ tests/testlib.sh | 14 +- 5 files changed, 608 insertions(+), 1147 deletions(-) create mode 100644 modules/common-utilities.sh create mode 100644 tests/test_common_utilities.sh diff --git a/README.md b/README.md index bb00d26..85683dd 100755 --- a/README.md +++ b/README.md @@ -1,82 +1,42 @@ # Linux Workstation Bootstrap -A reusable, distro-aware Linux workstation bootstrap utility for preparing and configuring Linux workstations in a consistent and controlled way. - -Linux Workstation Bootstrap is a modular replacement for a monolithic workstation setup script. It separates host detection, portable software intent, distribution-specific package resolution, external software installation, installation-state detection, user configuration, command execution, installable capabilities, and workstation profiles so each concern can evolve independently. - -## Current Capabilities - -The current bootstrap foundation provides: - -* Linux distribution detection using `/etc/os-release` -* Distribution version and codename detection -* Architecture detection and normalization -* Distribution-family identification -* Package-manager identification -* Interactive distribution profile confirmation and selection -* Command-line distribution profile override -* Optional user-local configuration -* Deterministic configuration precedence -* Validation of supported user configuration values -* Dry-run execution mode -* Persistent per-session logging -* Centralized application configuration -* Controlled command execution -* Centralized privileged command execution -* Distribution adapter loading and contract validation -* Ubuntu package-management adapter -* Debian package-management adapter -* Kali Linux package-management adapter -* Portable package capability resolution -* Independent package mappings per distribution profile -* Distribution-specific package-state detection -* Idempotent package installation filtering -* Controlled external software installer loading and validation -* External software installed-state detection -* Idempotent external software execution -* Repository, local-package, archive, and script installation methods -* Predictable temporary staging for external artifacts -* HTTPS-only external artifact downloads -* Local distribution package installation through distro adapters -* Controlled workstation module loading and validation -* Module capability requirement handling -* Optional module operation hooks -* Controlled workstation profile loading and validation -* Declarative profile-to-module composition -* Profile module validation before execution -* Profile execution through the shared module framework - -Supported distribution profiles: - -* Ubuntu -* Debian -* Kali Linux - -Current distribution adapter status: - -| Distribution | Adapter | Package Mapping | Package State | Local Packages | -| --- | --- | --- | --- | --- | -| Ubuntu | Implemented | Implemented | Implemented | Implemented | -| Debian | Implemented | Implemented | Implemented | Implemented | -| Kali Linux | Implemented | Implemented | Implemented | Implemented | +A modular, distro-aware Linux workstation bootstrap utility for configuring Linux workstations consistently and safely. + +Linux Workstation Bootstrap replaces a monolithic workstation setup script with reusable modules, portable software capabilities, distribution-specific adapters, execution planning, verification, and automated validation. + +## Features + +- Linux distribution, version, codename, and architecture detection +- Ubuntu, Debian, and Kali Linux support +- portable capability-to-package resolution +- distro-specific package adapters +- package installation-state detection +- idempotent package installation +- modular workstation capabilities +- declarative workstation profiles +- controlled external software installation +- dry-run execution +- execution planning and confirmation +- post-install verification and execution summaries +- user-local configuration +- persistent execution logging +- automated Bash tests +- ShellCheck and shfmt validation +- GitHub Actions CI + +## Supported Distributions + +| Distribution | Adapter | Package Mapping | Package State | +| --- | --- | --- | --- | +| Ubuntu | Yes | Yes | Yes | +| Debian | Yes | Yes | Yes | +| Kali Linux | Yes | Yes | Yes | + +The architecture is designed to support additional Linux distributions without adding distro-specific logic to shared workstation modules. ## Architecture -The project separates workstation intent from distro-specific implementation. - -Configuration is resolved using deterministic precedence: - -```text -Repository Defaults - | - v -User Configuration - | - v -Command-Line Arguments -``` - -Standard repository software follows the capability path: +The project separates workstation intent from operating-system implementation. ```text Workstation Profile @@ -84,1237 +44,346 @@ Workstation Profile v Modules | - | capabilities - v -Capability Resolver - | - | distro package mapping v - Package State Check - | - +----+----+ - | | -installed absent - | | - v v - skip Distribution Adapter - | - v - Execution Layer - | - v - System Package Manager -``` - -Software that cannot be installed exclusively from the standard distribution repositories follows a separate controlled path: - -```text - Modules +Portable Capabilities | - | external software requirement v -External Installer +Distro Package Mapping | v - External State Check +Package State Check | +----+----+ | | -installed absent - | | - v v - skip Installation Method - | - +-- Repository - +-- Local Package - +-- Archive - `-- Installer Script - | - v - Shared Execution Layer -``` - -The application entry flow coordinates these layers: - -```text -User - | - v -bin/linux-workstation-bootstrap - | - v -lib/bootstrap.sh - | - +-- Repository defaults - +-- CLI parsing - +-- Logging - +-- User configuration - +-- Runtime validation - +-- System detection - +-- Terminal UI - +-- Execution framework - +-- Distribution adapter framework - +-- Capability resolution - +-- External installation framework - +-- Module framework - `-- Profile framework + skip install + | + v + Distribution Adapter + | + v + Execution Layer ``` -The key separation is: +The core responsibility boundaries are: ```text -Repository defaults define built-in behaviour. -User configuration provides safe local overrides. -CLI arguments provide the highest-precedence runtime selections. -Profiles decide which workstation capabilities are wanted. -Modules declare portable software and operation requirements. -Capability mappings translate portable intent into distro package names. -Distribution adapters determine package state and perform package operations. -External installers define controlled non-standard installation paths. -State checks determine whether installation work is actually required. -The execution layer controls how system commands are run. +Profiles decide which modules are required +Modules describe workstation capabilities +Capability mappings translate capabilities into distro packages +Distro adapters perform operating-system-specific operations +Execution layer controls command execution and privileges +Verification confirms the resulting workstation state ``` -This prevents workstation modules from becoming coupled to one Linux distribution, package manager, vendor-specific installation mechanism, package-state implementation, or machine-specific configuration. +Software that cannot be installed through normal distribution repositories uses the separate external installer framework. -## Component Responsibilities +## Modules -### Entry Point - -`bin/linux-workstation-bootstrap` is the public executable. - -It enables safe Bash behavior, resolves the project root, loads the bootstrap orchestrator, and passes command-line arguments to `bootstrap_main`. - -### Configuration - -Repository-managed defaults live in: +Modules represent reusable workstation capabilities and live under: ```text -config/defaults.sh +modules/ ``` -They define built-in application values such as: - -* application identity and version -* minimum Bash version -* supported distribution profiles -* distro-family mappings -* package-manager mappings -* architecture normalization -* user configuration location -* logging defaults - -Distribution-specific package mappings live separately under: - -```text -config/packages/ -├── debian.sh -├── kali.sh -└── ubuntu.sh -``` +Modules declare portable capabilities rather than package-manager commands or distro-specific package names. -User configuration is handled by: +The first workstation module is: ```text -lib/config.sh +common-utilities ``` -The normal user configuration path is: - -```text -~/.config/linux-workstation-bootstrap/config.conf -``` - -If `XDG_CONFIG_HOME` is configured, it is used instead of `~/.config`. - -User configuration is optional. The bootstrap operates normally when the file does not exist. - -Configuration precedence is: - -```text -repository defaults - | - v -user configuration - | - v -CLI arguments -``` - -CLI arguments therefore remain the highest-precedence runtime input. - -### User Configuration - -User configuration allows supported defaults to be changed without editing repository-managed files. - -The configuration file is parsed as data rather than sourced as shell code. - -A minimal example is: - -```ini -# Linux Workstation Bootstrap - -distro_profile=ubuntu -``` - -The currently supported setting is: - -```text -distro_profile -``` - -Supported values are: - -```text -auto -ubuntu -debian -kali -``` - -`auto` preserves normal host detection and interactive selection behaviour. - -For example: - -```ini -distro_profile=ubuntu -``` - -sets Ubuntu as the user's default distribution profile. - -However: - -```bash -./bin/linux-workstation-bootstrap \ - --distro debian \ - --dry-run -``` - -still selects Debian because the CLI has higher precedence. - -Unsupported settings fail clearly rather than being silently ignored. - -For example: - -```ini -magic_setting=true -``` - -is rejected. - -Duplicate settings are also rejected: - -```ini -distro_profile=ubuntu -distro_profile=debian -``` - -This prevents ambiguous configuration. - -The user configuration parser does not perform shell expansion, command substitution, or arbitrary code execution. - -Values such as: - -```text -$(command) -``` - -are treated as configuration data and rejected when they are not valid for the setting. - -Internal constants that are not explicitly exposed as user settings remain controlled by repository configuration. - -### Bootstrap Orchestration - -`lib/bootstrap.sh` coordinates the application lifecycle. - -Its current startup order is: - -```text -Parse CLI - | - v -Handle help/version - | - v -Initialize logging - | - v -Load user configuration - | - v -Validate runtime - | - v -Detect system - | - v -Resolve distribution profile - | - v -Load distro adapter - | - v -Load package capability mapping -``` - -The bootstrap orchestrator does not contain distro-specific package commands, external software implementations, or workstation module implementations. - -### CLI Handling - -`lib/arguments.sh` parses command-line options and records runtime selections such as: - -* dry-run mode -* non-interactive confirmation -* distribution-profile overrides -* informational actions - -CLI selections remain separate from user configuration state. - -This makes precedence explicit rather than rewriting CLI state while configuration is loaded. - -### Core Utilities - -`lib/core.sh` contains reusable distro-independent runtime checks and helpers. - -### System Detection - -`lib/detect.sh` reads the host environment and determines: - -* distribution -* version -* codename -* distro family -* architecture -* expected package manager - -Detected host information remains separate from the distribution profile selected for execution. - -### Logging - -`lib/logging.sh` provides persistent per-session application logging. - -Configuration loading and selection decisions are recorded without executing or blindly persisting arbitrary user-supplied shell content. - -Package and external-software state decisions are also recorded so skipped and required actions remain visible across repeated bootstrap runs. - -### Terminal UI - -`lib/ui.sh` owns human-facing output, prompts, menus, help, version information, and execution summaries. - -### Execution Framework - -`lib/execution.sh` centralizes: - -* normal command execution -* privileged command execution -* dry-run behavior -* command result logging -* exit-code propagation - -System-changing code should use the shared execution layer rather than executing privileged commands directly. - -### Distribution Adapter Framework - -`lib/distro.sh` loads and validates the selected distribution implementation. - -Concrete adapters live under: - -```text -distros/ -├── debian.sh -├── kali.sh -└── ubuntu.sh -``` - -Every adapter implements: - -```text -distro_validate_environment -distro_update_package_index -distro_install_packages -distro_install_local_package -distro_package_state -``` - -`distro_package_state` provides distro-specific package-state detection. - -The function reports one of: - -```text -installed -absent -``` - -An absent package is a successfully determined state, not an error. - -A non-zero return code means that package state could not be determined reliably. The bootstrap treats that as a failure instead of assuming the package is absent. - -For the current Debian-family adapters, package state is determined using: - -```text -dpkg-query -``` - -The same abstraction allows future distributions to use their native mechanisms: - -```text -Debian family -> dpkg-query -Fedora -> RPM or DNF state mechanisms -Arch -> Pacman state mechanisms -``` - -`distro_install_local_package` provides the distro-specific implementation for a previously downloaded local package file. - -For example: - -```text -Ubuntu / Debian / Kali - | - v -apt-get install ./package.deb -``` - -Future adapters can provide their own equivalent operations without changing the external installation framework. - -Package-manager commands remain inside distribution adapters. - -### Capability Resolution - -`lib/capability.sh` translates portable capability identifiers into package names for the selected distribution profile. - -For example, a module can request: +It currently provides: ```text +curl +dos2unix +nano +sed +wget xz ``` -without knowing that the Debian-family package is: - -```text -xz-utils -``` - -The mapping is selected independently for each distro: +For Debian-family distributions, for example, the portable `xz` capability resolves to the `xz-utils` package. -```text -Module capability - | - v - xz - | - +-- Ubuntu -> xz-utils - +-- Debian -> xz-utils - `-- Kali -> xz-utils -``` - -Future distributions can provide different mappings without changing the module. - -For example: - -```text -Fedora -> xz -Arch -> xz -``` - -A capability may also map to more than one package where a workstation capability requires a package set. - -Resolved package names are deduplicated before package-state evaluation. - -Missing mappings fail explicitly rather than silently skipping requirements. - -### Idempotent Package Installation - -Resolved packages are checked before installation. - -For each package: - -```text -Resolved package - | - v -distro_package_state - | - +---+---+ - | | -installed absent - | | - v v - skip install -``` - -Packages that are already installed are not sent back to the package manager. - -For example: - -```text -curl -> installed -> skip -xz-utils -> absent -> install -``` - -If all package requirements for a module are already satisfied, package installation is skipped completely. - -Package-state detection remains centralized in distro adapters rather than being duplicated in each module. - -This makes repeated bootstrap runs safe and predictable. - -### External Installation Framework - -`lib/external.sh` provides a controlled path for software that is not installed exclusively from the selected distribution's standard package repositories. - -External installer definitions live under: - -```text -installers/.sh -``` - -Every external installer implements: +## Profiles -```text -external_id -external_name -external_description -external_method -external_state -``` - -`external_state` must report: - -```text -installed -``` - -or: - -```text -absent -``` - -A non-zero return code means the installer could not determine current software state. - -Unknown state is treated as an error rather than silently assuming the software is absent. - -Supported installation methods are: +Profiles compose modules into workstation configurations and live under: ```text -repository -package -archive -script +profiles/ ``` -Each method requires its matching implementation: - -```text -external_repository_apply -external_package_apply -external_archive_apply -external_script_apply -``` +A profile selects modules without duplicating their implementation. -An installer may optionally implement: +Conceptually: ```text -external_validate_environment -``` - -Environment validation and installation are only required when `external_state` reports that the software is absent. - -If the external software is already installed: - -```text -External installer - | - v - external_state - | - installed - | - v - skip -``` - -The framework provides controlled helpers for: - -* HTTPS artifact downloads -* temporary installation staging -* local distribution package installation -* installation of prepared system files -* tar archive extraction -* ZIP archive extraction -* staged installer-script execution - -External installation logic remains software-specific rather than being added to the bootstrap core. - -#### Repository Installations - -Repository-based software can prepare vendor repository metadata or signing material and install it through controlled system-file operations. - -Repository configuration remains distinct from normal package installation. - -#### Local Packages - -Downloaded distribution packages are passed back to the selected distro adapter. - -This preserves distro independence: - -```text -External Installer - | - v -Local package - | - v -Distribution Adapter - | - +-- Debian family -> .deb - +-- Fedora -> .rpm - `-- Arch -> local package format +developer +├── common-utilities +├── development-tools +├── containers +└── database-tools ``` -Fedora and Arch support are future targets and are not currently implemented. - -#### Archive Installations - -Archive-based software can be downloaded into temporary staging and extracted using controlled helpers. - -The framework currently supports tar-compatible archives and ZIP archives. +Profiles remain independent of the selected Linux distribution. -#### Installer Scripts +## Installation State and Idempotency -Installer scripts must first exist as local staged files. - -The framework intentionally does not provide a primitive equivalent to: +Package and external software state is checked before installation. ```text -curl | sh +installed -> skip +absent -> install +unknown/error -> stop safely ``` -A script must instead be: - -```text -downloaded - | - v -staged locally - | - v -executed through an explicit interpreter -``` +Repeated runs therefore avoid reinstalling already-satisfied requirements. -Artifact integrity and installer verification are handled by dedicated integrity work rather than silently trusting downloaded content. +Dry-run performs the same read-only state resolution without applying changes. -### Module Framework +## Execution Planning -`lib/module.sh` loads, validates, and executes reusable workstation capability modules. - -Modules live under: +Workstation changes are resolved before they are executed. ```text -modules/.sh +Profile + | + v +Modules + | + v +Capabilities + | + v +Current State + | + v +Execution Plan + | + v +Confirmation + | + v +Execution + | + v +Verification ``` -Every module implements: +The plan distinguishes between requirements that are already satisfied and those that require installation. -```text -module_id -module_name -module_description -module_capabilities -``` +## External Software -A module may optionally implement: +Software requiring mechanisms outside normal distro repositories is handled through controlled installer definitions under: ```text -module_apply +installers/ ``` -`module_capabilities` emits one portable capability identifier per line. - -The framework: - -1. resolves those capabilities using the selected distro package mapping -2. checks each resolved package through `distro_package_state` -3. skips packages already installed -4. sends only missing packages to `distro_install_packages` +Supported installer methods include: -Modules therefore do not implement their own package-state detection. +- repository configuration +- local distribution packages +- archives +- staged installer scripts -Modules also do not need to know whether the selected operating system uses `apt-get`, `dnf`, `pacman`, or another package-management implementation. +External downloads require HTTPS, and remote scripts are not piped directly into a shell. -### Profile Framework +## Configuration -`lib/profile.sh` loads, validates, and executes workstation profiles. - -Profiles live under: +Repository defaults live in: ```text -profiles/.sh +config/defaults.sh ``` -Every profile implements: +Optional user configuration normally lives at: ```text -profile_id -profile_name -profile_description -profile_modules +~/.config/linux-workstation-bootstrap/config.conf ``` -Profiles are intentionally declarative. They compose modules without containing package-manager commands, package lists, package-state logic, or duplicated module implementation. - -## Configuration Precedence - -Runtime configuration follows: +Configuration precedence is: ```text -repository defaults +Repository Defaults | v -user-local configuration +User Configuration | v -explicit CLI arguments -``` - -For example: - -```text -repository default : auto -user configuration: ubuntu -CLI --distro : debian -``` - -results in: - -```text -debian -``` - -If no CLI override is present: - -```text -repository default : auto -user configuration: ubuntu -``` - -results in: - -```text -ubuntu -``` - -If no user configuration exists: - -```text -repository default : auto -``` - -normal host detection and profile-selection behaviour applies. - -This precedence model will also support future workstation profile and module configuration without requiring repository-managed files to contain machine-specific preferences. - -## Execution Flow - -The bootstrap establishes the host and distro-specific foundation: - -```text -Parse CLI arguments - | - v -Initialize logging - | - v -Load user configuration - | - v -Validate runtime - | - v -Detect host system - | - v -Resolve distribution profile - | - v -Load distribution adapter - | - v -Validate adapter environment - | - v -Load distro capability mapping +CLI Arguments ``` -Distribution-profile resolution follows: - -```text -CLI --distro specified? - | - +----+----+ - | | - yes no - | | - v v - use CLI user config set? - | - +----+----+ - | | - yes no - | | - v v - use config detection / - interaction -``` - -Workstation configuration then builds on that foundation. - -Repository-backed capability: - -```text -Workstation profile - | - v -Module - | - v -Portable capability - | - v -Distro package mapping - | - v -Package state check - | - +----+----+ - | | -skip install - | - v -Distribution adapter - | - v -Shared execution layer -``` - -External software: +Example: -```text -Workstation module - | - v -External installer definition - | - v -External state check - | - +----+----+ - | | -skip install - | - v -Explicit installation method - | - v -External framework helper - | - v -Distribution adapter and/or -shared execution layer +```ini +distro_profile=ubuntu ``` -Workstation profile selection through the public CLI is introduced separately. Loading the frameworks themselves does not install software. - -## Distribution Profiles vs Workstation Profiles - -The project uses two different kinds of profiles. - -A **distribution profile** selects operating-system behavior: +Supported distro profile values are: ```text +auto ubuntu debian kali ``` -A **workstation profile** selects workstation capabilities: - -```text -default -developer -security -``` - -For example: - -```text -Ubuntu distribution profile - + -Developer workstation profile - | - v -Developer modules - | - v -Portable capabilities - | - v -Ubuntu package mappings - | - v -Package-state checks - | - v -Ubuntu adapter -``` - -Keeping these concepts separate allows the same workstation profile and modules to operate across multiple Linux distributions. - -## Package Capability Model - -Capability identifiers represent software intent rather than package-manager syntax. - -Examples: - -```text -curl -wget -nano -sed -dos2unix -xz -figlet -``` - -The identifiers should remain stable even when distro package names differ. - -For example: - -```text -Ubuntu -> xz-utils -Debian -> xz-utils -Kali -> xz-utils -Fedora -> xz -Arch -> xz -``` - -Fedora and Arch are future support targets. Their package managers, state mechanisms, and mappings should be added through distro adapters and capability mapping files rather than conditions inside shared modules. - -## Idempotency - -Linux Workstation Bootstrap is designed to be safely rerunnable. - -A repeated run evaluates the current workstation state before attempting installation. - -For repository packages: - -```text -already installed -> skip -missing -> install -unknown state -> fail safely -``` - -For external software: - -```text -already installed -> skip -missing -> execute installer -unknown state -> fail safely -``` - -A state-check failure is never silently interpreted as absence. - -This protects against unnecessary reinstallations and prevents uncertain state from triggering unintended system changes. - -Dry-run mode still evaluates current installation state so it can distinguish operations that are already satisfied from operations that would be required. - -## Standard vs External Software - -Software from the selected distribution's normal repositories should use: - -```text -Capability - | - v -Package Mapping - | - v -Package State - | - v -Distro Adapter -``` - -Software requiring additional mechanisms should use: - -```text -External Installer -``` - -Examples of external installation candidates include: - -```text -vendor repositories -vendor distribution packages -downloaded archives -language-package-manager tools -explicit installer scripts -``` - -The external path should not be used merely to bypass normal package capability resolution. - -## Modules and Profiles - -Modules represent reusable workstation capabilities such as: - -```text -common utilities -development tools -containers -database tooling -desktop and media applications -security tooling -hardware and power management -``` - -Profiles compose those modules into coherent workstation configurations without duplicating their implementation. - -For example: - -```text -developer profile -├── common utilities -├── development tools -├── containers -└── database tooling -``` - -## Requirements - -* Linux -* Bash 5 or newer -* `/etc/os-release` - -Ubuntu, Debian, and Kali Linux package operations additionally require: - -* APT -* `apt-get` -* `dpkg-query` - -Some external installation methods may additionally require utilities such as: - -* `curl` -* `tar` -* `unzip` - -Specific external installer definitions are responsible for validating the tools they require. +User configuration is parsed as data and is never sourced as arbitrary shell code. -## Clone and Run +## Usage Clone the repository: ```bash git clone https://github.com/LinuxEuphony/linux-workstation-bootstrap.git -``` - -Enter the project directory: - -```bash cd linux-workstation-bootstrap ``` -Make the bootstrap executable: - -```bash -chmod +x bin/linux-workstation-bootstrap -``` - Run the bootstrap: ```bash ./bin/linux-workstation-bootstrap ``` -The detected system configuration is displayed before a distribution profile, adapter, and package capability mapping are selected. - -## Usage - -```text -./bin/linux-workstation-bootstrap [options] -``` - Available options: ```text --distro Use a specific distribution profile ---dry-run Preview actions without applying system changes --y, --yes Accept a supported detected profile without prompting +--dry-run Preview actions without applying changes +-y, --yes Accept supported confirmations non-interactively --version Display the application version -h, --help Display help ``` Examples: -```bash -./bin/linux-workstation-bootstrap --help -``` - -```bash -./bin/linux-workstation-bootstrap --version -``` - ```bash ./bin/linux-workstation-bootstrap --dry-run --yes ``` ```bash -./bin/linux-workstation-bootstrap --distro ubuntu --dry-run -``` - -```bash -./bin/linux-workstation-bootstrap --distro debian --dry-run -``` - -```bash -./bin/linux-workstation-bootstrap --distro kali --dry-run -``` - -Selecting a different distribution profile does not alter the detected host information. - -An explicit `--distro` option overrides `distro_profile` from user configuration. - -## User Configuration Example - -Create the configuration directory: - -```bash -mkdir -p ~/.config/linux-workstation-bootstrap -``` - -Create: - -```text -~/.config/linux-workstation-bootstrap/config.conf -``` - -Example: - -```ini -# Linux Workstation Bootstrap - -distro_profile=ubuntu +./bin/linux-workstation-bootstrap \ + --distro ubuntu \ + --dry-run ``` -The file is user-local and lives outside the repository. - -Do not store machine-specific preferences in `config/defaults.sh`. - ## Logging -Each bootstrap execution creates a dedicated session log. - -By default, logs are stored under: +Execution logs are stored by default under: ```text ~/.local/state/linux-workstation-bootstrap/logs/ ``` -If `XDG_STATE_HOME` is configured, it is used as the state directory instead. - -The bootstrap displays the current session log and log directory during execution. +`XDG_STATE_HOME` is respected when configured. -Configuration loading, selection precedence, installation-state decisions, skipped package requirements, missing packages, external-software state checks, installation attempts, and failures are recorded in the session log. +Logs capture significant bootstrap, planning, state, execution, and verification events without enabling shell tracing or blindly logging command arguments. ## Project Structure ```text linux-workstation-bootstrap/ -├── bin/ -│ └── linux-workstation-bootstrap -├── config/ -│ ├── defaults.sh -│ └── packages/ -│ ├── debian.sh -│ ├── kali.sh -│ └── ubuntu.sh -├── lib/ -│ ├── arguments.sh -│ ├── bootstrap.sh -│ ├── capability.sh -│ ├── config.sh -│ ├── core.sh -│ ├── detect.sh -│ ├── distro.sh -│ ├── execution.sh -│ ├── external.sh -│ ├── logging.sh -│ ├── module.sh -│ ├── profile.sh -│ └── ui.sh -├── distros/ -│ ├── debian.sh -│ ├── kali.sh -│ └── ubuntu.sh -├── installers/ -├── modules/ -├── profiles/ -├── tests/ -├── docs/ +├── bin/ # Public CLI entrypoint +├── config/ # Defaults and distro package mappings +├── distros/ # Distribution-specific adapters +├── docs/ # Detailed project documentation +├── installers/ # External software installers +├── lib/ # Shared bootstrap infrastructure +├── modules/ # Reusable workstation capabilities +├── profiles/ # Workstation profile definitions +├── tests/ # Automated Bash tests └── README.md ``` -The `installers/` directory contains software-specific external installer definitions as they are introduced. +Important shared components include: -Concrete workstation modules and profiles are added independently of their shared frameworks. - -## Safety - -Linux Workstation Bootstrap is designed to make system changes deliberately and visibly. - -Repository defaults, user-local configuration, CLI arguments, host detection, distribution selection, capability resolution, package-state detection, external installer validation, external software state detection, adapter validation, module validation, profile validation, logging, and execution logic are separated so operations can be checked before system changes are applied. - -User configuration is parsed as data and is never sourced as arbitrary shell code. +```text +lib/bootstrap.sh application lifecycle +lib/capability.sh capability resolution +lib/config.sh user configuration +lib/distro.sh distro adapter framework +lib/execution.sh controlled command execution +lib/external.sh external installation framework +lib/module.sh module framework +lib/plan.sh execution planning +lib/profile.sh profile framework +lib/verification.sh post-install verification +``` -Only explicitly supported settings may be overridden. +## Development Validation -Invalid, duplicate, or unsupported configuration values fail clearly. +Run the automated test suite: -CLI selections retain higher precedence than user configuration. +```bash +bash tests/run.sh +``` -Machine-specific configuration remains outside repository-managed defaults. +For repository-wide shell validation: -Dry-run mode provides a way to preview execution without applying changes. +```bash +mapfile -d '' -t shell_files < <( + git ls-files -z \ + --cached \ + --others \ + --exclude-standard \ + -- '*.sh' 'bin/*' +) -Current installation state is evaluated before package or external software installation. +for shell_file in "${shell_files[@]}"; do + bash -n "$shell_file" +done -Already-satisfied requirements are skipped. +shellcheck -x "${shell_files[@]}" -Unknown or failed state checks stop execution rather than being interpreted as missing software. +shfmt \ + -d \ + -i 4 \ + -ci \ + "${shell_files[@]}" -Command execution and privilege escalation are centralized so system-changing operations use a consistent execution path. +bash tests/run.sh -Distribution-specific package names are isolated in package mapping files. +git diff --check +``` -Distribution-specific package-state checks remain inside distro adapters. +GitHub Actions performs the same core syntax, linting, formatting, and test validation on repository changes. -Package-manager commands remain inside distribution adapters. +## Safety -Modules request portable capabilities instead of embedding distro package names. +Linux Workstation Bootstrap is designed around explicit and reviewable system changes. -Modules do not duplicate package-state detection logic. +Key safeguards include: -External software uses explicit, reviewable installation methods and explicit installed-state checks. +- dry-run support +- package and software state checks before installation +- execution plans before modification +- centralized privilege handling +- distro-specific package operations isolated in adapters +- portable capabilities instead of package names in modules +- safe failure on unknown installation state +- staged external scripts instead of `curl | sh` +- post-install verification +- user configuration parsed as data +- automated syntax, lint, formatting, and test validation -External downloads require HTTPS. +## Documentation -Remote scripts are not piped directly into a shell. +Detailed design and implementation documentation belongs under: -Temporary external installation files are staged predictably and cleaned after execution. +```text +docs/ +``` -Profiles do not contain package-manager commands or duplicate module implementation. +The repository already includes: -Loading configuration, a distro adapter, capability mapping, external installer definition, module, or profile does not itself change the workstation. +```text +docs/testing.md +``` -Artifact integrity and external installer verification are separate concerns that build on the external installation framework. +Additional architecture and framework documentation is maintained separately from the README as those areas evolve. ## Project diff --git a/modules/common-utilities.sh b/modules/common-utilities.sh new file mode 100644 index 0000000..8400060 --- /dev/null +++ b/modules/common-utilities.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Linux Workstation Bootstrap +# Author: David Kariuki +# +# Common workstation utilities. +# +# Provides baseline command-line tools that are broadly useful across +# supported workstation profiles. +# +# Distribution-specific package names remain in config/packages rather than +# in this module. +# +# Module contract functions are loaded and invoked indirectly by the shared +# module framework. +# shellcheck disable=SC2317,SC2329 + +module_id() { + printf '%s\n' "common-utilities" +} + +module_name() { + printf '%s\n' "Common Utilities" +} + +module_description() { + printf '%s\n' \ + "Baseline command-line utilities used across workstation profiles." +} + +module_capabilities() { + printf '%s\n' \ + "curl" \ + "dos2unix" \ + "nano" \ + "sed" \ + "wget" \ + "xz" +} + +# Verify that the commands provided by this module are available after +# installation. Package-state verification remains handled by the execution +# framework; this check confirms the expected utilities are actually usable. +module_verify() { + local utility + local verification_failed=false + + local -a required_utilities=( + "curl" + "dos2unix" + "nano" + "sed" + "wget" + "xz" + ) + + for utility in "${required_utilities[@]}"; do + if command_exists "$utility"; then + continue + fi + + printf 'Error: required common utility is unavailable: %s\n' \ + "$utility" >&2 + + verification_failed=true + done + + if "$verification_failed"; then + return 1 + fi + + return 0 +} diff --git a/tests/run.sh b/tests/run.sh index 351f3a7..b315edc 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -12,6 +12,7 @@ declare -a TEST_FILES=( "$TEST_DIR/test_config.sh" "$TEST_DIR/test_execution.sh" "$TEST_DIR/test_distro.sh" + "$TEST_DIR/test_common_utilities.sh" ) failed_files=0 diff --git a/tests/test_common_utilities.sh b/tests/test_common_utilities.sh new file mode 100644 index 0000000..7adf99e --- /dev/null +++ b/tests/test_common_utilities.sh @@ -0,0 +1,308 @@ +#!/usr/bin/env bash + +set -uo pipefail + +TEST_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$TEST_DIR/.." && pwd)" + +# shellcheck source=./testlib.sh +source "$TEST_DIR/testlib.sh" + +LWBS_ROOT="$REPO_ROOT" + +# Source the complete bootstrap framework because these tests exercise the +# module through capability, profile, and execution-plan integration. +# shellcheck source=../lib/bootstrap.sh +source "$REPO_ROOT/lib/bootstrap.sh" + +test_common_utilities_module_contract() { + local temp_dir + local capabilities + + temp_dir="$(make_test_temp_dir)" + register_test_cleanup "$temp_dir" + + setup_test_logging "$temp_dir" + + load_module common-utilities + + assert_equal \ + "common-utilities" \ + "$LWBS_MODULE_ID" + + assert_equal \ + "Common Utilities" \ + "$LWBS_MODULE_NAME" + + assert_equal \ + "6" \ + "${#LWBS_MODULE_CAPABILITIES[@]}" + + capabilities="$( + printf '%s\n' "${LWBS_MODULE_CAPABILITIES[@]}" + )" + + assert_equal \ + $'curl\ndos2unix\nnano\nsed\nwget\nxz' \ + "$capabilities" + + assert_equal \ + "0" \ + "${#LWBS_MODULE_EXTERNAL_INSTALLERS[@]}" + + if ! declare -F module_verify >/dev/null 2>&1; then + printf 'common-utilities does not provide module_verify.\n' >&2 + return 1 + fi + + reset_module_contract +} + +test_common_utilities_resolve_on_supported_distros() { + local temp_dir + local distro + local resolved + + local -a resolved_packages=() + + temp_dir="$(make_test_temp_dir)" + register_test_cleanup "$temp_dir" + + setup_test_logging "$temp_dir" + + for distro in ubuntu debian kali; do + load_capability_map "$distro" + + resolved_packages=() + + resolve_capabilities \ + resolved_packages \ + curl \ + dos2unix \ + nano \ + sed \ + wget \ + xz + + resolved="$( + printf '%s\n' "${resolved_packages[@]}" + )" + + assert_equal \ + $'curl\ndos2unix\nnano\nsed\nwget\nxz-utils' \ + "$resolved" \ + "common utility capability mapping differs for $distro" + + reset_capability_map + done +} + +test_common_utilities_verification_succeeds() { + local temp_dir + + temp_dir="$(make_test_temp_dir)" + register_test_cleanup "$temp_dir" + + setup_test_logging "$temp_dir" + + load_module common-utilities + + # Verification dependency mocked so this test does not depend on which + # utilities happen to be installed on the developer workstation. + # shellcheck disable=SC2329 + command_exists() { + return 0 + } + + module_verify + + reset_module_contract +} + +test_common_utilities_verification_detects_missing_command() { + local temp_dir + + temp_dir="$(make_test_temp_dir)" + register_test_cleanup "$temp_dir" + + setup_test_logging "$temp_dir" + + load_module common-utilities + + # Simulate one package being present while its expected executable is not + # usable so module-level verification must reject the result. + # shellcheck disable=SC2329 + command_exists() { + local command_name="${1:-}" + + [[ "$command_name" != "dos2unix" ]] + } + + if module_verify >/dev/null 2>&1; then + printf 'module_verify accepted a missing required utility.\n' >&2 + return 1 + fi + + reset_module_contract +} + +test_common_utilities_can_be_selected_by_profile() { + local temp_dir + local profile_dir + + temp_dir="$(make_test_temp_dir)" + register_test_cleanup "$temp_dir" + + setup_test_logging "$temp_dir" + + profile_dir="$temp_dir/profiles" + mkdir -p -- "$profile_dir" + + cat >"$profile_dir/common-test-profile.sh" <<'EOF' +profile_id() { + printf '%s\n' "common-test-profile" +} + +profile_name() { + printf '%s\n' "Common Utilities Test Profile" +} + +profile_description() { + printf '%s\n' "Selects the common utilities module for testing." +} + +profile_modules() { + printf '%s\n' "common-utilities" +} +EOF + + LWBS_PROFILES_DIR_OVERRIDE="$profile_dir" + + load_profile common-test-profile + + assert_equal \ + "common-test-profile" \ + "$LWBS_PROFILE_ID" + + assert_equal \ + "1" \ + "${#LWBS_PROFILE_MODULES[@]}" + + assert_equal \ + "common-utilities" \ + "${LWBS_PROFILE_MODULES[0]}" + + reset_profile_contract +} + +test_common_utilities_dry_run_execution_plan() { + local temp_dir + local profile_dir + local output_file + local output + + temp_dir="$(make_test_temp_dir)" + register_test_cleanup "$temp_dir" + + setup_test_logging "$temp_dir" + + profile_dir="$temp_dir/profiles" + output_file="$temp_dir/execution-plan.out" + + mkdir -p -- "$profile_dir" + + cat >"$profile_dir/common-test-profile.sh" <<'EOF' +profile_id() { + printf '%s\n' "common-test-profile" +} + +profile_name() { + printf '%s\n' "Common Utilities Test Profile" +} + +profile_description() { + printf '%s\n' "Selects the common utilities module for planning tests." +} + +profile_modules() { + printf '%s\n' "common-utilities" +} +EOF + + LWBS_PROFILES_DIR_OVERRIDE="$profile_dir" + + DISTRO_ADAPTER_PROFILE="ubuntu" + + load_capability_map ubuntu + + LWBS_DRY_RUN=true + LWBS_ASSUME_YES=false + + # Planning needs package state but must not perform installation during + # dry-run. All requirements are marked absent to produce a complete plan. + # shellcheck disable=SC2329 + distro_package_state() { + printf '%s\n' "absent" + } + + # Redirect output to a file rather than using command substitution. + # Command substitution would execute run_profile in a subshell and discard + # the execution-plan state that this test needs to inspect afterward. + run_profile common-test-profile >"$output_file" + + output="$(<"$output_file")" + + assert_equal \ + "previewed" \ + "$LWBS_PLAN_STATUS" + + assert_equal \ + "6" \ + "$LWBS_PLAN_INSTALL_ACTION_COUNT" + + assert_equal \ + "0" \ + "$LWBS_PLAN_SKIP_ACTION_COUNT" + + assert_contains \ + "$output" \ + "Common Utilities" + + assert_contains \ + "$output" \ + "curl [install]" + + assert_contains \ + "$output" \ + "xz-utils [install]" + + assert_contains \ + "$output" \ + "Dry-run mode: execution plan was not applied." +} + +run_test \ + "load the common utilities module contract" \ + test_common_utilities_module_contract + +run_test \ + "resolve common utility capabilities on all supported distros" \ + test_common_utilities_resolve_on_supported_distros + +run_test \ + "verify available common utilities" \ + test_common_utilities_verification_succeeds + +run_test \ + "detect a missing common utility during verification" \ + test_common_utilities_verification_detects_missing_command + +run_test \ + "select common utilities through a workstation profile" \ + test_common_utilities_can_be_selected_by_profile + +run_test \ + "resolve common utilities through a dry-run execution plan" \ + test_common_utilities_dry_run_execution_plan + +finish_tests diff --git a/tests/testlib.sh b/tests/testlib.sh index fb3136b..ada8ff1 100644 --- a/tests/testlib.sh +++ b/tests/testlib.sh @@ -17,6 +17,7 @@ declare -a LWBS_TEST_CLEANUP_PATHS=() run_test() { local test_name="${1:-}" local test_function="${2:-}" + local test_exit_code if [[ -z "$test_name" || -z "$test_function" ]]; then printf 'Error: run_test requires a name and test function.\n' >&2 @@ -27,10 +28,19 @@ run_test() { printf 'TEST: %s\n' "$test_name" - if ( + # Do not execute the subshell directly as the condition of an if + # statement. Bash suppresses errexit semantics for commands whose status + # is being tested by a conditional, which can allow failed assertions + # inside the test function to be followed by a successful command and + # incorrectly report the test as passing. + ( set -Eeuo pipefail "$test_function" - ); then + ) + + test_exit_code=$? + + if ((test_exit_code == 0)); then printf 'PASS: %s\n\n' "$test_name" return 0 fi