This repository reverse-engineers PlatformIO's tool-stm32flash package
into a buildable recipe that produces an equivalent stm32flash binary
from source, and keeps it up to date automatically by tracking upstream's
master branch (see "Automation" below) instead of a single fixed release.
Grab the exact filename for your platform from the
latest release,
then in platformio.ini:
[env:your_env]
platform_packages =
tool-stm32flash @ https://github.com/meshmy/tool-stm32flash/releases/download/<tag>/tool-stm32flash-<system>-<version>.tar.gz<system> is one of darwin_arm64, darwin_x86_64, linux_x86_64,
linux_aarch64, linux_armv7l, windows_amd64 -- pick the one matching
the machine that will run PlatformIO (dev box or CI runner). The
name=uri form (tool-stm32flash @ <url>) makes PlatformIO install it
under the tool-stm32flash directory name regardless of what platform
(ststm32, etc.) would otherwise pull in. There's no per-machine
auto-detection with this form, so pick the URL per [env:] section, or
set it via PLATFORMIO_PLATFORM_PACKAGES in CI.
Prefer automatic per-machine resolution instead (like the stock package gets)? Publish these builds under your own PlatformIO account:
pio account register # one-time
pio pkg publish --type tool --owner <you> --release tool-stm32flash-darwin_arm64-<version>.tar.gz
# repeat per downloaded system archivethen:
platform_packages =
<you>/tool-stm32flash@<version>resolves per-machine automatically, same as platformio/tool-stm32flash
does today, just under your own namespace (publishing as platformio
isn't possible without owning that account).
This repo tracks stm32flash's upstream master branch rather than a
single fixed tag, and cuts a new release automatically -- roughly every
time upstream goes quiet for a few weeks, not on a fixed calendar
schedule. Two pieces work together:
renovate.json+renovate.ymlrun self-hosted Renovate on a schedule. It opens/updates a PR the moment upstream gets a new commit -- purely for visibility, so you can see incoming upstream changes as a diff before they ship. It can't do more than that: Renovate'sgit-refsdatasource has no commit-timestamp data, so its usual "wait N days before updating" feature (minimumReleaseAge) has nothing to gate on here.auto-release.ymlruns on its own schedule and asks GitLab's commit API directly (which does have timestamps) whether upstream's HEAD has been sitting still for 21 days. Once it has -- and only then -- it bumpsSTM32FLASH_GIT_COMMITinscripts/versions.sh, commits straight tomain, tags the result, and closes out whatever Renovate PR was tracking that same commit (now superseded).
So a new commit landing upstream shows up as a Renovate PR right away,
but nothing actually ships until that commit has been stable for 3 weeks.
release.yml (build + publish) is unchanged by any of this -- it just
reacts to whatever tag shows up, same as a manually pushed one would.
Because upstream's own VERSION macro is a hardcoded "0.7" that never
moves, this repo derives its own PlatformIO package version from the
pinned commit instead: 0.7.<commit-date:YYYYMMDD>+<short-sha>, computed
by lib.sh's package_version(). It's a normal, monotonically increasing
semver as far as PlatformIO/npm-style comparisons are concerned (verified
against PlatformIO's actual semantic_version-based comparator) -- the
+<short-sha> is build metadata for traceability, not part of ordering.
Unlike tool-openocd (repackaged
xPack build with no first-party attribution), this one barely needed any
detective work: the registry API itself already tells you where the code
comes from.
curl -s https://api.registry.platformio.org/v3/packages/platformio/tool/tool-stm32flash
returns, directly:
"repository_url": "https://gitlab.com/stm32flash/stm32flash.git""license": "GPL-2.0-or-later""homepage": "https://sourceforge.net/projects/stm32flash""dependencies_count": 0
So PlatformIO's package is the public stm32flash project (mirrored from its original home on SourceForge), repackaged as-is, with nothing bundled or patched in.
Before this repo switched to tracking master (see "Automation" above),
it pinned the exact commit PlatformIO's own 0.7.0 registry release was
built from, recovered like this:
stm32flash's VERSION is a hardcoded macro in main.c
(#define VERSION "0.7"), not a git describe string, so the version
banner alone doesn't distinguish the tagged release from any later commit.
The exact commit was instead recovered by diffing dev_table.c device-table
entries embedded in the shipped binary against the upstream git history:
- The
v0.7tag (5ac0ffb0a33d88cd4dbb2ac1971720dcce7f2be1, the commit the annotated tag objectbe36c67d9ba52fe79e93c5ea7332a61794980d93points to) was cut 2022-03-27. - ~19 commits landed after it before this package was built, adding device table entries for STM32C011xx, STM32H72xxx/73xxx, STM32H7A3xx/B3xx, STM32L4P5xx/Q5xx, STM32L552xx/562xx, STM32WB10xx/15xx, STM32WB30xx/50xx, and others.
- None of those device-name strings appear anywhere in the shipped
binaries (
strings ... | grep STM32) -- only the ones already active indev_table.cat thev0.7tag. The commented-out placeholder entries for those later-added chips are present inv0.7's owndev_table.c(as/* ... */), so their absence as live strings in the binary is conclusive: it was built fromv0.7exactly, not master.
This was corroborated independently: the macOS binaries embed a Mach-O
debug map (nm -a, OSO stab entries) recording each .o's build mtime as
Unix time 0x67f3d41a = 2025-04-07 13:33:14 UTC -- 43 minutes before
the registry's own "added_at": "2025-04-07T14:16:10Z". PlatformIO checked
out the v0.7 tag fresh that day and built it, rather than floating
master. Every platform's binary embeds the same build-path fragment,
.../fuzzy-octo-spoon/fuzzy-octo-spoon/stm32flash/... -- apparently an
internal PlatformIO CI repo left at GitHub's own randomly-generated default
repo name.
dependencies_count: 0 in the registry API is confirmed at the binary
level: every shipped binary's only linked libraries are the platform's own
C runtime --
| Platform | otool -L / objdump -p NEEDED/imports |
|---|---|
| darwin (arm64 + x86_64) | /usr/lib/libSystem.B.dylib only |
| linux (x86_64, aarch64, armv7l) | libc.so.6 only |
| windows_amd64 | KERNEL32.dll, msvcrt.dll only |
so there's no equivalent of tool-openocd's build-deps.sh stage here at
all -- stm32flash is a single self-contained C program.
| Component | Source |
|---|---|
| stm32flash | https://gitlab.com/stm32flash/stm32flash.git, master branch HEAD (see "Automation") |
Pinned commit lives in one place: scripts/versions.sh.
It builds via stm32flash's own plain (non-autotools) root Makefile --
the path its own INSTALL file documents as primary ("A set of static
makefiles is provided ... 1. Build executable: make"). No ./configure,
no autoreconf: the Makefile only requires $CC/$AR to be set (it
hard-errors otherwise), and serial_platform.c itself #includes either
serial_posix.c or serial_w32.c based on __WIN32__/__CYGWIN__, so one
unmodified Makefile already produces the right binary on every platform --
including automatically getting a .exe suffix under a mingw $CC.
Matches the exact 6 systems the platformio/tool-stm32flash registry
package ships (confirmed against the live registry API -- see
"Releasing" below):
| PlatformIO system | GitHub Actions runner | Notes |
|---|---|---|
darwin_arm64 |
macos-15 |
Native. Validated locally against the real registry archive: identical symbol table (undefined + defined), identical embedded strings; only cosmetic differences (libSystem.B.dylib minor version, debug-info size from a different Xcode/clang point release) |
darwin_x86_64 |
macos-15-intel |
Native. Also validated locally via clang -arch x86_64 cross-codegen (Apple clang can target x86_64 from an arm64 host without Rosetta) -- identical symbol table to the real registry archive |
linux_x86_64 |
ubuntu-latest |
Native, validated only via CI |
linux_aarch64 |
ubuntu-24.04-arm |
Native, validated only via CI |
linux_armv7l |
ubuntu-latest (cross-compiled) |
No GitHub-hosted runner is 32-bit ARM (confirmed against the current actions/runner-images matrix -- all hosted ARM runners are arm64-only). Cross-compiled from an x86_64 host using Ubuntu's own crossbuild-essential-armhf package (arm-linux-gnueabihf-gcc/-ar), matching the real binary's ABI exactly: EABI5 hard-float (armhf), interpreter /lib/ld-linux-armhf.so.3. Smoke-tested in CI via qemu-user-static |
windows_amd64 |
windows-latest + MSYS2 MINGW64 |
Also served for the windows_arm64 system tag (Windows on Arm runs the x86_64 build via its built-in x64 emulation; PlatformIO doesn't ship a separate arm64 binary either) |
Per-platform packaging is trivial here compared to tool-openocd: every
registry archive for this package is just the bare binary + package.json
at the archive root (no bin/ subdirectory, no bundled libraries, no
README/license/man page) -- confirmed by extracting and find-ing all 6
real archives.
This is the one place this repo makes the opposite choice from the
tool-openocd exercise, and it's worth calling out explicitly so it isn't
"corrected" back by habit: the real shipped windows_amd64 archive's own
stm32flash.exe imports msvcrt.dll (the legacy CRT), not the modern
Universal CRT. Confirmed via objdump -p:
DLL Name: KERNEL32.dll
DLL Name: msvcrt.dll
and via strings embedded by the compiler itself --
GCC: (Rev2, Built by MSYS2 project) 14.2.0 and include paths like
D:/a/msys64/mingw64/include (note mingw64, not ucrt64). So
.github/workflows/release.yml deliberately configures
msys2/setup-msys2 with msystem: MINGW64 and installs
mingw-w64-x86_64-toolchain, not the UCRT64 equivalents. The general rule
from the tool-openocd writeup stands -- confirm empirically per package
via objdump -p against the real archive, don't assume either environment
is "the" right one.
Requires only a C compiler, ar, and GNU/BSD make -- no autotools, no
third-party libraries.
# macOS: already present via Xcode Command Line Tools
xcode-select --install
# Linux (Debian/Ubuntu)
sudo apt-get install -y build-essential
# Windows: use MSYS2's MINGW64 shell (not UCRT64), see
# .github/workflows/release.yml for the exact package list.
# linux_armv7l cross-compile from an x86_64 Linux host:
sudo apt-get install -y crossbuild-essential-armhfThen:
make # fetch source, build, package, archive
# or step by step:
make fetch
make build
make package
make archive # produces dist/tool-stm32flash-<system>-<version>.tar.gzCross-compiling (e.g. linux_armv7l from an x86_64 host) or pinning a
specific compiler: set CC, AR, and TARGET_SYSTEM before invoking
make, e.g.
CC=arm-linux-gnueabihf-gcc AR=arm-linux-gnueabihf-ar TARGET_SYSTEM=linux_armv7l makeOutput lands in dist/tool-stm32flash/, laid out exactly like the real
PlatformIO package -- just the binary and package.json, nothing else:
dist/tool-stm32flash/
stm32flash # or stm32flash.exe on Windows
package.json # includes "system": ["<platformio-system-tag>", ...]
make clean removes build artifacts but keeps fetched sources;
make distclean removes everything including dist/.
Built locally on macOS arm64 (and cross-compiled for macOS x86_64) and diffed directly against the real registry archives for both:
strings ... | grep -E '\.c$'(embedded source file list) -- identicalstrings ... | grep STM32(device table names) -- identicalstm32flash 0.7version banner -- identicalnmsymbol table (undefined + all defined globals, addresses excluded) -- byte-for-byte identical, same count (172 symbols) on both arm64 and the x86_64 cross-buildotool -L-- identical (libSystem.B.dylibonly), differing only in its reported minor version (a function of the build machine's OS/Xcode version, not a real difference -- same caveat noted in thetool-openocdexercise)
The other 4 platforms (linux_x86_64, linux_aarch64, linux_armv7l,
windows_amd64) can't be built or run natively on this dev machine;
validated via CI logs plus the same kind of archive diff instead, done
once CI produces them (see commit history for that run's results).
.github/workflows/release.yml builds all 6 platforms in a matrix and
publishes a GitHub Release whenever a version tag is pushed. In normal
operation you won't push that tag by hand -- auto-release.yml does it
once upstream has gone quiet for 3 weeks (see "Automation" above). To
force one manually anyway:
# scripts/versions.sh's STM32FLASH_GIT_COMMIT must already be the commit
# you want to release; the tag's version (after "v") must match what
# `source scripts/lib.sh && package_version` derives from it.
git tag "v$(source scripts/lib.sh && package_version)"
git push origin --tagsA check-version job fails the whole run fast if the pushed tag doesn't
match the version derived from scripts/versions.sh's pinned commit, so
the release version and the archives' package.json version can't drift
apart. Each matrix leg uploads tool-stm32flash-<system>-<version>.tar.gz;
the final release job collects all 6 and attaches them to the GitHub
Release.
linux_armv7lis cross-compiled, not natively built. No GitHub-hosted runner offers 32-bit ARM; see the platform table above. Functionally equivalent (same ABI, same toolchain family) but not built on real armv7 hardware, unlike whatever build machine PlatformIO itself used for that archive.- Debug info is not stripped, matching the real shipped binaries
exactly (
stm32flash's ownMakefilebuilds with-gand never runsstrip; the Linux registry archives are themselves reported byfileas "not stripped" with embeddeddebug_info). This is intentional parity, not an oversight -- don't add a stripping step expecting it to be an improvement. - No man page, README, or license file bundled in the archive, matching
every real registry archive checked (all 6 contain only the binary and
package.json).stm32flash.1andgpl-2.0.txtexist in the upstream source tree but the shipped PlatformIO package doesn't carry them either. - Tracks upstream HEAD, not PlatformIO's registry release. After the
first release (matching PlatformIO's own pinned
v0.7commit exactly, see "Provenance"), this repo diverges from whatever PlatformIO itself ships going forward -- it stays current with upstreammasterinstead of whatever PlatformIO next decides to package (if anything).
stm32flash itself is GPL-2.0-or-later (see its own gpl-2.0.txt). The
build scripts in this repository are original work reverse-engineering the
build recipe, not the shipped binary's code itself.