Zephyr West Workspace: A Reproducible Dev Environment in 2024
West workspaces are powerful and under-documented. A setup that works across macOS, Linux, and a CI container with pinned tool versions.

West workspaces are powerful and under-documented in the places that matter: reproducible pins across macOS, Linux, and CI. This note documents a setup that survived three developer laptops, one M1 Mac, one Ubuntu 22.04 CI container, and a Zephyr 3.5 → 3.6 upgrade without "works on my machine" regressions.
Related: Zephyr vs FreeRTOS scheduling, MCUboot integration notes.
Answer first: what to pin
Pin these in version control:
west.ymlmanifest with project revisions (SHAs or tags, not floating branches)- Docker image digest for CI
- Host toolchain version file (
toolchain-version.txt) - Python venv requirements (
requirements-west.txt)
Do not pin "latest Zephyr SDK" in prose — pin the installer URL with version.
Directory layout
firmware/
├── .west/
│ └── config # west configuration
├── west.yml # manifest (or link to app manifest)
├── toolchain-version.txt # e.g. 0.16.5
├── docker/
│ └── Dockerfile.zephyr # CI image
├── requirements-west.txt
└── app/
├── CMakeLists.txt
├── prj.conf
└── src/
Initialize once:
west init -l app
west update
west zephyr-export
-l uses local manifest in app/ if you prefer manifest inside application — either works; pick one and document it in README.
west.yml manifest (excerpt)
manifest:
version: "0.13"
defaults:
remote: origin
remotes:
- name: origin
url-base: https://github.com/zephyrproject-rtos
projects:
- name: zephyr
remote: origin
revision: v3.6.0
import:
name-allowlist:
- cmsis
- hal_nordic
- mbedtls
- mcuboot
- net-tools
self:
path: app
name-allowlist keeps west update from pulling every optional module. Faster CI, fewer surprise dependencies.
Bump Zephyr by changing revision, running west update, and fixing breaking changes in a dedicated PR — never mixed with feature work.
Toolchain: Zephyr SDK 0.16.5
toolchain-version.txt:
0.16.5
Install script (Linux/macOS):
SDK_VERSION=$(cat toolchain-version.txt)
SDK_ROOT="${HOME}/zephyr-sdk-${SDK_VERSION}"
if [ ! -d "$SDK_ROOT" ]; then
wget "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${SDK_VERSION}/zephyr-sdk-${SDK_VERSION}_linux-x86_64.tar.xz"
tar xf "zephyr-sdk-${SDK_VERSION}_linux-x86_64.tar.xz" -C "${HOME}"
"${SDK_ROOT}/setup.sh" -t arm-zephyr-eabi
fi
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
export ZEPHYR_SDK_INSTALL_DIR="${SDK_ROOT}"
macOS: use _macos-x86_64 or _macos-aarch64 tarball matching CI and dev machines — do not mix arch toolchains across team if you share build artifacts.
Python environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-west.txt
requirements-west.txt:
west==1.2.0
intelhex==2.3.0
pyelftools==0.31
Pin west Python package separately from Zephyr release — they version independently.
Host dependencies by OS
Ubuntu 22.04:
sudo apt install --no-install-recommends \
git cmake ninja-build gperf ccache dfu-util \
device-tree-compiler wget python3-dev python3-venv \
libsdl2-dev libmagic1
macOS (Homebrew):
brew install cmake ninja gperf ccache dtc python3 wget
Common footgun: Apple Clang vs Zephyr SDK GCC — Zephyr builds should use SDK GCC (CONFIG_ZEPHYR_TOOLCHAIN_VARIANT=zephyr). If CMake picks /usr/bin/cc, check ZEPHYR_SDK_INSTALL_DIR.
CI container (reproducible)
docker/Dockerfile.zephyr:
FROM ubuntu:22.04@sha256:abc123... # pin digest
ARG SDK_VERSION=0.16.5
ARG ZEPHYR_VERSION=v3.6.0
RUN apt-get update && apt-get install -y --no-install-recommends \
git cmake ninja-build gperf ccache dfu-util device-tree-compiler \
wget python3 python3-pip python3-venv file libmagic1 \
&& rm -rf /var/lib/apt/lists/*
# SDK install
RUN wget -qO- "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${SDK_VERSION}/zephyr-sdk-${SDK_VERSION}_linux-x86_64.tar.xz" | tar xJ -C /opt \
&& /opt/zephyr-sdk-${SDK_VERSION}/setup.sh -t arm-zephyr-eabi -h -c
WORKDIR /work
COPY requirements-west.txt .
RUN python3 -m venv /venv && /venv/bin/pip install -r requirements-west.txt
ENV PATH="/venv/bin:$PATH"
CI job:
west init -l app
west update
west build -b nrf52840dk_nrf52840 app -- -DEXTRA_CONF_FILE=ci.conf
Cache ~/.cache/ccache and modules/ between builds — 40% time savings on our pipeline.
macOS vs Linux divergence we hit
| Issue | macOS | Linux | Fix |
|---|---|---|---|
| Case-sensitive paths | APFS insensitive | sensitive | Avoid Foo vs foo in #include |
| Serial port | /dev/tty.usbmodem* | /dev/ttyACM0 | udev rules doc for Linux |
| SDK arch | aarch64 vs x86_64 Rosetta | x86_64 | Standardize machine types in team |
west flash | nrfutil/jlink works | same | JLink exe path in west flash args |
Document serial flash commands per OS in app/README.md — not in Slack.
Manifest repo vs monorepo
We use monorepo with vendored manifest at root. Alternative: separate manifest repo with self path pointing to application repo. Monorepo wins for small teams — one PR updates app + Zephyr pin.
Large orgs: manifest repo with revision bumps reviewed by platform team.
Upgrade playbook (Zephyr 3.5 → 3.6)
- Branch
bump/zephyr-3.6 - Change
revisioninwest.yml west update— commit lock file if you generate one- Read Zephyr release notes Breaking Changes section
- Full build all boards in
boards.yml - Run
twistersubset or hardware-in-loop smoke tests - Merge only when CI green on pinned Docker digest
Skipped step 4 once — CONFIG_LOG deprecation cost a day.
What I would add next
Pre-commit hook running west manifest --validate and git diff --exit-code after forbidden floating revisions.
SBOM export from west manifest for supply chain audits — Zephyr 3.x docs cover SPDX generation.
Related
Zephyr vs FreeRTOS scheduling once your workspace builds. MCUboot integration for OTA alongside west manifest modules.
Template repo placeholder from metadata: structure matches what we use internally; swap nrf52840dk for your board.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

