Integrate G15 submodules with Debian 13 packaging and devcontainer

This commit is contained in:
Mikhail Yevchenko committed 2026-09-07 15:44:03 +03:00
commit 795db7924b
28 files changed
+714

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
compose.usb.yaml
compose.usb.yaml.tmp.*
+9
View File
@@ -0,0 +1,9 @@
services:
dev:
image: g15-packaging:latest
build:
context: ../packaging
dockerfile: Dockerfile
command: ["sleep", "infinity"]
volumes:
- ..:/work
+9
View File
@@ -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"
}
+23
View File
@@ -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"
+62
View File
@@ -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"'
+30
View File
@@ -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
}