Skip to content

tools: a failed redraw cannot cut the card picture the repository ships - #3587

Merged
Gathin23 merged 1 commit into
Gathin23:mainfrom
xbsheng:tools/card-picture-staging
Nov 9, 2026
Merged

Gathin23 merged 1 commit into
Gathin23:mainfrom
xbsheng:tools/card-picture-staging

Conversation

@xbsheng

@xbsheng xbsheng commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Tested on: no TV; the three scripts and tools/picture_file.py run against the checkout, unit tests only.

tools/readme_stats.py, tools/readme_featured.py and tools/readme_card.py each ended with img.save(OUT, ...), and Pillow opens the destination before it encodes (ImageFile.save() opens it w+b, then calls the encoder). A full disk, a raising encoder or a file the run may not rewrite therefore left whatever the encoder had written under the name the front page ships the picture at — and the script still printed "Nothing was written.". docs/stats.png is also an input: readme_card.py loads it to stack the card, so one truncated strip stopped the card being redrawn until the picture was restored with git.

Reproduced without a stand-in encoder, on origin/main 294d3d5 and on this branch, same command each time:

$ cp docs/stats.png /tmp/repro/docs/stats.png       # 21,186 bytes
$ ( ulimit -f 16; python3 tools/readme_stats.py launcher/app/src/main/res/font/outfit.ttf /tmp/repro/docs/stats.png )
main:   cannot write /tmp/repro/docs/stats.png: [Errno 27] File too large. Nothing was written.
        exit 1; the picture the repository ships is now 16,384 bytes
branch: cannot write /tmp/repro/docs/stats.png: [Errno 27] File too large. Nothing was written.
        exit 1; the picture is still 21,186 bytes, and no staging folder is left beside it

What changed

  • tools/picture_file.py (new, 73 lines): save(image, path, **options) — the shape tools/scenes_wallpapers.py, tools/blocky_wallpapers.py and tools/wallpaper_prep.py already settled on for a picture that must survive a failed write. The picture is encoded into a staging folder beside its destination and os.replaced over it, so the destination is either the file that was there or the complete new one. A symlink keeps its link and its target is replaced, as img.save(path) wrote through it; a destination that is not a regular file, or that has a second hard link, is refused before anything is encoded; a read-only file is refused without being truncated (a rename alone can replace a file its caller may not write); an existing file's mode is carried onto the new one and a fresh file keeps the mode open() left; a staging folder that cannot be removed after the rename is a warning on stderr, because the picture is in place and the caller's message must not say nothing was written.
  • tools/readme_stats.py, readme_featured.py, readme_card.py: the closing img.save(OUT, ...) becomes picture_file.save(...); the except (OSError, ValueError) handler, its message and the exit code are unchanged — the sentence "Nothing was written." is now true. No other line of the three scripts changed.
  • tests/test_card_picture_staging.py (new, 12 tests): a failed encode leaves the old picture and no staging folder; a symlink keeps its link; a second hard link and a FIFO/folder are refused before the encoder runs; the old mode is carried onto the new picture; a fresh file keeps 0666 & ~umask; a staging folder that cannot be removed after the rename only warns; each script publishes through the helper rather than .save(OUT; and two end-to-end redraws with real Pillow (mode kept, old picture replaced; a hard-linked destination refused with the script's own message, no traceback, both names untouched).
  • Four existing tests copy a card script into a stand-in tree, so they copy tools/picture_file.py beside it: tests/test_card_input_pictures.py, tests/test_gen_tables.py, tests/test_gen_tables_font_redraw.py; tests/test_tools_table.py gains the helper in the set of files that are not reader-facing tools (it has no command line and is linked from nothing), and tests/test_card_out_path.py's "the write at the end is guarded" case now holds the scripts to picture_file.save( instead of .save(OUT.

+12 tests, all of which fail against origin/main and pass here.

Checks

  • CLEANYOURTV_STRICT=1 tools/venv/bin/python -m unittest discover -s tests → Ran 3221 tests, OK (skipped=37) = main 294d3d5's own 3209 + 12 (the extra skip is the read-only case, which root may write regardless of the mode).
  • tools/venv/bin/python -I tools/gen_tables.py --check → exit 0 (no picture or table in this PR).
  • No shell changed; no list, tv.json, theme, generated table, PROMPT.md, tools/disable.sh / check.py logic, Home guard, .github/ or launcher/ file touched.

Dedupe: 87 open PRs, every file list read through the REST API. Three touch one of the three scripts, and each edits the STATS constant at @@ -71,7 +71,7 @@ alone — #3040, #2569, #2545, all older card-count PRs; this PR's hunks are the import block at @@ -9,6 +9,9 @@ and the closing write at @@ -110,8 +113,10 @@, so no shared line. Nobody touches tests/test_card_picture_staging.py or tools/picture_file.py. Merged history: #714ae253 and #6856949b guarded the picture argument before Pillow opens it and 6664fc3 refused an output path that is one of the script's own inputs — all three act before the draw; nothing changed how the write itself publishes, which is what this PR fixes.

readme_stats.py, readme_featured.py and readme_card.py each ended with
img.save(OUT), and Pillow opens the destination before it encodes: a full
disk, a failing encoder or a file the run may not rewrite left 16 KB of a
21 KB docs/stats.png under the name the front page shows, while the script
printed "Nothing was written.". readme_card.py reads docs/stats.png to stack
the card, so one truncated strip also stopped the card being redrawn.

The three now publish through tools/picture_file.py, the shape the theme
wallpaper scripts already use: encode into a staging folder beside the file,
carry the old mode across, os.replace it into place. A symlink keeps its
link; a second hard link or a non-regular file is refused before anything is
encoded; a read-only file is refused without being truncated; a staging
folder that cannot be removed after the rename is a warning, because the
picture is already in place.
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

maintainer-bot review · suggests merge (p2)

Thanks for the clear ulimit repro showing how a failed save could leave a cut-short docs/stats.png and still say "Nothing was written". Writing the picture beside the file and then renaming it into place, with tests, is a sound tool fix and looks ready to merge.

Checks
  • Note: possibly collides with Add NVY TV with GAIA OS #2569 (both PRs modify tools/readme_stats.py in adjacent lines (STATS tuple and later the save logic))
  • Size: size/M (+342 -16, 10 files)

This is a suggestion; a maintainer makes the final call. A maintainer can comment /bot review to run it again.

Reviewed by maintainer-bot · runs on 0G Private Computer

@github-actions github-actions Bot added bot-suggests:merge p2 scripts build-and-install, disable, restore, tv.py and the other tools labels Nov 8, 2026
@Gathin23
Gathin23 merged commit 735e28f into Gathin23:main Nov 9, 2026
4 checks passed
@Gathin23

Gathin23 commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Thanks @xbsheng, the new tools/picture_file.py stages the card PNG and swaps it in with os.replace(), so a failed redraw no longer cuts docs/stats.png short, with 12 tests behind it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-suggests:merge p2 scripts build-and-install, disable, restore, tv.py and the other tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants