diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..e01e0b17 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,108 @@ +name: Build + +on: + push: + branches: [main] + paths-ignore: + - '**.md' + - 'docs/**' + - 'img/**' + - 'tools/companion/**' + - 'tools/*.exe' + pull_request: + paths-ignore: + - '**.md' + - 'docs/**' + - 'img/**' + - 'tools/companion/**' + - 'tools/*.exe' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PIO_VERSION: '6.1.19' + +jobs: + checks: + name: Checks + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + envs: ${{ steps.envs.outputs.envs }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Portal script syntax + run: node --check web/app.js + + - name: Error tables match the portal mirror + run: python -c "import sys; sys.path.insert(0, 'tools'); import release; release.check_error_tables()" + + - name: List environments + id: envs + run: | + python - <<'EOF' >> "$GITHUB_OUTPUT" + import glob, json, re + envs = [] + for f in ["platformio.ini"] + sorted(glob.glob("boards/*.ini")): + envs += re.findall(r"^\[env:([^\]]+)\]", open(f, encoding="utf-8").read(), re.M) + print("envs=" + json.dumps(envs)) + EOF + + build: + name: ${{ matrix.env }} + needs: checks + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + env: ${{ fromJson(needs.checks.outputs.envs) }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache PlatformIO packages + uses: actions/cache@v4 + with: + path: | + ~/.platformio/packages + ~/.platformio/platforms + ~/.platformio/.cache + key: pio-${{ runner.os }}-${{ env.PIO_VERSION }}-${{ hashFiles('platformio.ini', 'boards/*.ini') }} + restore-keys: | + pio-${{ runner.os }}-${{ env.PIO_VERSION }}- + + - name: Install PlatformIO + run: pip install "platformio==${PIO_VERSION}" + + - name: Build + run: pio run -e "${{ matrix.env }}" + + - name: Image fits its app partition + run: python tools/ci_slot_check.py "${{ matrix.env }}" + + - name: Upload firmware + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.env }} + path: .pio/build/${{ matrix.env }}/firmware.bin + retention-days: 14 + if-no-files-found: error diff --git a/boards/sensecap_indicator.ini b/boards/sensecap_indicator.ini index 93462f03..fdb0c07a 100644 --- a/boards/sensecap_indicator.ini +++ b/boards/sensecap_indicator.ini @@ -17,8 +17,8 @@ platform_packages = platformio/framework-arduinoespressif32 @ https://github.com/mverch67/arduino-esp32/archive/aef7fef6de3329ed6f75512d46d63bba12b09bb5.zip lib_deps = https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip - knolleary/PubSubClient@^2.8 - bblanchon/ArduinoJson@^7.0 + knolleary/PubSubClient@2.8.0 + bblanchon/ArduinoJson@7.4.3 ; Improv comes from the vendored lib/ImprovWiFi copy - the registry package's ; depends=WiFi pulls the Arduino WiFi-shield library, which shadows the ESP32 ; core WiFi and breaks the build. diff --git a/boards/tzt_2432.ini b/boards/tzt_2432.ini index 1d900ced..ad699a6a 100644 --- a/boards/tzt_2432.ini +++ b/boards/tzt_2432.ini @@ -13,7 +13,7 @@ upload_speed = 230400 board_build.partitions = min_spiffs.csv lib_deps = ${common.lib_deps} - https://github.com/PaulStoffregen/XPT2046_Touchscreen.git + https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#f956c5d8ce3bf39169c7378416b89e7cfe70a034 build_flags = ; --- Touch (XPT2046 - same pins as CYD) --- -D USE_XPT2046=1 diff --git a/platformio.ini b/platformio.ini index 4db97448..97840a23 100644 --- a/platformio.ini +++ b/platformio.ini @@ -48,10 +48,12 @@ extra_scripts = pre:tools/gen_web_assets.py ; Improv WiFi Library is vendored in lib/ImprovWiFi (see its library.json) - ; the registry package pulls in arduino-libraries/WiFi via `depends=WiFi`, ; which shadows the ESP32 core WiFi library and breaks the build. +; Exact versions on purpose: a range resolves differently on every fresh +; install, so two builds of one commit would not ship the same firmware. lib_deps = - lovyan03/LovyanGFX@^1.2.19 - knolleary/PubSubClient@^2.8 - bblanchon/ArduinoJson@^7.0 + lovyan03/LovyanGFX@1.2.36 + knolleary/PubSubClient@2.8.0 + bblanchon/ArduinoJson@7.4.3 ; --- No C++ exceptions: the 4 MB-flash envs (1.75 MB app slots) --- ; ESP-IDF's own default; the Arduino core turns exceptions on. Dropping them from @@ -120,7 +122,7 @@ upload_speed = 230400 board_build.partitions = min_spiffs.csv lib_deps = ${common.lib_deps} - https://github.com/PaulStoffregen/XPT2046_Touchscreen.git + https://github.com/PaulStoffregen/XPT2046_Touchscreen.git#f956c5d8ce3bf39169c7378416b89e7cfe70a034 build_flags = ; --- CYD touch (XPT2046 on separate SPI bus) --- -D USE_XPT2046=1 @@ -282,7 +284,7 @@ board_build.f_flash = 80000000L board_build.flash_mode = qio lib_deps = ${common.lib_deps} - moononournation/GFX Library for Arduino@~1.5.0 + moononournation/GFX Library for Arduino@1.5.9 build_flags = -D BOARD_VARIANT=\"jc3248w535\" -D BOARD_NAME=\"Guition_JC3248W535\" diff --git a/tools/ci_slot_check.py b/tools/ci_slot_check.py new file mode 100644 index 00000000..186d0288 --- /dev/null +++ b/tools/ci_slot_check.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""CI helper: check one env's firmware.bin against its app partition. + +Reuses the partition lookup from tools/release.py. A release-set board that does +not fit fails the job; any other env only gets a warning (some DIY envs are +over their slot and are not shipped). + +Usage: python tools/ci_slot_check.py +""" + +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import release # noqa: E402 + + +def main(): + if len(sys.argv) != 2: + sys.exit("usage: ci_slot_check.py ") + env = sys.argv[1] + image = release.REPO_ROOT / ".pio" / "build" / env / "firmware.bin" + if not image.is_file(): + sys.exit(f"::error::{image} not found") + + shipped = env in release.WEB_FLASHER_BOARDS + size = image.stat().st_size + csv_name = release._partition_csv_for(env) + slot = release._app_slot_bytes(csv_name) if csv_name else None + + if slot is None: + line = f"{env}: {size:,} B, partition table {csv_name!r} not found - not checked" + print(f"::warning::{line}") + status = 0 + else: + free = slot - size + line = f"{env}: {size:,} / {slot:,} B ({free:+,} B)" + if free >= 0: + print(line) + status = 0 + elif shipped: + print(f"::error::{line} - over its app slot, would brick on first OTA") + status = 1 + else: + print(f"::warning::{line} - over its app slot (not in the release set)") + status = 0 + + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if summary: + with open(summary, "a", encoding="utf-8") as fh: + fh.write(f"- `{line}`{' (release set)' if shipped else ''}\n") + sys.exit(status) + + +if __name__ == "__main__": + main()