Skip to content
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/deploy-website.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ jobs:
jq -e '.os_list[] |
(.extract_size | type == "number") and
(.image_download_size | type == "number")' "$out" > /dev/null
# Every entry should carry at least one hardware tag — an
# untagged entry is silently dropped by Imager's
# exclusive-matching devices (e.g. the Raspberry Pi 5). This
# is a non-fatal warning, not a hard gate: a release built
# before the generator started emitting `devices` would
# otherwise block every website deploy until the next
# release. New releases always include the field.
if ! jq -e '[.os_list[] |
(.devices | type == "array") and (.devices | length > 0)]
| all' "$out" > /dev/null; then
echo "::warning::rpi-imager.json has entries without a non-empty 'devices' tag; Imager may hide them on exclusive-matching devices (e.g. Pi 5). This release predates the devices change — re-cut a release."
fi
jq -e '[.os_list[] | .name | test("pi1")] | any | not' "$out" > /dev/null
jq -e '[.os_list[] | .url | startswith("https://")] | all' "$out" > /dev/null
echo "rpi-imager.json validation passed"
Expand Down
11 changes: 8 additions & 3 deletions tools/raspberry_pi_imager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ This tool generates the JSON file used by [Raspberry Pi Imager](https://www.rasp
## Supported Boards

- **pi2** (maintenance mode)
- **pi3** (maintenance mode)
- **pi3-64**
- **pi4-64**
- **pi5**

Pi 1 and Pi Zero are no longer supported.
Pi 1 and Pi Zero are no longer supported. The 32-bit armhf `pi3` image
is still built and remains directly downloadable from the release, but
it is not listed in Imager — Pi 3 users are steered to the 64-bit Qt6
`pi3-64` stream.

## Local Development

Expand All @@ -23,5 +26,7 @@ python raspberry_pi_imager/bin/build-pi-imager-json.py
1. Fetches the latest release from GitHub
2. Filters `.zst` assets to only include supported boards
3. For each matching asset, fetches the corresponding `.json` metadata
4. Patches URLs and file sizes, appends maintenance mode notice for pi2/pi3
4. Patches URLs and file sizes, tags each entry with its hardware
`devices` (so Imager's device picker doesn't hide it), and appends a
maintenance mode notice for pi2/pi3
Comment on lines +29 to +31
5. Outputs a JSON file compatible with Raspberry Pi Imager
27 changes: 26 additions & 1 deletion tools/raspberry_pi_imager/build_pi_imager_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,37 @@
# website-deploy job indefinitely. The job runs on every push to master
# and CI's overall budget is in the minutes, not hours.
HTTP_TIMEOUT = 30
SUPPORTED_BOARDS = {'pi2', 'pi3', 'pi3-64', 'pi4-64', 'pi5'}
# Boards surfaced in Raspberry Pi Imager. The 32-bit armhf/Qt5 `pi3`
# stream is intentionally omitted: the image is still built and remains
# directly downloadable from the release, but Pi 3 users are steered to
# the current 64-bit Qt6 `pi3-64` stream rather than the frozen 32-bit
# one. `pi2` stays because the Pi 2 has no 64-bit option.
SUPPORTED_BOARDS = {'pi2', 'pi3-64', 'pi4-64', 'pi5'}
# Boards surfaced with the maintenance/legacy suffix. The 32-bit
# armhf/Qt5 streams (pi2, pi3) are frozen; the 64-bit Qt6 `pi3-64`
# stream is the current recommendation and is NOT a maintenance board.
# `pi3` is kept here so that if it is ever re-listed it carries the
# suffix, even though it is no longer in SUPPORTED_BOARDS.
MAINTENANCE_BOARDS = {'pi2', 'pi3'}
MAINTENANCE_SUFFIX = (
' [Maintenance mode - consider upgrading to Pi 4 or later]'
)

# Hardware-filter tags consumed by Raspberry Pi Imager's device picker.
# Imager (1.9+/2.x) makes the user choose a device first, then drops any
# OS entry whose `devices` array does not intersect that device's tags.
# The Raspberry Pi 5 entry in the official catalog uses
# `matching_type: "exclusive"`, which also drops *untagged* entries — so
# without these tags Anthias vanishes entirely once a Pi 5 is selected.
# Each Anthias image is board-specific, so it maps to exactly one tag.
BOARD_DEVICE_TAGS = {
'pi2': ['pi2-32bit'],
'pi3': ['pi3-32bit'],
'pi3-64': ['pi3-64bit'],
'pi4-64': ['pi4-64bit'],
'pi5': ['pi5-64bit'],
}

REQUIRED_FIELDS = {
'name',
'description',
Expand All @@ -36,6 +58,7 @@
'image_download_sha256',
'release_date',
'url',
'devices',
}


Expand Down Expand Up @@ -106,6 +129,8 @@ def retrieve_and_patch_json(url: str) -> dict[str, Any]:
image_json['image_download_size'] = int(image_json['image_download_size'])

board = get_board_from_url(url)
if board and board in BOARD_DEVICE_TAGS:
image_json['devices'] = BOARD_DEVICE_TAGS[board]
if board and board in MAINTENANCE_BOARDS:
image_json['description'] += MAINTENANCE_SUFFIX

Expand Down
41 changes: 41 additions & 0 deletions tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from tools.raspberry_pi_imager.build_pi_imager_json import (
BOARD_DEVICE_TAGS,
MAINTENANCE_SUFFIX,
REQUIRED_FIELDS,
SUPPORTED_BOARDS,
Expand Down Expand Up @@ -198,6 +199,15 @@ def test_get_asset_list_excludes_pi1(
assert all(get_board_from_url(u) != 'pi1' for u in urls)


def test_get_asset_list_hides_32bit_pi3_but_keeps_pi3_64(
mock_release_assets: MagicMock,
) -> None:
boards = {get_board_from_url(u) for u in get_asset_list(RELEASE_TAG)}

assert 'pi3' not in boards
assert 'pi3-64' in boards


def test_get_asset_list_excludes_non_zst(
mock_release_assets: MagicMock,
) -> None:
Expand Down Expand Up @@ -278,6 +288,27 @@ def test_retrieve_and_patch_json_has_all_required_fields(
assert not missing, f'Missing fields: {missing}'


@pytest.mark.parametrize('board, expected_tags', BOARD_DEVICE_TAGS.items())
def test_retrieve_and_patch_json_adds_device_tags(
mock_requests_get: MagicMock, board: str, expected_tags: list[str]
) -> None:
mock_requests_get.return_value = _build_side_effect(
make_image_metadata(board)
)
url = f'{BASE_RELEASE_URL}/2025-01-01-anthias-{board}.img.zst'

assert retrieve_and_patch_json(url)['devices'] == expected_tags


def test_every_supported_board_has_device_tags() -> None:
# An untagged entry is silently dropped by Imager's exclusive-matching
# devices (e.g. the Raspberry Pi 5), so every listed board must map to
# at least one hardware tag. BOARD_DEVICE_TAGS may carry extras (e.g.
# the hidden pi3) that are no longer in SUPPORTED_BOARDS.
assert SUPPORTED_BOARDS <= set(BOARD_DEVICE_TAGS)
assert all(BOARD_DEVICE_TAGS[board] for board in SUPPORTED_BOARDS)


# ---------------------------------------------------------------------------
# build_imager_json
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -305,3 +336,13 @@ def test_build_imager_json_excludes_pi1(mock_full_build: MagicMock) -> None:
result = build_imager_json()

assert all('(pi1)' not in entry['name'] for entry in result['os_list'])


def test_build_imager_json_tags_every_entry_for_its_board(
mock_full_build: MagicMock,
) -> None:
result = build_imager_json()

for entry in result['os_list']:
board = get_board_from_url(entry['url'])
assert entry['devices'] == BOARD_DEVICE_TAGS[board]
Loading