From GCC to BitBake: How to Compile and Install a C Program the Traditional Way vs Yocto

If you've been writing embedded C for years, you know the drill โ write code, run gcc, flash the binary. Simple.
But when you move to a Yocto-based embedded Linux project, that familiar workflow disappears. Suddenly everyone's talking about recipes, layers, and BitBake. It feels like a lot of ceremony for something that used to take one command.
In this post I'll show you both approaches side by side โ traditional compilation and the Yocto way โ so the Yocto method actually makes sense instead of feeling like magic.
Our Example Program
Let's keep it simple. We'll compile and install this C program:
/* hello_embedded.c */
#include <stdio.h>
int main(void)
{
printf("Hello from embedded Linux!\n");
return 0;
}
Goal: compile it, install the binary to /usr/bin/hello_embedded, and make it runnable on our target device.
Method 1: The Traditional Way (Native GCC)
This is what every embedded C developer knows. On your host machine or directly on the target:
Step 1: Compile
gcc hello_embedded.c -o hello_embedded
That's it. GCC compiles your source file and produces an executable binary.
Step 2: Cross-compile for your target (ARM, MIPS, etc.)
If your target is an ARM board, you need a cross-compiler:
arm-linux-gnueabihf-gcc hello_embedded.c -o hello_embedded
Step 3: Install manually
Copy the binary to your target device and place it in /usr/bin:
# Copy to target via SSH
scp hello_embedded root@192.168.1.100:/usr/bin/
# Or manually set permissions
chmod 755 hello_embedded
cp hello_embedded /usr/bin/hello_embedded
Step 4: Run it
# On your target device
hello_embedded
# Output: Hello from embedded Linux!
That's the traditional method. Fast, direct, familiar. Works great for a single program on a single device.
The Problem with the Traditional Method at Scale
The traditional method breaks down when:
- You have 50+ packages to build and install
- You need reproducible builds across a team
- You need to track exact versions of every dependency
- You need to rebuild the whole image from scratch six months later
- You need licence compliance across all packages
This is exactly why Yocto exists.
Method 2: The Yocto Way
In Yocto, you don't compile manually. Instead, you write a recipe โ a .bb file that tells BitBake how to fetch, compile, and install your program automatically.
Your Project Structure
meta-mylayer/
โโโ conf/
โ โโโ layer.conf
โโโ recipes-example/
โโโ hello-embedded/
โโโ hello-embedded_1.0.bb โ your recipe
โโโ files/
โโโ hello_embedded.c โ your source file
Step 1: Create the layer (if you don't have one)
bitbake-layers create-layer meta-mylayer
cd meta-mylayer
Step 2: Create the recipe file
Create recipes-example/hello-embedded/hello-embedded_1.0.bb:
# Recipe: hello-embedded
# Description: Simple Hello World for embedded Linux
SUMMARY = "Hello Embedded โ a simple demo application"
DESCRIPTION = "Demonstrates how to compile and install a C program using Yocto"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
# Tell BitBake where to find our source file
SRC_URI = "file://hello_embedded.c"
# Where BitBake puts the source during build
S = "${WORKDIR}"
# Compile step โ this is your gcc command
do_compile() {
\({CC} \){CFLAGS} ${LDFLAGS} hello_embedded.c -o hello_embedded
}
# Install step โ equivalent to cp + chmod
do_install() {
install -d \({D}\){bindir}
install -m 0755 hello_embedded \({D}\){bindir}/hello_embedded
}
Breaking it down line by line:
| Line | What it does |
|---|---|
SUMMARY |
Human-readable description of your package |
LICENSE |
Licence type โ important for GPL compliance |
LIC_FILES_CHKSUM |
MD5 checksum of the licence file โ Yocto verifies this |
SRC_URI |
Where to find your source โ local file, Git repo, tarball |
S = "${WORKDIR}" |
Tells BitBake where source files land during build |
${CC} |
Cross-compiler automatically set by Yocto for your target |
${CFLAGS} |
Compiler flags set by your machine configuration |
${D} |
The destination directory (staging area before packaging) |
${bindir} |
Resolves to /usr/bin on the target |
install -d |
Creates the directory if it doesn't exist |
install -m 0755 |
Copies file and sets permissions (rwxr-xr-x) |
Step 3: Add your layer to bblayers.conf
bitbake-layers add-layer /path/to/meta-mylayer
Or manually edit conf/bblayers.conf:
BBLAYERS ?= " \
/path/to/poky/meta \
/path/to/poky/meta-poky \
/path/to/meta-mylayer \
"
Step 4: Build your recipe
bitbake hello-embedded
BitBake will:
- Find your recipe
- Fetch the source file
- Run
do_compile()using the correct cross-compiler for your target - Run
do_install()to stage the binary - Package it into a
.ipkor.rpmfile
Step 5: Include it in your image
Add your package to your image recipe (core-image-minimal.bbappend or your custom image):
IMAGE_INSTALL:append = " hello-embedded"
Then rebuild your image:
bitbake core-image-minimal
Your binary is now baked into the image at /usr/bin/hello_embedded โ ready to flash to your target device.
Traditional vs Yocto โ Side by Side
| Step | Traditional (GCC) | Yocto (BitBake) |
|---|---|---|
| Compile | gcc hello.c -o hello |
do_compile() in recipe |
| Cross-compile | arm-linux-gnueabihf-gcc ... |
${CC} โ automatically set |
| Install | cp hello /usr/bin/ |
do_install() with install -m |
| Permissions | chmod 755 |
install -m 0755 |
| Reproducibility | Manual, error-prone | Automatic, version-controlled |
| Scale | Good for 1โ5 packages | Essential for 50+ packages |
| Licence tracking | Manual | Built into recipe (LIC_FILES_CHKSUM) |
Bonus: Creating a Kernel Recipe
Yocto also lets you customise the Linux kernel using a kernel recipe. Here's how it works:
Adding a kernel configuration fragment
The most common task โ enabling or disabling a kernel config option without replacing the entire kernel recipe:
Create recipes-kernel/linux/linux-yocto_%.bbappend:
# Kernel recipe append โ customise without replacing the whole kernel recipe
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
# Add our custom kernel config fragment
SRC_URI:append = " file://custom-driver.cfg"
Create recipes-kernel/linux/files/custom-driver.cfg:
# Enable our custom driver
CONFIG_MY_CUSTOM_DRIVER=y
CONFIG_USB_SERIAL=y
Full custom kernel recipe (advanced)
If you need to use a completely custom kernel:
# linux-custom_5.15.bb
SUMMARY = "Custom Linux kernel for our embedded board"
LICENSE = "GPL-2.0-only"
inherit kernel
# Point to your kernel source โ Git repo with specific branch/tag
SRC_URI = "git://github.com/myorg/linux-custom.git;branch=v5.15-custom;protocol=https"
SRCREV = "abc123def456" # exact commit hash โ ensures reproducibility
S = "${WORKDIR}/git"
# Point to your defconfig
KERNEL_DEFCONFIG = "my_board_defconfig"
# Compatible machines
COMPATIBLE_MACHINE = "my-board"
Key kernel recipe concepts:
| Concept | What it does |
|---|---|
inherit kernel |
Pulls in all Yocto kernel build logic automatically |
SRCREV |
Pins the exact Git commit โ critical for reproducible builds |
KERNEL_DEFCONFIG |
Your board's default kernel configuration |
COMPATIBLE_MACHINE |
Restricts recipe to specific hardware targets |
.bbappend |
Extends existing recipe without replacing it โ always prefer this |
.cfg fragment |
Small config file that patches your kernel config cleanly |
The Mental Model to Remember
Once you understand this mapping, Yocto stops feeling mysterious:
Traditional Yocto equivalent
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
gcc hello.c -o hello โ do_compile()
cp hello /usr/bin/ โ do_install() + install -m 0755
chmod 755 โ install -m 0755
Makefile โ .bb recipe file
make install โ bitbake <recipe-name>
Yocto isn't doing anything fundamentally different from what you do manually. It's automating it, versioning it, and making it reproducible โ for every package, every time.
What's Next
Now that you understand the basics of a Yocto recipe, the natural next steps are:
- Fetching from a Git repo instead of a local file (
SRC_URI = "git://...") - Using a Makefile in your recipe (
inherit autotoolsor manualdo_compile) - Writing a
.bbappendto customise an existing community recipe without forking it
I'll cover these in upcoming posts.
Found this useful? Follow the blog for more practical embedded Linux and Yocto guides. Questions or corrections? Drop them in the comments below.


