diff --git a/INSTALL.md b/INSTALL.md index d97665e..13926e4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,97 +1,113 @@ -# Installation +Installation +============ -### Prerequisites +Prerequisites +------------- -LinApple uses **CMake** as its build system. +LinApple uses __CMake__ as its build system. -#### Debian / Ubuntu / RetroPie + # Debian / Ubuntu / RetroPie + sudo apt update + sudo apt install git g++ cmake \ + libzip-dev libcurl4-openssl-dev zlib1g-dev imagemagick + sudo apt install libsdl3-dev libsdl3-image-dev # Debian 13+ -```bash -sudo apt-get update -sudo apt-get install git g++ cmake libzip-dev libsdl3-dev libsdl3-image-dev libcurl4-openssl-dev zlib1g-dev imagemagick -``` + # Fedora / RHEL / CentOS + sudo dnf install git gcc-c++ cmake libcurl-devel libzip-devel ImageMagick + sudo dnf SDL3-devel SDL3_image-devel -#### Fedora / RHEL / CentOS + # Arch Linux + sudo pacman -Syu + sudo pacman -S base-devel cmake imagemagick libzip libcurl-gnutls zlib + sudo sdl3 sdl3_image -```bash -sudo dnf install git gcc-c++ cmake SDL3-devel SDL3_image-devel libcurl-devel libzip-devel ImageMagick -``` +Clone the repo with: -#### Arch Linux + git clone https://github.com/linappleii/linapple.git + cd linapple -```bash -sudo pacman -Syu -sudo pacman -S base-devel cmake imagemagick libzip sdl3 sdl3_image libcurl-gnutls zlib -``` -### Clone +Configure and Compile +--------------------- -```bash -git clone https://github.com/linappleii/linapple.git -cd linapple -``` + # Create a build directory and configure with CMake (skipping tests + # for a faster build). For further configuration options, see below. + cmake -B build -DBUILD_TESTING=OFF -### Configure and Compile + # Compile using all available CPU cores + cmake --build build -j$(nproc) -```bash -# Create a build directory and configure with CMake (skipping tests for a faster build) -cmake -B build -DBUILD_TESTING=OFF +#### Configuration Options -# Compile using all available CPU cores -cmake --build build -j$(nproc) -``` - -#### Build Options You can pass various options to the `cmake` configuration step: -- `-DFRONTEND=sdl3` : (Default) Build the emulator with the SDL3-based graphical frontend. -- `-DFRONTEND=tui` : Build for the terminal using 24-bit color and Unicode characters (no GUI dependencies required). -- `-DFRONTEND=headless` : Build the emulator without GUI or SDL dependencies (useful for automated testing or server environments). -- `-DBUILD_TESTING=OFF` : Skip building the test suite (saves compilation time and dependency fetching). -- `-DREGISTRY_WRITEABLE=ON` : Enable saving emulator configuration settings back to the config file. +- `-DFRONTEND=sdl3` : (Default) Build the emulator with the SDL3-based + graphical frontend. +- `-DFRONTEND=tui` : Build for the terminal using 24-bit color and Unicode + characters (no GUI dependencies required). +- `-DFRONTEND=headless` : Build the emulator without GUI or SDL + dependencies (useful for automated testing or server environments). +- `-DBUILD_TESTING=OFF` : Skip building the test suite (saves compilation + time and dependency fetching). +- `-DREGISTRY_WRITEABLE=ON` : Enable saving emulator configuration settings + back to the config file. +- `-DFRONTEND=sdl3` : (Default) Build the emulator with the SDL3-based + graphical frontend. +- `-DFRONTEND=headless` : Build the emulator without GUI or SDL + dependencies (useful for automated testing or server environments). +- `-DBUILD_TESTING=OFF` : Skip building the test suite (saves compilation + time and dependency fetching). +- `-DREGISTRY_WRITEABLE=ON` : Enable saving emulator configuration settings + back to the config file. - `-DPROFILING=ON` : Enable `gprof` profiling output. -- `-DCMAKE_BUILD_TYPE=Debug` : Build with debugging symbols instead of release optimizations. +- `-DCMAKE_BUILD_TYPE=Debug` : Build with debugging symbols instead of + release optimizations. -### Run Locally +### Run Locally to Test -After building, you can run the emulator directly from the build output directory: +After building, you can run the emulator directly from the build output +directory: -```bash -cd build -./linapple -``` + cd build + ./linapple -Or, to boot automatically into the standard Apple floppy disk provided with LinApple: + # Or to use the standard floppy image provided with LinApple: + ./linapple --autoboot --d1 ../res/Master.dsk -```bash -./linapple --autoboot --d1 ../res/Master.dsk -``` -### Installation +Installation +------------ To install LinApple so it can be run from anywhere, use the `install` target. -```bash -cmake --install build -``` + cmake --install build #### XDG Compliance (Linux) -The build system uses standard `GNUInstallDirs` and automatically adapts to your privileges: -- **Non-root install (Default):** If you run `cmake --install build` as a regular user without overriding the prefix, it will install to `~/.local/bin/`, `~/.local/share/linapple/`, and `~/.config/linapple/`. This is fully compliant with XDG Base Directory specifications and does not require `sudo`. -- **System-wide install (Root):** If you run `sudo cmake --install build`, it will install system-wide to `/usr/local/bin/`, `/usr/local/share/linapple/`, and `/usr/local/etc/linapple/`. + +The build system uses standard `GNUInstallDirs` and automatically adapts to +your privileges: +- __Non-root install (Default):__ If you run `cmake --install build` as a + regular user without overriding the prefix, it will install to + `~/.local/bin/`, `~/.local/share/linapple/`, and `~/.config/linapple/`. + This is fully compliant with XDG Base Directory specifications and does + not require `sudo`. +- __System-wide install (Root):__ If you run `sudo cmake --install build`, + it will install system-wide to `/usr/local/bin/`, + `/usr/local/share/linapple/`, and `/usr/local/etc/linapple/`. You can also explicitly define your installation prefix during configuration: -```bash -cmake -B build -DCMAKE_INSTALL_PREFIX=/usr -sudo cmake --install build -``` + + cmake -B build -DCMAKE_INSTALL_PREFIX=/usr + sudo cmake --install build After installation, simply run: -```bash -linapple -``` + + linapple ### Configuration and Assets -LinApple expects to find its configuration file (`linapple.conf`) in your configuration directory (e.g., `~/.config/linapple/linapple.conf`). +LinApple expects to find its configuration file (`linapple.conf`) in your +configuration directory (e.g., `~/.config/linapple/linapple.conf`). -If the emulator cannot find a required asset (like `Master.dsk` or character fonts) in the current directory, it will automatically search the `share` and `config` directories established during installation. +If the emulator cannot find a required asset (like `Master.dsk` or +character fonts) in the current directory, it will automatically search the +`share` and `config` directories established during installation. diff --git a/Test b/Test new file mode 100755 index 0000000..2d4b549 --- /dev/null +++ b/Test @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +trap 'ec=$?; echo 1>&2 "INTERNAL ERROR: ec=$ec line=$LINENO cmd=$BASH_COMMAND"; + exit $ec;' ERR + +#################################################################### + +error() { echo -e 1>&2 "● ERROR: $(basename "$0"):" "$@"; } +die() { local ec=$1; shift; error "$@"; exit $ec; } + +# Given $1 as a {distro}-{release}-{frontend} string, +# set globals $distro $release $frontend to its components, +# and $platform to colon-separated $distro:$release. +# These are implicit parameters to many routines. +parse_build_spec() { + declare -g platform distro release frontend + local spec="$1"; shift + [[ $spec =~ ^([^-]+)-([^-]+)-([^-]+)$ ]] || die 2 "Bad build spec: $spec" + distro="${BASH_REMATCH[1]}" + release="${BASH_REMATCH[2]}" + frontend="${BASH_REMATCH[3]}" + platform="$distro:$release" +} + +docker_unavailable() { + die 3 'Cannot run $docker due to setup_docker() failure (see above).' +} + +setup_docker() { + declare -g docker=docker + if ! $docker --version >/dev/null 2>&1; then + echo 1>&2 "WARNING: Cannot run '$docker' command. Check path?" + docker=docker_unavailable + elif ! $docker info >/dev/null 2>&1; then + docker='sudo docker' + if ! sudo -v 2>/dev/null; then + echo 1>&2 "WARNING: Cannot sudo to run '$docker'; start proxy?" + docker=docker_unavailable + fi + fi +} + +#################################################################### + +build_image() { + echo "───── Build image $platform" + # XXX Actually, it would be nice to use a single image for all + # frontends, but we'd have to figure out how we pre-install all + # the frontend deps, rather than just one. + declare -g image_name="linapple-build" \ + image_tag="$distro-$release-$frontend" + $docker build \ + --tag "$image_name:$image_tag" \ + --build-arg PLATFORM="$platform" \ + --build-arg FRONTEND="$frontend" \ + "$PROJDIR/btools" +} + +run_container() { + echo "───── Run container" + # --chown-to leaves the bind-mounted build dir owned by the host user. + $docker run --rm \ + --volume "$PROJDIR:$PROJDIR" \ + --workdir "$PROJDIR" \ + "$image_name:$image_tag" \ + btools/build --chown-to "$(id -u):$(id -g)" "$frontend" +} + +#################################################################### +# Usage and argparse. + +# Default list if no distros given as $@. +# XXX but how do we provide non-default SDLs? +default_builds=( + debian-13-sdl3 debian-12-sdl2 debian-12-sdl1.2 + #debian:12 debian:13 ubuntu:24.04 ubuntu:26.04 + #fedora:38 fedora::43 + #arch:latest +) + +usage() { + local exitcode=$1; shift + [[ -n ${1:-} ]] && error "$@" + cat <<_____ +Usage: $(basename "$0") --current [BUILD-ARGS] + $(basename "$0") [OPTS] [BUILD-SPEC...] + +Calls btools/build for one or more Linux build specifications, which are in +{distro}-{release}-{frontend} format, e.g. 'ubuntu-24.04-sdl2'. All +builds (whether local or in containers) are placed under +'build/{distro}-{release}-{frontend}/'. + +With --current, does a build for the current host rather than using +containers. BUILD-ARGS are passed to btools/build. (Run 'btools/build -h' +for further information.) + +Otherwise starts a container for each BUILD-SPEC and does the build and +unit tests in that container, leaving the results under build/ on the host. + +The default build specs are: ${default_builds[@]}. + +_____ + exit $exitcode +} + +build_current=false +while [[ $# -gt 0 ]]; do case "$1" in + -h|--help) usage 0;; + --current) shift; build_current=true;; + -*) usage 2 "Unknown option: $1";; + --) break;; + *) break;; +esac; done + +#################################################################### +# Main + +PROJDIR=$(command cd $(dirname "$0") && pwd -P) + +$build_current && { + "$PROJDIR/btools/build" "$@" + exit 0 +} + +setup_docker +for build_spec in "${@:-${default_builds[@]}}"; do + echo "━━━━━ $build_spec" + parse_build_spec "$build_spec" # sets $distro $release $frontend $platform + build_image + run_container +done + +# XXX --current vs. --all ? +# XXX -d DISTRO -f FRONTEND (either implies --current?) +# no, maybe better limits --all? patterns? `-d deb` for deb*? diff --git a/btools/Dockerfile b/btools/Dockerfile new file mode 100644 index 0000000..c407d57 --- /dev/null +++ b/btools/Dockerfile @@ -0,0 +1,9 @@ +# check=skip=InvalidDefaultArgInFrom $PLATFORM unset produces good error +ARG PLATFORM +FROM ${PLATFORM} +ARG FRONTEND PLATFORM +# Cache the installed dependencies in the image so that, at least for +# a while, the build based on this image will find all prerequisites +# already installed, speeding the build. +COPY build /tmp/ +RUN /tmp/build --prereq-only ${FRONTEND} && rm /tmp/build diff --git a/btools/build b/btools/build new file mode 100755 index 0000000..893ed3b --- /dev/null +++ b/btools/build @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# +# btools/build - Build linapple for a distro/frontend +# +# See usage() for details. +# +set -Eeuo pipefail +trap 'ec=$?; echo 1>&2 "INTERNAL ERROR: ec=$ec line=$LINENO cmd=$BASH_COMMAND"; + exit $ec;' ERR + +error() { echo -e 1>&2 "● ERROR: $(basename "$0"):" "$@"; } +die() { local ec=$1; shift; error "$@"; exit $ec; } + +# $1 is a timestamp dir/file. If modified within the last hour, +# return 1. Otherwise touch it and return 0. +needs_update() { + local timestamp="$1"; shift + [[ -e $timestamp ]] || { touch "$timestamp"; return 0; } + local age=$(( $(date +%s) - $(stat -c %Y "$timestamp") )) + (( age <= ${1:-3600} )) || { touch "$timestamp"; return 0; } + return 1 +} + +sudo= +set_sudo() { + declare -g sudo + [[ $(id -u) -eq 0 ]] || sudo=sudo +} + +chown_builddir() { # Used by --chown-to. + local to="$1"; shift + chown -R "$to" "$PROJDIR/$BUILDDIR_REL" \ + || echo 1>&2 "WARNING: chown -R '$to' '$BUILDDIR_REL' failed" +} + +#################################################################### + +# Set variables defined in /etc/os-release, in particular $ID and +# $VERSION_ID. If /etc/os-release doesn't exist these may be synthesised +# through other means, otherwise this exits with an error. +os_release() { + [[ -r /etc/os-release ]] \ + || die 1 "/etc/os-release not found; cannot autodetect distro" + source /etc/os-release + [[ -n ${ID:-} && -n ${VERSION_ID:-} ]] \ + || die 5 '$ID and/or $VERSION_ID not set in /etc/os-release' +} + +# We know os_release but no FRONTEND specified: choose a default or +# exit with an error indicating we have no default. +default_frontend() { + local rel=$VERSION_ID + case $ID in + debian) [[ $rel -ge 13 ]] && echo sdl3 || echo sdl2;; + ubuntu) [[ $rel > 24.04 ]] && echo sdl3 || echo sdl2;; + *) die 4 "No default frontends known for distro $ID";; + esac +} + +install_prerequisites() { + echo '━━━━━ Install prerequisites' + + # This needs to be on a filesystem local to the container, if we're using + # one, so that we don't honour a stale timestamp in a new container. + local tsfile="/tmp/linappleii-linapple/prereq-$ID-$VERSION_ID-$FRONTEND" + mkdir -p "$(dirname "$tsfile")" + # If we've updated and re-installed in the last hour, just skip. + needs_update "$tsfile" \ + || { echo 'updated in last hour; skipping'; return 0; } + + local known_ids=( # all these must be in the `case` below. + debian ubuntu + ) + local id k + for distro in "$ID" ${ID_LIKE:-}; do # split $ID_LIKE + for k in "${known_ids[@]}"; do + # Stop both loops at first one we know. + [[ $distro == "$k" ]] && break 2 + done + die 4 "This system's IDs match no known distros" \ + "\n• Known:" "${known_ids[@]}" \ + "\n• ID=$ID ID_LIKE=${ID_LIKE:-}" + done + + case "$distro" in + debian|ubuntu) install_deps_debian;; + *) die 10 "Internal error: set unknown distro '$distro'";; + esac +} + +install_deps_debian() { + echo '───── apt update' + $sudo apt update && $sudo touch /var/lib/apt/lists + echo '───── apt install' + $sudo apt install -y -q \ + git g++ cmake \ + libzip-dev libcurl4-openssl-dev zlib1g-dev imagemagick + case "$FRONTEND" in + sdl1.2) $sudo apt install -y -q libsdl1.2-dev;; + sdl2) $sudo apt install -y -q libsdl2-dev libsdl2-image-dev;; + sdl3) $sudo apt install -y -q libsdl3-dev libsdl3-image-dev;; + *) die 4 "Unknown frontend: $FRONTEND";; + esac +} + +#################################################################### +# Usage and argument parsing. + +usage() { + local exitcode=$1; shift + [[ -n ${1:-} ]] && error "$@" + cat <<_____ +Usage: $(basename "$0") [OPTS] [-D DISTRO] [FRONTEND] +For the current distro and given (or default) frontend, installs +prerequisites, builds linapple and runs tests. + +• The distro is auto-detected from /etc/os-release, but can be overridden with + e.g. '-D ubuntu:24.04' (but you should never need this). +Options: + -h,--help Print this message. + -D ID:VERSION_ID Use given distro information instead of autodetecting + from /etc/os-release. E.g. 'ubuntu:24.04'. + --prereq-only Only install build prerequisites; do not build. + --chown OWNER On exit, chown -R the build dir to OWNER (UID:GID or + user:group). For container builds writing to a bind- + mounted host dir; failures warn but do not abort. +_____ + exit $exitcode +} + +chown_to= +force_distro= +prereq_only=false +FRONTEND= +while [[ $# -gt 0 ]]; do case "$1" in + -h|--help) usage 0;; + -D) shift; force_distro="$1"; shift;; + --prereq-only) shift; prereq_only=true;; + --chown-to) shift; chown_to="$1"; shift;; + -*) usage 2 "Unknown option: $1";; + --) break;; + *) break;; +esac; done +[[ $# -gt 0 ]] && { FRONTEND="$1"; shift; } +[[ $# -eq 0 ]] || usage 2 "Too many arguments." + +if [[ -z $force_distro ]]; then + os_release +else + if [[ $force_distro =~ ^([^:]+):([^:]+)$ ]]; then + ID=${BASH_REMATCH[1]} + VERSION_ID=${BASH_REMATCH[2]} + else + die 2 "DISTRO must be 'name:ver'" + fi +fi + +[[ -n $FRONTEND ]] || FRONTEND=$(default_frontend) + +#################################################################### + +PROJDIR=$(command cd $(dirname "$0")/.. && pwd -P) +# No colon in builddir path because even on Linux it makes Cmake upset. +# This is relative because we need to `cd` to `cmake` anyway, so why +# not keep it short for messages etc. +echo "▶ DISTRO=$ID:$VERSION_ID FRONTEND=$FRONTEND" + +install_prerequisites +$prereq_only && exit 0 + +command cd "$PROJDIR" +echo '━━━━━ Cmake setup' +BUILDDIR_REL="build/$ID$VERSION_ID-$FRONTEND" +echo "▶ BUILDDIR=$BUILDDIR_REL" +[[ -n $chown_to ]] && trap "chown_builddir $chown_to" EXIT +cmake -B "$BUILDDIR_REL" -DFRONTEND=$FRONTEND +echo '━━━━━ Cmake build' +echo ● cmake --quiet --build "$BUILDDIR_REL" -j$(nproc) +cmake --build "$BUILDDIR_REL" -j$(nproc)