Menyiapkan CI untuk proyek Rust kernel-style dengan QEMU dan Cargo berbeda dari aplikasi Rust biasa. Anda tidak hanya memeriksa fmt, clippy, dan build, tetapi juga harus memastikan image boot benar-benar bisa dijalankan di emulator, berhenti dengan exit code yang bisa diverifikasi, dan menghasilkan log yang cukup saat gagal.

Untuk proyek low-level yang terinspirasi dari pendekatan seperti linux-0.11-rs, tujuan CI bukan sekadar “build hijau”, melainkan release confidence: perubahan kecil pada linker script, target custom, kode boot, atau layout image harus cepat terdeteksi. Cara paling praktis adalah memisahkan pemeriksaan cepat untuk semua commit dan pemeriksaan emulasi yang lebih lambat untuk branch utama, pull request, atau rilis.

Tujuan pipeline CI pada proyek Rust low-level

Pada proyek kernel-style, bug sering muncul di area yang tidak disentuh pipeline aplikasi biasa: inisialisasi awal, layout memori, format image boot, interaksi dengan bootloader, atau asumsi perangkat virtual. Karena itu, pipeline CI sebaiknya memverifikasi beberapa lapisan berikut:

  • Kualitas kode cepat: cargo fmt, cargo clippy, dan build dasar.
  • Kompilasi target khusus: termasuk no_std, target JSON, atau arsitektur non-host bila relevan.
  • Pembuatan artefak bootable: ELF, binary mentah, disk image, atau image yang dibungkus skrip build.
  • Smoke test boot di QEMU: memverifikasi kernel benar-benar mulai, mengeluarkan sinyal lulus, lalu keluar dengan kode yang bisa dicek CI.
  • Diagnostik saat gagal: log serial, artefak image, dan informasi build agar kegagalan bisa direproduksi lokal.

Jika semua ini digabung dalam satu job besar, feedback ke developer akan lambat. Praktik yang lebih sehat adalah memisahkan job menjadi fast checks dan emulation checks.

Struktur workflow yang disarankan

1. Fast checks untuk feedback cepat

Job ini berjalan pada hampir semua push dan pull request. Tujuannya memfilter kesalahan murah lebih dulu:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets
  • cargo build untuk crate host-side seperti tool pembuat image
  • cargo build --target ... untuk kernel atau komponen no_std

Manfaatnya sederhana: sebagian besar kesalahan sintaks, lint, impor, atau build target akan terdeteksi dalam hitungan menit tanpa menunggu QEMU.

2. Emulation checks untuk validasi boot

Job ini menjalankan image hasil build di QEMU. Cakupannya bisa dimulai dari smoke test paling kecil:

  1. Build kernel atau image bootable.
  2. Jalankan QEMU dalam mode non-grafis.
  3. Tangkap output serial ke file log.
  4. Tunggu sinyal sukses, atau biarkan QEMU keluar dengan kode tertentu.
  5. Fail jika timeout, panic, triple fault, atau tidak ada sinyal sukses.

Untuk tim kecil, ini biasanya sudah cukup baik sebagai baseline. Anda tidak perlu langsung membuat test matrix yang berat bila target utama Anda adalah memastikan “kernel masih bisa boot”.

Menangani target khusus dan crate no_std

Proyek kernel-style sering memakai target custom atau setidaknya target non-default. Di sini ada beberapa pola umum:

  • Kernel crate memakai #![no_std] dan mungkin #![no_main].
  • Host tool tetap memakai target host normal untuk membangun image, menggabungkan binary, atau menjalankan langkah packaging.
  • Build script tambahan dipakai untuk menyalin artefak, menjalankan objcopy, atau menyusun image disk.

Pemisahan ini penting untuk CI karena dependensinya berbeda. Host tool bisa diuji dengan cargo test biasa, sedangkan kernel biasanya diverifikasi lewat cargo build dan QEMU.

Jika Anda memakai target JSON atau toolchain tambahan, buat setup CI yang eksplisit. Jangan mengandalkan lingkungan runner seolah sudah berisi semua komponen. Untuk proyek low-level, kegagalan “works on my machine” sering terjadi karena:

  • target belum ditambahkan
  • komponen rust-src belum tersedia bila dibutuhkan
  • tool eksternal seperti qemu-system-*, objcopy, atau nasm belum terpasang
  • skrip build mengasumsikan path lokal tertentu

Contoh langkah build yang umum

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo build --workspace
cargo build -p kernel --target x86_64-unknown-none
./scripts/build-image.sh

Nama target dan skrip tentu harus disesuaikan dengan proyek Anda. Poin utamanya adalah host build dan kernel build diperlakukan sebagai langkah terpisah.

Membangun image boot dan merancang smoke test QEMU

Prinsip smoke test yang efektif

Smoke test terbaik untuk CI bukan test yang paling canggih, melainkan test yang deterministik, cepat, dan mudah di-debug. Untuk kernel-style project, pola yang paling umum adalah:

  • kernel mencetak penanda awal ke serial, misalnya BOOT_OK atau TEST_PASS
  • kernel kemudian memicu jalur keluar yang bisa diterjemahkan menjadi status sukses di CI
  • bila terjadi panic, kernel mencetak panic message ke serial sebelum berhenti

Output serial jauh lebih berguna dibanding hanya melihat bahwa QEMU hang. Karena itu, konfigurasikan kernel agar log boot minimal bisa dikirim ke port serial virtual.

Mengapa serial log penting

Pada lingkungan CI, Anda tidak punya monitor grafis atau debugger interaktif secara default. Output serial memberi tiga keuntungan:

  • mudah ditangkap ke file
  • mudah diunggah sebagai artifact
  • mudah dipakai sebagai dasar keputusan pass/fail dengan grep atau parser sederhana

Contoh command QEMU untuk smoke test

timeout 30s qemu-system-x86_64 \
  -nographic \
  -serial file:qemu.log \
  -drive format=raw,file=build/os.img \
  -no-reboot \
  -no-shutdown

Contoh di atas sengaja generik. Opsi detail seperti mesin, CPU, memory, kernel loading method, atau device tambahan harus mengikuti kebutuhan proyek Anda. Yang penting untuk CI adalah:

  • -nographic agar QEMU tidak butuh display
  • serial ke file agar log bisa dianalisis
  • timeout agar job tidak menggantung tanpa batas
  • kontrol reboot/shutdown agar kegagalan tidak tersembunyi oleh restart otomatis

Menentukan kondisi lulus atau gagal

Ada dua pendekatan umum:

  1. Berdasarkan isi log: CI memeriksa apakah qemu.log mengandung penanda sukses seperti TEST_PASS.
  2. Berdasarkan exit code: kernel atau test harness memicu mekanisme keluar yang menghasilkan status proses yang bisa diverifikasi.

Pendekatan log lebih mudah diterapkan lebih awal. Pendekatan exit code lebih rapi jika proyek Anda sudah punya test harness kernel sendiri. Dalam praktiknya, banyak tim memakai keduanya: log untuk diagnosis, exit code untuk keputusan pass/fail.

grep -q "TEST_PASS" qemu.log

Jika Anda memilih verifikasi berbasis log, buat penanda yang unik dan stabil. Hindari string terlalu umum yang bisa muncul di panic path atau output boot biasa.

Contoh workflow GitHub Actions

Berikut contoh struktur workflow yang memisahkan job cepat dan job emulasi. Contoh ini tidak mengasumsikan detail spesifik proyek, tetapi cukup realistis untuk dijadikan fondasi.

name: ci

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  fast-checks:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: Install build dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y qemu-system-x86 binutils

      - name: Format
        run: cargo fmt --all -- --check

      - name: Clippy
        run: cargo clippy --workspace --all-targets -- -D warnings

      - name: Build workspace
        run: cargo build --workspace

      - name: Build kernel target
        run: cargo build -p kernel --target x86_64-unknown-none

  boot-smoke:
    runs-on: ubuntu-latest
    needs: fast-checks
    timeout-minutes: 10
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: Install runtime dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y qemu-system-x86 binutils

      - name: Build boot image
        run: ./scripts/build-image.sh

      - name: Run QEMU smoke test
        run: |
          set -euxo pipefail
          timeout 30s qemu-system-x86_64 \
            -nographic \
            -serial file:qemu.log \
            -drive format=raw,file=build/os.img \
            -no-reboot \
            -no-shutdown || true

          test -f qemu.log
          grep -q "TEST_PASS" qemu.log

      - name: Upload logs
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: qemu-logs
          path: |
            qemu.log
            build/

Ada beberapa hal yang perlu diperhatikan dari contoh ini:

  • needs: fast-checks mencegah job QEMU berjalan bila langkah dasar sudah gagal.
  • timeout-minutes pada level job dan timeout 30s pada command sama-sama berguna. Timeout job mencegah deadlock besar; timeout command membatasi proses QEMU itu sendiri.
  • || true dipakai agar workflow tetap sempat memeriksa log dan mengunggah artifact. Tanpa ini, proses bisa langsung berhenti sebelum file diagnostik dikumpulkan.
  • artifact upload dengan if: always() sangat penting untuk investigasi kegagalan.

Jika proyek Anda memakai target JSON atau komponen toolchain tertentu, tambahkan langkah setup yang eksplisit sebelum build.

Caching dependency tanpa membuat CI rapuh

Caching pada Rust bisa menghemat waktu cukup besar, terutama bila workspace berisi host tools, builder, dan kernel crate. Namun, cache yang terlalu agresif dapat menyembunyikan masalah atau membuat hasil sulit direproduksi.

Apa yang layak di-cache

  • registry crate Cargo
  • git dependency Cargo
  • hasil kompilasi target directory, bila strategi cache Anda mendukungnya dengan aman

Untuk GitHub Actions, pendekatan yang umum adalah memakai action cache yang memang dibuat untuk Rust agar key cache mengikuti perubahan yang relevan seperti Cargo.lock.

Trade-off caching

  • Lebih cepat: build incremental lebih pendek.
  • Lebih rumit: cache bisa invalid atau kurang efektif saat target banyak.
  • Berpotensi bias: bug setup dependency kadang tidak terlihat karena tertolong cache.

Untuk tim kecil, pilihan yang masuk akal adalah:

  • cache dependency dan build host-side
  • jangan terlalu mengoptimalkan sampai pipeline sulit dipahami
  • sesekali jalankan build bersih pada branch utama atau workflow terjadwal untuk memastikan setup tetap valid dari nol

Pemisahan fast checks vs emulation checks

Ini salah satu keputusan paling penting dalam CI untuk proyek Rust kernel-style dengan QEMU dan Cargo. Bila semua pemeriksaan dijalankan untuk setiap commit kecil, feedback loop akan lambat. Bila QEMU jarang dijalankan, Anda kehilangan jaminan bahwa image masih bisa boot.

Strategi yang realistis untuk tim kecil

  • Setiap pull request: fmt, clippy, build host tool, build kernel target, dan satu smoke test boot.
  • Branch utama: tambahkan satu atau dua variasi emulasi tambahan bila memang dibutuhkan.
  • Rilis atau nightly: jalankan verifikasi lebih luas, misalnya kombinasi image berbeda, mode debug, atau test scenario tambahan.

Pendekatan ini menjaga dua hal sekaligus: developer tetap mendapat feedback cepat, dan branch utama tetap punya tingkat kepercayaan yang layak.

Kapan menambah cakupan verifikasi

Tambahkan tes emulasi lebih luas jika proyek Anda sudah sering gagal di area berikut:

  • perubahan bootloader atau format image
  • inisialisasi memori awal
  • driver perangkat virtual dasar
  • regresi panic handler atau output serial

Jangan menambah cakupan hanya karena “terlihat lebih lengkap”. Pada proyek low-level, setiap tes tambahan punya biaya waktu dan maintenance yang nyata.

Strategi debugging saat boot test gagal di CI

Kegagalan paling umum pada boot test biasanya terlihat seperti salah satu pola berikut:

  • QEMU timeout tanpa output berarti
  • log berhenti sangat awal
  • panic terjadi sebelum penanda sukses
  • image tidak ditemukan atau format image salah
  • QEMU langsung exit karena opsi runtime tidak cocok

1. Pastikan log serial benar-benar ada

Jika qemu.log kosong, masalahnya bisa berada di beberapa titik:

  • kernel belum menginisialisasi serial
  • jalur boot gagal sebelum serial aktif
  • opsi QEMU salah sehingga output tidak diarahkan ke file yang diharapkan

Mulailah dari penanda paling awal yang mungkin, misalnya satu karakter atau string pendek segera setelah entry point relevan dijalankan.

2. Unggah artefak yang cukup

Jangan hanya unggah qemu.log. Bila memungkinkan, unggah juga:

  • image boot hasil build
  • binary kernel atau ELF
  • map file atau symbol file jika tersedia
  • log build image dari skrip packaging

Ini membantu membedakan apakah masalahnya ada pada kompilasi, packaging image, atau runtime QEMU.

3. Cetak command akhir yang dijalankan CI

Kesalahan path file image atau nama target sangat sering terjadi. Gunakan shell yang ketat seperti:

set -euxo pipefail

Dengan ini, command yang dijalankan akan terlihat jelas di log GitHub Actions dan pipeline akan gagal lebih cepat jika ada variabel kosong atau command antara yang rusak.

4. Bedakan timeout dengan panic

Timeout biasanya berarti salah satu dari dua hal:

  • kernel hang atau masuk loop tak berujung
  • kondisi sukses tidak pernah tercapai walau boot sebagian berhasil

Sebaliknya, panic yang masih sempat mencetak log biasanya lebih mudah ditangani. Karena itu, panic handler yang mengirim pesan ke serial sangat membantu pada CI.

5. Reproduksi lokal dengan command yang sama

Salah satu kebiasaan terbaik adalah menjaga agar command QEMU di CI juga bisa dipakai developer secara lokal. Misalnya, definisikan satu skrip:

./scripts/run-qemu-smoke.sh

Lalu panggil skrip yang sama dari GitHub Actions. Dengan begitu, Anda mengurangi perbedaan antara lingkungan lokal dan CI.

Common mistakes yang sering muncul

  • Menganggap build sukses berarti boot sukses. Pada proyek kernel-style, ini asumsi yang salah.
  • Tidak memberi timeout. Satu hang di QEMU bisa menghabiskan menit CI tanpa informasi berguna.
  • Tidak menangkap serial log. Akibatnya semua kegagalan terlihat sama: job merah tanpa konteks.
  • Menggabungkan semua verifikasi dalam satu job. Feedback lambat dan sulit tahu titik gagalnya.
  • Terlalu banyak matrix sejak awal. Pipeline jadi mahal dan rapuh sebelum kebutuhan nyatanya jelas.
  • Tidak mengunggah artifact saat gagal. Ini memperlambat investigasi secara signifikan.

Rekomendasi baseline yang praktis

Jika Anda baru mulai, baseline berikut biasanya cukup seimbang:

  1. Job fast-checks: fmt, clippy, build workspace, build kernel target.
  2. Job boot-smoke: buat image, jalankan QEMU non-grafis, tangkap serial log, fail jika penanda sukses tidak muncul dalam timeout yang wajar.
  3. Upload artifact untuk log dan image pada setiap kegagalan.
  4. Gunakan cache Cargo, tetapi tetap sediakan jalur build bersih sesekali.
  5. Pakailah skrip build dan run yang sama antara lokal dan CI.

Dengan baseline ini, Anda sudah menutup banyak sumber regresi yang khas pada proyek Rust low-level tanpa membebani tim kecil dengan infrastruktur test yang terlalu besar.

Penutup

Pipeline CI untuk proyek Rust kernel-style dengan QEMU dan Cargo sebaiknya dirancang untuk dua kebutuhan sekaligus: feedback cepat bagi developer dan kepercayaan bahwa image benar-benar bisa boot. Kombinasi job cepat, build target khusus, image boot yang terotomasi, smoke test QEMU dengan timeout, verifikasi log atau exit code, serta artifact diagnostik memberi hasil yang jauh lebih berguna daripada sekadar menjalankan cargo build.

Untuk tim kecil, fokuslah pada alur yang sederhana namun konsisten. Mulai dari smoke test boot yang deterministik, pastikan log mudah dibaca saat gagal, lalu tambah cakupan verifikasi hanya ketika benar-benar dibutuhkan oleh pola bug proyek Anda.