Skip to main content

Command Palette

Search for a command to run...

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

Updated
โ€ข7 min read
From GCC to BitBake: How to Compile and Install a C Program the Traditional Way vs Yocto
S
Hi! I'm Sangeetha ๐Ÿ‘‹ โ€” embedded engineer, Yocto survivor, and firmware security nerd. This is where I turn a decade of hard-won embedded knowledge into posts you can actually use.

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:

  1. Find your recipe
  2. Fetch the source file
  3. Run do_compile() using the correct cross-compiler for your target
  4. Run do_install() to stage the binary
  5. Package it into a .ipk or .rpm file

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 autotools or manual do_compile)
  • Writing a .bbappend to 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.

1 views