Merge pull request #12 from aarmea/u/aarmea/2/setup-scripts

Add setup scripts and documentation
This commit is contained in:
Albert Armea 2026-01-01 16:15:46 -05:00 committed by GitHub
commit 73203b3458
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2192 additions and 134 deletions

View file

@ -8,20 +8,6 @@ on:
env: env:
CARGO_TERM_COLOR: always 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: jobs:
build: build:
@ -30,17 +16,18 @@ jobs:
container: container:
image: ubuntu:25.10 image: ubuntu:25.10
steps: steps:
- name: Install git and dependencies - name: Install git
run: | run: |
apt-get update apt-get update
apt-get install -y git curl ${{ env.SYSTEM_DEPS }} apt-get install -y git curl
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Install build dependencies
run: | run: ./scripts/shepherd deps install build
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Add Rust to PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Cache cargo registry and build - name: Cache cargo registry and build
uses: actions/cache@v4 uses: actions/cache@v4
@ -56,7 +43,7 @@ jobs:
- name: Build - name: Build
run: | run: |
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
cargo build --all-targets ./scripts/shepherd build
test: test:
name: Test name: Test
@ -64,17 +51,18 @@ jobs:
container: container:
image: ubuntu:25.10 image: ubuntu:25.10
steps: steps:
- name: Install git and dependencies - name: Install git
run: | run: |
apt-get update apt-get update
apt-get install -y git curl ${{ env.SYSTEM_DEPS }} apt-get install -y git curl
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Install build dependencies
run: | run: ./scripts/shepherd deps install build
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Add Rust to PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Cache cargo registry and build - name: Cache cargo registry and build
uses: actions/cache@v4 uses: actions/cache@v4
@ -98,19 +86,23 @@ jobs:
container: container:
image: ubuntu:25.10 image: ubuntu:25.10
steps: steps:
- name: Install git and dependencies - name: Install git
run: | run: |
apt-get update apt-get update
apt-get install -y git curl ${{ env.SYSTEM_DEPS }} apt-get install -y git curl
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Install build dependencies
run: ./scripts/shepherd deps install build
- name: Add clippy component
run: | run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
rustup component add clippy 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 - name: Cache cargo registry and build
uses: actions/cache@v4 uses: actions/cache@v4
@ -127,3 +119,21 @@ jobs:
run: | run: |
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
cargo clippy --all-targets -- -D warnings 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

View file

@ -4,7 +4,8 @@
### tl;dr ### 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 ### Requirements
@ -15,15 +16,41 @@ You need a Wayland-capable Linux system, Rust, and a small set of system depende
2. **System dependencies** 2. **System dependencies**
* Platform-specific packages are required. * Platform-specific packages are required for building and running.
* For Ubuntu, see the [`SYSTEM_DEPS` section in the CI configuration](./.github/workflows/ci.yml). * 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
`shepherd-launcher` provides a unified script system for managing dependencies, building, and running:
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | 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 ### Running in development
Start a development instance: Start a development instance:

View file

@ -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 > [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 ## Core concepts
* **Launcher-first**: only one foreground activity at a time * **Launcher-first**: only one foreground activity at a time
@ -113,16 +90,12 @@ such as:
## Installation ## Installation
`shepherd-launcher` is pre-alpha and in active development. As such, end-user `shepherd-launcher` is pre-alpha and in active development. The helper at
binaries and installation instructions are not yet available. `./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. Check out this repository and run `./scripts/shepherd --help` or see
[INSTALL.md](./docs/INSTALL.md) for more.
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))
## Example configuration ## 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 daily_quota_seconds = 3600 # 1 hour per day
cooldown_seconds = 600 # 10 minute cooldown cooldown_seconds = 600 # 10 minute cooldown
[[entries.warnings]]
seconds_before = 600
severity = "info"
message = "10 minutes left - start wrapping up!"
[[entries.warnings]] [[entries.warnings]]
seconds_before = 120 seconds_before = 120
severity = "warn" severity = "warn"
@ -178,7 +146,12 @@ See [config.example.toml](./config.example.toml) for more.
## Development ## 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 ## Written in 2025, responsibly

66
docs/INSTALL.md Normal file
View file

@ -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.

View file

@ -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/<user>/
```
* 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

60
run-dev
View file

@ -1,59 +1,5 @@
#!/usr/bin/env bash #!/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 exec ./scripts/shepherd dev run "$@"
# 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

148
scripts/README.md Normal file
View file

@ -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/<user>/` 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.

42
scripts/admin Executable file
View file

@ -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 <<EOF
admin - Administrative wrapper for shepherd
Usage: admin <command> [options]
This is a convenience wrapper. All commands are equivalent to:
shepherd install ...
shepherd harden ...
Commands:
install <subcommand> Install shepherd components
harden <subcommand> 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

31
scripts/deps/build.pkgs Normal file
View file

@ -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

11
scripts/deps/dev.pkgs Normal file
View file

@ -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

14
scripts/deps/run.pkgs Normal file
View file

@ -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

6
scripts/dev Executable file
View file

@ -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 "$@"

143
scripts/lib/build.sh Executable file
View file

@ -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 <<EOF
Usage: shepherd build [OPTIONS]
Options:
--release, -r Build in release mode (optimized)
clean Clean build artifacts
help Show this help
Examples:
shepherd build # Debug build
shepherd build --release # Release build
shepherd build clean # Clean artifacts
EOF
return
;;
*)
die "Unknown build option: $1 (try: shepherd build help)"
;;
esac
done
build_cargo "$release"
}

193
scripts/lib/common.sh Executable file
View file

@ -0,0 +1,193 @@
#!/usr/bin/env bash
# Common utilities for shepherd scripts
# Logging, error handling, and sudo helpers
set -euo pipefail
# Colors for terminal output (disabled if not a tty)
if [[ -t 1 ]]; then
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
else
RED=''
YELLOW=''
GREEN=''
BLUE=''
NC=''
fi
# Logging functions
info() {
echo -e "${BLUE}[INFO]${NC} $*" >&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" ]]
}

206
scripts/lib/deps.sh Executable file
View file

@ -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 <build|run|dev>"
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 <build|run|dev>"
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 <build|run|dev>"
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 <<EOF
Usage: shepherd deps <command> <set>
Commands:
print <set> Print packages in the set (one per line)
install <set> Install packages from the set
check <set> 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
}

425
scripts/lib/harden.sh Executable file
View file

@ -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" <<EOF
# Shepherd hardening: deny SSH access for kiosk user
DenyUsers $user
EOF
chmod 0644 "$shepherd_sshd_config"
record_action "$user" "file" "$shepherd_sshd_config"
# Reload sshd if running
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
# =========================================================================
# 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" <<EOF
$shepherd_access_marker
# Deny console login for kiosk user (allow display manager access)
-:$user:tty1 tty2 tty3 tty4 tty5 tty6 tty7
EOF
fi
record_action "$user" "file" "$pam_access"
# =========================================================================
# 5. Set up autologin to shepherd session (systemd override)
# =========================================================================
info "Configuring auto-login (if applicable)..."
# Create getty override for auto-login (optional - only if desired)
# This doesn't force auto-login, but prepares the override if needed
local getty_override_dir="/etc/systemd/system/getty@tty1.service.d"
local getty_override="$getty_override_dir/shepherd-autologin.conf"
save_for_restore "$user" "$getty_override"
mkdir -p "$getty_override_dir"
cat > "$getty_override" <<EOF
# Shepherd hardening: auto-login for kiosk user
# Uncomment the following lines to enable auto-login to tty1
# [Service]
# ExecStart=
# ExecStart=-/sbin/agetty --autologin $user --noclear %I \$TERM
EOF
chmod 0644 "$getty_override"
record_action "$user" "file" "$getty_override"
# =========================================================================
# 6. Lock down sudo access
# =========================================================================
info "Restricting sudo access..."
local sudoers_file="/etc/sudoers.d/shepherd-$user"
save_for_restore "$user" "$sudoers_file"
# Explicitly deny sudo for this user
cat > "$sudoers_file" <<EOF
# Shepherd hardening: deny sudo access for kiosk user
$user ALL=(ALL) !ALL
EOF
chmod 0440 "$sudoers_file"
record_action "$user" "file" "$sudoers_file"
# =========================================================================
# 7. Set restrictive file permissions on user home
# =========================================================================
info "Securing home directory permissions..."
# Save original permissions
stat -c "%a" "$user_home" > "$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 <<EOF
Usage: shepherd harden <command> --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/<user>/
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
}

306
scripts/lib/install.sh Executable file
View file

@ -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" <<EOF
[Desktop Entry]
Name=Shepherd Kiosk
Comment=Shepherd game launcher kiosk mode
Exec=sway -c $SWAY_CONFIG_DIR/$SHEPHERD_SWAY_CONFIG
Type=Application
DesktopNames=shepherd
EOF
chmod 0644 "$dst_entry"
success "Installed desktop entry"
}
# Deploy user configuration
install_config() {
local user="${1:-}"
local source_config="${2:-}"
if [[ -z "$user" ]]; then
die "Usage: shepherd install config --user USER [--source CONFIG]"
fi
validate_user "$user"
local repo_root
repo_root="$(get_repo_root)"
# Default source is the example config
if [[ -z "$source_config" ]]; then
source_config="$repo_root/config.example.toml"
fi
if [[ ! -f "$source_config" ]]; then
die "Source config not found: $source_config"
fi
# Get user's config directory
local user_home
user_home="$(get_user_home "$user")"
local user_config_dir="$user_home/.config/shepherd"
local dst_config="$user_config_dir/config.toml"
info "Installing user config to $dst_config..."
# Create config directory owned by user
maybe_sudo mkdir -p "$user_config_dir"
maybe_sudo chown "$user:$user" "$user_config_dir"
maybe_sudo chmod 0755 "$user_config_dir"
# Copy config file
maybe_sudo cp "$source_config" "$dst_config"
maybe_sudo chown "$user:$user" "$dst_config"
maybe_sudo chmod 0644 "$dst_config"
success "Installed user configuration for $user"
}
# Install the system-wide shepherd config directory
install_system_config() {
local destdir="${DESTDIR:-}"
local repo_root
repo_root="$(get_repo_root)"
require_root
local src_config="$repo_root/config.example.toml"
local dst_dir="$destdir/etc/shepherd"
local dst_config="$dst_dir/config.toml"
if [[ ! -f "$src_config" ]]; then
die "Source config not found: $src_config"
fi
info "Installing system config to $dst_config..."
ensure_dir "$dst_dir" 0755
if [[ ! -f "$dst_config" ]]; then
install -m 0644 "$src_config" "$dst_config"
success "Installed system configuration"
else
info "System configuration already exists, not overwriting"
fi
}
# Install everything
install_all() {
local user="${1:-}"
local prefix="${2:-$DEFAULT_PREFIX}"
if [[ -z "$user" ]]; then
die "Usage: shepherd install all --user USER [--prefix PREFIX]"
fi
require_root
validate_user "$user"
info "Installing shepherd-launcher (prefix: $prefix)..."
install_bins "$prefix"
install_sway_config "$prefix"
install_system_config
install_desktop_entry "$prefix"
install_config "$user"
success "Installation complete!"
info ""
info "Next steps:"
info " 1. Edit /etc/shepherd/config.toml for your system"
info " 2. Edit user config at ~$user/.config/shepherd/config.toml"
info " 3. Select 'Shepherd Kiosk' session at login"
info " 4. Optionally run 'shepherd harden apply --user $user' for kiosk mode"
}
# Main install command dispatcher
install_main() {
local subcmd="${1:-}"
shift || true
local user=""
local prefix="$DEFAULT_PREFIX"
local source_config=""
# Parse remaining arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--user)
user="$2"
shift 2
;;
--prefix)
prefix="$2"
shift 2
;;
--source)
source_config="$2"
shift 2
;;
*)
die "Unknown option: $1"
;;
esac
done
case "$subcmd" in
bins)
install_bins "$prefix"
;;
config)
install_config "$user" "$source_config"
;;
sway-config)
install_sway_config "$prefix"
;;
desktop-entry)
install_desktop_entry "$prefix"
;;
system-config)
install_system_config
;;
all)
install_all "$user" "$prefix"
;;
""|help|-h|--help)
cat <<EOF
Usage: shepherd install <command> [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
}

152
scripts/lib/sway.sh Executable file
View file

@ -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
}

128
scripts/shepherd Executable file
View file

@ -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 <<EOF
shepherd - Unified tooling for shepherd-launcher
Usage: shepherd <command> [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 <command> 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 <<EOF
Usage: shepherd dev <command>
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 "$@"