diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9dcf3a..046430e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,20 +8,6 @@ on: env: CARGO_TERM_COLOR: always - SYSTEM_DEPS: >- - build-essential - pkg-config - libglib2.0-dev - libgtk-4-dev - libadwaita-1-dev - libcairo2-dev - libpango1.0-dev - libgdk-pixbuf-xlib-2.0-dev - libwayland-dev - libx11-dev - libxkbcommon-dev - libgirepository1.0-dev - libgtk4-layer-shell-dev jobs: build: @@ -30,17 +16,18 @@ jobs: container: image: ubuntu:25.10 steps: - - name: Install git and dependencies + - name: Install git run: | apt-get update - apt-get install -y git curl ${{ env.SYSTEM_DEPS }} + apt-get install -y git curl - uses: actions/checkout@v4 - - name: Setup Rust toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Install build dependencies + run: ./scripts/shepherd deps install build + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Cache cargo registry and build uses: actions/cache@v4 @@ -56,7 +43,7 @@ jobs: - name: Build run: | . "$HOME/.cargo/env" - cargo build --all-targets + ./scripts/shepherd build test: name: Test @@ -64,17 +51,18 @@ jobs: container: image: ubuntu:25.10 steps: - - name: Install git and dependencies + - name: Install git run: | apt-get update - apt-get install -y git curl ${{ env.SYSTEM_DEPS }} + apt-get install -y git curl - uses: actions/checkout@v4 - - name: Setup Rust toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Install build dependencies + run: ./scripts/shepherd deps install build + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Cache cargo registry and build uses: actions/cache@v4 @@ -98,19 +86,23 @@ jobs: container: image: ubuntu:25.10 steps: - - name: Install git and dependencies + - name: Install git run: | apt-get update - apt-get install -y git curl ${{ env.SYSTEM_DEPS }} + apt-get install -y git curl - uses: actions/checkout@v4 - - name: Setup Rust toolchain + - name: Install build dependencies + run: ./scripts/shepherd deps install build + + - name: Add clippy component run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y . "$HOME/.cargo/env" rustup component add clippy - echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Add Rust to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Cache cargo registry and build uses: actions/cache@v4 @@ -127,3 +119,21 @@ jobs: run: | . "$HOME/.cargo/env" cargo clippy --all-targets -- -D warnings + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install ShellCheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Run ShellCheck + run: | + # SC1091: Not following sourced files (info only, safe to ignore) + shellcheck -e SC1091 scripts/shepherd scripts/dev scripts/admin + shellcheck -e SC1091 scripts/lib/*.sh + shellcheck -e SC1091 run-dev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e14d92c..6818fe8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,8 @@ ### tl;dr -You need a Wayland-capable Linux system, Rust, and a small set of system dependencies. Once installed, `./run-dev` will start a development instance. +You need a Wayland-capable Linux system, Rust, and a small set of system +dependencies. Once installed, `./run-dev` will start a development instance. ### Requirements @@ -15,14 +16,40 @@ You need a Wayland-capable Linux system, Rust, and a small set of system depende 2. **System dependencies** - * Platform-specific packages are required. - * For Ubuntu, see the [`SYSTEM_DEPS` section in the CI configuration](./.github/workflows/ci.yml). + * Platform-specific packages are required for building and running. + * View packages with: `./scripts/shepherd deps print dev` + * Install all dev dependencies: `./scripts/shepherd deps install dev` + * **Note**: Rust is automatically installed via rustup when installing build or dev dependencies. -3. **Rust toolchain** +### Unified script system - ```sh - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` +`shepherd-launcher` provides a unified script system for managing dependencies, building, and running: + +```sh +# View and install dependencies +./scripts/shepherd deps print dev # List all dev dependencies +./scripts/shepherd deps install dev # Install all dev dependencies + +# Build binaries +./scripts/shepherd build # Debug build +./scripts/shepherd build --release # Release build + +# Development +./scripts/shepherd dev run # Build and run in nested Sway +``` + +For CI/build-only environments: +```sh +./scripts/shepherd deps install build # Build dependencies only +./scripts/shepherd build --release # Production build +``` + +For runtime-only systems: +```sh +./scripts/shepherd deps install run # Runtime dependencies only +``` + +See `./scripts/shepherd --help` for all available commands. ### Running in development diff --git a/README.md b/README.md index 06361d6..1314839 100644 --- a/README.md +++ b/README.md @@ -74,29 +74,6 @@ If it can run on Linux in *any way, shape, or form*, it can be supervised by > [A Short Hike](https://ashorthike.com/) running via Steam -Contributions are welcome for improvements and not yet implemented backends, -such as: -* Content-aware media player with supervised libraries - ([#3](https://github.com/aarmea/shepherd-launcher/issues/3)) -* Pre-booted Steam to improve launch time - ([#4](https://github.com/aarmea/shepherd-launcher/issues/4)) -* Android apps via Waydroid, including pre-booting Android if necessary - ([#5](https://github.com/aarmea/shepherd-launcher/issues/5)) -* Legacy Win9x via DOSBox, QEMU, or PCem, including scripts to create a boot-to-app image - ([#6](https://github.com/aarmea/shepherd-launcher/issues/6)) -* Chrome - ([#7](https://github.com/aarmea/shepherd-launcher/issues/7)), - including strict sandboxing and support for firewall rules - ([#8](https://github.com/aarmea/shepherd-launcher/issues/8)) -* Awareness of whether an Internet connection is available, and an availability - rule that gates activities based on this - ([#9](https://github.com/aarmea/shepherd-launcher/issues/9)) -* Porting to other *host* platforms, such as a Microsoft Windows shell - replacement or macOS kiosk via MDM. `shepherd-launcher` is architected such - that `shepherdd`, the core enforcement service, does not render any UI and - performs platform-specific functions like process management in - platform-specific crates. - ## Core concepts * **Launcher-first**: only one foreground activity at a time @@ -113,16 +90,12 @@ such as: ## Installation -`shepherd-launcher` is pre-alpha and in active development. As such, end-user -binaries and installation instructions are not yet available. +`shepherd-launcher` is pre-alpha and in active development. The helper at +`./scripts/shepherd` can be used to build and install a *fully functional* +local kiosk setup from source: -See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to run in development. - -Contributions are welcome for: -* a CI step that generates production binaries - ([#1](https://github.com/aarmea/shepherd-launcher/issues/1)) -* an installation script - ([#2](https://github.com/aarmea/shepherd-launcher/issues/2)) +Check out this repository and run `./scripts/shepherd --help` or see +[INSTALL.md](./docs/INSTALL.md) for more. ## Example configuration @@ -158,11 +131,6 @@ max_run_seconds = 1800 # 30 minutes (roughly 3 in-game days) daily_quota_seconds = 3600 # 1 hour per day cooldown_seconds = 600 # 10 minute cooldown -[[entries.warnings]] -seconds_before = 600 -severity = "info" -message = "10 minutes left - start wrapping up!" - [[entries.warnings]] seconds_before = 120 severity = "warn" @@ -178,7 +146,12 @@ See [config.example.toml](./config.example.toml) for more. ## Development -See [CONTRIBUTING.md](./CONTRIBUTING.md) +Build instructions and contribution guidelines are described in +[CONTRIBUTING.md](./CONTRIBUTING.md). + +If you'd like to help out, look on +[GitHub Issues](https://github.com/aarmea/shepherd-launcher/issues) for +potential work items. ## Written in 2025, responsibly diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..a5e1982 --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,66 @@ +# Installation + +`shepherd-launcher` can be installed on Linux with a modern Wayland compositor. +It is currently developed and tested on Ubuntu 25.10. + +`shepherd-launcher` currently must be built from source. `./scripts/shepherd` +can help set up your build environment and manage your installation. + +## Basic setup + +```sh +# 0. Install build dependencies +sudo ./scripts/shepherd deps build run + +# 1. Install runtime dependencies +sudo ./scripts/shepherd deps install run + +# 2. Build release binaries +./scripts/shepherd build --release + +# 3. Install everything for a kiosk user +sudo ./scripts/shepherd install all --user kiosk +``` + +This installs: +- Binaries to `/usr/local/bin/` +- System Sway configuration to `/etc/sway/shepherd.conf` +- Display manager desktop entry ("Shepherd Kiosk" session) +- System config template to `/etc/shepherd/config.toml` +- User config to `~kiosk/.config/shepherd/config.toml` + +For custom installation paths: +```sh +# Install to /usr instead of /usr/local +sudo ./scripts/shepherd install all --user kiosk --prefix /usr +``` + +Individual installation steps can be run separately: +```sh +sudo ./scripts/shepherd install bins --prefix /usr/local +sudo ./scripts/shepherd install config --user kiosk +``` + +## Kiosk hardening (optional) + +Kiosk hardening is optional and intended for devices primarily used by +children, not developer machines. + +```sh +sudo ./scripts/shepherd harden apply --user kiosk +``` + +This restricts the user to only access the Shepherd session by: +- Denying SSH access +- Restricting console (TTY) login +- Denying sudo access +- Restricting shell to Sway sessions only + +To revert (all changes are reversible): +```sh +sudo ./scripts/shepherd harden revert --user kiosk +``` + +## Complete documentation + +See the scripts' [README](../scripts/README.md) for more. diff --git a/docs/ai/history/2025-12-31 001 scripts.md b/docs/ai/history/2025-12-31 001 scripts.md new file mode 100644 index 0000000..cd62078 --- /dev/null +++ b/docs/ai/history/2025-12-31 001 scripts.md @@ -0,0 +1,231 @@ +This prompt created the setup script ecosystem, including CI setup, dev setup, and end-user installation and configuration. It was provided to Claude Opus 4.5 via VSCode Agent mode. + +The first draft was written by ChatGPT after some back-and-forth [in this conversation](https://chatgpt.com/share/6955f5ee-46f4-800b-87bc-db0e3df16541). + +The output was committed as fc0c1ce51d739aafcdce66f6958f008ef056a76a. + +----- + +# Task: Implement unified script system for dev, build, install, and user hardening + +## Objective + +Implement a **single, composable script system** that: + +* Avoids duplication between CI, developer, and runtime workflows +* Exposes clear, reversible primitives for build, install, and user hardening +* Uses one authoritative entrypoint with subcommands +* Allows CI, developers, and administrators to invoke the *same logic* with different scopes + +The system must support: + +* CI build-only environments +* Runtime-only systems +* Developer systems (union of build + runtime deps) +* Production installation and kiosk-style user hardening + +--- + +## High-level design constraints + +* **One CLI entrypoint** (script-based), with subcommands +* **Shared logic lives in libraries**, not duplicated scripts +* **All tasks must be independently callable** +* **All destructive actions must be reversible** +* **Dependency sets must be defined once and derived programmatically** +* Shell scripts are acceptable and preferred for system integration + +--- + +## Required repository layout + +``` +scripts/ + shepherd # main CLI dispatcher + dev # thin wrapper → shepherd dev run + admin # optional wrapper → shepherd admin ... + + lib/ + common.sh # logging, error handling, sudo helpers + deps.sh # dependency set logic + build.sh # cargo build logic + sway.sh # nested sway execution helpers + install.sh # binary + config installation + harden.sh # harden / unharden user logic + + deps/ + build.pkgs # build-only system packages + run.pkgs # runtime-only system packages + dev.pkgs # optional dev-only extras +``` + +No business logic should live in `Makefile` (if present). + +--- + +## CLI interface (must be implemented) + +### Dependency management + +``` +shepherd deps print build|run|dev +shepherd deps install build|run|dev +``` + +Behavior: + +* `build` = build.pkgs +* `run` = run.pkgs +* `dev` = union(build, run, dev.pkgs) +* No duplicated lists anywhere + +The build packages are currently listed in the CI definition (./.github/workflows/ci.yml). The CI definition should be updated to use this script for package management. +You may leave a stub for the runtime packages that only includes `sway`. + +You may assume Ubuntu 25.10 or higher. Warn but do not fail if the environment indicates otherwise. + +--- + +### Build + +``` +shepherd build # debug build +shepherd build --release # production build +``` + +Internals: + +* Wrap `cargo build` +* Centralize binary names and target paths +* No logic duplication in dev/run/install paths + +--- + +### Development run + +``` +shepherd dev run +``` + +This should be functionally equivalent to what is currently in `./run-dev` and will serve as its replacement. + +Behavior: + +* Builds (debug) +* Launches **nested sway** +* Executes built binaries with correct environment + +This should reuse shared sway helpers (not inline logic). + +--- + +### Installation (composable steps) + +Each step must be callable independently. + +``` +shepherd install bins [--prefix PREFIX] +shepherd install config --user USER [--source CONFIG] +shepherd install all --user USER [--prefix PREFIX] +``` + +Requirements: + +* Install release binaries to standard paths +* Install our global Sway configuration to a standard path +* Install desktop entry that tells the display manager to run shepherd-launcher via the Sway configuration (you will need to write this) +* Support PREFIX and DESTDIR +* Deploy example config to a specified user +* Do not assume hardening is enabled + +--- + +### User hardening (reversible) + +``` +shepherd harden apply --user USER +shepherd harden revert --user USER +``` + +Requirements: + +* Harden a user so it can only run shepherd-launcher +* Support reverting to original system state +* Persist rollback state under: + + ``` + /var/lib/shepherdd/hardening// + ``` +* Hardening and unharden must be idempotent + +No assumptions about display manager; logic should be isolated. + +--- + +## Shared library responsibilities + +### `lib/common.sh` + +* Logging (`info`, `warn`, `error`) +* `require_root`, `maybe_sudo` +* Safe command execution helpers + +### `lib/deps.sh` + +* Read package lists +* Compute unions +* Install or print packages + +### `lib/build.sh` + +* Cargo build abstraction +* Debug vs release handling +* Binary discovery + +### `lib/sway.sh` + +* Nested sway environment setup +* Command execution inside sway +* Shared by dev and production paths + +### `lib/install.sh` + +* Binary install logic +* Config deployment logic + +### `lib/harden.sh` + +* Apply hardening +* Revert hardening +* Track system state changes + +--- + +## Wrapper scripts + +* `./run-dev` → `exec ./scripts/shepherd dev run` +* Optional `Makefile` targets may call `shepherd`, but must contain no logic + +--- + +## Non-goals + +* No Docker/Nix/devcontainer required +* No GUI tooling +* No distro-specific packaging beyond apt-style package lists +* No Rust rewrite of scripts unless strictly necessary + +--- + +## Acceptance criteria + +* CI can install **only build deps** and run `shepherd build` +* Runtime system can install **only run deps** +* Developer can install **dev deps** and run `./run-dev` +* Admin can: + + * install binaries + * deploy config + * harden and unharden a user safely +* No duplicated dependency lists +* No duplicated build or sway logic diff --git a/run-dev b/run-dev index 7e562aa..17a3b9c 100755 --- a/run-dev +++ b/run-dev @@ -1,59 +1,5 @@ #!/usr/bin/env bash +# Development run script - thin wrapper for shepherd dev run +# This script is kept for backwards compatibility with existing workflows (i.e. lazy devs) -set -e - -# Set up dev runtime directory -DEV_RUNTIME="./dev-runtime" -DATA_DIR="$DEV_RUNTIME/data" -SOCKET_PATH="$DEV_RUNTIME/shepherd.sock" - -mkdir -p "$DATA_DIR" - -# Kill any existing shepherd dev instances before starting -echo "Cleaning up any existing dev instances..." -pkill -f "sway -c ./sway.conf" 2>/dev/null || true -pkill -f "shepherdd" 2>/dev/null || true -pkill -f "shepherd-launcher" 2>/dev/null || true -pkill -f "shepherd-hud" 2>/dev/null || true -# Remove stale socket -rm -f "$SOCKET_PATH" -sleep 0.5 - -# Export environment variables for shepherd binaries -export SHEPHERD_SOCKET="$SOCKET_PATH" -export SHEPHERD_DATA_DIR="$DATA_DIR" - -# Note: Since shepherdd now runs inside sway, spawned apps automatically -# use the nested compositor's display. No SHEPHERD_WAYLAND_DISPLAY override needed. - -# Build all binaries -echo "Building shepherd binaries..." -cargo build - -# Function to cleanup background processes on exit -cleanup() { - echo "Cleaning up..." - # Kill the nested sway - this will clean up everything inside it (including shepherdd) - if [ ! -z "$SWAY_PID" ]; then - kill $SWAY_PID 2>/dev/null || true - fi - # Also explicitly kill any shepherd processes that might have escaped - pkill -f "shepherdd" 2>/dev/null || true - pkill -f "shepherd-launcher" 2>/dev/null || true - pkill -f "shepherd-hud" 2>/dev/null || true - # Remove socket - rm -f "$SOCKET_PATH" -} -trap cleanup EXIT - -# Note: shepherdd is started by sway.conf so it runs INSIDE the nested compositor. -# This ensures all spawned processes (games, apps) use the nested compositor's display. - -# Start sway with the launcher and HUD -# The HUD and launcher are started by sway.conf so they run INSIDE the nested compositor -echo "Starting nested sway with shepherd-launcher..." -WLR_BACKENDS=wayland WLR_LIBINPUT_NO_DEVICES=1 sway -c ./sway.conf & -SWAY_PID=$! - -# Wait for sway to exit -wait $SWAY_PID +exec ./scripts/shepherd dev run "$@" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..4d7d009 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,148 @@ +# Shepherd Scripts System + +This directory contains the unified script system for shepherd-launcher. + +## Quick Reference + +```sh +# Main entry point +./shepherd --help + +# Dependencies +./shepherd deps print build|run|dev +./shepherd deps install build|run|dev + +# Building +./shepherd build [--release] + +# Development +./shepherd dev run + +# Installation +./shepherd install all --user USER [--prefix PREFIX] +./shepherd install bins [--prefix PREFIX] +./shepherd install config --user USER + +# Hardening +./shepherd harden apply --user USER +./shepherd harden revert --user USER +``` + +## Structure + +``` +scripts/ +├── shepherd # Main CLI dispatcher +├── dev # Wrapper → shepherd dev run +├── admin # Wrapper → shepherd install/harden +├── lib/ # Shared libraries +│ ├── common.sh # Logging, error handling, sudo helpers +│ ├── deps.sh # Dependency management +│ ├── build.sh # Cargo build logic +│ ├── sway.sh # Nested sway execution +│ ├── install.sh # Installation logic +│ └── harden.sh # User hardening/unhardening +└── deps/ # Package lists + ├── build.pkgs # Build-time dependencies + ├── run.pkgs # Runtime dependencies + └── dev.pkgs # Development extras +``` + +## Design Principles + +1. **Single source of truth**: All dependency lists are defined once in `deps/*.pkgs` +2. **Composable**: Each command can be called independently +3. **Reversible**: All destructive actions (hardening, installation) can be undone +4. **Shared logic**: Business logic lives in libraries, not duplicated across scripts +5. **Clear separation**: Build-only, runtime-only, and development dependencies are separate + +## Usage Examples + +### For Developers + +```sh +# First time setup (installs system packages + Rust via rustup) +./shepherd deps install dev +./shepherd dev run + +# Or use the convenience wrapper +./run-dev +``` + +### For CI + +```sh +# Install only build dependencies (includes Rust via rustup) +./shepherd deps install build + +# Build release binaries +./shepherd build --release +``` + +### For Production Deployment + +```sh +# On a runtime-only system +sudo ./shepherd deps install run +./shepherd build --release +sudo ./shepherd install all --user kiosk --prefix /usr + +# Optional: lock down the kiosk user +sudo ./shepherd harden apply --user kiosk +``` + +### For Package Maintainers + +```sh +# Print package lists for your distro +./shepherd deps print build > build-deps.txt +./shepherd deps print run > runtime-deps.txt + +# Install with custom prefix and DESTDIR +make -j$(nproc) # or equivalent +sudo DESTDIR=/tmp/staging ./shepherd install bins --prefix /usr +``` + +## Dependency Sets + +- **build**: Packages needed to compile the Rust code (GTK, Wayland dev libs, etc.) + Rust toolchain via rustup +- **run**: Packages needed to run the compiled binaries (Sway, GTK runtime libs) +- **dev**: Union of build + run + dev-specific tools (git, gdb, strace) + Rust toolchain + +The dev set is computed as the union of all three package lists, automatically deduplicated. + +## Hardening + +The hardening system makes reversible changes to restrict a user to kiosk mode: + +```sh +# Apply hardening +sudo ./shepherd harden apply --user kiosk + +# Check status +sudo ./shepherd harden status --user kiosk + +# Revert all changes +sudo ./shepherd harden revert --user kiosk +``` + +All changes are tracked in `/var/lib/shepherdd/hardening//` for rollback. + +Applied restrictions: +- SSH access denied +- Console (TTY) login restricted +- Sudo access denied +- Shell restricted to Sway sessions only +- Home directory permissions secured + +## Adding New Dependencies + +Edit the appropriate package list in `deps/`: + +- `deps/build.pkgs` - Build-time dependencies +- `deps/run.pkgs` - Runtime dependencies +- `deps/dev.pkgs` - Developer tools + +Format: One package per line, `#` for comments. + +The CI workflow will automatically use these lists. diff --git a/scripts/admin b/scripts/admin new file mode 100755 index 0000000..b0136da --- /dev/null +++ b/scripts/admin @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Admin wrapper - convenience script for administrative tasks +# Provides quick access to install and harden commands + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat < [options] + +This is a convenience wrapper. All commands are equivalent to: + shepherd install ... + shepherd harden ... + +Commands: + install Install shepherd components + harden Manage user hardening + +Examples: + admin install all --user kiosk + admin harden apply --user kiosk + admin harden revert --user kiosk + +Run 'shepherd install help' or 'shepherd harden help' for details. +EOF +} + +case "${1:-}" in + install|harden) + exec "$SCRIPT_DIR/shepherd" "$@" + ;; + -h|--help|help|"") + usage + ;; + *) + echo "Unknown command: $1" >&2 + echo "Run 'admin help' for usage." >&2 + exit 1 + ;; +esac diff --git a/scripts/deps/build.pkgs b/scripts/deps/build.pkgs new file mode 100644 index 0000000..9ef8903 --- /dev/null +++ b/scripts/deps/build.pkgs @@ -0,0 +1,31 @@ +# Build-time system packages for shepherd-launcher +# These are required to compile the Rust code +# One package per line, comments start with # + +# Core build tools +build-essential +pkg-config + +# GLib/GTK development libraries +libglib2.0-dev +libgtk-4-dev +libadwaita-1-dev +libcairo2-dev +libpango1.0-dev +libgdk-pixbuf-xlib-2.0-dev + +# Wayland development libraries +libwayland-dev +libxkbcommon-dev + +# X11 (for XWayland support) +libx11-dev + +# GObject introspection +libgirepository1.0-dev + +# Layer shell for HUD overlay +libgtk4-layer-shell-dev + +# Required for rustup +curl diff --git a/scripts/deps/dev.pkgs b/scripts/deps/dev.pkgs new file mode 100644 index 0000000..ddd6e81 --- /dev/null +++ b/scripts/deps/dev.pkgs @@ -0,0 +1,11 @@ +# Developer-only packages for shepherd-launcher +# These are extras useful during development +# The full dev set is: build.pkgs + run.pkgs + dev.pkgs +# One package per line, comments start with # + +# Git for version control +git + +# Useful debugging tools +strace +gdb diff --git a/scripts/deps/run.pkgs b/scripts/deps/run.pkgs new file mode 100644 index 0000000..49a2ce2 --- /dev/null +++ b/scripts/deps/run.pkgs @@ -0,0 +1,14 @@ +# Runtime system packages for shepherd-launcher +# These are required to run the compiled binaries +# One package per line, comments start with # + +# Sway compositor (required for kiosk mode) +sway + +# Wayland session utilities +swayidle + +# Runtime libraries (may be pulled in automatically, but explicit is safer) +libgtk-4-1 +libadwaita-1-0 +libgtk4-layer-shell0 diff --git a/scripts/dev b/scripts/dev new file mode 100755 index 0000000..54fcd6c --- /dev/null +++ b/scripts/dev @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Development wrapper - thin convenience script for developers +# Equivalent to: shepherd dev run + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/shepherd" dev run "$@" diff --git a/scripts/lib/build.sh b/scripts/lib/build.sh new file mode 100755 index 0000000..8bfef84 --- /dev/null +++ b/scripts/lib/build.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Build logic for shepherd-launcher +# Wraps cargo build with project-specific settings + +# Get the directory containing this script +BUILD_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$BUILD_LIB_DIR/common.sh" + +# Binary names produced by the build +SHEPHERD_BINARIES=( + "shepherdd" + "shepherd-launcher" + "shepherd-hud" +) + +# Get the target directory for binaries +get_target_dir() { + local release="${1:-false}" + local repo_root + repo_root="$(get_repo_root)" + + if [[ "$release" == "true" ]]; then + echo "$repo_root/target/release" + else + echo "$repo_root/target/debug" + fi +} + +# Get the path to a specific binary +get_binary_path() { + local binary="$1" + local release="${2:-false}" + + echo "$(get_target_dir "$release")/$binary" +} + +# Check if all binaries exist +binaries_exist() { + local release="${1:-false}" + local target_dir + target_dir="$(get_target_dir "$release")" + + for binary in "${SHEPHERD_BINARIES[@]}"; do + if [[ ! -x "$target_dir/$binary" ]]; then + return 1 + fi + done + return 0 +} + +# Build the project +build_cargo() { + local release="${1:-false}" + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + require_command cargo rust + + cd "$repo_root" || die "Failed to change directory to $repo_root" + + local build_type + if [[ "$release" == "true" ]]; then + build_type="release" + info "Building shepherd (release mode)..." + cargo build --release + else + build_type="debug" + info "Building shepherd (debug mode)..." + cargo build + fi + + # Verify binaries were created + if ! binaries_exist "$release"; then + die "Build completed but some binaries are missing" + fi + + local target_dir + target_dir="$(get_target_dir "$release")" + + success "Built binaries ($build_type):" + for binary in "${SHEPHERD_BINARIES[@]}"; do + info " $target_dir/$binary" + done +} + +# Clean build artifacts +build_clean() { + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + require_command cargo rust + + cd "$repo_root" || die "Failed to change directory to $repo_root" + + info "Cleaning build artifacts..." + cargo clean + success "Build artifacts cleaned" +} + +# Main build command dispatcher +build_main() { + local release=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --release|-r) + release=true + shift + ;; + clean) + build_clean + return + ;; + help|-h|--help) + cat <&2 +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $*" >&2 +} + +error() { + echo -e "${RED}[ERROR]${NC} $*" >&2 +} + +success() { + echo -e "${GREEN}[OK]${NC} $*" >&2 +} + +die() { + error "$@" + exit 1 +} + +# Check if running as root +is_root() { + [[ $EUID -eq 0 ]] +} + +# Require root or exit +require_root() { + if ! is_root; then + die "This command must be run as root (use sudo)" + fi +} + +# Run a command with sudo if not already root +maybe_sudo() { + if is_root; then + "$@" + else + sudo "$@" + fi +} + +# Check if a command exists +command_exists() { + command -v "$1" &>/dev/null +} + +# Require a command or exit with helpful message +require_command() { + local cmd="$1" + local pkg="${2:-$1}" + if ! command_exists "$cmd"; then + die "Required command '$cmd' not found. Install it with: apt install $pkg" + fi +} + +# Get the repository root directory +get_repo_root() { + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../.." && pwd) +} + +# Verify we're in the shepherd repository +verify_repo() { + local repo_root + repo_root="$(get_repo_root)" + if [[ ! -f "$repo_root/Cargo.toml" ]]; then + die "Not in shepherd repository (Cargo.toml not found at $repo_root)" + fi + # Check it's the right project + if ! grep -q 'shepherd-launcher-ui' "$repo_root/Cargo.toml" 2>/dev/null; then + die "This doesn't appear to be the shepherd-launcher repository" + fi +} + +# Check Ubuntu version and warn if not supported +check_ubuntu_version() { + local min_version="${1:-25.10}" + + if [[ ! -f /etc/os-release ]]; then + warn "Cannot determine OS version (not Linux or missing /etc/os-release)" + return 0 + fi + + # shellcheck source=/dev/null + source /etc/os-release + + if [[ "${ID:-}" != "ubuntu" ]]; then + warn "This system is not Ubuntu (detected: ${ID:-unknown}). Some features may not work." + return 0 + fi + + local version="${VERSION_ID:-0}" + if [[ "$(printf '%s\n' "$min_version" "$version" | sort -V | head -n1)" != "$min_version" ]]; then + warn "Ubuntu version $version detected. Recommended: $min_version or higher." + fi +} + +# Safe file backup - creates timestamped backup +backup_file() { + local file="$1" + local backup_dir="${2:-}" + + if [[ ! -e "$file" ]]; then + return 0 + fi + + local timestamp + timestamp="$(date +%Y%m%d_%H%M%S)" + local backup_name + + if [[ -n "$backup_dir" ]]; then + mkdir -p "$backup_dir" + backup_name="$backup_dir/$(basename "$file").$timestamp" + else + backup_name="$file.$timestamp.bak" + fi + + cp -a "$file" "$backup_name" + echo "$backup_name" +} + +# Create directory with proper permissions +ensure_dir() { + local dir="$1" + local mode="${2:-0755}" + local owner="${3:-}" + + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" + chmod "$mode" "$dir" + if [[ -n "$owner" ]]; then + chown "$owner" "$dir" + fi + fi +} + +# Validate username exists +validate_user() { + local user="$1" + if ! id "$user" &>/dev/null; then + die "User '$user' does not exist" + fi +} + +# Get user's home directory +get_user_home() { + local user="$1" + getent passwd "$user" | cut -d: -f6 +} + +# Kill processes matching a pattern (silent if none found) +kill_matching() { + local pattern="$1" + pkill -f "$pattern" 2>/dev/null || true +} + +# Wait for a file/socket to appear +wait_for_file() { + local file="$1" + local timeout="${2:-30}" + local elapsed=0 + + while [[ ! -e "$file" ]] && [[ $elapsed -lt $timeout ]]; do + sleep 0.5 + elapsed=$((elapsed + 1)) + done + + [[ -e "$file" ]] +} diff --git a/scripts/lib/deps.sh b/scripts/lib/deps.sh new file mode 100755 index 0000000..3b62a6b --- /dev/null +++ b/scripts/lib/deps.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Dependency management for shepherd-launcher +# Provides functions to read, union, and install package sets + +# Get the directory containing this script +DEPS_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$DEPS_LIB_DIR/common.sh" + +# Directory containing package lists +DEPS_DIR="$(get_repo_root)/scripts/deps" + +# Rust installation URL +RUSTUP_URL="https://sh.rustup.rs" + +# Check if Rust is installed +is_rust_installed() { + command_exists rustc && command_exists cargo +} + +# Install Rust via rustup +install_rust() { + if is_rust_installed; then + info "Rust is already installed ($(rustc --version))" + return 0 + fi + + info "Installing Rust via rustup..." + + # Download and run rustup installer + curl --proto '=https' --tlsv1.2 -sSf "$RUSTUP_URL" | sh -s -- -y --profile minimal + + # Source cargo env for current session + # shellcheck source=/dev/null + source "$HOME/.cargo/env" 2>/dev/null || true + + if is_rust_installed; then + success "Rust installed successfully ($(rustc --version))" + else + die "Rust installation failed. Please run: curl --proto '=https' --tlsv1.2 -sSf $RUSTUP_URL | sh" + fi +} + +# Read a package file, stripping comments and empty lines +read_package_file() { + local file="$1" + + if [[ ! -f "$file" ]]; then + die "Package file not found: $file" + fi + + # Strip comments (# to end of line) and empty lines, trim whitespace + grep -v '^\s*#' "$file" | grep -v '^\s*$' | sed 's/#.*//' | tr -s '[:space:]' '\n' | grep -v '^$' +} + +# Get packages for a specific set +get_packages() { + local set_name="$1" + + case "$set_name" in + build) + read_package_file "$DEPS_DIR/build.pkgs" + ;; + run) + read_package_file "$DEPS_DIR/run.pkgs" + ;; + dev) + # Union of all three sets, deduplicated + { + read_package_file "$DEPS_DIR/build.pkgs" + read_package_file "$DEPS_DIR/run.pkgs" + read_package_file "$DEPS_DIR/dev.pkgs" + } | sort -u + ;; + *) + die "Unknown package set: $set_name (valid: build, run, dev)" + ;; + esac +} + +# Print packages for a set (one per line) +deps_print() { + local set_name="${1:-}" + + if [[ -z "$set_name" ]]; then + die "Usage: shepherd deps print " + fi + + get_packages "$set_name" +} + +# Install packages for a set +deps_install() { + local set_name="${1:-}" + + if [[ -z "$set_name" ]]; then + die "Usage: shepherd deps install " + fi + + check_ubuntu_version + + info "Installing $set_name dependencies..." + + # Get the package list + local packages + packages=$(get_packages "$set_name" | tr '\n' ' ') + + if [[ -z "$packages" ]]; then + warn "No packages to install for set: $set_name" + return 0 + fi + + info "Packages: $packages" + + # Install using apt + maybe_sudo apt-get update + # shellcheck disable=SC2086 + maybe_sudo apt-get install -y $packages + + # For build and dev sets, also install Rust + if [[ "$set_name" == "build" ]] || [[ "$set_name" == "dev" ]]; then + install_rust + fi + + success "Installed $set_name dependencies" +} + +# Check if all packages for a set are installed +deps_check() { + local set_name="${1:-}" + + if [[ -z "$set_name" ]]; then + die "Usage: shepherd deps check " + fi + + local packages + packages=$(get_packages "$set_name") + + local missing=() + while IFS= read -r pkg; do + if ! dpkg -l "$pkg" &>/dev/null; then + missing+=("$pkg") + fi + done <<< "$packages" + + # For build and dev sets, also check Rust + if [[ "$set_name" == "build" ]] || [[ "$set_name" == "dev" ]]; then + if ! is_rust_installed; then + warn "Rust is not installed" + return 1 + fi + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + warn "Missing packages: ${missing[*]}" + return 1 + fi + + success "All $set_name dependencies are installed" + return 0 +} + +# Main deps command dispatcher +deps_main() { + local subcmd="${1:-}" + shift || true + + case "$subcmd" in + print) + deps_print "$@" + ;; + install) + deps_install "$@" + ;; + check) + deps_check "$@" + ;; + ""|help|-h|--help) + cat < + +Commands: + print Print packages in the set (one per line) + install Install packages from the set + check Check if all packages from the set are installed + +Package sets: + build Build-time dependencies (+ Rust via rustup) + run Runtime dependencies only + dev All dependencies (build + run + dev extras + Rust) + +Note: The 'build' and 'dev' sets automatically install Rust via rustup. + +Examples: + shepherd deps print build + shepherd deps install dev + shepherd deps check run +EOF + ;; + *) + die "Unknown deps command: $subcmd (try: shepherd deps help)" + ;; + esac +} diff --git a/scripts/lib/harden.sh b/scripts/lib/harden.sh new file mode 100755 index 0000000..0064f9c --- /dev/null +++ b/scripts/lib/harden.sh @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# User hardening logic for shepherd-launcher +# Applies and reverts kiosk-style user restrictions + +# Get the directory containing this script +HARDEN_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$HARDEN_LIB_DIR/common.sh" + +# State directory for hardening rollback +HARDENING_STATE_DIR="/var/lib/shepherdd/hardening" + +# Get the state directory for a user +get_user_state_dir() { + local user="$1" + echo "$HARDENING_STATE_DIR/$user" +} + +# Check if a user is currently hardened +is_hardened() { + local user="$1" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + [[ -f "$state_dir/hardened" ]] +} + +# Save a file for later restoration +save_for_restore() { + local user="$1" + local file="$2" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + local relative_path="${file#/}" + local backup_path="$state_dir/backup/$relative_path" + + mkdir -p "$(dirname "$backup_path")" + + if [[ -e "$file" ]]; then + cp -a "$file" "$backup_path" + echo "exists" > "$backup_path.meta" + else + echo "absent" > "$backup_path.meta" + fi +} + +# Restore a previously saved file +restore_file() { + local user="$1" + local file="$2" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + local relative_path="${file#/}" + local backup_path="$state_dir/backup/$relative_path" + local meta_file="$backup_path.meta" + + if [[ ! -f "$meta_file" ]]; then + warn "No backup metadata for $file, skipping" + return 0 + fi + + local original_state + original_state="$(cat "$meta_file")" + + if [[ "$original_state" == "exists" ]]; then + if [[ -e "$backup_path" ]]; then + cp -a "$backup_path" "$file" + info " Restored: $file" + else + warn "Backup file missing for $file" + fi + else + # File didn't exist originally, remove it + rm -f "$file" + info " Removed: $file (didn't exist before)" + fi +} + +# Record a change action for rollback +record_action() { + local user="$1" + local action="$2" + local target="$3" + local state_dir + state_dir="$(get_user_state_dir "$user")" + + echo "$action|$target" >> "$state_dir/actions.log" +} + +# Apply hardening to a user +harden_apply() { + local user="$1" + + require_root + validate_user "$user" + + local state_dir + state_dir="$(get_user_state_dir "$user")" + local user_home + user_home="$(get_user_home "$user")" + + if is_hardened "$user"; then + warn "User $user is already hardened. Use 'shepherd harden revert' first." + return 0 + fi + + info "Applying hardening to user: $user" + + # Create state directory + mkdir -p "$state_dir/backup" + chmod 0700 "$state_dir" + + # Initialize actions log + : > "$state_dir/actions.log" + + # ========================================================================= + # 1. Set user shell to restricted shell or nologin for non-sway access + # ========================================================================= + info "Configuring user shell..." + + local original_shell + original_shell="$(getent passwd "$user" | cut -d: -f7)" + echo "$original_shell" > "$state_dir/original_shell" + + # Keep bash for sway to work, but we'll restrict other access methods + # The shell restriction is handled by PAM and session limits instead + record_action "$user" "shell" "$original_shell" + + # ========================================================================= + # 2. Configure user's .bashrc to be restricted + # ========================================================================= + info "Configuring shell restrictions..." + + local bashrc="$user_home/.bashrc" + save_for_restore "$user" "$bashrc" + + # Append restriction to bashrc (if not in sway, exit) + cat >> "$bashrc" <<'EOF' + +# Shepherd hardening: restrict to sway session only +if [[ -z "${WAYLAND_DISPLAY:-}" ]] && [[ -z "${SWAYSOCK:-}" ]]; then + echo "This account is restricted to the Shepherd kiosk environment." + exit 1 +fi +EOF + chown "$user:$user" "$bashrc" + record_action "$user" "file" "$bashrc" + + # ========================================================================= + # 3. Disable SSH access for this user + # ========================================================================= + info "Restricting SSH access..." + + local shepherd_sshd_config="/etc/ssh/sshd_config.d/shepherd-$user.conf" + + save_for_restore "$user" "$shepherd_sshd_config" + + # Create a drop-in config to deny this user + mkdir -p /etc/ssh/sshd_config.d + cat > "$shepherd_sshd_config" </dev/null || systemctl is-active --quiet ssh 2>/dev/null; then + systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true + fi + + # ========================================================================= + # 4. Disable virtual console (TTY) access via PAM + # ========================================================================= + info "Restricting console access..." + + local pam_access="/etc/security/access.conf" + local shepherd_access_marker="# Shepherd hardening for user: $user" + + save_for_restore "$user" "$pam_access" + + # Add rule to deny console access (but allow via display managers) + if ! grep -q "$shepherd_access_marker" "$pam_access" 2>/dev/null; then + cat >> "$pam_access" < "$getty_override" < "$sudoers_file" < "$state_dir/home_perms" + + # Set restrictive permissions + chmod 0700 "$user_home" + record_action "$user" "perms" "$user_home" + + # ========================================================================= + # Mark as hardened + # ========================================================================= + date -Iseconds > "$state_dir/hardened" + echo "$user" > "$state_dir/user" + + success "Hardening applied to user: $user" + info "" + info "The following restrictions are now active:" + info " - SSH access denied" + info " - Console (TTY) login restricted" + info " - Sudo access denied" + info " - Shell restricted to Sway sessions" + info " - Home directory secured (mode 0700)" + info "" + info "To revert: shepherd harden revert --user $user" +} + +# Revert hardening from a user +harden_revert() { + local user="$1" + + require_root + validate_user "$user" + + local state_dir + state_dir="$(get_user_state_dir "$user")" + local user_home + user_home="$(get_user_home "$user")" + + if ! is_hardened "$user"; then + warn "User $user is not currently hardened." + return 0 + fi + + info "Reverting hardening for user: $user" + + # ========================================================================= + # Restore all saved files + # ========================================================================= + if [[ -f "$state_dir/actions.log" ]]; then + while IFS='|' read -r action target; do + case "$action" in + file) + restore_file "$user" "$target" + ;; + perms) + if [[ -f "$state_dir/home_perms" ]]; then + local original_perms + original_perms="$(cat "$state_dir/home_perms")" + chmod "$original_perms" "$target" + info " Restored permissions on: $target" + fi + ;; + shell) + # Shell wasn't changed, nothing to revert + ;; + esac + done < "$state_dir/actions.log" + fi + + # ========================================================================= + # Reload services that may have been affected + # ========================================================================= + if systemctl is-active --quiet sshd 2>/dev/null || systemctl is-active --quiet ssh 2>/dev/null; then + systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true + fi + + # ========================================================================= + # Clean up state directory + # ========================================================================= + rm -rf "$state_dir" + + success "Hardening reverted for user: $user" + info "" + info "All restrictions have been removed. The user can now:" + info " - Access via SSH" + info " - Login at console" + info " - Use sudo (if previously allowed)" +} + +# Show hardening status +harden_status() { + local user="$1" + + validate_user "$user" + + local state_dir + state_dir="$(get_user_state_dir "$user")" + + if is_hardened "$user"; then + local hardened_date + hardened_date="$(cat "$state_dir/hardened")" + echo "User '$user' is HARDENED (since $hardened_date)" + + if [[ -f "$state_dir/actions.log" ]]; then + echo "" + echo "Applied restrictions:" + while IFS='|' read -r action target; do + echo " - $action: $target" + done < "$state_dir/actions.log" + fi + else + echo "User '$user' is NOT hardened" + fi +} + +# Main harden command dispatcher +harden_main() { + local subcmd="${1:-}" + shift || true + + local user="" + + # Parse remaining arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --user) + user="$2" + shift 2 + ;; + *) + die "Unknown option: $1" + ;; + esac + done + + case "$subcmd" in + apply) + if [[ -z "$user" ]]; then + die "Usage: shepherd harden apply --user USER" + fi + harden_apply "$user" + ;; + revert) + if [[ -z "$user" ]]; then + die "Usage: shepherd harden revert --user USER" + fi + harden_revert "$user" + ;; + status) + if [[ -z "$user" ]]; then + die "Usage: shepherd harden status --user USER" + fi + harden_status "$user" + ;; + ""|help|-h|--help) + cat < --user USER + +Commands: + apply Apply kiosk hardening to a user + revert Revert hardening and restore original state + status Show hardening status for a user + +Options: + --user USER Target user for hardening operations (required) + +Hardening includes: + - Denying SSH access + - Restricting console (TTY) login + - Denying sudo access + - Restricting shell to Sway sessions only + - Securing home directory permissions + +State is preserved in: $HARDENING_STATE_DIR// + +Examples: + shepherd harden apply --user kiosk + shepherd harden status --user kiosk + shepherd harden revert --user kiosk +EOF + ;; + *) + die "Unknown harden command: $subcmd (try: shepherd harden help)" + ;; + esac +} diff --git a/scripts/lib/install.sh b/scripts/lib/install.sh new file mode 100755 index 0000000..8c8ae0b --- /dev/null +++ b/scripts/lib/install.sh @@ -0,0 +1,306 @@ +#!/usr/bin/env bash +# Installation logic for shepherd-launcher +# Handles binary installation, config deployment, and desktop entry setup + +# Get the directory containing this script +INSTALL_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$INSTALL_LIB_DIR/common.sh" + +# Source build utilities for binary paths +# shellcheck source=build.sh +source "$INSTALL_LIB_DIR/build.sh" + +# Default installation paths +DEFAULT_PREFIX="/usr/local" +DEFAULT_BINDIR="bin" + +# Standard sway config location +SWAY_CONFIG_DIR="/etc/sway" +SHEPHERD_SWAY_CONFIG="shepherd.conf" + +# Desktop entry location +DESKTOP_ENTRY_DIR="share/wayland-sessions" +DESKTOP_ENTRY_NAME="shepherd.desktop" + +# Install release binaries +install_bins() { + local prefix="${1:-$DEFAULT_PREFIX}" + local destdir="${DESTDIR:-}" + + require_root + + local bindir="$destdir$prefix/$DEFAULT_BINDIR" + local target_dir + target_dir="$(get_target_dir true)" + + # Ensure release build exists + if ! binaries_exist true; then + die "Release binaries not found. Run 'shepherd build --release' first." + fi + + info "Installing binaries to $bindir..." + + ensure_dir "$bindir" 0755 + + for binary in "${SHEPHERD_BINARIES[@]}"; do + local src="$target_dir/$binary" + local dst="$bindir/$binary" + + info " Installing $binary..." + install -m 0755 "$src" "$dst" + done + + success "Installed binaries to $bindir" +} + +# Install the sway configuration +install_sway_config() { + local destdir="${DESTDIR:-}" + local repo_root + repo_root="$(get_repo_root)" + + require_root + + local src_config="$repo_root/sway.conf" + local dst_dir="$destdir$SWAY_CONFIG_DIR" + local dst_config="$dst_dir/$SHEPHERD_SWAY_CONFIG" + + if [[ ! -f "$src_config" ]]; then + die "Source sway.conf not found at $src_config" + fi + + info "Installing sway configuration to $dst_config..." + + ensure_dir "$dst_dir" 0755 + + # Create a production version of the sway config + # Replace debug paths with installed paths + local prefix="${1:-$DEFAULT_PREFIX}" + local bindir="$prefix/$DEFAULT_BINDIR" + + # Copy and modify the config for production use + sed \ + -e "s|./target/debug/shepherd-launcher|$bindir/shepherd-launcher|g" \ + -e "s|./target/debug/shepherd-hud|$bindir/shepherd-hud|g" \ + -e "s|./target/debug/shepherdd|$bindir/shepherdd|g" \ + -e "s|./config.example.toml|/etc/shepherd/config.toml|g" \ + -e "s|-c ./sway.conf|-c $dst_config|g" \ + "$src_config" > "$dst_config" + + chmod 0644 "$dst_config" + + success "Installed sway configuration" +} + +# Install desktop entry for display manager +install_desktop_entry() { + local prefix="${1:-$DEFAULT_PREFIX}" + local destdir="${DESTDIR:-}" + + require_root + + local dst_dir="$destdir$prefix/$DESKTOP_ENTRY_DIR" + local dst_entry="$dst_dir/$DESKTOP_ENTRY_NAME" + + info "Installing desktop entry to $dst_entry..." + + ensure_dir "$dst_dir" 0755 + + cat > "$dst_entry" < [OPTIONS] + +Commands: + bins Install release binaries + config Deploy user configuration + sway-config Install sway configuration + desktop-entry Install display manager desktop entry + system-config Install system-wide configuration + all Install everything + +Options: + --user USER Target user for config deployment (required for config/all) + --prefix PREFIX Installation prefix (default: $DEFAULT_PREFIX) + --source CONFIG Source config file (default: config.example.toml) + +Environment: + DESTDIR Installation root for packaging (default: empty) + +Examples: + shepherd install bins --prefix /usr/local + shepherd install config --user kiosk + shepherd install all --user kiosk --prefix /usr +EOF + ;; + *) + die "Unknown install command: $subcmd (try: shepherd install help)" + ;; + esac +} diff --git a/scripts/lib/sway.sh b/scripts/lib/sway.sh new file mode 100755 index 0000000..2aeb900 --- /dev/null +++ b/scripts/lib/sway.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Sway compositor helpers for shepherd-launcher +# Handles nested sway execution for development and production + +# Get the directory containing this script +SWAY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$SWAY_LIB_DIR/common.sh" + +# Source build utilities for binary paths +# shellcheck source=build.sh +source "$SWAY_LIB_DIR/build.sh" + +# PID of the running sway process (for cleanup) +SWAY_PID="" + +# Default directories +DEFAULT_DEV_RUNTIME="./dev-runtime" +DEFAULT_DATA_DIR="$DEFAULT_DEV_RUNTIME/data" +DEFAULT_SOCKET_PATH="$DEFAULT_DEV_RUNTIME/shepherd.sock" + +# Cleanup function for sway processes +sway_cleanup() { + info "Cleaning up sway session..." + + # Kill the nested sway - this will clean up everything inside it + if [[ -n "${SWAY_PID:-}" ]]; then + kill "$SWAY_PID" 2>/dev/null || true + fi + + # Explicitly kill any shepherd processes that might have escaped + kill_matching "shepherdd" + kill_matching "shepherd-launcher" + kill_matching "shepherd-hud" + + # Remove socket + if [[ -n "${SHEPHERD_SOCKET:-}" ]]; then + rm -f "$SHEPHERD_SOCKET" + fi +} + +# Kill any existing dev instances +sway_kill_existing() { + info "Cleaning up any existing dev instances..." + kill_matching "sway -c.*sway.conf" + kill_matching "shepherdd" + kill_matching "shepherd-launcher" + kill_matching "shepherd-hud" + + # Remove stale socket if it exists + if [[ -n "${SHEPHERD_SOCKET:-}" ]] && [[ -e "$SHEPHERD_SOCKET" ]]; then + rm -f "$SHEPHERD_SOCKET" + fi + + # Brief pause to allow cleanup + sleep 0.5 +} + +# Set up environment for shepherd binaries +sway_setup_env() { + local data_dir="${1:-$DEFAULT_DATA_DIR}" + local socket_path="${2:-$DEFAULT_SOCKET_PATH}" + + # Create directories + mkdir -p "$data_dir" + + # Export environment variables + export SHEPHERD_SOCKET="$socket_path" + export SHEPHERD_DATA_DIR="$data_dir" +} + +# Generate a sway config for development +# Uses debug binaries and development paths +sway_generate_dev_config() { + local repo_root + repo_root="$(get_repo_root)" + + # Use the existing sway.conf as template + local sway_config="$repo_root/sway.conf" + + if [[ ! -f "$sway_config" ]]; then + die "sway.conf not found at $sway_config" + fi + + # Return path to the sway config (we use the existing one for dev) + echo "$sway_config" +} + +# Start a nested sway session for development +sway_start_nested() { + local sway_config="$1" + + require_command sway + + info "Starting nested sway session..." + + # Set up cleanup trap + trap sway_cleanup EXIT + + # Start sway with wayland backend (nested in current session) + WLR_BACKENDS=wayland WLR_LIBINPUT_NO_DEVICES=1 sway -c "$sway_config" & + SWAY_PID=$! + + info "Sway started with PID $SWAY_PID" + + # Wait for sway to exit + wait "$SWAY_PID" +} + +# Run a development session (build + nested sway) +sway_dev_run() { + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + cd "$repo_root" || die "Failed to change directory to $repo_root" + + # Set up environment + sway_setup_env "$DEFAULT_DATA_DIR" "$DEFAULT_SOCKET_PATH" + + # Kill any existing instances + sway_kill_existing + + # Build debug binaries + info "Building shepherd binaries..." + build_cargo false + + # Get sway config + local sway_config + sway_config="$(sway_generate_dev_config)" + + # Start nested sway (blocking) + sway_start_nested "$sway_config" +} + +# Main sway command dispatcher (internal use) +sway_main() { + local subcmd="${1:-}" + shift || true + + case "$subcmd" in + run) + sway_dev_run "$@" + ;; + *) + # Default to run for backwards compatibility + sway_dev_run "$@" + ;; + esac +} diff --git a/scripts/shepherd b/scripts/shepherd new file mode 100755 index 0000000..6cf44a5 --- /dev/null +++ b/scripts/shepherd @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Shepherd CLI - Main entrypoint for shepherd-launcher tooling +# Provides unified interface for build, development, installation, and hardening + +set -euo pipefail + +# Get the directory containing this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$SCRIPT_DIR/lib" + +# Source shared libraries +# shellcheck source=lib/common.sh +source "$LIB_DIR/common.sh" +# shellcheck source=lib/deps.sh +source "$LIB_DIR/deps.sh" +# shellcheck source=lib/build.sh +source "$LIB_DIR/build.sh" +# shellcheck source=lib/sway.sh +source "$LIB_DIR/sway.sh" +# shellcheck source=lib/install.sh +source "$LIB_DIR/install.sh" +# shellcheck source=lib/harden.sh +source "$LIB_DIR/harden.sh" + +# Version +VERSION="0.1.0" + +# Print usage information +usage() { + cat < [subcommand] [options] + +Commands: + deps Manage system dependencies + build Build shepherd binaries + dev Development commands + install Install shepherd to the system + harden User hardening for kiosk mode + +Development: + shepherd deps install dev Install all development dependencies + shepherd build Build debug binaries + shepherd dev run Build and run in nested sway + +CI/Build: + shepherd deps install build Install build-only dependencies + shepherd build --release Build release binaries + +Runtime: + shepherd deps install run Install runtime dependencies only + +Installation: + shepherd install all --user USER Full installation + shepherd install bins --prefix /usr Install binaries only + shepherd install config --user USER Deploy user config + +Hardening: + shepherd harden apply --user USER Apply kiosk restrictions + shepherd harden revert --user USER Remove restrictions + +Options: + -h, --help Show this help message + -V, --version Show version + +Run 'shepherd help' for detailed command information. +EOF +} + +# Main entry point +main() { + local cmd="${1:-}" + shift || true + + case "$cmd" in + deps) + deps_main "$@" + ;; + build) + build_main "$@" + ;; + dev) + local subcmd="${1:-}" + shift || true + case "$subcmd" in + run) + sway_dev_run "$@" + ;; + ""|help|-h|--help) + cat < + +Commands: + run Build and run shepherd in a nested sway session + +Examples: + shepherd dev run +EOF + ;; + *) + die "Unknown dev command: $subcmd (try: shepherd dev help)" + ;; + esac + ;; + install) + install_main "$@" + ;; + harden) + harden_main "$@" + ;; + -h|--help|help|"") + usage + ;; + -V|--version|version) + echo "shepherd $VERSION" + ;; + *) + error "Unknown command: $cmd" + echo "" + usage + exit 1 + ;; + esac +} + +# Run main with all arguments +main "$@"