From 795db7924bd170651cb194ff8d35c4104a62b5e0 Mon Sep 17 00:00:00 2001 From: Mikhail Yevchenko Date: Mon, 7 Sep 2026 15:44:03 +0300 Subject: [PATCH] Integrate G15 submodules with Debian 13 packaging and devcontainer --- .devcontainer/.gitignore | 2 + .devcontainer/compose.yaml | 9 ++ .devcontainer/devcontainer.json | 9 ++ .devcontainer/initialize.sh | 23 +++++ .devcontainer/test.sh | 62 ++++++++++++ .devcontainer/usb.sh | 30 ++++++ .github/workflows/debian.yml | 72 ++++++++++++++ .gitignore | 3 + .gitmodules | 15 +++ README.md | 77 +++++++++++++++ g15daemon | 1 + g15macro | 1 + g15stats | 1 + libg15 | 1 + libg15render | 1 + packaging/Dockerfile | 8 ++ packaging/NOTES.md | 106 +++++++++++++++++++++ packaging/build.sh | 63 ++++++++++++ packaging/container-build.sh | 7 ++ packaging/g110.udev | 3 + packaging/overlays/v1/README.md | 25 +++++ packaging/overlays/v1/apply.sh | 27 ++++++ packaging/overlays/v1/g15macro-build.patch | 19 ++++ packaging/overlays/v1/g15macro.control | 15 +++ packaging/overlays/v1/g15macro.rules | 6 ++ packaging/rebuild.sh | 23 +++++ packaging/run.sh | 19 ++++ packaging/test.sh | 86 +++++++++++++++++ 28 files changed, 714 insertions(+) create mode 100644 .devcontainer/.gitignore create mode 100644 .devcontainer/compose.yaml create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/initialize.sh create mode 100644 .devcontainer/test.sh create mode 100644 .devcontainer/usb.sh create mode 100644 .github/workflows/debian.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 160000 g15daemon create mode 160000 g15macro create mode 160000 g15stats create mode 160000 libg15 create mode 160000 libg15render create mode 100644 packaging/Dockerfile create mode 100644 packaging/NOTES.md create mode 100644 packaging/build.sh create mode 100644 packaging/container-build.sh create mode 100644 packaging/g110.udev create mode 100644 packaging/overlays/v1/README.md create mode 100644 packaging/overlays/v1/apply.sh create mode 100644 packaging/overlays/v1/g15macro-build.patch create mode 100644 packaging/overlays/v1/g15macro.control create mode 100644 packaging/overlays/v1/g15macro.rules create mode 100644 packaging/rebuild.sh create mode 100644 packaging/run.sh create mode 100644 packaging/test.sh 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