This post aims to introduce two things.

  • Firstly Earthly as a tool to help me build software.
  • Secondly a github repo of some earth files I have created. This post will have a focus on the MicroPython earth file.

Earth files are definitions of commands and build environments that the earthly tool will execute.

Whats earthly ?

Earthy is a build tool that acts on top of Docker containers.
It aims to make building software as reproducible as possible.
By defining the build process in an earth file, Earthly then ensures that the build environment and build steps are identical no matter where they are running.
This means my build environment for my MicroPython projects is always consistent and portable across my machines. It also allows me to share my build environments with others.

Template repository

I wanted a central place to store and share my earth files, so I created the earthly-recipes git repository.
So far this only contains two templates:

  • Earthfile-MicroPython
  • Earthfile-lattice-ulx3s-fpga

This post will only cover the usage of Earthfile-MicroPython.


This earth file allows you to build, flash and erase ESP32 devices that support MicroPython.
This removes the need for your dev machine to have the ESP/IDF/MicroPython build tools and frame work installed, its all done in docker container.
The earth file contains a variable called ESP_IDF_VERSION, that defines the version of the ESP IDF framework that the earth file will build MicroPython on top of.
At the time of this blog post that version is 4.2.
The earth file supports three build targets:

  • firmware
  • flash
  • erase

You must run the firmware target before any other targets
The firmware target builds a MicroPython firmware binary and any tooling to flash the devices. It pulls in main.py and the entire modules directory from the current working directory and outputs build artifacts to the artifacts directory, you do not need to create the artifacts directory as earthly will take care of that for you.
Example of running this target:

$ earthly +firmware
           buildkitd | Starting buildkit daemon as a docker container (earthly-buildkitd)...
      buildkitd-pull | Pulling buildkitd image...
      buildkitd-pull | ...Done
           buildkitd | ...Done
            python:3 | --> Load metadata linux/amd64
             ongoing | python:3 (5 seconds ago)
               +base | --> FROM python:3
             context | --> local context .
               +base | [          ] resolve docker.io/library/python:3@sha256:e6654afa815122b13242fc9ff513e2d14b00548ba6eaf4d3b03f2f261d85272d ... 0%              [██████████] resolve docker.io/library/python:3@sha256:e6654afa815122b13242fc9ff513e2d14b00548ba6eaf4d3b03f2f261d85272d ... 100%
             context | transferred 49 file(s) for context . (439 kB, 2884 file/dir stats)
             ongoing |
             +base | --> RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y cmake
<<<<<<<<< SNIP >>>>>>>>>
              output | --> exporting outputs
              output | [██████████] copying files ... 100%
================================ SUCCESS [main] ================================
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/.bin_timestamp as local artifacts/.bin_timestamp
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/CMakeCache.txt as local artifacts/CMakeCache.txt
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/CMakeFiles as local artifacts/CMakeFiles
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/Makefile as local artifacts/Makefile
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/bootloader.bin as local artifacts/bootloader.bin
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/bootloader.elf as local artifacts/bootloader.elf
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/bootloader.map as local artifacts/bootloader.map
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/cmake_install.cmake as local artifacts/cmake_install.cmake
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/compile_commands.json as local artifacts/compile_commands.json
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/config as local artifacts/config
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/config.env as local artifacts/config.env
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/esp-idf as local artifacts/esp-idf
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/kconfigs.in as local artifacts/kconfigs.in
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/kconfigs_projbuild.in as local artifacts/kconfigs_projbuild.in
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/project_description.json as local artifacts/project_description.json
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/project_elf_src.c as local artifacts/project_elf_src.c
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/partition-table.bin as local artifacts/partition-table.bin
           +firmware | Artifact github.com/brendanhoran/python-clock:master+firmware/micropython.bin as local artifacts/micropython.bin
               +base | Artifact github.com/brendanhoran/python-clock:master+base/esptool.py as local artifacts/esptool.py

The flash and erase targets take an additional command line argument to define the serial port that the ESP32 is connected to.
These two targets run in local mode, so your host machine will execute the esptool.py command, thus you must have a working Python3 interpreter installed and in your path.

The flash target flashes the following two binary's that are build artifacts from the firmware target:

  • artifacts/bootloader.bin
  • artifacts/micropython.bin

The flash target runs the build artifact artifacts/esptool.py to flash the above to binary's to the ESP32 device. You need to specify the serial port with the --build-arg of SERIAL_PORT=.
Example of running this target with the esp32 on serial port /dev/ttyUSB0:

$ earthly --build-arg SERIAL_PORT=/dev/ttyUSB0 +flash

           buildkitd | Found buildkit daemon as docker container (earthly-buildkitd)
            python:3 | --> Load metadata linux/amd64
               +base | --> FROM python:3
              +flash *local* | SERIAL_PORT=/dev/ttyUSB0
              +flash *local* | --> RUN artifacts/esptool.py -p $SERIAL_PORT -b 460800 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x1000 artifacts/bootloader.bin 0x8000 artifacts/partition-table.bin 0x10000 artifacts/micropython.bin
<<<<<<<<< SNIP >>>>>>>>>
              +flash *local* | Wrote 1483760 bytes (980289 compressed) at 0x00010000 in 23.9 seconds (effective 497.5 kbit/s)...
              +flash *local* | Hash of data verified.
              +flash *local* | Leaving...
              +flash *local* | Hard resetting via RTS pin...
              output | --> exporting outputs
              output | [██████████] copying files ... 100%
================================ SUCCESS [main] ================================
               +base | Artifact github.com/brendanhoran/python-clock:master+base/esptool.py as local artifacts/esptool.py

The erase target will just erase the esp32 device. This target also needs the --build-arg of SERIAL_PORT.
To erase the flash run the target like this:

$ earthly --build-arg SERIAL_PORT=/dev/ttyUSB0 +erase

If you want to see how this fits into an actual MicroPython project, I use it for my Nixie clock project.