Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- name: Verify tag matches library.json version
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') || (github.event_name == 'workflow_dispatch' && startsWith(inputs.tag, 'v'))
# Pass the tag via --tag arg, not via an env var named GITHUB_REF_NAME.
Expand All @@ -63,7 +64,7 @@ jobs:
# warning on the direct ${{ }} expansion inside a shell string.
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: python scripts/build/verify_version.py --tag "$TAG"
run: uv run scripts/build/verify_version.py --tag "$TAG"

build-esp32:
needs: verify-version
Expand Down Expand Up @@ -148,26 +149,37 @@ jobs:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
# CMakeLists.txt calls find_program(UV_EXECUTABLE … REQUIRED) so the
# build-host Python (gzip / build_info.h) is reached through uv. The
# runners don't ship uv by default β€” install it before package_desktop.
- uses: astral-sh/setup-uv@v3
- name: Build + package macOS arm64
run: python scripts/build/package_desktop.py
run: uv run scripts/build/package_desktop.py
- uses: actions/upload-artifact@v4
with:
name: desktop-macos
path: dist/

# build-windows: pending the Windows platform-layer port. The job was
# removed (rather than left red) so the release pipeline can produce a
# tagged 1.0 from the matching `dist/projectMM-macos-arm64-vX.Y.Z.tar.gz`
# + the four ESP32 firmwares. Re-add the build-windows job and the
# corresponding `dist/projectMM-*.zip` glob in `release` once the port
# lands; the surrounding scaffolding stays identical.
build-windows:
needs: verify-version
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# Same uv prerequisite as build-macos β€” see the comment there.
- uses: astral-sh/setup-uv@v3
- name: Build + package Windows x64
run: uv run scripts/build/package_desktop.py
- uses: actions/upload-artifact@v4
with:
name: desktop-windows
path: dist/

release:
# Publish a release on: version tag push, main branch push, or workflow_dispatch.
# Plain branch push without matching paths is filtered by the top-level `paths:`
# so the build jobs already won't run; this condition is the belt to their braces.
if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
needs: [build-esp32, build-macos]
needs: [build-esp32, build-macos, build-windows]
runs-on: ubuntu-latest
permissions:
contents: write # softprops/action-gh-release needs this
Expand Down Expand Up @@ -272,6 +284,7 @@ jobs:
--pattern 'firmware-*.bin' \
--pattern 'manifest-*.json' \
--pattern 'projectMM-*.tar.gz' \
--pattern 'projectMM-*.zip' \
|| echo " (skip $T β€” not yet released or no matching assets)"
done

Expand All @@ -282,6 +295,7 @@ jobs:
cp dist/firmware-*.bin "pages/install/releases/$TAG/" || true
cp pages-manifests/manifest-*.json "pages/install/releases/$TAG/"
cp dist/projectMM-*.tar.gz "pages/install/releases/$TAG/" || true
cp dist/projectMM-*.zip "pages/install/releases/$TAG/" || true

echo "Pages release tree:"
find pages/install/releases -maxdepth 2 -type f | sort
Expand Down Expand Up @@ -357,11 +371,11 @@ jobs:
fail_on_unmatched_files: true
# The `files:` block below is a multi-line literal string β€” every line
# is a glob pattern, the action does NOT strip `#` as comment syntax.
# Add `dist/projectMM-*.zip` back when build-windows is restored.
files: |
dist/firmware-*.bin
dist/manifest-*.json
dist/projectMM-*.tar.gz
dist/projectMM-*.zip

- id: deploy-pages
uses: actions/deploy-pages@v4
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
/build/
/build-*/

# Packaging output from scripts/build/package_desktop.py β€” the .tar.gz / .zip
# bundles attached to GitHub releases. CI is the authoritative producer
# (release.yml on tag/main push); local runs are scratch for dev verification.
# Never commit these β€” they bloat the repo and can drift from source.
/dist/

# CMake generated files
CMakeFiles/
CMakeCache.txt
Expand Down
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ See `docs/architecture.md` for system design. This file contains only rules and
- **No duplication, in code or docs.** Same logic in two places belongs in one shared function; same fact in two docs belongs in one place the other links to. A comment or doc paragraph that restates what the code already says is duplication too β€” delete it. (Reuse a recognisable shape rather than inventing one β€” see *Common patterns first* above.)
- **Data over objects in the hot path.** Where speed and memory matter most, design around plain contiguous data, not an object graph: a flat buffer of elements that one stage writes and the next stage reads, following the producer/consumer data flow in [docs/architecture.md](docs/architecture.md). This is a deliberate performance choice β€” a contiguous buffer is cache-friendly and lets a stage do integer math straight on the array, whereas per-element objects with virtual accessors are cache-hostile and allocation-heavy, exactly what the hot-path rules forbid. The one deliberate class hierarchy is the module tree (one `MoonModule` base, shallow subclasses, a single virtual-dispatch boundary), because uniform polymorphism is what lets the UI render any module generically with zero per-module UI code. Don't add inheritance elsewhere, and don't wrap hot-path buffer data in objects.
- **Concrete first, abstract later.** Build one working feature end-to-end before extracting patterns into shared abstractions. Don't build the framework before the domain logic works.
- **Robust to any input.** A running device tolerates any sequence of UI actions or API calls β€” add, delete, replace, or reconfigure any module in any order, at any grid size, and it keeps running. Degraded or idle is acceptable; crashed is not. This robustness is a defining strongpoint of projectMM, and it's guarded by the test framework, not by hope: a discovered crash drives a new test that pins the fix (see the Hard Rule). Out of scope: power loss, malformed OTA, brown-out, and other physical/electrical faults the firmware can't intercept β€” this principle is about what the software accepts as input.
- **Domain-neutral core.** Separate core infrastructure from the light domain as much as practical. When mixing is necessary, use domain-neutral naming so the code stays open to future separation.
- **Present tense only.** Code, comments, and documentation describe the system as it is now. No changelogs, no roadmaps. History lives in git commits. Exceptions: `docs/backlog/` (forward-looking) and `docs/history/` (backward-looking).

Expand All @@ -32,6 +33,8 @@ The design rationale for each rule below lives in [docs/architecture.md](docs/ar

**Effects must run at every grid size and tick rate.** No crash on 0Γ—0Γ—0; animation math doesn't truncate to zero on fast devices. Full rule + rationale: [architecture.md Β§ Effects](docs/architecture.md#effects).

**Robust to any input.** No UI action or API-call sequence crashes or wedges a running device β€” including deleting, replacing, or clearing modules in any order. A crash or hang is a bug, not the user's fault; the fix is incomplete until a test reproduces the sequence. Full rule + rationale: [architecture.md Β§ Robustness](docs/architecture.md#robustness).

## Process Rules

**Specs before code.** Module docs (`docs/moonmodules/*.md`) and the UI spec must be sufficient to implement from before writing code. What's sufficient is case by case. When in doubt, ask.
Expand All @@ -50,6 +53,10 @@ Then check the recommendation against [Β§ Principles](#principles) (minimalism,

**Plan before implementing.** Use `/plan` mode before every feature. Review plans for: unnecessary files, inheritance where structs suffice, modifications outside the relevant directory. Reject and regenerate bad plans.

**Use `uv` for every Python invocation.** Never type `python` or `python3` directly β€” always go through `uv run` (e.g. `uv run scripts/build/build_desktop.py`, `uv run python -c "…"`). This applies to shell commands, CMake `add_custom_command` / `execute_process`, documentation examples, and anything that shells out. In CMake, resolve `find_program(UV_EXECUTABLE NAMES uv REQUIRED HINTS "$ENV{USERPROFILE}/.local/bin" "$ENV{HOME}/.local/bin")` once and use `${UV_EXECUTABLE} run python …` thereafter. Reason: uv manages the project venv and is the project standard ([scripts/MoonDeck.md](scripts/MoonDeck.md)); bare `python3` isn't on PATH on Windows (and macOS Python Launcher pops a Store prompt). If you catch yourself about to type `python`, stop and prefix with `uv run`.

The **one exception** is `esp32/main/CMakeLists.txt`: ESP-IDF builds use IDF's bundled Python venv, not the project venv β€” adding uv to ESP-IDF docker would be a bigger CI lift than the portability win pays for. That file uses `find_package(Python3 REQUIRED COMPONENTS Interpreter)` and invokes `${Python3_EXECUTABLE}`, so CMake locates whichever Python IDF set up (`.venv\Scripts\python.exe` on Windows IDF, `.venv/bin/python3` on macOS/Linux IDF). The shared `src/ui/embed_ui.cmake` script takes a `PYTHON_CMD` parameter that callers pass: desktop passes `${UV_EXECUTABLE};run;python`, ESP32 passes `${Python3_EXECUTABLE}`.

**Consider extending before creating.** When adding a feature, check if an existing module can be extended cleanly. If a new file is genuinely cleaner, that's fine β€” but justify it.

**Do not remove comments** unless they are outdated or factually wrong. Comments document intent and context. Removing them silently loses knowledge.
Expand Down Expand Up @@ -112,7 +119,7 @@ A commit that touches *only* `.github/`, `docs/`, `scripts/` (non-test), `README

**When "commit now" is received** β€” compile the commit message in this format and execute the commit:

8. Commit message format:
8. Commit message format (the structure below uses hard newlines *between* its parts β€” title, summary, KPI line, bullets are each their own line. But do **not** hard-wrap *within* a part: the summary paragraph and each bullet are a single unbroken line that the viewer soft-wraps β€” same reasoning as the no-hard-wraps-in-markdown rule in [coding-standards.md](docs/coding-standards.md), keeps diffs clean and renders correctly on GitHub. Only the title obeys a length cap; everything else runs as long as it needs to on one line):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor | ⚑ Quick win

Fix ordered-list prefix style to satisfy MD029.

Line 118 uses 8. while your markdownlint configuration expects 1/1/1 list-prefix style, so this keeps the warning active.

Suggested fix
-8. Commit message format (the structure below uses hard newlines *between* its parts β€” title, summary, KPI line, bullets are each their own line. But do **not** hard-wrap *within* a part: the summary paragraph and each bullet are a single unbroken line that the viewer soft-wraps β€” same reasoning as the no-hard-wraps-in-markdown rule in [coding-standards.md](docs/coding-standards.md), keeps diffs clean and renders correctly on GitHub. Only the title obeys a length cap; everything else runs as long as it needs to on one line):
+1. Commit message format (the structure below uses hard newlines *between* its parts β€” title, summary, KPI line, bullets are each their own line. But do **not** hard-wrap *within* a part: the summary paragraph and each bullet are a single unbroken line that the viewer soft-wraps β€” same reasoning as the no-hard-wraps-in-markdown rule in [coding-standards.md](docs/coding-standards.md), keeps diffs clean and renders correctly on GitHub. Only the title obeys a length cap; everything else runs as long as it needs to on one line):
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
8. Commit message format (the structure below uses hard newlines *between* its parts β€” title, summary, KPI line, bullets are each their own line. But do **not** hard-wrap *within* a part: the summary paragraph and each bullet are a single unbroken line that the viewer soft-wraps β€” same reasoning as the no-hard-wraps-in-markdown rule in [coding-standards.md](docs/coding-standards.md), keeps diffs clean and renders correctly on GitHub. Only the title obeys a length cap; everything else runs as long as it needs to on one line):
1. Commit message format (the structure below uses hard newlines *between* its parts β€” title, summary, KPI line, bullets are each their own line. But do **not** hard-wrap *within* a part: the summary paragraph and each bullet are a single unbroken line that the viewer soft-wraps β€” same reasoning as the no-hard-wraps-in-markdown rule in [coding-standards.md](docs/coding-standards.md), keeps diffs clean and renders correctly on GitHub. Only the title obeys a length cap; everything else runs as long as it needs to on one line):
🧰 Tools
πŸͺ› markdownlint-cli2 (0.22.1)

[warning] 118-118: Ordered list item prefix
Expected: 1; Actual: 8; Style: 1/1/1

(MD029, ol-prefix)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 118, The ordered list prefix on the "Commit message
format" item uses "8." which triggers MD029; update the numeric prefix (the
leading "8." in that list item) to the "1." style expected by the markdownlint
rule (or change all ordered-list items in that block to use the "1." prefix) so
the list follows the 1/1/1 ordered-list-prefix style; ensure the rest of the
line text remains unchanged (look for the "Commit message format" list entry and
the leading numeric prefix).

Source: Linters/SAST tools

- **Title line** β€” short imperative summary of the change (≀ 72 chars), e.g. `Add MirrorModifier and fix PreviewDriver sampling`
- **Short summary** β€” a TL;DR for the commit: 1–3 sentences max, end-user readable, plain language. State *what* changed and *why* at the level a release-notes reader cares about β€” do NOT recap the change sections that follow (the bullets do that), and do NOT enumerate files. If your draft is longer than three sentences or restates section headers, cut it. A reader who only sees the title + this paragraph should know what shipped and why.
- **KPI one-liner** β€” the `tick:Xus(FPS:Y)` line from step 7. Omit if the KPI gate didn't run (no `src/` changes).
Expand Down Expand Up @@ -185,8 +192,10 @@ docs/
backlog.md ← the prioritised to-build list
moonmodules_draft/ ← draft specs for unimplemented modules (promoted out as they ship)
history/ ← backward-looking: accumulated wisdom
README.md ← index: what's here + cross-repo trends + digest prompt
decisions.md ← actions, lessons, proven patterns
*-inventory.md ← prior-project surveys (v1, v2, moonlight)
<repo>.md ← friend-repo monthly activity digests (FastLED, WLED, …)
moonmodules/ ← one page per MoonModule (specs before code)
```

Expand Down
49 changes: 42 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,39 @@ set(CMAKE_CXX_EXTENSIONS OFF)
# desktop build runs cleanly on Windows runners (no -W flags on MSVC) and on
# macOS/Linux (no /W flags on Clang/GCC).
if(MSVC)
add_compile_options(/W4 /WX)
# /W4 /WX matches the GCC/Clang `-Wall -Wextra -Werror` discipline. A few
# MSVC-only warnings have no GCC equivalent at -Wall -Wextra and are
# suppressed below rather than fought at every call site:
# C4702 β€” "unreachable code" inside `if constexpr` early-return branches
# (a well-known MSVC quirk: it analyses the discarded branch as
# live and flags everything after `return;`). GCC understands the
# constexpr discard and doesn't warn.
# C4244 / C4267 — implicit narrowing (int→smaller, size_t→smaller). GCC
# only warns under -Wconversion, which is not part of -Wall
# -Wextra. The codebase uses explicit static_cast at the sites
# where loss-of-data is the real concern; suppressing here brings
# MSVC noise level back in line with GCC.
add_compile_options(/W4 /WX /wd4702 /wd4244 /wd4267)
# MSVC marks the standard C library (strcpy, sprintf, fopen, localtime, …)
# as deprecated and emits C4996; /WX makes that fatal. Silence the family β€”
# the code uses these standard functions deliberately and consistently with
# the POSIX side, and the "secure" `_s` variants are a Microsoft extension.
add_compile_definitions(_CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_WARNINGS
_WINSOCK_DEPRECATED_NO_WARNINGS)
# Static MSVC runtime so the Windows binary doesn't need vcredist.
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
add_compile_options(-Wall -Wextra -Werror)
endif()

# `uv` is the project's Python launcher (see CLAUDE.md / scripts/MoonDeck.md).
# The build invokes Python helpers for build_info.h generation and UI embedding;
# resolving them through `uv run python …` keeps Windows (where `python3` isn't
# on PATH) on the same path as macOS / Linux without a `find_package(Python3)`
# detour and without per-host scaffolding.
find_program(UV_EXECUTABLE NAMES uv REQUIRED
HINTS "$ENV{USERPROFILE}/.local/bin" "$ENV{HOME}/.local/bin")

# Core library. Most modules ship header-only (single .h file with implementation
# inline). Core service modules that bridge to the platform (HTTP server,
# filesystem, network, …) ship as .h + .cpp per CLAUDE.md β€” list those .cpps
Expand All @@ -33,28 +59,37 @@ target_link_libraries(mm_core PUBLIC mm_platform)
# Platform library (desktop)
add_library(mm_platform src/platform/desktop/platform_desktop.cpp)
target_include_directories(mm_platform PUBLIC src/ src/platform/desktop/)
# Winsock for the desktop socket surface on Windows.
target_link_libraries(mm_platform PUBLIC $<$<PLATFORM_ID:Windows>:ws2_32>)

# Generate build_info.h from library.json (carries version, build date, board name).
add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/src/core/build_info.h
COMMAND ${CMAKE_COMMAND} -E env python3 ${CMAKE_SOURCE_DIR}/scripts/build/generate_build_info.py
COMMAND ${UV_EXECUTABLE} run python ${CMAKE_SOURCE_DIR}/scripts/build/generate_build_info.py
DEPENDS ${CMAKE_SOURCE_DIR}/library.json ${CMAKE_SOURCE_DIR}/scripts/build/generate_build_info.py
COMMENT "Generating build_info.h"
)
add_custom_target(build_info_gen DEPENDS ${CMAKE_SOURCE_DIR}/src/core/build_info.h)

# Embed UI files as C arrays
# Embed UI files as C arrays. Pass UV_EXECUTABLE through as a single value
# (no semicolons on the command line β€” those would either get split by `make`
# on POSIX hosts or land as literals in the value on MSBuild) and let
# embed_ui.cmake assemble the `<uv> run python` list internally.
add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/src/ui/ui_embedded.h
COMMAND ${CMAKE_COMMAND} -DUI_DIR=${CMAKE_SOURCE_DIR}/src/ui -DOUT=${CMAKE_SOURCE_DIR}/src/ui/ui_embedded.h -P ${CMAKE_SOURCE_DIR}/src/ui/embed_ui.cmake
COMMAND ${CMAKE_COMMAND} -DUI_DIR=${CMAKE_SOURCE_DIR}/src/ui -DOUT=${CMAKE_SOURCE_DIR}/src/ui/ui_embedded.h -DUV_EXECUTABLE=${UV_EXECUTABLE} -P ${CMAKE_SOURCE_DIR}/src/ui/embed_ui.cmake
Comment thread
coderabbitai[bot] marked this conversation as resolved.
DEPENDS ${CMAKE_SOURCE_DIR}/src/ui/index.html ${CMAKE_SOURCE_DIR}/src/ui/app.js ${CMAKE_SOURCE_DIR}/src/ui/style.css ${CMAKE_SOURCE_DIR}/src/ui/install-picker.js ${CMAKE_SOURCE_DIR}/src/ui/preview3d.js ${CMAKE_SOURCE_DIR}/src/ui/moonlight-logo.png ${CMAKE_SOURCE_DIR}/src/ui/embed_ui.cmake
COMMENT "Embedding UI files"
)
add_custom_target(ui_embed DEPENDS ${CMAKE_SOURCE_DIR}/src/ui/ui_embedded.h)

# mm_core's HttpServerModule.cpp consumes ui_embedded.h β€” wire the dep so a
# clean build orders them correctly.
add_dependencies(mm_core ui_embed)
# mm_core's HttpServerModule.cpp consumes ui_embedded.h and SystemModule.h
# (a mm_core public header) consumes build_info.h. Anything that links mm_core
# transitively includes those headers, so both generated files must exist
# before any mm_core compile unit starts. CI's parallel clean build exposes
# the race (test/ compiles SystemModule.h while build_info_gen is still
# running); wiring the dep here makes the ordering explicit on every consumer.
add_dependencies(mm_core ui_embed build_info_gen)

# Application
add_executable(projectMM src/main.cpp src/platform/desktop/main_desktop.cpp)
Expand Down
Loading