Skip to content

fix(install): survive the rgbmatrix build on low-memory Pis - #430

Merged
ChuckBuilds merged 3 commits into
mainfrom
claude/install-script-ram-limits-vs4m83
Aug 3, 2026
Merged

fix(install): survive the rgbmatrix build on low-memory Pis#430
ChuckBuilds merged 3 commits into
mainfrom
claude/install-script-ram-limits-vs4m83

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Pull Request

Summary

The one-shot installer failed at Step 6 on a low-memory Pi with Failed building wheel for rgbmatrix, and told the user to install build tools they already had. The real cause was the kernel OOM killer. This caps build parallelism based on available RAM, adds a temporary swapfile for the compile, keeps pip's build tree off tmpfs, and correctly diagnoses OOM failures instead of always blaming the toolchain.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor (no functional change)
  • Build / CI
  • Plugin work (link to the plugin)

Related issues

Reported on Discord by wahf, who lost about an hour to the misleading error message before diagnosing it as memory and working around it with a 2GB dphys-swapfile swap.

Root cause

Upstream hzeller/rpi-rgb-led-matrix's pyproject.toml declares no [tool.scikit-build] options, so scikit-build-core uses the Ninja generator at its own default of nproc + 2 — six concurrent compiles on a 4-core Pi. CMakeLists.txt compiles the same 14 sources three times (the shared library plus the core and graphics extensions), about 45 translation units, two of which are Cython-generated C++ where a single cc1plus peaks near 800MB at -O3. That does not fit in 512MB–1GB of RAM.

Compounding it, one-shot-install.sh exported TMPDIR=/tmp and Debian 13 mounts /tmp as tmpfs, so the entire C++ build tree was held in RAM alongside the compiler.

The failure was then misreported: the OOM killer writes nothing to pip's output, so the handler's fixed "install build tools" message was the only thing the user saw.

What changed

New scripts/install/lib_lowmem.sh, sourced by first_time_install.sh (with a fail-soft fallback if absent):

  • Parallelism capped at max(1, min(cores, RAM/768)) via CMAKE_BUILD_PARALLEL_LEVEL, which is what cmake --build actually reads. MAKEFLAGS is ignored by Ninja and is set only as a Makefile-generator fallback. A 4GB Pi 4 still gets 4 jobs; 512MB and 1GB boards get 1.
  • Temporary swapfile sized to bring RAM + swap to 3GB, capped at 2GB, removed once the build finishes. An EXIT trap is the backstop for the error path. Nothing is written to /etc/fstab or /etc/dphys-swapfile. Existing swap is measured excluding zram, since zram is compressed RAM and does not help a build OOM.
  • Build tree kept off tmpfs when /tmp is memory-backed.
  • OOM diagnosis from both the build log and the kernel ring buffer, with an accurate message naming the RAM/swap/job numbers in play and why swap was skipped if it was. The old build-tools message is retained for genuine toolchain failures.
  • Preflight reporting of RAM and the chosen job count in Step 1, plus a heartbeat during the compile so a deliberately serial 15–25 minute build does not look like a hang.

New flags --skip-swap and --build-jobs N, with LEDMATRIX_SKIP_SWAP / LEDMATRIX_BUILD_JOBS equivalents.

Two unrelated cleanups found while in here: the one-shot installer and first_time_install.sh each ran apt-get update a minute apart, so the second is now skipped; and the dphys-swapfile advice in diagnose_dependencies.sh was missing the CONF_MAXSWAP line, without which raising CONF_SWAPSIZE above 2048 is silently clamped.

Test plan

  • Ran on a real Raspberry Pi with hardware
  • Ran in emulator mode (EMULATOR=true python3 run.py)
  • Ran the dev preview server (scripts/dev_server.py)
  • Ran the test suite (pytest)
  • Manually verified the affected code path in the web UI

Details:

  • New test/test_install_lowmem.py — 31 tests, all passing, running in the existing pytest CI job. Covers the job-sizing table (512MB→1 job … 8GB→4), the swap-sizing table, zram exclusion, and OOM detection. The last of those is the direct regression test for the misdiagnosis: it asserts a kernel-log-only OOM is detected and that a missing-Python.h failure is not flagged as OOM.
  • shellcheck -S warning is clean on all four shell scripts; bash -n passes on each.
  • Swap lifecycle exercised for real as root: create → swapon --show → teardown, plus the refusal paths (enough RAM, disk starvation, stale swapfile from a killed run) and confirmation that the EXIT trap releases swap even when the ERR handler exits.
  • Env propagation confirmed with a stub python3 on PATH: the build receives CMAKE_BUILD_PARALLEL_LEVEL, MAKEFLAGS and the redirected TMPDIR.

Not verified on hardware. I have no low-memory Pi in this environment, so the end-to-end claim — that a 512MB/1GB board now completes Step 6 — rests on the unit tests plus the reporter's own confirmation that swap resolved it. Worth one real run on a Pi 3B/3B+ before merging; sudo LEDMATRIX_FORCE_LOW_RAM=512 ./first_time_install.sh -y --no-reboot-prompt --force-rebuild forces the low-memory path on any device.

Documentation

  • I updated README.md if user-facing behavior changed
  • I updated the relevant doc in docs/ if developer behavior changed
  • I added/updated docstrings on new public functions
  • N/A — no docs needed

README.md gains a RAM note in Hardware Requirements; docs/TROUBLESHOOTING.md gains an "Installation & Build Issues" section (the file had none) documenting this exact failure.

Plugin compatibility

  • No plugin breakage expected
  • Some plugins will need updates — listed below
  • N/A — change doesn't touch the plugin system

Checklist

  • My commits follow the message convention in CONTRIBUTING.md
  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • I've not committed any secrets or hardcoded API keys
  • If this adds a new config key, the form in the web UI was verified (N/A — no new config keys)

Notes for reviewer

Judgment calls worth a second opinion:

  • Temporary vs permanent swap. This creates a plain swapfile and removes it after the build, rather than following the reporter's dphys-swapfile route. That avoids editing a config file the user owns and limits SD-card wear, but it means a 1GB Pi gets no runtime swap afterwards — arguably useful for running the app. Easy to flip if you'd rather it persist.
  • Slower builds on 1GB devices that currently succeed. Capping to 1 job trades speed for correctness. The heartbeat and revised time estimate are there so it doesn't read as a hang.
  • 4GB Pi 4 goes from Ninja's 6 jobs to 4. Marginally slower, but nproc is the saner default and it keeps one formula across all devices.
  • The 768MB-per-job divisor and the 3GB swap target are estimates from the Cython TU's footprint, not measurements on hardware. If a real Pi 3B+ run shows headroom, they can be relaxed.
  • LEDMATRIX_APT_UPDATED relies on sudo -E preserving the variable. If a sudoers policy strips it, the fallback is simply the redundant apt-get update we have today — no breakage.

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Improved installation support for low-memory Raspberry Pi boards.
    • Added automatic build parallelism adjustment and optional temporary swapfile support.
    • Added --skip-swap and --build-jobs installer options.
    • Added clearer memory, storage, build-time, and out-of-memory diagnostics.
  • Documentation

    • Expanded installation prerequisites and troubleshooting guidance for build failures.
    • Added instructions for managing swap space and resolving slow or failed builds.

The one-shot installer failed at Step 6 on a 1GB Pi with "Failed building
wheel for rgbmatrix", and told the user to install build tools they already
had. The real cause was the kernel OOM killer.

Upstream's pyproject.toml declares no [tool.scikit-build] options, so
scikit-build-core drives Ninja at its default of nproc+2 jobs -- six
concurrent compiles on a 4-core Pi. CMakeLists.txt compiles the same 14
sources three times (~45 translation units), two of them Cython-generated
C++ where a single cc1plus peaks near 800MB. That does not fit in 512MB-1GB
of RAM.

Add scripts/install/lib_lowmem.sh and wire it into the installer:

- Cap build parallelism at max(1, min(cores, RAM/768)) via
  CMAKE_BUILD_PARALLEL_LEVEL, which is what cmake --build actually reads.
  MAKEFLAGS is ignored by Ninja and is set only as a Makefile-generator
  fallback. A 4GB Pi 4 still gets 4 jobs; 512MB and 1GB boards get 1.
- Add a temporary swapfile sized to bring RAM+swap to 3GB (capped at 2GB),
  removed once the build finishes. An EXIT trap is the backstop for the
  error path. Nothing is written to /etc/fstab or /etc/dphys-swapfile.
  Existing swap is measured excluding zram, which is compressed RAM and so
  does not help a build OOM.
- Keep pip's build tree off tmpfs. Debian 13 mounts /tmp as tmpfs, so the
  default held the whole C++ build tree in RAM alongside the compiler.
- Diagnose OOM failures from the build log and the kernel ring buffer,
  instead of always blaming missing build tools. The OOM killer writes
  nothing to pip's output, which is why this was misreported.
- Report RAM and the chosen job count in the Step 1 preflight, and emit a
  heartbeat during the compile so a deliberately serial 15-25 minute build
  does not look like a hang.

New flags --skip-swap and --build-jobs N, with LEDMATRIX_SKIP_SWAP and
LEDMATRIX_BUILD_JOBS equivalents.

Also skip the duplicate apt-get update that the one-shot installer and
first_time_install.sh each ran a minute apart, and complete the
dphys-swapfile advice in diagnose_dependencies.sh with the CONF_MAXSWAP
line, without which raising CONF_SWAPSIZE above 2048 is silently clamped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdsJs65WnUo8BtKHMAGA1q
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a691f27-a4a3-4190-97c7-700225fefccb

📥 Commits

Reviewing files that changed from the base of the PR and between a9ad486 and af91e9e.

📒 Files selected for processing (2)
  • first_time_install.sh
  • scripts/install/one-shot-install.sh
📝 Walkthrough

Walkthrough

The installers now support low-memory boards through memory checks, reduced build parallelism, temporary swap, disk-backed build storage, progress reporting, and OOM diagnostics. Tests cover the helper library. Documentation describes setup, troubleshooting, and swap configuration.

Changes

Low-memory installation flow

Layer / File(s) Summary
Resource helpers and validation
scripts/install/lib_lowmem.sh, test/test_install_lowmem.py
The helper library detects RAM, swap, filesystem types, and OOM failures. Tests cover resource detection, sizing, temporary directories, and strict-mode loading.
One-shot prerequisite wiring
scripts/install/one-shot-install.sh
The one-shot installer reports available memory, runs a non-fatal memory check, and exports an APT update marker.
Controlled RGB Matrix build
first_time_install.sh
The installer adds build options, selects compiler parallelism, manages temporary swap and disk-backed temporary storage, emits progress updates, and reports OOM-specific failures.
Installation and troubleshooting guidance
README.md, docs/TROUBLESHOOTING.md, scripts/diagnose_dependencies.sh
Documentation covers low-memory requirements, serial builds, diagnostics, temporary swap, storage requirements, and permanent swap configuration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant one-shot-install.sh
  participant first_time_install.sh
  participant lib_lowmem.sh
  participant rgbmatrix_build
  one-shot-install.sh->>first_time_install.sh: pass LEDMATRIX_APT_UPDATED=1
  first_time_install.sh->>lib_lowmem.sh: detect memory, swap, and temporary storage
  lib_lowmem.sh-->>first_time_install.sh: build jobs and swap requirements
  first_time_install.sh->>rgbmatrix_build: run controlled parallel build
  rgbmatrix_build-->>first_time_install.sh: build output and status
  first_time_install.sh->>lib_lowmem.sh: remove temporary swap
Loading

Possibly related PRs

  • ChuckBuilds/LEDMatrix#369: Both changes update first_time_install.sh with improved RGB Matrix build failure diagnostics and resilience.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: improving rgbmatrix builds on low-memory Raspberry Pi systems.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/install-script-ram-limits-vs4m83

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In `@first_time_install.sh`:
- Around line 1078-1089: Check the exit status of mkdir -p in the
LOWMEM_AVAILABLE branch before retaining BUILD_TMPDIR. If directory creation
fails, report a clear disk-space/build-directory error and continue without
using the invalid path, preserving the existing run_rgbmatrix_build flow for
successful creation.
- Around line 304-357: Move the existing positive-integer validation for
BUILD_JOBS_OVERRIDE to the beginning of check_memory, before the
LOWMEM_AVAILABLE early return, so invalid --build-jobs or LEDMATRIX_BUILD_JOBS
values always produce the clear validation error. Preserve the current
assignment behavior for valid overrides and avoid duplicating the validation in
the low-memory branch.

In `@scripts/install/one-shot-install.sh`:
- Around line 260-267: Update the non-root installation path around the apt
update in one-shot-install.sh so LEDMATRIX_APT_UPDATED=1 is passed explicitly
through the environment used for the sudo -E child, ensuring
first_time_install.sh receives it even when sudo does not preserve exported
variables. Keep the root path unchanged and preserve the existing apt-update
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 865a0bf1-1eac-431e-bf24-e49497ca1c31

📥 Commits

Reviewing files that changed from the base of the PR and between 21825cb and a9ad486.

📒 Files selected for processing (7)
  • README.md
  • docs/TROUBLESHOOTING.md
  • first_time_install.sh
  • scripts/diagnose_dependencies.sh
  • scripts/install/lib_lowmem.sh
  • scripts/install/one-shot-install.sh
  • test/test_install_lowmem.py

Comment thread first_time_install.sh
Comment thread first_time_install.sh
Comment thread scripts/install/one-shot-install.sh
claude added 2 commits August 3, 2026 18:31
Three fixes from PR review:

- Validate --build-jobs / LEDMATRIX_BUILD_JOBS before check_memory's
  fallback return. When lib_lowmem.sh is absent that return also honoured
  the override, so a non-numeric value skipped validation and instead blew
  up later in an arithmetic test in Step 6 with a generic error.
- Fall back to the default TMPDIR when the disk-backed build directory
  cannot be created, rather than pointing the build at a path that does
  not exist. A nearly-full disk is the likely cause on exactly the devices
  this targets.
- Pass LEDMATRIX_APT_UPDATED explicitly to the sudo child instead of
  relying on -E, which a sudoers env_reset/env_keep policy can strip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdsJs65WnUo8BtKHMAGA1q
The build progress heartbeat slept for the full 30s report interval
before re-checking whether the compile had finished, so every build paid
up to 30 seconds of dead wall time -- including fast ones on a Pi 4/5 and
every --force-rebuild run.

Poll every 2s and report every 30s instead. Measured: 30s of overhead on
an instant build drops to 2s, with heartbeats still emitted on the same
schedule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdsJs65WnUo8BtKHMAGA1q
@ChuckBuilds
ChuckBuilds merged commit 16fbb7e into main Aug 3, 2026
9 checks passed
@ChuckBuilds
ChuckBuilds deleted the claude/install-script-ram-limits-vs4m83 branch August 3, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants