From Repetitive to Elegant: My Improved Pico Ada Makefile
Table of Contents
To make development smoother I normally create a classic Makefile to handle and document the tasks that Alire cannot manage itself. Alire already takes care of dependency management, which makes writing Makefiles much simpler. I started with something like this, which can build, convert to UF2, and deploy to the Pico:
Project_Name := pico_ada_c01_led
App_0 := blink
deploy_0: bin/${App_0}.uf2
gcp --verbose "bin/${App_0}.uf2" "${Pico_Upload}"
sync
bin/${App_0}.uf2: bin/${App_0}
bin/${App_0}: src/${App_0}.adb ${Project_Name}.gpr alire.toml src/*.ad?
development:
alr build --development
release:
alr build --release
validation:
alr build --validation
pretty: development
alr exec -P1 -- gnatpp
doc: release
alr exec -P1 -- gnatdoc -l -b -p
clean:
-rm *.log
-rm -r -f alire
-rm -r bin
-rm -r config
-rm -r obj
bin/%.uf2: bin/%
picotool uf2 convert $(<) -t elf $(@)
bin/%: src/%.adb ${Project_Name}.gpr alire.toml
alr build --development $(<)
Because I am planning one crate per chapter of the Freenove Ultimate Starter Kit for Raspberry Pi Pico C
Tutorial — and some chapters contain several
example programs — there would soon be deploy_1, deploy_2, and so on. That quickly leads to repetition. Deployment
would also fail silently if ${Pico_Upload} was not set.
Having learnt Makefiles with nmake many years ago, I suspected I might have missed some useful modern GNU Make
features. So I asked Grok whether there was a cleaner way to write it. And yes — there was quite a lot that could be
improved.
This is the much-improved version we arrived at together¹:
.PHONY: development release validation pretty doc clean help
# ────────────────────────────────────────────────
# Configuration
# ────────────────────────────────────────────────
Project_Name := pico_ada_c01_led
Project_Apps := blink
# ────────────────────────────────────────────────
# Pico mount point auto-detection / guard
# ────────────────────────────────────────────────
ifeq ($(Pico_Upload),)
# Try common Linux locations first
Pico_Upload := $(firstword $(wildcard \
/media/$(USER)/RPI-RP2 \
/run/media/$(USER)/RPI-RP2 \
/media/$(USER)/RPI_RP2 \
/media/usb/RPI-RP2))
# Then try macOS (most reliable name)
ifeq ($(Pico_Upload),)
Pico_Upload := $(wildcard /Volumes/RPI-RP2)
endif
# Final fallback: still empty? → error
ifeq ($(Pico_Upload),)
$(error Pico bootloader mount point not found automatically. \
Please plug in the Pico while holding the BOOTSEL button, then set: \
export Pico_Upload=/path/to/RPI-RP2 \
Common locations: \
Linux .......... /media/$(USER)/RPI-RP2 or /run/media/$(USER)/RPI-RP2 \
macOS .......... /Volumes/RPI-RP2 \
See 'make help' for full instructions.)
endif
# Inform user what we auto-detected (helps debugging)
$(info Detected Pico_Upload = $(Pico_Upload))
endif
# ────────────────────────────────────────────────
# Template for each application
# ────────────────────────────────────────────────
define App_Template
deploy_${1}: bin/${1}.uf2 check-pico-upload
cp -v "bin/${1}.uf2" "$(Pico_Upload)/"
sync
@echo ""
@echo "Deployed ${1} to Pico."
@echo "(macOS users: ignore any 'not ejected properly' warning.)"
bin/${1}.uf2: bin/${1}
bin/${1}: src/${1}.adb ${Project_Name}.gpr alire.toml src/*.ad?
endef
# Instantiate the template for every app
$(foreach app,$(Project_Apps),$(eval $(call App_Template,$(app))))
# ────────────────────────────────────────────────
# Generic / shared rules
# ────────────────────────────────────────────────
development:
alr build --development
release:
alr build --release
validation:
alr build --validation
pretty: development
alr exec -P1 -- gnatpp
doc: release
alr exec -P1 -- gnatdoc -l -b -p
clean:
-rm -rf *.log alire bin config obj
bin/%.uf2: bin/%
picotool uf2 convert $(<) -t elf $(@)
bin/%: src/%.adb ${Project_Name}.gpr alire.toml
alr build --development $(<)
check-pico-upload:
@if [ ! -d "$(Pico_Upload)" ]; then \
echo "Error: '$(Pico_Upload)' does not exist or is not mounted."; \
echo " 1. Hold BOOTSEL button on the Pico"; \
echo " 2. Plug it into USB while holding the button"; \
echo " 3. Release button once it appears (RPI-RP2)"; \
echo "Then retry 'make deploy_0'"; \
exit 1; \
fi
help:
@echo "pico_ada_c01_led – quick help"
@echo ""
@echo "Build targets:"
@echo " make development → build all with debug active"
@echo " make validation → build all with contracts active"
@echo " make release → build all with optimizer active"
@echo " make doc → generate gnatdoc documentation"
@echo " make pretty → pretty-print source code"
@echo " make clean → clean build artefacts"
@echo ""
@echo "Deploy targets (development version, Pico is in BOOTSEL mode):"
@for app in $(Project_Apps); do \
printf " make deploy_%-12s → deploy %s\n" "$$app" "$$app"; \
done
@echo ""
@echo "Current Pico_Upload = $(or $(Pico_Upload),not set – see instructions below)"
@echo ""
@echo "To deploy:"
@echo " 1. Hold BOOTSEL while plugging in Pico"
@echo " 2. It appears as RPI-RP2"
@echo " 3. macOS: /Volumes/RPI-RP2"
@echo " Linux: usually /media/$$USER/RPI-RP2"
@echo " 4. export Pico_Upload=/path/to/RPI-RP2"
So what actually got better? #
Help target and comments
The Makefile now includes a clear help target and plenty of section comments. Anyone who opens the file immediately
understands what is possible and how to use it — very useful for readers following along with the tutorial.
Smarter Pico mount-point detection
Instead of simply complaining that ${Pico_Upload} is unset, the Makefile now tries to find the RPI-RP2 volume
automatically on both Linux and macOS using common mount locations. If it still cannot find anything, it prints a
detailed, step-by-step error message with examples. Much friendlier.
DRY with a template
I had never used define / eval / foreach in Make before — it is genuinely elegant. Now I only need to list the program names once in Project_Apps := blink led_switch … and all the corresponding deploy_…, .uf2 and build rules are generated automatically. The help target updates itself too. Very little repetition.
1 #
I’m not “vibe coding”. I think vibe coding is daft and I check every line Grok suggests. This is proper pair programming.