Integrate G15 submodules with Debian 13 packaging and devcontainer
This commit is contained in:
commit
795db7924b
28 files changed
+714
No files matched your search
@@ -0,0 +1,2 @@
|
||||
compose.usb.yaml
|
||||
compose.usb.yaml.tmp.*
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
dev:
|
||||
image: g15-packaging:latest
|
||||
build:
|
||||
context: ../packaging
|
||||
dockerfile: Dockerfile
|
||||
command: ["sleep", "infinity"]
|
||||
volumes:
|
||||
- ..:/work
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"'
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user