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
14 changes: 13 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,15 @@ jobs:
FROM --platform=$TARGETPLATFORM debian:trixie-slim AS fetch
ARG TARGETARCH
COPY projectmm_$TARGETARCH.deb /tmp/projectmm.deb
RUN dpkg-deb -x /tmp/projectmm.deb /rootfs
# libcurl4t64 is installed because the binary LINKS it (the one outbound HTTPS call), and
# the ldd sweep then collects it plus everything it needs. Naming the libraries by hand
# would be wrong the first time that chain moved.
RUN apt-get update \
&& apt-get install -y --no-install-recommends libcurl4t64 \
&& rm -rf /var/lib/apt/lists/* \
&& dpkg-deb -x /tmp/projectmm.deb /rootfs \
&& mkdir -p /deps \
&& ldd /rootfs/usr/bin/projectMM | awk '/=> \//{print $3}' | sort -u | grep -vE '/(libc|libm|libstdc\+\+|libgcc_s)\.so' | xargs -I{} cp -L {} /deps/
# debian13, NOT debian12: the amd64 binary is built on ubuntu-24.04 (glibc 2.39) and
# needs glibc >= 2.38, where bookworm ships 2.36 and it dies at startup. The arm64 binary
# is built on ubuntu-22.04-arm and floors at 2.35, so it would also run on a bookworm
Expand All @@ -772,6 +780,10 @@ jobs:
# amd64 and arm64; buildx resolves the right manifest per platform.
FROM gcr.io/distroless/cc-debian13@sha256:9b615fff20e1a4fad29c2b30562580b212c7dd5e2225236735cca0070ed11c78
COPY --from=fetch /rootfs/usr/bin/projectMM /usr/bin/projectMM
# The libraries the loader resolved, collected in the fetch stage. Without them the
# container dies at startup on "libcurl.so.4: cannot open shared object file": this job
# builds the image but never runs it, so that failure passed CI and reached a user.
COPY --from=fetch /deps/ /usr/lib/
ENV XDG_DATA_HOME=/data
VOLUME /data
EXPOSE 8080
Expand Down
9 changes: 7 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ target_include_directories(mm_platform PUBLIC src/ src/platform/desktop/)
# build needs no SDK and the binary runs without it.
# `dl` on Linux: miniaudio (platform_desktop_audio.cpp) runtime-links ALSA/PulseAudio via
# dlopen, so no audio SDK is a build requirement, the same no-SDK rule as Npcap above.
# `winhttp` on Windows: the one outbound HTTPS call (MoonCloud). It is the OS's own client, in the
# SDK Visual Studio already installs, so Windows needs no libcurl and no vendored TLS. The other
# platforms use libcurl for the same reason, each taking the stack its OS ships.
target_link_libraries(mm_platform PUBLIC $<$<PLATFORM_ID:Windows>:ws2_32> $<$<PLATFORM_ID:Windows>:iphlpapi>
$<$<PLATFORM_ID:Windows>:winhttp>
$<$<PLATFORM_ID:Linux>:dl> $<$<PLATFORM_ID:Linux>:pthread>)

# The two libraries genuinely need each other: mm_core calls into the platform surface, and the
Expand Down Expand Up @@ -201,8 +205,9 @@ elseif(DEFINED ENV{MM_PACKAGING} AND NOT WIN32)
# including the sanitizer lanes that configure CMake directly and legitimately have no libcurl,
# so keying on it failed builds that produce no artifact at all.
#
# Windows is excluded until its job installs libcurl (vcpkg), which is real work rather than an
# apt line. Until then it warns like any other machine, and its binary cannot report.
# Windows is exempt, and for a reason that is now permanent rather than pending: it sends
# through WinHTTP (httpsPost), so libcurl being absent there costs it nothing. Failing a
# Windows build over a library it does not use would block a binary that reports perfectly.
message(FATAL_ERROR
"libcurl not found while packaging a release. A published build with the client compiled "
"out asks for consent it can never honor. Install it in the job "
Expand Down
38 changes: 29 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
# **When L2 matters.** mDNS discovery (finding boards, being found by them) is multicast and does
# not cross a bridge network, and Art-Net's broadcast mode has the same problem. For those, attach
# the container to the host's network directly (`--network host`, or an L2 CNI on Kubernetes).
# Unicast output needs none of it. NOT verified on a Linux host yet: on macOS and Windows, Docker
# Desktop runs a Linux VM, so `--network host` joins the VM rather than the machine's LAN and the
# question cannot be answered there.
# Unicast output needs none of it. Verified on a NanoPi R28S (arm64, Debian 13): the container
# serves its UI and reaches the LAN through `--network host`. On macOS and Windows the question
# cannot be answered, because Docker Desktop runs a Linux VM and host networking joins the VM
# rather than the machine's LAN.
#
# **Capabilities.** None. It binds 8080 as an ordinary process and needs no added capability.
#
Expand Down Expand Up @@ -52,8 +53,12 @@ ARG TARGETARCH
ARG RELEASE=latest
ARG REPO=MoonModules/projectMM

# libcurl4t64 is installed, not merely downloaded: the release binary links it (the one outbound
# HTTPS call), so the image must carry it AND everything it in turn needs. Letting apt resolve that
# is the point: the chain runs deep (TLS, Kerberos, LDAP, SASL, libssh2, compression), and a
# hand-written COPY list would be wrong the first time any of them changed.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& apt-get install -y --no-install-recommends ca-certificates curl libcurl4t64 \
&& if [ "$RELEASE" = "stable" ]; then \
api="https://api.github.com/repos/${REPO}/releases/latest"; \
else \
Expand All @@ -62,13 +67,23 @@ RUN apt-get update \
&& url=$(curl -fsSL "$api" | grep -o "https://[^\"]*_${TARGETARCH}\.deb" | head -1) \
&& test -n "$url" || { echo "no ${TARGETARCH} .deb in release ${RELEASE}" >&2; exit 1; } \
&& curl -fsSL -o /tmp/projectmm.deb "$url" \
&& dpkg-deb -x /tmp/projectmm.deb /rootfs
&& dpkg-deb -x /tmp/projectmm.deb /rootfs \
# Every shared object the binary resolves to, gathered by asking the loader rather than by
# listing names: ldd walks the whole transitive chain, so this stays correct as that chain moves.
# The four the distroless base already carries (libc, libstdc++, libm, libgcc_s) are EXCLUDED
# rather than copied over: the base and this trixie stage are pinned independently, so shipping
# both would put two glibc builds in one image and let the loader pick by path order.
&& mkdir -p /deps \
&& ldd /rootfs/usr/bin/projectMM | awk '/=> \//{print $3}' | sort -u | grep -vE '/(libc|libm|libstdc\+\+|libgcc_s)\.so' | xargs -I{} cp -L {} /deps/

# --- stage 2: the image that ships ------------------------------------------------------------
# Distroless: the binary plus its four shared libraries, with no shell and no package manager, so
# the attack surface is the application rather than a distribution. `ldd` on the release binary
# lists exactly libstdc++, libm, libgcc_s and libc, which is the whole reason this fits: nothing
# else has to come along. 45 MB against 140 MB for the full-Debian form.
# Distroless: the binary plus the shared libraries it resolves, with no shell and no package
# manager, so the attack surface is the application rather than a distribution.
#
# It used to be four libraries (libstdc++, libm, libgcc_s, libc), all of them in the base. Linking
# libcurl for the one outbound HTTPS call added a chain of its own (TLS, Kerberos, LDAP, compression),
# which is why the fetch stage now collects what the loader actually resolves instead of the image
# relying on the base to happen to carry it.
#
# **debian13, NOT debian12**, and this is load-bearing. The release is built on ubuntu-24.04
# (glibc 2.39), so the binary requires glibc >= 2.38. The debian12/bookworm images ship 2.36, where
Expand All @@ -80,6 +95,11 @@ RUN apt-get update \
FROM gcr.io/distroless/cc-debian13@sha256:9b615fff20e1a4fad29c2b30562580b212c7dd5e2225236735cca0070ed11c78

COPY --from=fetch /rootfs/usr/bin/projectMM /usr/bin/projectMM
# The libraries the loader resolved in the fetch stage, collected there rather than named here.
# Without this the container starts and dies immediately on "libcurl.so.4: cannot open shared
# object file", which is what shipped between dev.126 and this fix: the publish job builds the
# image but never runs it, so a missing library passes CI and fails on a user's board.
COPY --from=fetch /deps/ /usr/lib/

# WHERE THE CONFIG LIVES, and why this line is required rather than a convenience. The desktop
# build resolves its data directory from the environment (platform_desktop.cpp, userDataDir): on
Expand Down
11 changes: 6 additions & 5 deletions docs/explanation/mooncloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ It is deliberately small. Each member is a separate choice with its own checkbox

## Stats

One report about this install, sent once when the firmware is installed or upgraded, and the totals from everyone else back on the same card.
One report about this install, sent once when the firmware is installed or upgraded, and again whenever you press **send update** on the card, for a setup that changed without a version change. The totals from everyone else come back on the same card.

### Why we collect this
### Why you might like this

projectMM is built by a small group of volunteers, so where the effort goes is the most consequential decision the project makes. Without numbers that decision is made from whoever spoke up most recently on Discord, which is a real signal but a badly skewed one: it over-weights the loud, the new, and the broken.
Switching this on gets you the totals back on the same card: what other people run, on what hardware, at what scale. That is the half you can see immediately, and it is worth having on its own.

- **To build what people use.** Which effects, layouts, modifiers, drivers and services run on real devices tells us where the next improvement is worth the most. An effect on nearly every install earns polish; one almost nobody enables does not get rewritten ahead of it.
- **To know what we can stop carrying.** Every feature costs flash, memory and maintenance forever, and on an ESP32 that budget is genuinely scarce. Something no install uses is a candidate for removal, and that is hard to justify on a hunch.
- **To test on the hardware people own.** Chip, flash, PSRAM and device model tell us which boards to keep on the bench and which variants must keep building. We would rather find a break on a board we own than have you find it.
- **To size things for real installations.** How many lights are driven, and how much memory is free, say whether a default is sensible or whether we tuned it for a device nobody runs. A layout that assumes 256 lights is the wrong default if most walls are far bigger.
- **To show you what other people run.** The totals come back onto the same card, so the effects, layouts and scripts other installations use are visible to you too. That is worth having on its own: it is a way to find something you did not know existed and try it tonight.
- **To know whether an upgrade reached anyone.** The running version against the last reported one distinguishes an upgrade from a fresh install, which is what tells us whether a release is being picked up or a problem is stranding people on an old one.

Development is not held hostage to these numbers: something rare and excellent stays. They inform the decision rather than make it.
Expand All @@ -30,11 +31,11 @@ The totals are shown on the same card that asks: contributing earns the answer b

A public message board between projectMM devices. Off until you turn it on, and a message is sent only because you typed one and pressed send.

Everything posted is public and permanent: no private message, no recipient, no delete. Your device name rides along only if you separately switch that on; otherwise messages show the first 8 characters of your installation id, which groups them without naming you.
Everything posted is public and permanent: no private message, no recipient, no delete. Your device name rides along only if you separately switch that on; otherwise messages show the first 8 characters of your installation id, which groups them without naming you, the way a Meshtastic node id does.

## Sync (planned)

Device to device over the internet, for a joint show across houses. Not built yet, and not built on Stats: it shares this container and the installation id, nothing else. It will be its own opt-in, described in the privacy policy before it ships.
Device to device over the internet, for a joint show across houses. Not built yet, and not built on Stats: it shares this container and the installation id, nothing else. Like every other member it will be its own separate choice, off until you turn it on.

## Turning it off

Expand Down
5 changes: 3 additions & 2 deletions docs/moonmodules/core/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Detail: [technical](moxygen/FirmwareUpdateModule.md)

The container for everything projectMM does with a server MoonModules runs. It holds no settings of its own: each thing MoonCloud does is a child with its own consent, because a user who wants one has not thereby agreed to the other.

- **Stats**, below: one opt-in report per install or upgrade, and the totals back.
- **Stats**, below: one opt-in report per install or upgrade, plus one whenever you press send update, and the totals back.
- **Talk**, below: a public message board between devices.
- **Sync** (planned): device to device over the internet, a joint show across houses. Not built on Stats; they share this container and the installation id, nothing else.

Expand All @@ -132,8 +132,9 @@ One opt-in report about this install, sent once when the firmware is installed o

- `consent`: a checkbox, off by default. Nothing is sent, and no identifier is computed, while it is off.
- read-only: `version` (what is running) and `reportedVersion` (what last produced a report). They differ exactly when a report is due, which is what makes one upgrade send one report and a reboot send nothing.
- `send update`: a button that reports again now, for a setup that changed without a version change. It replaces this install's row rather than adding one, and says on the card whether it sent. Distinct from the ⟲ above the charts, which only re-reads the totals.

The report carries hardware and configuration: chip, flash, PSRAM, SDK, device model, total and free memory, how many lights are driven, and which drivers, services, layouts, effects and modifiers you added, each tagged by role. It carries no device name, no addresses, no credentials and no text you typed, and a unit test asserts those cannot appear in it.
The report carries hardware and configuration: chip, flash, PSRAM, SDK, device model, total and free memory, how many lights are driven, and which drivers, services, layouts, effects and modifiers you added, each tagged by role. A scripted module also names the script it runs, but only when that script is one we ship: a script you wrote yourself is counted under its module type and its name is never sent. It carries no device name, no addresses, no credentials and no text you typed, and a unit test asserts those cannot appear in it.

The card also shows the totals everyone else reported: contributing earns the answer back where you already are. The charts are drawn empty until consent is on, so what saying yes gets you is visible before you say it.

Expand Down
Loading
Loading