diff --git a/.github/workflows/deploy-website.yaml b/.github/workflows/deploy-website.yaml index 758c66dc6..df4e9d1cc 100644 --- a/.github/workflows/deploy-website.yaml +++ b/.github/workflows/deploy-website.yaml @@ -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" diff --git a/tools/raspberry_pi_imager/README.md b/tools/raspberry_pi_imager/README.md index 8dbd0b495..7dd79b03d 100644 --- a/tools/raspberry_pi_imager/README.md +++ b/tools/raspberry_pi_imager/README.md @@ -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 @@ -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 5. Outputs a JSON file compatible with Raspberry Pi Imager diff --git a/tools/raspberry_pi_imager/build_pi_imager_json.py b/tools/raspberry_pi_imager/build_pi_imager_json.py index 9eac97dea..f43a32a69 100644 --- a/tools/raspberry_pi_imager/build_pi_imager_json.py +++ b/tools/raspberry_pi_imager/build_pi_imager_json.py @@ -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', @@ -36,6 +58,7 @@ 'image_download_sha256', 'release_date', 'url', + 'devices', } @@ -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 diff --git a/tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py b/tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py index 84016739c..da4290f8d 100644 --- a/tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py +++ b/tools/raspberry_pi_imager/tests/test_build_pi_imager_json.py @@ -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, @@ -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: @@ -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 # --------------------------------------------------------------------------- @@ -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]