Skip to content

Commit 8f6201b

Browse files
authored
Merge pull request #373 from sysprog21/devtools
Add QEMU-based devtools
2 parents 6e9c9c5 + 7f03cd3 commit 8f6201b

15 files changed

Lines changed: 1499 additions & 8 deletions
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
name: devtools-release
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
paths:
7+
- 'devtools/config.defaults'
8+
- 'devtools/kernel.config'
9+
- 'devtools/initramfs/init'
10+
- 'devtools/setup.sh'
11+
- 'devtools/pack-prebuilt.sh'
12+
13+
workflow_dispatch:
14+
15+
permissions:
16+
contents: write
17+
18+
concurrency:
19+
group: devtools-release
20+
cancel-in-progress: true
21+
22+
jobs:
23+
build-and-release:
24+
runs-on: ubuntu-24.04
25+
timeout-minutes: 45
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v6
29+
30+
- name: Install dependencies
31+
run: |
32+
sudo apt-get update
33+
sudo apt-get install -y --no-install-recommends \
34+
build-essential bc flex bison libelf-dev libssl-dev \
35+
cpio rsync
36+
37+
- name: Compute release tag
38+
id: tag
39+
run: |
40+
. devtools/config.defaults
41+
case "$(uname -m)" in
42+
x86_64|amd64) HOST_ARCH=x86_64 ;;
43+
aarch64|arm64) HOST_ARCH=arm64 ;;
44+
*) HOST_ARCH=$(uname -m) ;;
45+
esac
46+
HASH=$(cat \
47+
devtools/kernel.config \
48+
devtools/config.defaults \
49+
devtools/initramfs/init \
50+
devtools/setup.sh \
51+
devtools/pack-prebuilt.sh \
52+
2>/dev/null | sha256sum | cut -c1-8)
53+
TAG="devtools-v${KERNEL_VERSION}-${HOST_ARCH}-${HASH}"
54+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
55+
echo "kernel_version=$KERNEL_VERSION" >> "$GITHUB_OUTPUT"
56+
echo "host_arch=$HOST_ARCH" >> "$GITHUB_OUTPUT"
57+
echo "Release tag: $TAG"
58+
59+
- name: Check if release already exists
60+
id: existing
61+
run: |
62+
if gh release view "${{ steps.tag.outputs.tag }}" >/dev/null 2>&1; then
63+
echo "exists=true" >> "$GITHUB_OUTPUT"
64+
echo "Release ${{ steps.tag.outputs.tag }} already exists, skipping build."
65+
else
66+
echo "exists=false" >> "$GITHUB_OUTPUT"
67+
fi
68+
env:
69+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70+
71+
- name: Restore kernel source cache
72+
if: steps.existing.outputs.exists == 'false'
73+
uses: actions/cache@v5
74+
with:
75+
path: |
76+
devtools/.cache/linux-*.tar.xz
77+
devtools/.cache/linux-*/
78+
key: devtools-kernel-src-${{ runner.os }}-${{ hashFiles('devtools/config.defaults') }}
79+
80+
- name: Restore kernel build cache
81+
if: steps.existing.outputs.exists == 'false'
82+
uses: actions/cache@v5
83+
with:
84+
path: devtools/.cache/kernel-build
85+
key: devtools-kernel-build-${{ runner.os }}-${{ steps.tag.outputs.host_arch }}-${{ hashFiles('devtools/config.defaults', 'devtools/kernel.config') }}
86+
87+
- name: Restore busybox cache
88+
if: steps.existing.outputs.exists == 'false'
89+
uses: actions/cache@v5
90+
with:
91+
path: |
92+
devtools/.cache/busybox-*.tar.bz2
93+
devtools/.cache/busybox-*/
94+
key: devtools-busybox-${{ runner.os }}-${{ hashFiles('devtools/config.defaults') }}
95+
96+
- name: Build kernel and initramfs
97+
if: steps.existing.outputs.exists == 'false'
98+
run: devtools/setup.sh
99+
100+
- name: Pack prebuilt tarball
101+
if: steps.existing.outputs.exists == 'false'
102+
run: devtools/pack-prebuilt.sh
103+
104+
- name: Publish release
105+
if: steps.existing.outputs.exists == 'false'
106+
run: |
107+
TAG="${{ steps.tag.outputs.tag }}"
108+
KVER="${{ steps.tag.outputs.kernel_version }}"
109+
TARBALL=$(ls devtools/.cache/lkmpg-prebuilt-*.tar.xz)
110+
111+
gh release create "$TAG" "$TARBALL" \
112+
--title "devtools: kernel $KVER prebuilt (${{ steps.tag.outputs.host_arch }})" \
113+
--notes "$(cat <<EOF
114+
Prebuilt kernel build output for lkmpg devtools.
115+
116+
- Kernel: $KVER
117+
- Host arch: ${{ steps.tag.outputs.host_arch }}
118+
- Inputs hash: $(echo "$TAG" | sed 's/.*-//')
119+
- Contents: bzImage, kbuild headers/scripts, initramfs
120+
121+
Used automatically by \`devtools/setup.sh\` to skip the ~15 min kernel build.
122+
The kernel source tarball is still downloaded from kernel.org (required for out-of-tree module compilation).
123+
vmlinux is not included; rebuild from source for GDB debugging.
124+
EOF
125+
)" \
126+
--prerelease
127+
env:
128+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/status-check.yaml

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,26 @@ jobs:
1919
detect-changes:
2020
runs-on: ubuntu-24.04
2121
outputs:
22-
examples_changed: ${{ steps.changed-files.outputs.any_changed }}
22+
examples_changed: ${{ steps.changed-examples.outputs.any_changed }}
23+
devtools_changed: ${{ steps.changed-devtools.outputs.any_changed }}
2324
steps:
2425
- name: Checkout code
2526
uses: actions/checkout@v6
2627
- name: Detect changed example files
27-
id: changed-files
28+
id: changed-examples
2829
uses: tj-actions/changed-files@v47
2930
with:
3031
files: |
3132
examples/**
3233
.ci/**
34+
- name: Detect changed devtools files
35+
id: changed-devtools
36+
uses: tj-actions/changed-files@v47
37+
with:
38+
files: |
39+
devtools/**
40+
examples/**
41+
.ci/non-working
3342
3443
lint:
3544
needs: detect-changes
@@ -131,12 +140,95 @@ jobs:
131140
path: ${{ runner.temp }}/status-check-logs
132141
if-no-files-found: ignore
133142

143+
devtools-check:
144+
needs: detect-changes
145+
if: ${{ github.event_name == 'workflow_dispatch' ||
146+
needs.detect-changes.outputs.devtools_changed == 'true' }}
147+
runs-on: ubuntu-24.04
148+
timeout-minutes: 5
149+
steps:
150+
- name: Checkout code
151+
uses: actions/checkout@v6
152+
- name: Install shellcheck
153+
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends shellcheck
154+
- name: Run devtools offline checks
155+
run: devtools/check.sh
156+
157+
devtools-qemu:
158+
needs:
159+
- detect-changes
160+
- devtools-check
161+
if: ${{ github.event_name == 'workflow_dispatch' ||
162+
needs.detect-changes.outputs.devtools_changed == 'true' }}
163+
runs-on: ubuntu-24.04
164+
timeout-minutes: 45
165+
steps:
166+
- name: Checkout code
167+
uses: actions/checkout@v6
168+
- name: Install dependencies
169+
run: |
170+
sudo apt-get update
171+
sudo apt-get install -y --no-install-recommends \
172+
build-essential bc flex bison libelf-dev libssl-dev \
173+
cpio qemu-system-x86
174+
- name: Restore kernel source cache
175+
uses: actions/cache@v5
176+
with:
177+
path: |
178+
devtools/.cache/linux-*.tar.xz
179+
devtools/.cache/linux-*/
180+
key: devtools-kernel-src-${{ runner.os }}-${{ hashFiles('devtools/config.defaults') }}
181+
- name: Restore kernel build cache
182+
uses: actions/cache@v5
183+
with:
184+
path: devtools/.cache/kernel-build
185+
key: devtools-kernel-build-${{ runner.os }}-${{ hashFiles('devtools/config.defaults', 'devtools/kernel.config') }}
186+
restore-keys: |
187+
devtools-kernel-build-${{ runner.os }}-
188+
- name: Restore busybox cache
189+
uses: actions/cache@v5
190+
with:
191+
path: |
192+
devtools/.cache/busybox-*.tar.bz2
193+
devtools/.cache/busybox-*/
194+
key: devtools-busybox-${{ runner.os }}-${{ hashFiles('devtools/config.defaults') }}
195+
- name: Restore initramfs cache
196+
uses: actions/cache@v5
197+
with:
198+
path: |
199+
devtools/.cache/initramfs
200+
devtools/.cache/initramfs.cpio.gz
201+
devtools/.cache/.initramfs-stamp
202+
key: devtools-initramfs-${{ runner.os }}-${{ hashFiles('devtools/config.defaults', 'devtools/initramfs/init') }}
203+
- name: Build kernel and initramfs
204+
run: devtools/setup.sh
205+
- name: Build kernel modules
206+
run: devtools/build-modules.sh
207+
- name: Run QEMU module tests
208+
timeout-minutes: 10
209+
run: devtools/test-modules.sh --no-build
210+
- name: Collect guest log
211+
if: failure()
212+
run: |
213+
mkdir -p "${{ runner.temp }}/devtools-logs"
214+
sudo dmesg --color=never | tail -n 200 \
215+
> "${{ runner.temp }}/devtools-logs/dmesg.log" 2>/dev/null || true
216+
- name: Upload diagnostics
217+
if: failure()
218+
uses: actions/upload-artifact@v7
219+
with:
220+
name: devtools-qemu-logs
221+
path: ${{ runner.temp }}/devtools-logs
222+
if-no-files-found: ignore
223+
134224
validate:
135225
needs:
136226
- detect-changes
137227
- lint
138228
- static-analysis
139229
- build-and-run
230+
- devtools-check
231+
- devtools-qemu
140232
if: ${{ always() }}
141233
runs-on: ubuntu-24.04
142234
steps:
@@ -146,19 +238,32 @@ jobs:
146238
result_lint="${{ needs.lint.result }}"
147239
result_sa="${{ needs.static-analysis.result }}"
148240
result_bar="${{ needs.build-and-run.result }}"
149-
changed="${{ needs.detect-changes.outputs.examples_changed }}"
241+
result_check="${{ needs.devtools-check.result }}"
242+
result_qemu="${{ needs.devtools-qemu.result }}"
243+
examples_changed="${{ needs.detect-changes.outputs.examples_changed }}"
244+
devtools_changed="${{ needs.detect-changes.outputs.devtools_changed }}"
150245
151246
if [[ "${result_detect}" != "success" ]]; then
152247
echo "Change-detection job did not succeed: ${result_detect}"
153248
exit 1
154249
fi
155250
156-
# When examples changed (or manual dispatch), jobs must succeed
157-
if [[ "${changed}" == "true" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
251+
# When examples changed (or manual dispatch), lint/analysis/build jobs must succeed
252+
if [[ "${examples_changed}" == "true" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
158253
for r in "${result_lint}" "${result_sa}" "${result_bar}"; do
159254
if [[ "${r}" != "success" ]]; then
160255
echo "Required job did not succeed: ${r}"
161256
exit 1
162257
fi
163258
done
164259
fi
260+
261+
# When devtools or examples changed (or manual dispatch), devtools jobs must succeed
262+
if [[ "${devtools_changed}" == "true" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
263+
for r in "${result_check}" "${result_qemu}"; do
264+
if [[ "${r}" != "success" ]]; then
265+
echo "devtools job did not succeed: ${r}"
266+
exit 1
267+
fi
268+
done
269+
fi

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ lkmpg.synctex.gz
4040

4141
# format checks
4242
expected-format
43+
44+
# devtools build cache and local overrides
45+
devtools/.cache/
46+
devtools/config.local

devtools/boot.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
3+
# devtools/boot.sh -- launch QEMU with the lkmpg kernel and initramfs.
4+
# The project root is shared into the guest via 9p virtfs at /mnt/lkmpg/.
5+
#
6+
# Usage:
7+
# devtools/boot.sh # interactive shell
8+
# devtools/boot.sh --gdb # wait for GDB on localhost:1234
9+
# devtools/boot.sh --test CMD # run CMD in guest, exit with its status
10+
# devtools/boot.sh -- EXTRA # pass extra args to QEMU
11+
12+
set -euo pipefail
13+
14+
DEVTOOLS_DIR="$(cd "$(dirname "$0")" && pwd)"
15+
PROJECT_ROOT="$(cd "$DEVTOOLS_DIR/.." && pwd)"
16+
. "$DEVTOOLS_DIR/config.defaults"
17+
[ -f "$DEVTOOLS_DIR/config.local" ] && . "$DEVTOOLS_DIR/config.local"
18+
19+
die() { echo "ERROR: $*" >&2; exit 1; }
20+
21+
base64_nowrap() {
22+
base64 | tr -d '\n'
23+
}
24+
25+
# Parse arguments before expensive checks so --help works without setup
26+
GDB_MODE=0
27+
TEST_CMD=""
28+
EXTRA_QEMU_ARGS=()
29+
30+
while [ $# -gt 0 ]; do
31+
case "$1" in
32+
--gdb) GDB_MODE=1; shift ;;
33+
--test) shift; TEST_CMD="${1:?--test requires a command}"; shift ;;
34+
--) shift; EXTRA_QEMU_ARGS=("$@"); break ;;
35+
-h|--help)
36+
echo "Usage: $0 [--gdb] [--test CMD] [-- QEMU_ARGS...]"
37+
exit 0 ;;
38+
*) die "Unknown option: $1" ;;
39+
esac
40+
done
41+
42+
BZIMAGE="$KERNEL_BUILD/arch/x86/boot/bzImage"
43+
[ -f "$BZIMAGE" ] || die "Kernel not built. Run devtools/setup.sh first."
44+
[ -f "$INITRAMFS_CPIO" ] || die "Initramfs not built. Run devtools/setup.sh first."
45+
command -v "$QEMU_BIN" >/dev/null 2>&1 || die "$QEMU_BIN not found. Install QEMU."
46+
47+
# Kernel command line
48+
KCMD="console=ttyS0 loglevel=7 nokaslr"
49+
if [ -n "$TEST_CMD" ]; then
50+
# Base64-encode the command to survive kernel cmdline word-splitting.
51+
# Strip newlines portably so this works with both GNU and BSD base64.
52+
# The init script decodes it before execution.
53+
KCMD="$KCMD lkmpg.cmd64=$(printf '%s' "$TEST_CMD" | base64_nowrap)"
54+
fi
55+
56+
# Build QEMU arguments
57+
QEMU_ARGS=(
58+
-kernel "$BZIMAGE"
59+
-initrd "$INITRAMFS_CPIO"
60+
-nographic
61+
-m "$QEMU_MEM"
62+
-smp "$QEMU_SMP"
63+
-no-reboot
64+
-virtfs "local,id=lkmpg,path=$PROJECT_ROOT,security_model=none,mount_tag=lkmpg"
65+
-append "$KCMD"
66+
)
67+
68+
# KVM acceleration (Linux only, when available)
69+
if [ -w /dev/kvm ] 2>/dev/null; then
70+
QEMU_ARGS+=(-enable-kvm -cpu host)
71+
fi
72+
73+
# GDB mode: start paused, waiting for debugger
74+
if [ "$GDB_MODE" -eq 1 ]; then
75+
QEMU_ARGS+=(-gdb "tcp::$QEMU_GDB_PORT" -S)
76+
echo "QEMU waiting for GDB on localhost:$QEMU_GDB_PORT"
77+
if [ -f "$KERNEL_BUILD/vmlinux" ]; then
78+
echo "Connect with: gdb $KERNEL_BUILD/vmlinux -ex 'target remote :$QEMU_GDB_PORT'"
79+
else
80+
echo "WARNING: vmlinux not found (prebuilt kernels exclude it for size)."
81+
echo "Rebuild from source for full GDB support: LKMPG_NO_PREBUILT=1 devtools/setup.sh"
82+
echo "Connect with: gdb -ex 'target remote :$QEMU_GDB_PORT'"
83+
fi
84+
fi
85+
86+
# Append any extra user-provided QEMU args
87+
QEMU_ARGS+=("${EXTRA_QEMU_ARGS[@]+"${EXTRA_QEMU_ARGS[@]}"}")
88+
89+
exec "$QEMU_BIN" "${QEMU_ARGS[@]}"

0 commit comments

Comments
 (0)