commit 795db7924bd170651cb194ff8d35c4104a62b5e0 Author: Mikhail Yevchenko Date: Mon Sep 7 15:44:03 2026 +0300 Integrate G15 submodules with Debian 13 packaging and devcontainer diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 0000000..2714559 --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,2 @@ +compose.usb.yaml +compose.usb.yaml.tmp.* \ No newline at end of file diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml new file mode 100644 index 0000000..46a43d8 --- /dev/null +++ b/.devcontainer/compose.yaml @@ -0,0 +1,9 @@ +services: + dev: + image: g15-packaging:latest + build: + context: ../packaging + dockerfile: Dockerfile + command: ["sleep", "infinity"] + volumes: + - ..:/work \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..02f9a2e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "G15 Debian 13", + "dockerComposeFile": ["compose.yaml", "compose.usb.yaml"], + "service": "dev", + "workspaceFolder": "/work", + "initializeCommand": ["bash", "${localWorkspaceFolder}/.devcontainer/initialize.sh"], + "overrideCommand": false, + "shutdownAction": "stopCompose" +} \ No newline at end of file diff --git a/.devcontainer/initialize.sh b/.devcontainer/initialize.sh new file mode 100644 index 0000000..395ee0d --- /dev/null +++ b/.devcontainer/initialize.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Require a local native Linux daemon, not SSH/TCP or Docker Desktop's VM. +[[ $(uname -s) == Linux ]] || { echo 'Local Linux Docker required.' >&2; exit 1; } +if [[ -n ${DOCKER_CONTEXT:-} ]]; then + endpoint=$(docker context inspect "$DOCKER_CONTEXT" --format '{{.Endpoints.docker.Host}}') +elif [[ -n ${DOCKER_HOST:-} ]]; then + endpoint=$DOCKER_HOST +else + endpoint=$(docker context inspect --format '{{.Endpoints.docker.Host}}') +fi +[[ $endpoint == unix://* ]] || { echo 'Remote Docker endpoints are unsupported.' >&2; exit 1; } +os=$(docker info --format '{{.OperatingSystem}}') +[[ $os != *"Docker Desktop"* ]] || { echo 'Docker Desktop USB mapping is unsupported.' >&2; exit 1; } + +# shellcheck source=.devcontainer/usb.sh +source "$(dirname "${BASH_SOURCE[0]}")/usb.sh" +output="$(dirname "${BASH_SOURCE[0]}")/compose.usb.yaml" +tmp=$(mktemp "$output.tmp.XXXXXX") +trap 'rm -f "$tmp"' EXIT +generate_usb /sys/bus/usb/devices /dev/bus/usb > "$tmp" +mv "$tmp" "$output" \ No newline at end of file diff --git a/.devcontainer/test.sh b/.devcontainer/test.sh new file mode 100644 index 0000000..ced596f --- /dev/null +++ b/.devcontainer/test.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail +for command in devcontainer docker node; do + command -v "$command" >/dev/null || { echo "Required command missing: $command" >&2; exit 1; } +done +workspace=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +devcontainer up --workspace-folder "$workspace" > "$tmp/up.json" +container=$(node -e ' +const result = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); +if (result.outcome !== "success" || !result.containerId) throw Error("CLI startup failed"); +process.stdout.write(result.containerId); +' "$tmp/up.json") +docker inspect "$container" > "$tmp/inspect.json" +node - "$tmp/inspect.json" "$workspace" <<'NODE' +const fs = require('fs'); +const assert = require('assert/strict'); +const [container] = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); +const host = container.HostConfig; +const expected = []; +const products = new Set(['c222', 'c225', 'c227', 'c22b', 'c22d', 'c22e']); +for (const entry of fs.readdirSync('/sys/bus/usb/devices')) { + const base = `/sys/bus/usb/devices/${entry}`; + const read = name => fs.readFileSync(`${base}/${name}`, 'utf8').trim(); + try { + if (read('idVendor') !== '046d' || !products.has(read('idProduct'))) continue; + const bus = read('busnum'), number = read('devnum'); + assert.match(bus, /^\d{1,3}$/); + assert.match(number, /^\d{1,3}$/); + const node = `/dev/bus/usb/${String(Number(bus)).padStart(3, '0')}/${String(Number(number)).padStart(3, '0')}`; + if (fs.statSync(node).isCharacterDevice()) expected.push(node); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } +} +assert.equal(container.State.Running, true, 'container must be running'); +assert.equal(host.Privileged, false, 'privileged mode forbidden'); +assert.notEqual(host.NetworkMode, 'host', 'host networking forbidden'); +assert.deepEqual(host.DeviceCgroupRules || [], [], 'device cgroup overrides forbidden'); +assert.deepEqual(host.DeviceRequests || [], [], 'additional device requests forbidden'); +const devices = host.Devices || []; +assert.deepEqual(devices.map(device => device.PathOnHost).sort(), expected.sort(), + 'USB mappings must match current host allowlist; reconnects may require container recreation'); +for (const device of devices) { + assert.equal(device.PathInContainer, device.PathOnHost, 'device remapping forbidden'); + assert.equal(device.CgroupPermissions, 'rw', 'only rw device access allowed'); +} +// Only the checkout bind and the CLI's optional editor volume are permitted. +assert(container.Mounts.some(mount => mount.Type === 'bind' && + mount.Source === process.argv[3] && mount.Destination === '/work'), 'workspace bind missing'); +for (const mount of container.Mounts) { + assert((mount.Type === 'bind' && mount.Source === process.argv[3] && mount.Destination === '/work') || + (mount.Type === 'volume' && mount.Name === 'vscode' && mount.Destination === '/vscode'), + `unexpected mount: ${mount.Source} -> ${mount.Destination}`); +} +console.log(`PASS: real Docker configuration; ${devices.length} exact allowed USB mapping(s)`); +NODE +# Expand PWD inside the container, not on the host. +# shellcheck disable=SC2016 +devcontainer exec --workspace-folder "$workspace" bash -c \ + 'set -euo pipefail; test "$PWD" = /work; test -f .devcontainer/devcontainer.json; test ! -e /dev/uinput; printf "PASS: CLI exec and no /dev/uinput; no keyboard I/O performed\n"' \ No newline at end of file diff --git a/.devcontainer/usb.sh b/.devcontainer/usb.sh new file mode 100644 index 0000000..d7b761b --- /dev/null +++ b/.devcontainer/usb.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Initialization supplies the real host sysfs and USB device paths. +generate_usb() { + local sys=$1 dev=$2 entry vendor product bus number node + local -a nodes=() + for entry in "$sys"/*; do + [[ -r $entry/idVendor && -r $entry/idProduct ]] || continue + read -r vendor < "$entry/idVendor" || continue + read -r product < "$entry/idProduct" || continue + [[ $vendor == 046d ]] || continue + case $product in c222|c225|c227|c22b|c22d|c22e) ;; *) continue ;; esac + [[ -r $entry/busnum && -r $entry/devnum ]] || continue + read -r bus < "$entry/busnum" || continue + read -r number < "$entry/devnum" || continue + [[ $bus =~ ^[0-9]{1,3}$ && $number =~ ^[0-9]{1,3}$ ]] || continue + printf -v node '%s/%03d/%03d' "$dev" "$((10#$bus))" "$((10#$number))" + [[ -c $node ]] || continue + nodes+=("$node") + done + printf 'services:\n dev:\n' + if ((${#nodes[@]})); then + printf ' devices:\n' + for node in "${nodes[@]}"; do + printf ' - "%s:%s:rw"\n' "$node" "$node" + done + else + printf ' devices: []\n' + fi +} \ No newline at end of file diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml new file mode 100644 index 0000000..ff31a5a --- /dev/null +++ b/.github/workflows/debian.yml @@ -0,0 +1,72 @@ +name: Debian 13 packages +on: + pull_request: + paths: + - 'packaging/**' + - '.github/**' + - '.devcontainer/**' + - '.gitmodules' + - '.gitignore' + - 'README.md' + - 'libg15' + - 'libg15render' + - 'g15daemon' + - 'g15macro' + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + submodules: recursive + - name: Build and validate in Debian 13 + shell: bash + run: | + set -euo pipefail + for script in packaging/*.sh packaging/overlays/v1/*.sh .devcontainer/*.sh; do bash -n "$script"; done + # Real devcontainer CLI validation is a separate local check; see README.md. + docker run --rm -v "$GITHUB_WORKSPACE/.github:/repo/.github:ro" -w /repo \ + rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 \ + .github/workflows/debian.yml + mkdir -p "$RUNNER_TEMP/debs" + bash packaging/run.sh "$RUNNER_TEMP/debs" + docker run --rm -v "$GITHUB_WORKSPACE/packaging:/checks/packaging:ro" \ + -v "$GITHUB_WORKSPACE/.devcontainer:/checks/.devcontainer:ro" \ + -w /checks g15-packaging bash -c 'shellcheck packaging/*.sh packaging/overlays/v1/*.sh .devcontainer/*.sh' + test -z "$(git submodule foreach --quiet --recursive 'git status --porcelain')" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: debian13-amd64 + path: ${{ runner.temp }}/debs/* + if-no-files-found: error + + release: + if: github.event_name == 'release' + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: debian13-amd64 + path: artifacts + - name: Attach verified packages and sources + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + cd artifacts + sha256sum -c SHA256SUMS + gh release upload "$RELEASE_TAG" -- ./* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b807b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.vscode/ +/dist/ +/distro.zip \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d8583fc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "libg15"] + path = libg15 + url = https://gt.uo1.net/mikhail/libg15 +[submodule "g15daemon"] + path = g15daemon + url = https://gt.uo1.net/mikhail/g15daemon +[submodule "g15macro"] + path = g15macro + url = https://gt.uo1.net/mikhail/g15macro +[submodule "libg15render"] + path = libg15render + url = https://gt.uo1.net/mikhail/libg15render +[submodule "g15stats"] + path = g15stats + url = https://gt.uo1.net/mikhail/g15stats diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ae349f --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# G15 tools: Debian 13 development and packages + +This outer repository builds and packages four pinned Git submodules: `libg15`, +`libg15render`, `g15daemon`, and `g15macro`. A fifth submodule, `g15stats`, is +present but is not integrated into the build or packaging pipeline. +Initialize a fresh clone with +`git submodule update --init --recursive`. Builds reject missing, dirty, or +misaligned submodules; they export committed, unpatched Debian sources and +never modify submodule working trees or refs. + +## Build and verify release packages + +Create an empty directory outside the checkout (for example with `mktemp -d`) +and run `bash packaging/run.sh /absolute/path/to/that-directory`. +The runner uses disposable Debian 13 Docker containers to build all four +projects in dependency order, install their own development packages before +downstream builds, test distro replacement, and rebuild all four source packages +without the checkout mounted. It mounts only an allowlist of build inputs; +`.vscode` is not exposed to release build containers. + +Outputs include eight binary packages, debug symbols, four standalone Debian +source packages, build metadata, submodule provenance and checksums. See +`packaging/NOTES.md` for versions, overlays, QA and release behavior. The old +outer `git archive` ZIP is not a source distribution: it omits submodule content. +Use the verified Debian source artifacts instead. + +**Safety limitation:** legacy macro memory/configuration handling has known +preexisting defects. Compatibility overlays do not repair the macro engine. +Do not run it as root or use untrusted configuration. Build and install tests +are not hardware or security certification; see `packaging/overlays/v1/README.md`. + +## Recommended: Debian 13 container and packages + +Use local Linux Docker Engine with Compose and the Dev Containers CLI (or +VS Code's Reopen in Container). From the repository root, run +`devcontainer up --workspace-folder .`, then +`devcontainer exec --workspace-folder . bash`. +The container reuses `packaging/Dockerfile` (Debian 13/trixie), with GCC, +Make, Autotools, Debian packaging tools, libusb-1.0, FreeType and XTest headers. +The image does not preinstall distro G15 development libraries: package builds +install the versions built from the submodules. Its normal packaging command +is replaced with `sleep infinity`. +No host package installation is needed beyond your existing container tooling. + +For distributable packages, follow `packaging/NOTES.md` +using its disposable build container, not the development shell: the packaging +scripts install their outputs. + +Initialization regenerates ignored `.devcontainer/compose.usb.yaml`, including +only existing exact USB character nodes for Logitech `046d` keyboards +`c222`, `c225`, `c227`, `c22b`, `c22d`, `c22e`. No keyboard is required for +build-only use. Speakers and gamepanels are excluded. Remote SSH/TCP Docker +endpoints and Docker Desktop are rejected; use a native local Linux daemon +whose USB paths match this host (do not use a forwarded Unix socket). +After reconnecting a keyboard, recreate the container with +`devcontainer up --workspace-folder . --remove-existing-container` to refresh +device mappings. A device disappearing during startup may require retrying. + +There are no whole-USB mounts, wildcard device rules, privileged mode, Docker +socket mounts, or host networking. `/dev/uinput` is deliberately omitted: +host input injection does not work by default. If needed, manually configure +only that device and host permissions after reviewing the security implications. +Rootless Docker/device permissions may further limit hardware access. Building +and container checks do not establish keyboard functionality; do not run a +second daemon against a keyboard already owned by a host service. + +Run `bash .devcontainer/test.sh` with local Linux Docker Engine, Compose, +the installed Dev Containers CLI and host Node.js. It runs real CLI `up` and +`exec` with the absolute checkout path, then checks `docker inspect` against +the current host sysfs allowlist and rejects unexpected devices, mounts, +device cgroup overrides, privileged mode and host networking. It also checks +that `/dev/uinput` is absent. No keyboard is required; an empty allowlist must +produce no device mappings. The container is left running for development. +If USB devices changed, recreate the container as described above and rerun. +These configuration checks perform no keyboard I/O, USB driver changes or +keyboard functional validation. See `packaging/NOTES.md` for release test +coverage and safety limitations. diff --git a/g15daemon b/g15daemon new file mode 160000 index 0000000..21349f7 --- /dev/null +++ b/g15daemon @@ -0,0 +1 @@ +Subproject commit 21349f789225c7d2ddb6e1e3d9eb4298932e68e1 diff --git a/g15macro b/g15macro new file mode 160000 index 0000000..b2f8cd2 --- /dev/null +++ b/g15macro @@ -0,0 +1 @@ +Subproject commit b2f8cd21957d8b975bd91d53602092ca9c664ea8 diff --git a/g15stats b/g15stats new file mode 160000 index 0000000..95de11a --- /dev/null +++ b/g15stats @@ -0,0 +1 @@ +Subproject commit 95de11a526346b3d9fd33df051b330c36b1c8a16 diff --git a/libg15 b/libg15 new file mode 160000 index 0000000..1cba422 --- /dev/null +++ b/libg15 @@ -0,0 +1 @@ +Subproject commit 1cba4225b6e9c175a0395bf8802f28c142c5e8ce diff --git a/libg15render b/libg15render new file mode 160000 index 0000000..b29a7d0 --- /dev/null +++ b/libg15render @@ -0,0 +1 @@ +Subproject commit b29a7d05ea8309b0fcd8d48c1b96cd64436bd7a8 diff --git a/packaging/Dockerfile b/packaging/Dockerfile new file mode 100644 index 0000000..2b1e869 --- /dev/null +++ b/packaging/Dockerfile @@ -0,0 +1,8 @@ +FROM debian:trixie-slim +ENV DEBIAN_FRONTEND=noninteractive +RUN printf 'deb-src http://deb.debian.org/debian trixie main\n' > /etc/apt/sources.list.d/g15-source.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends build-essential autoconf automake libtool pkg-config debhelper dpkg-dev libusb-1.0-0-dev git ca-certificates fakeroot lintian devscripts dh-sequence-movetousr libfreetype-dev libxtst-dev quilt shellcheck \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /work +CMD ["bash", "packaging/container-build.sh"] \ No newline at end of file diff --git a/packaging/NOTES.md b/packaging/NOTES.md new file mode 100644 index 0000000..4413535 --- /dev/null +++ b/packaging/NOTES.md @@ -0,0 +1,106 @@ +# Debian 13 packaging + +## Running + +Use a regular, initialized Git clone on Linux with Docker Engine. Create an +empty output directory with `mktemp -d`, then run +`bash packaging/run.sh /absolute/path/to/output`. No host package installation, +API keys or `.env` are needed. Build scripts install packages and require a +disposable root container; do not run them in the hardware-enabled devcontainer. +`G15_BUILD_IMAGE` optionally changes the shared image tag. + +`run.sh` builds `packaging/Dockerfile`, mounts only Git metadata, the four +submodules and `packaging/` read-only, then runs build and lifecycle tests. +A second fresh container mounts only artifacts and `rebuild.sh`, proving that +the source packages do not need this repository. All working sources are in +container temporary directories. Outputs and logs should remain outside the +checkout. Linked Git worktrees with external gitdirs are not supported by the +allowlisted mount layout. No container receives USB, uinput or a Docker socket. + +## Sources, patches and versions + +Order: `libg15`, `libg15render`, `g15daemon`, `g15macro`. Each stage installs +its own runtime and development packages before the next stage resolves build +dependencies. The base image does not supply distro G15 development packages. +Each of these four build submodules must match its indexed gitlink and be clean. +The additional `g15stats` submodule is not built, packaged or recorded in package +provenance. Git +archives export immutable HEADs, not working-tree contents. No checkout, +patching, committing, ref updates or pushing occurs in submodules. + +Orig tarballs contain the complete committed upstream tree without `debian/`; +tracked generated Autotools inputs are preserved rather than guessed away. +No Git metadata, untracked objects or local build state is exported. Debian +quilt patches stay unapplied in orig tarballs and are applied by dpkg-source. +The existing libusb-1.0 port and daemon buffer-overflow fix are retained. + +`overlays/v1/` is the explicit local compatibility layer, applied only in +temporary copies. It backports daemon -14.1's t64 transition from -14, adds +G110 systemd hotplug activation, and replaces macro's historical compat-5 CDBS +packaging with debhelper 13. Macro receives only two upstream build fixes: +remove bare `-I` and make an implicit `int` parameter explicit. All changes are +included in the Debian tarballs; macro C/build changes are quilt patches. +The source packages carry `debian/README.local-build` and original attribution. + +The daemon control file and SONAME override are compared against the official +`g15daemon=1.9.5.3-14.1` source obtained through authenticated APT indexes, before +changing the local Maintainer. This preserves architecture-dependent +`${t64:Provides}`, `Replaces: libg15daemon-client1` and versioned `Breaks`. +Runtime package filenames/manpages are renamed, without changing SONAME. +The inherited `dh_installsystemd --no-start` remains in force. + +Versions use `+g15.git<12-character-commit>-1`. Macro's upstream +version is 1.0.5 (configure.ac), not the historical Debian changelog's 1.0.3. +Tests require versions above every available trixie candidate, including binNMUs. +There is no trixie macro candidate: macro is tested as a fresh installation, +not falsely reported as a distro upgrade. Increment the local Debian revision +when changing overlays for an already-published source commit. Git hashes are +identifiers, not monotonically increasing versions between local releases. + +Orig tar ordering, ownership, timestamps and changelog dates are deterministic +from the source commit; `SOURCE_DATE_EPOCH` is set. `.buildinfo` records installed +dependencies. Debian mirrors and the trixie image intentionally float, so this +is repeatable source export and standalone rebuilding, NOT a claim of bit-for-bit +binary reproducibility. Keep complete released artifacts. No signing is performed. + +## Outputs and gates + +Eight packages: `libg15-1`, `libg15-dev`, `libg15render1`, `libg15render-dev`, +`g15daemon`, `libg15daemon-client1t64`, `libg15daemon-client-dev`, `g15macro`; +plus five debug-symbol packages on amd64. Each of the four sources has a `.dsc`, +orig tarball, Debian tarball, `.buildinfo` and `.changes`. `SUBMODULES` records +pins and `SHA256SUMS` covers all artifacts. The outer Git ZIP is not a substitute. + +Tests check exact package names, candidate ordering, checksums, Lintian errors, +clean installs, distro upgrades, all three libraries' exported symbols and +SONAMEs, executable linking, development headers/linking, t64 fields, service +and udev installation, macro help/version and purge cleanup. Source rebuilds +install newly rebuilt dependencies, not binaries from the first build. + +CI uses recursive checkout on relevant PRs, manual dispatch and published +releases. Build permissions are read-only; a separate write-permission job +downloads the verified artifact and checks checksums before uploading release +assets. All actions are commit-pinned. Existing assets are not overwritten. +Lint failures, build failures and source rebuild failures block publication. + +## Safety and validation limits + +Development-container configuration is checked separately with +`bash .devcontainer/test.sh` using local Docker, the Dev Containers CLI and +host Node.js (see `README.md`). It starts the real container, inspects exact +USB mappings against host sysfs and checks isolation, then exercises CLI exec +without keyboard I/O or driver changes. Package CI syntax-checks this script +but does not run it; package gates do not imply devcontainer or keyboard +functional validation. + +Risk: high (legacy native code and distro replacement). Macro has preexisting +memory initialization, recording bounds, configuration-index and string-copy +defects. These are explicitly outside this build-compatibility change; compiler +warnings are not suppressed. Do not run macro as root or load untrusted config. +Its historical Debian archive import did not verify the descriptor signature; +that provenance limitation remains in `debian/README.source`. + +Hardware operation, X11 recording/playback, hotplug under booted systemd, +32-bit t64 ABI behavior and actual GitHub publishing are not covered by local +package tests. No runtime-safety or security certification is claimed. Passing +packaging gates does not resolve those upstream security concerns. \ No newline at end of file diff --git a/packaging/build.sh b/packaging/build.sh new file mode 100644 index 0000000..9f4f6f5 --- /dev/null +++ b/packaging/build.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Installs dependencies and outputs: disposable Debian 13 containers ONLY. +set -euo pipefail +export LC_ALL=C +root=$(cd -- "$(dirname -- "$0")/.." && pwd) +[[ -f /.dockerenv && $EUID == 0 ]] || { echo 'Disposable root Docker container required' >&2; exit 1; } +# shellcheck source=/dev/null +. /etc/os-release +[[ $ID == debian && $VERSION_ID == 13 ]] || exit 1 +out=$(realpath "${1:?empty artifact directory required}") +[[ -d $out && -z $(find "$out" -mindepth 1 -print -quit) ]] || exit 1 +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +export DEB_BUILD_OPTIONS=${DEB_BUILD_OPTIONS:-parallel=2} +export DEBFULLNAME='G15 packaging contributors' DEBEMAIL='noreply@example.invalid' +git config --global --add safe.directory "$root" +for source in libg15 libg15render g15daemon g15macro; do + git config --global --add safe.directory "$root/$source" + pin=$(git -C "$root" ls-files --stage -- "$source" | awk '$1 == 160000 {print $2}') + [[ -n $pin && $(git -C "$root/$source" rev-parse HEAD) == "$pin" ]] + [[ -z $(git -C "$root/$source" status --porcelain --untracked-files=all) ]] +done +git -C "$root" submodule status --recursive -- libg15 libg15render g15daemon g15macro > "$out/SUBMODULES" +if grep -q '^[+U-]' "$out/SUBMODULES"; then echo 'Submodule pin mismatch or missing module' >&2; exit 1; fi +for source in libg15 libg15render g15daemon g15macro; do + mkdir "$work/$source" + cd "$work/$source" + sha=$(git -C "$root/$source" rev-parse HEAD) + export SOURCE_DATE_EPOCH + SOURCE_DATE_EPOCH=$(git -C "$root/$source" show -s --format=%ct HEAD) + mkdir tree + git -C "$root/$source" archive HEAD | tar -x -C tree + [[ ! -d tree/.pc ]] || { echo 'Expected unapplied quilt checkout' >&2; exit 1; } + base=$(dpkg-parsechangelog -l tree/debian/changelog -S Version) + upstream=${base%-*} + [[ $source != g15macro ]] || upstream=1.0.5 + upstream+=+g15.git${sha:0:12} + version=$upstream-1 + mv tree "$source-$upstream" + tree=$PWD/$source-$upstream + tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner \ + --exclude="$source-$upstream/debian" -cJf "${source}_${upstream}.orig.tar.xz" "$source-$upstream" + (cd "$tree" && bash "$root/packaging/overlays/v1/apply.sh" "$source" "$root") + if [[ $source == g15daemon ]]; then + mkdir official + (cd official && apt-get source --download-only g15daemon=1.9.5.3-14.1 && dpkg-source --skip-patches -x ./*.dsc source) + diff -u official/source/debian/control "$tree/debian/control" + diff -u official/source/debian/libg15daemon-client1t64.lintian-overrides "$tree/debian/libg15daemon-client1t64.lintian-overrides" + fi + cd "$tree" + sed -i "s/^Maintainer:.*/Maintainer: $DEBFULLNAME <$DEBEMAIL>/" debian/control + dch --newversion "$version" --distribution trixie --force-distribution \ + "Export $source commit $sha; apply documented outer packaging overlay v1." + sed -i "0,/^ -- /s/^ -- .*/ -- $DEBFULLNAME <$DEBEMAIL> $(date -u -R -d "@$SOURCE_DATE_EPOCH")/" debian/changelog + cp "$root/packaging/overlays/v1/README.md" debian/README.local-build + apt-get build-dep -y . + dpkg-buildpackage -us -uc -sa + cd .. + cp ./*.deb ./*.dsc ./*.tar.* ./*.buildinfo ./*.changes "$out/" + apt-get install -y ./*.deb +done +cd "$out" +sha256sum -- *.deb *.dsc *.tar.* *.buildinfo *.changes SUBMODULES > SHA256SUMS \ No newline at end of file diff --git a/packaging/container-build.sh b/packaging/container-build.sh new file mode 100644 index 0000000..f1b69d8 --- /dev/null +++ b/packaging/container-build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail +# This entry point installs packages: use only in a disposable container. +[[ -f /.dockerenv && $EUID == 0 ]] || { echo 'Disposable root Docker container required' >&2; exit 1; } +apt-get update +bash packaging/build.sh /out +bash packaging/test.sh /out \ No newline at end of file diff --git a/packaging/g110.udev b/packaging/g110.udev new file mode 100644 index 0000000..d68af01 --- /dev/null +++ b/packaging/g110.udev @@ -0,0 +1,3 @@ + +# Repository G110 backend (extra keys, no LCD). +SUBSYSTEM=="usb", ATTR{idVendor}=="046d", ATTR{idProduct}=="c22b", TAG+="systemd", ENV{SYSTEMD_WANTS}+="g15daemon.service" \ No newline at end of file diff --git a/packaging/overlays/v1/README.md b/packaging/overlays/v1/README.md new file mode 100644 index 0000000..2c1b267 --- /dev/null +++ b/packaging/overlays/v1/README.md @@ -0,0 +1,25 @@ +# Local build overlay v1 (2026-09-07) + +Applied only to temporary git archives. Original submodules are never patched. +All upstream Debian quilt series, including the daemon client buffer-overflow +fix and libg15 libusb-1.0 conversion, remain intact. Orig tarballs contain the +unpatched committed source; local upstream changes are additional quilt patches. +Debian metadata changes are carried in each emitted Debian tarball. + +The daemon overlay backports the Debian 1.9.5.3-14.1 t64 control fields and +file renames; the builder compares control and the SONAME override against the +authenticated trixie source archive. G110 hotplug activation is added separately. + +Macro: replace historical compat-5 CDBS with debhelper 13, remove obsolete +configure switches, obsolete watch file and redundant install manifest. Keep +copyright, historical provenance, manual and changelog. Autoreconf regenerates +the stale generated build system. The build patch removes an invalid bare -I +and makes map_gkey's historical implicit int parameter explicit (GCC 14). +Package upstream version is 1.0.5, matching configure.ac, not historical 1.0.3. + +This is build compatibility, NOT a macro-engine security repair. Preexisting +unchecked configuration indices, string copies, recording bounds and incorrect +memory initialization remain. Do not run macro as root or consume untrusted +configuration. Hardware, X11 playback and booted systemd require separate tests. +The imported historical macro archive signature was not verified; its existing +debian/README.source preserves that limitation. No security certification is made. \ No newline at end of file diff --git a/packaging/overlays/v1/apply.sh b/packaging/overlays/v1/apply.sh new file mode 100644 index 0000000..1b81d88 --- /dev/null +++ b/packaging/overlays/v1/apply.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Called with cwd in a disposable git archive, NEVER a submodule. +set -euo pipefail +source=${1:?source} root=${2:?repository} +[[ ! -e .git && -f debian/control ]] || exit 1 +case $source in + libg15|libg15render) ;; + g15daemon) + # shellcheck disable=SC2016 # Debian substvars must remain literal. + sed -i 's/^Build-Depends:$/Build-Depends: dpkg-dev (>= 1.22.5),/; s/libg15daemon-client1/libg15daemon-client1t64/g; /Package: libg15daemon-client1t64/a Provides: ${t64:Provides}\nReplaces: libg15daemon-client1\nBreaks: libg15daemon-client1 (<< ${source:Version})' debian/control + mv debian/libg15daemon-client1.install debian/libg15daemon-client1t64.install + mv debian/libg15daemon-client1.manpages debian/libg15daemon-client1t64.manpages + printf '%s\n' 'libg15daemon-client1t64: package-name-doesnt-match-sonames libg15daemon-client1' > debian/libg15daemon-client1t64.lintian-overrides + cat "$root/packaging/g110.udev" >> debian/g15daemon.udev + ;; + g15macro) + cp "$root/packaging/overlays/v1/g15macro.control" debian/control + cp "$root/packaging/overlays/v1/g15macro.rules" debian/rules + chmod +x debian/rules + rm debian/compat debian/g15macro.install debian/watch + mkdir -p debian/source debian/patches + printf '3.0 (quilt)\n' > debian/source/format + cp "$root/packaging/overlays/v1/g15macro-build.patch" debian/patches/local-build.patch + printf 'local-build.patch\n' > debian/patches/series + ;; + *) exit 1 ;; +esac \ No newline at end of file diff --git a/packaging/overlays/v1/g15macro-build.patch b/packaging/overlays/v1/g15macro-build.patch new file mode 100644 index 0000000..ad62094 --- /dev/null +++ b/packaging/overlays/v1/g15macro-build.patch @@ -0,0 +1,19 @@ +Description: Fix legacy build include switch and implicit-int declaration +Author: G15 packaging contributors +Forwarded: not-needed +Last-Update: 2026-09-07 +--- a/Makefile.am ++++ b/Makefile.am +@@ -11,7 +11,6 @@ + image_DATA = ${top_builddir}/g15macro.wbmp + + LIBS = -Wl,--no-add-needed -lg15daemon_client -lg15render -lX11 -lXtst -lpthread +-IMCLUDES = -I + g15macro_SOURCES = g15macro.c + + +--- a/g15macro.c ++++ b/g15macro.c +@@ -191,1 +191,1 @@ +-int map_gkey(keystate){ ++int map_gkey(int keystate){ diff --git a/packaging/overlays/v1/g15macro.control b/packaging/overlays/v1/g15macro.control new file mode 100644 index 0000000..853f2d6 --- /dev/null +++ b/packaging/overlays/v1/g15macro.control @@ -0,0 +1,15 @@ +Source: g15macro +Section: utils +Priority: optional +Maintainer: G15 packaging contributors +Build-Depends: debhelper-compat (= 13), libxtst-dev, libg15-dev, libg15render-dev, libg15daemon-client-dev +Standards-Version: 4.7.0 +Homepage: https://sourceforge.net/projects/g15tools/ +Rules-Requires-Root: no + +Package: g15macro +Architecture: any +Depends: g15daemon, ${shlibs:Depends}, ${misc:Depends} +Description: Macro recording/playback application for G15Daemon + G15macro records and plays back keyboard macros using the M and G keys + on Logitech G15 keyboards in an X11 session. diff --git a/packaging/overlays/v1/g15macro.rules b/packaging/overlays/v1/g15macro.rules new file mode 100644 index 0000000..d8309f6 --- /dev/null +++ b/packaging/overlays/v1/g15macro.rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +%: + dh $@ diff --git a/packaging/rebuild.sh b/packaging/rebuild.sh new file mode 100644 index 0000000..8493dc6 --- /dev/null +++ b/packaging/rebuild.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Standalone source QA: mount artifacts and this script, NOT the repository. +set -euo pipefail +export LC_ALL=C +[[ -f /.dockerenv && $EUID == 0 ]] || exit 1 +out=$(realpath "${1:?artifact directory}") +(cd "$out" && sha256sum -c SHA256SUMS) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +export DEB_BUILD_OPTIONS=parallel=2 +for source in libg15 libg15render g15daemon g15macro; do + mkdir "$work/$source" + cd "$work/$source" + dsc=("$out/${source}_"*.dsc) + [[ ${#dsc[@]} == 1 ]] + dpkg-source -x "${dsc[0]}" tree + cd tree + apt-get build-dep -y . + dpkg-buildpackage -us -uc -b + cd .. + apt-get install -y ./*.deb +done +echo 'PASS: all four source artifacts rebuilt standalone in dependency order.' diff --git a/packaging/run.sh b/packaging/run.sh new file mode 100644 index 0000000..4fb5c2e --- /dev/null +++ b/packaging/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Host orchestration: no package installation or generated source in workspace. +set -euo pipefail +root=$(cd -- "$(dirname -- "$0")/.." && pwd) +out=$(realpath "${1:?create an empty output directory outside the checkout}") +[[ -d $out && -z $(find "$out" -mindepth 1 -print -quit) ]] || exit 1 +case $out in "$root"|"$root"/*) echo 'Output must be outside the checkout' >&2; exit 1 ;; esac +[[ -d $root/.git ]] || { echo 'Regular clone required (external gitdirs unsupported)' >&2; exit 1; } +image=${G15_BUILD_IMAGE:-g15-packaging} +docker build -t "$image" -f "$root/packaging/Dockerfile" "$root/packaging" +# Allowlist mounts: .vscode and other unrelated local files are not exposed. +mounts=() +for path in .git .gitmodules packaging libg15 libg15render g15daemon g15macro; do + mounts+=(--mount "type=bind,src=$root/$path,dst=/work/$path,readonly") +done +docker run --rm "${mounts[@]}" --mount "type=bind,src=$out,dst=/out" "$image" +docker run --rm --mount "type=bind,src=$out,dst=/out,readonly" \ + --mount "type=bind,src=$root/packaging/rebuild.sh,dst=/rebuild.sh,readonly" \ + "$image" bash -c 'apt-get update && bash /rebuild.sh /out' diff --git a/packaging/test.sh b/packaging/test.sh new file mode 100644 index 0000000..d746db3 --- /dev/null +++ b/packaging/test.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Destructive package lifecycle checks, only in a disposable container. +set -euo pipefail +export LC_ALL=C +[[ -f /.dockerenv && $EUID == 0 ]] || exit 1 +out=$(realpath "${1:?artifact directory required}") +cd "$out" +sha256sum -c SHA256SUMS +packages=(libg15-1 libg15-dev libg15render1 libg15render-dev g15daemon libg15daemon-client1t64 libg15daemon-client-dev g15macro) +distro=() +for package in "${packages[@]}"; do + files=("${package}"_*.deb) + [[ ${#files[@]} == 1 && -f ${files[0]} ]] + [[ $(dpkg-deb -f "${files[0]}" Package) == "$package" ]] + candidate=$(apt-cache madison "$package" | awk 'NR == 1 {print $3}') + if [[ -n $candidate ]]; then + dpkg --compare-versions "$(dpkg-deb -f "${files[0]}" Version)" gt "$candidate" + distro+=("$package=$candidate") + else + [[ $package == g15macro ]] || exit 1 + echo 'g15macro has no trixie candidate; fresh installation only.' + fi +done +lintian --fail-on error ./*.changes +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +multiarch=$(dpkg-architecture -qDEB_HOST_MULTIARCH) +libraries=(libg15 libg15render libg15daemon_client) +mapfile -t allpackages < <(for deb in ./*.deb; do dpkg-deb -f "$deb" Package; done) +purge() { + local package + local -a installed=() + for package in "${allpackages[@]}"; do + if dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed; then + installed+=("$package") + fi + done + if ((${#installed[@]})); then apt-get purge -y "${installed[@]}"; fi +} +smoke() { + local package files + for package in "${packages[@]}"; do + files=("${package}"_*.deb) + [[ $(dpkg-query -W -f='${Version}' "$package") == "$(dpkg-deb -f "${files[0]}" Version)" ]] + done + for binary in /usr/sbin/g15daemon /usr/bin/g15macro; do + ldd "$binary" > "$scratch/ldd" + cat "$scratch/ldd" + if grep -q 'not found' "$scratch/ldd"; then exit 1; fi + done + for library in "${libraries[@]}"; do + readelf -d "/usr/lib/$multiarch/$library.so.1" | grep "SONAME.*$library.so.1" + done + test -f /usr/lib/systemd/system/g15daemon.service + grep -q c22b /usr/lib/udev/rules.d/*g15daemon.rules + printf '#include \n#include \n#include \nint main(void) { g15canvas c; g15r_initCanvas(&c); return g15daemon_version() == 0; }\n' | \ + cc -x c - -Wl,--no-as-needed -lg15 -lg15render -lg15daemon_client -o "$scratch/client-smoke" + "$scratch/client-smoke" + /usr/bin/g15macro --version + /usr/bin/g15macro --help +} +purge +apt-get install -y ./*.deb +smoke +purge +apt-get install -y "${distro[@]}" +for library in "${libraries[@]}"; do + nm -D --defined-only "/usr/lib/$multiarch/$library.so.1" | awk '{print $3}' | sort > "$scratch/$library.before" +done +apt-get install -y ./*.deb +for library in "${libraries[@]}"; do + nm -D --defined-only "/usr/lib/$multiarch/$library.so.1" | awk '{print $3}' | sort > "$scratch/$library.after" + comm -23 "$scratch/$library.before" "$scratch/$library.after" > "$scratch/$library.missing" + test ! -s "$scratch/$library.missing" || { cat "$scratch/$library.missing"; exit 1; } +done +version=$(dpkg-query -W -f='${Version}' libg15daemon-client1t64) +[[ $(dpkg-query -W -f='${Replaces}' libg15daemon-client1t64) == libg15daemon-client1 ]] +[[ $(dpkg-query -W -f='${Breaks}' libg15daemon-client1t64) == "libg15daemon-client1 (<< $version)" ]] +# amd64 retains the old ABI; dpkg generates architecture-dependent t64 Provides. +if [[ $(dpkg --print-architecture) == amd64 ]]; then + [[ $(dpkg-query -W -f='${Provides}' libg15daemon-client1t64) == "libg15daemon-client1 (= $version)" ]] +fi +smoke +purge +test ! -e /usr/lib/systemd/system/g15daemon.service +echo 'PASS: fresh install, distro upgrade, symbols, linking, t64 metadata and purge; no hardware I/O.' \ No newline at end of file