This came out of preparing for GSoC 2026 with AGL.
Automotive Grade Linux runs the infotainment systems in production Mazdas and Subarus. It's backed by most major automakers and compiles entirely from source - kernel, C library, every system tool. I spent five days building an AGL image from scratch, wrote a Flutter app, and baked it into the OS. This is what actually happened.
The Scale of the Problem
You can't sudo apt install agl. AGL is built using Yocto, an industry-standard build system for custom embedded Linux distributions. Yocto doesn't download a pre-built OS. It compiles everything from source: the kernel, the C library, every system tool, the Flutter engine, and the app itself.
My laptop had neither the compute nor the disk space. I spun up a GCP VM:
- Machine: e2-standard-8 (8 vCPUs, 32 GB RAM)
- OS: Ubuntu 22.04 LTS
- Disk: 200 GB
First attempt failed overnight at 74%:
WARNING: The free space is running low (0.823GB left)
ERROR: No new tasks can be executed since the disk space monitor action is "STOPTASKS"
Yocto needs more than 200 GB. I hit a quota limit trying to expand the disk in Asia, deleted the VM, recreated it in us-central1 with 400 GB, and started over.
8 hours later, after 12,145 compilation tasks:
Tasks Summary: Attempted 12145 tasks of which 0 didn't need to be rerun and all succeeded.
I booted it in QEMU:
Automotive Grade Linux 21.90.0 qemux86-64 ttyS0
qemux86-64 login: root
root@qemux86-64:~#
AGL 21.90.0. Codename: vimba. A virtual car computer, inside a cloud VM, in Iowa.
Writing the Flutter App
The app reads /etc/os-release at runtime to display the AGL version, which means the same binary shows Ubuntu values during local development and AGL values on the actual image - no build flags, no conditionals. The relevant field is PRETTY_NAME:
Future<void> _loadAglVersion() async {
final file = File('/etc/os-release');
final contents = await file.readAsString();
final lines = contents.split('\n');
for (final line in lines) {
if (line.startsWith('PRETTY_NAME')) {
setState(() {
_aglVersion = line.split('=')[1].replaceAll('"', '');
});
break;
}
}
}

Flutter app on local machine. For the image: Levi Ackerman from Attack on Titan. The sound button plays an audio clip I will not describe further.
Baking It In: Yocto Layers and Recipes
Yocto builds from "layers" - folders that each contribute something to the final image. AGL ships with layers for its core system, demo apps, and Flutter engine support. To add my app, I created meta-agl-prachi:
meta-agl-prachi/
├── conf/
│ └── layer.conf
└── recipes-apps/
└── agl-quiz-app/
├── agl-quiz-app.bb
└── files/
└── agl_quiz_app.desktop
The .bb file (a "recipe") tells Yocto: where to fetch the source, how to build it, where to install it. Mine pointed to my GitHub repo and used inherit flutter-app, a class provided by meta-flutter that handles all the Flutter-specific build logic.
Then I added my layer to the build and ran:
bitbake agl-ivi-demo-flutter
It finished in minutes. Suspiciously fast, only 5 tasks rerun. Yocto had used cached output from the previous build and skipped my layer entirely.
The Lockfile Problem
I ran bitbake agl-quiz-app in isolation to see what was actually failing:
ERROR: agl-quiz-app-1.0-r0 do_archive_pub_cache:
flutter pub get --enforce-lockfile failed: 1
The error named the failed command. It didn't say where that command came from or how to change it. So I followed the source:
find ~/AGL/master/external/meta-flutter -name "*.bbclass"
# → flutter-app.bbclass
cat flutter-app.bbclass
# → require conf/include/flutter-app.inc
cat flutter-app.inc
# → require conf/include/common.inc
cat common.inc
Three files deep. In common.inc I found it:
if d.getVar("PUBSPEC_IGNORE_LOCKFILE") == "1":
pubspec_lock = os.path.join(app_root, 'pubspec.lock')
if os.path.exists(pubspec_lock):
run_command(d, 'rm -rf pubspec.lock', app_root, env)
# ...later...
run_command(d, 'flutter pub get --enforce-lockfile', app_root, env)
flutter pub get --enforce-lockfile requires the lockfile to exactly match resolved dependencies. My lockfile was generated with a slightly different Dart SDK version than the build VM. The fix was a single line in my recipe:
PUBSPEC_IGNORE_LOCKFILE = "1"
One line. After two days of debugging.
Getting the Display Working
QEMU runs headless by default. To see the AGL UI, I exposed its display over VNC:
runqemu qemux86-64 serialstdio slirp qemuparams="-display vnc=:1"
I opened port 5901 in GCP's firewall and connected with TigerVNC. First connection: the AGL warning screen, rotated 90 degrees. AGL IVI is designed for portrait car dashboards. One more line in weston.ini fixed the backend:
backend=vnc
Getting my app to render required understanding AGL's Wayland setup. The compositor runs as agl-driver (uid 1001). Root cannot access agl-driver's Wayland socket - not a permissions workaround, just how Wayland works. The socket lives at /run/user/1001/wayland-0 and only the user who started the compositor can connect to it.
I found the correct environment by reading the existing service file:
cat /usr/lib/systemd/system/flutter-ics-homescreen.service
Which revealed the exact variables and paths needed. With those:
su agl-driver -s /bin/sh -c '
WAYLAND_DISPLAY=wayland-0
XDG_RUNTIME_DIR=/run/user/1001/
LD_PRELOAD=/usr/lib/librive_text.so
LIBCAMERA_LOG_LEVELS=*:ERROR
flutter-auto -b /usr/share/flutter/agl_quiz_app/3.38.3/release --xdg-shell-app-id agl_quiz_app'
The app launched.
The Result
AGL 21.90.0 (vimba). The version string - "Automotive Grade Linux 21.90.0 (vimba)", pulled live from /etc/os-release at runtime. Sound doesn't come through QEMU yet (that requires additional ALSA configuration), but everything else works.
Takeaways
The most useful thing I practiced here wasn't Flutter or Yocto syntax. It was following require statements until I found the line actually doing the thing. common.inc wasn't linked from anywhere in the docs. Three files of reading got me there. When something breaks in Yocto, the error names the failed task, and that task is a readable function somewhere in the layer files. Start there and keep reading.
The structure itself is simpler than it looks: layers are folders, recipes are config files, classes are reusable logic. The surface area is large but not deep.
The full code and Yocto layer are on GitHub: here



Top comments (0)