Skip to content

Commit b60a2bd

Browse files
florentianayuwonoCopilotCopilot
authored
feat: add armhf (32-bit ARM) architecture support [ISD-5856] (#233)
* feat: add armhf (32-bit ARM) architecture support Add "armhf"/"arm" as a buildable image architecture. armhf images are built on arm64 (aarch64) hosts running a 32-bit userspace and use the GitHub runner binary published by the canonical/github-actions-runner fork (actions-runner-linux-arm-<version>.tar.gz). Changes: - charm state.py: add ARM to the Arch enum and map the "armhf"/"arm"/"armv7l" config values to it in Arch.from_charm. - app config.py: add ARM to the Arch enum (drives the runner download arch "arm") with to_openstack() -> "armhf"; add ARM_ADDITIONAL_APT_PACKAGES (libicu74, libatomic1) required at runtime by the self-contained .NET runtime bundled in the linux-arm runner tarball. - app cloud_image.py: map Arch.ARM to the "armhf" Ubuntu cloud-image arch. - app openstack_builder.py: install the armhf runtime apt packages when building for Arch.ARM. - charmcraft.yaml: document the "armhf" architecture option. Unit tests added for every new arch mapping (TDD). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * feat: pre-install rustup and docker-buildx on armhf images Add rustup and docker-buildx to the armhf-only additional apt packages and set the default Rust toolchain for the ubuntu user during image build. - config.py: ARM_ADDITIONAL_APT_PACKAGES gains rustup and docker-buildx (docker.io is already in the default package set). - cloud-init.sh.j2: after configuring system users, run `sudo -u ubuntu rustup default stable`, guarded on the arm arch so it is a no-op on other architectures (rustup is only installed on armhf images). Test updated: the armhf cloud-init assertion now expects the extra packages and the rustup toolchain step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: restrict armhf builds to noble+ base images The armhf additional apt packages (libicu74, rustup, docker-buildx) are only available from the noble (24.04) archive onwards. On focal/jammy these package names do not exist, so an apt-get install would fail midway through the image build with a confusing error. Fail fast in _generate_cloud_init_script: when arch == ARM and the requested base image is older than noble, raise UnsupportedArchitectureError with a clear message instead of producing a cloud-init script that is guaranteed to fail. Tests: the cloud-init render test now exercises armhf on a supported (noble) base, and a new test asserts UnsupportedArchitectureError is raised for armhf on focal/jammy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: document UnsupportedArchitectureError in _generate_cloud_init_script Add the missing Raises: section for the fail-fast guard that rejects armhf images on pre-noble base images, addressing review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(tests): move TESTDATA_TEST_SCRIPT_URL to commands.py Breaks the commands.py <-> helpers.py circular import so command constants can be imported without the heavy OpenStack/fabric deps, enabling unit tests for arch-conditional command selection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add arch-conditional armhf runner assertions Add ARM_RUNNER_COMMANDS (rustc/cargo, docker buildx, 32-bit Runner.Listener) and a pure commands_for_arch() selector, with unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: run arch-specific commands in openstack integration test Thread arch through run_openstack_tests so armhf images additionally assert rustup, docker-buildx, and the 32-bit runner binary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: map --arch armhf to Arch.ARM in integration conftest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: add armhf integration-test leg on noble+resolute Reuses the arm64 OpenStack tenant (creds/network/flavor) with --arch armhf. armhf is restricted to noble and resolute to match ARM_SUPPORTED_BASE_IMAGES. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: allow --arch armhf in integration pytest option The pytest --arch option restricted choices to amd64/arm64/s390x/ppc64le, so pytest rejected 'armhf' before arch_fixture could map it. Add armhf to the allowed choices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use armv7l as OpenStack architecture for armhf Nova validates the glance image 'architecture' property against its architecture enum, which uses 'armv7l' for 32-bit ARM; 'armhf' (the Ubuntu userland ABI name) is rejected with 'Architecture name armhf is not valid'. The cloud-image download filename still uses 'armhf' via cloud_image.py, which is unaffected. Surfaced by the new armhf integration test when booting an OpenStack server from the built image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): set virtio image properties so 32-bit ARM images boot The armhf base image is tagged architecture=armv7l and booted on aarch64 compute hosts, which use the QEMU "virt" machine type. That machine type has no IDE bus, so libvirt defaulted to an IDE controller for the root disk and the config-drive CD-ROM and rejected the domain with "IDE controllers are unsupported for this QEMU binary or machine type", leaving the builder VM in ERROR. Set hw_machine_type=virt and virtio/scsi disk/cdrom buses on ARM image uploads so the guest boots. amd64 keeps the default PC machine type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): put config drive on virtio-blk bus The armhf builder VMs booted on the aarch64 virt machine type but never became SSH-reachable. The root disk used virtio-blk (so the VM booted), but the config drive was attached as a virtio-scsi CD-ROM. The 32-bit armhf guest kernel lacks the virtio-scsi driver, so it could not read the config drive, cloud-init received no network metadata, and networking never came up. Attach the config drive on the same virtio-blk bus as the root disk and drop the now-unneeded virtio-scsi model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): revert config drive to virtio-scsi CD-ROM bus virtio-blk cannot back ejectable CD-ROM media (libvirt rejects the domain with "disk type of 'vdb' does not support ejectable media"), so the config-drive CD-ROM must ride a virtio-scsi controller. This restores the bus layout that at least boots the guest so its serial console can be inspected to root-cause the SSH-unreachable behaviour on the aarch64 hosts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(armhf): add design for arm64 boot image + 32-bit runner payload Native armhf VMs (32-bit kernel) do not boot on the ps7 arm64 hosts (empty console). The proven-working model from runner PRs #155/#169/#171 is an arm64 kernel running 32-bit armhf userland via native AArch32. This spec redefines the image-builder armhf path to build an arm64 boot image carrying the 32-bit linux-arm runner agent plus armhf multiarch userland. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): map Arch.ARM glance architecture to aarch64 The armhf runner image is an arm64 boot image carrying a 32-bit runner payload; native armhf (32-bit kernel) images do not boot on the aarch64 hosts. Schedule/boot the base image and snapshot as aarch64. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): download arm64 base cloud image for Arch.ARM The builder VM must boot a 64-bit kernel on the aarch64 hosts. Download the arm64 base cloud image; the 32-bit runner payload is added via multiarch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): drop obsolete virtio glance properties With the armhf runner image now built from an arm64 base (64-bit kernel), it boots with the cloud's normal defaults. The virtio-scsi/machine-type workaround for booting a native armhf (32-bit kernel) image is no longer needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): install armhf-qualified runtime deps for the 32-bit runner The 32-bit linux-arm runner agent needs the armhf loader (libc6:armhf) and armhf builds of its .NET runtime deps (libicu74:armhf, libatomic1:armhf) to exec on the arm64 image via native AArch32. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(armhf): enable armhf multiarch in cloud-init Add dpkg --add-architecture armhf (guarded to the arm runner) before apt install so the arm64 image can install and run the 32-bit linux-arm runner and its armhf runtime deps via native AArch32. armhf packages are served by ports.ubuntu.com, which the arm64 base already uses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(armhf): add arm64 multiarch image implementation plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: stop tracking superpowers planning artifacts The docs/superpowers plan and spec are internal agent planning artifacts, not product documentation. Untrack them and gitignore the directory so the Vale docs linter does not scan them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): fetch armhf packages from ports.ubuntu.com The arm64 cloud image's apt mirrors (clouds.archive/security.ubuntu.com) only carry the native architecture; armhf is a ports architecture and its package indices 404 there. Pin the stock sources to the native arch and add a ports.ubuntu.com source for armhf so the 32-bit runner's runtime deps (libc6:armhf, libicu74:armhf, libatomic1:armhf) resolve. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): resolve armhf apt package conflicts Two armhf package-selection bugs surfaced once the builder VM boots and runs apt: - rustup conflicts with the distro cargo/rustc packages ('held broken packages'). rustup provides the armhf/armv7 Rust toolchain, so drop cargo and rustc from the default apt set on armhf. - libicu's soname is release-specific (noble ships libicu74, resolute ships libicu78); the hardcoded libicu74:armhf is 'Unable to locate' on resolute. Select the armhf libicu matching the base image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(armhf): preserve pinned apt sources on runner VM The runner VM boots from a snapshot whose cloud-init state was cleared via "cloud-init clean --configs all" before snapshotting. On first boot cloud-init regenerates the stock apt sources, dropping the native-arch pin added by enable_armhf_multiarch, while the armhf dpkg architecture persists. apt-get update then requests binary-armhf indexes from the primary/security mirrors (which only serve arm64) and 404s, failing the runner's apt-get update. Bake an /etc/cloud/cloud.cfg.d drop-in with apt.preserve_sources_list: true so cloud-init keeps the pinned sources baked into the image. The drop-in lives outside /var/lib/cloud so it survives cloud-init clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: bump copyright year to 2026 in test_integration_commands Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 82fe06b commit b60a2bd

19 files changed

Lines changed: 320 additions & 28 deletions

.github/workflows/integration_test_app.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
image: [focal, jammy, noble, resolute]
18-
arch: [amd64, arm64, s390x, ppc64le]
18+
arch: [amd64, arm64, armhf, s390x, ppc64le]
1919
exclude:
2020
- image: focal
2121
arch: ppc64le
2222
- image: focal
2323
arch: s390x
24+
- image: focal
25+
arch: armhf
2426
- image: jammy
2527
arch: arm64
2628
- image: jammy
2729
arch: ppc64le
2830
- image: jammy
2931
arch: s390x
32+
- image: jammy
33+
arch: armhf
3034
- image: resolute
3135
arch: arm64
3236
- image: resolute
@@ -59,6 +63,13 @@ jobs:
5963
run: |
6064
tox -e integration -- --arch arm64 --image=${{ matrix.image }} ${{ secrets.INTEGRATION_TEST_ARGS_APP_ARM64 }}
6165
working-directory: app
66+
- name: Run integration tests (armhf)
67+
if: matrix.arch == 'armhf'
68+
env:
69+
OPENSTACK_PASSWORD: ${{ secrets.OPENSTACK_PASSWORD_ARM64 }}
70+
run: |
71+
tox -e integration -- --arch armhf --image=${{ matrix.image }} ${{ secrets.INTEGRATION_TEST_ARGS_APP_ARM64 }}
72+
working-directory: app
6273
- name: Run integration tests (s390x)
6374
if: matrix.arch == 's390x'
6475
env:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ testing_key.pem
3030
.vale/styles/config/vocabularies/*
3131
!.vale/styles/config/vocabularies/local
3232
# END VALE WORKFLOW IGNORE
33+
34+
# Superpowers internal planning artifacts (not product docs)
35+
docs/superpowers/

app/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def pytest_addoption(parser: Parser):
4141
"--arch",
4242
action="store",
4343
help="The architecture to build for.",
44-
choices=["amd64", "arm64", "s390x", "ppc64le"],
44+
choices=["amd64", "arm64", "armhf", "s390x", "ppc64le"],
4545
)
4646
parser.addoption(
4747
"--openstack-network-name",

app/src/github_runner_image_builder/cloud_image.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
logger = logging.getLogger(__name__)
2222

23-
SupportedBaseImageArch = typing.Literal["amd64", "arm64", "s390x", "ppc64el"]
23+
SupportedBaseImageArch = typing.Literal["amd64", "arm64", "s390x", "ppc64el", "armhf"]
2424

2525
CHECKSUM_BUF_SIZE = 65536 # 65kb
2626

@@ -95,6 +95,11 @@ def _get_supported_runner_arch(arch: Arch) -> SupportedBaseImageArch:
9595
return "s390x"
9696
case Arch.PPC64LE:
9797
return "ppc64el" # cloud-images.ubuntu.com uses ppc64el instead of ppc64le
98+
case Arch.ARM:
99+
# Download the arm64 (64-bit) base cloud image, not armhf. The armhf runner image is
100+
# an arm64 boot image (64-bit kernel) that runs the 32-bit linux-arm runner via
101+
# multiarch; a native armhf (32-bit kernel) image does not boot on the aarch64 hosts.
102+
return "arm64"
98103
case _:
99104
raise UnsupportedArchitectureError(f"Detected system arch: {arch} is unsupported.")
100105

app/src/github_runner_image_builder/config.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ class Arch(str, Enum):
2020
X64: Represents an X64/AMD64 system architecture.
2121
S390X: Represents an S390X system architecture.
2222
PPC64LE: Represents a PPC64LE system architecture.
23+
ARM: Represents an ARM (32-bit armhf) system architecture.
2324
"""
2425

2526
ARM64 = "arm64"
2627
X64 = "x64"
2728
S390X = "s390x"
2829
PPC64LE = "ppc64le"
30+
ARM = "arm"
2931

3032
def to_openstack(self) -> str:
3133
"""Convert the architecture to OpenStack compatible arch string.
@@ -42,6 +44,13 @@ def to_openstack(self) -> str:
4244
return "s390x"
4345
case Arch.PPC64LE:
4446
return "ppc64le"
47+
case Arch.ARM:
48+
# The armhf runner image is an arm64 boot image (64-bit kernel) that carries a
49+
# 32-bit linux-arm runner payload. Native armhf images (32-bit kernel) do not boot
50+
# on the aarch64 "virt" machine, so the glance architecture is aarch64 so the base
51+
# image and snapshot schedule and boot on arm64 hosts. The 32-bit runner is
52+
# installed via multiarch (see cloud-init.sh.j2 and ARM_ADDITIONAL_APT_PACKAGES).
53+
return "aarch64"
4554
raise ValueError # pragma: nocover
4655

4756

@@ -127,6 +136,26 @@ def from_str(cls, tag_or_name: str) -> "BaseImage":
127136
"wget",
128137
]
129138
S390X_PPC64LE_ADDITIONAL_APT_PACKAGES = ["dotnet-runtime-8.0"]
139+
# The 32-bit linux-arm runner agent runs via the host's native AArch32 support on the arm64
140+
# image. It needs the armhf loader (ld-linux-armhf.so.3 from libc6:armhf) and the armhf build of
141+
# libatomic (a .NET runtime dependency). rustup provides the armhf/armv7 Rust toolchain and
142+
# docker-buildx enables arm32 container builds. libicu (the other .NET runtime dependency) is
143+
# release-specific and handled by ARM_LIBICU_APT_PACKAGE_BY_BASE below.
144+
ARM_ADDITIONAL_APT_PACKAGES = [
145+
"libc6:armhf",
146+
"libatomic1:armhf",
147+
"rustup",
148+
"docker-buildx",
149+
]
150+
# rustup (installed on armhf images) conflicts with the distro cargo and rustc packages, so these
151+
# are dropped from the default apt package set on armhf; rustup provides cargo and rustc instead.
152+
ARM_EXCLUDED_DEFAULT_APT_PACKAGES = ("cargo", "rustc")
153+
# The linux-arm runner's bundled .NET runtime dlopens libicu, whose soname is release-specific
154+
# (noble ships libicu74, resolute ships libicu78). Install the armhf build matching the base image.
155+
ARM_LIBICU_APT_PACKAGE_BY_BASE = {
156+
BaseImage.NOBLE: "libicu74:armhf",
157+
BaseImage.RESOLUTE: "libicu78:armhf",
158+
}
130159

131160
_LOG_LEVELS = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR)
132161
LOG_LEVELS = tuple(

app/src/github_runner_image_builder/openstack_builder.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
import github_runner_image_builder.errors
4141
from github_runner_image_builder import cloud_image, config, store
4242
from github_runner_image_builder.config import (
43+
ARM_ADDITIONAL_APT_PACKAGES,
44+
ARM_EXCLUDED_DEFAULT_APT_PACKAGES,
45+
ARM_LIBICU_APT_PACKAGE_BY_BASE,
4346
FORK_RUNNER_BINARY_REPO,
4447
IMAGE_DEFAULT_APT_PACKAGES,
4548
S390X_PPC64LE_ADDITIONAL_APT_PACKAGES,
@@ -60,6 +63,10 @@
6063
SHARED_SECURITY_GROUP_NAME = "github-runner-image-builder-v1"
6164
EXTERNAL_SCRIPT_PATH = pathlib.Path("/root/external.sh")
6265

66+
# armhf additional apt packages (libicu74, rustup, docker-buildx) are only available from the
67+
# noble (24.04) archive onwards, so armhf images can only be built on these base images.
68+
ARM_SUPPORTED_BASE_IMAGES = (BaseImage.NOBLE, BaseImage.RESOLUTE)
69+
6370
# Server operation timeout constants (in seconds)
6471
CREATE_SERVER_TIMEOUT = 20 * 60 # 20 minutes
6572
DELETE_SERVER_TIMEOUT = 20 * 60 # 20 minutes
@@ -540,6 +547,10 @@ def _generate_cloud_init_script(
540547
image_config: The target image configuration values.
541548
proxy: The proxy to enable while setting up the VM.
542549
550+
Raises:
551+
UnsupportedArchitectureError: If an armhf image is requested on a base image older than
552+
the supported ARM base images (noble and newer).
553+
543554
Returns:
544555
The cloud-init script to create snapshot image.
545556
"""
@@ -552,6 +563,29 @@ def _generate_cloud_init_script(
552563
apt_packages = IMAGE_DEFAULT_APT_PACKAGES
553564
if image_config.arch in (Arch.S390X, Arch.PPC64LE):
554565
apt_packages = IMAGE_DEFAULT_APT_PACKAGES + S390X_PPC64LE_ADDITIONAL_APT_PACKAGES
566+
elif image_config.arch == Arch.ARM:
567+
# The armhf additional apt packages (rustup, docker-buildx, the armhf multiarch libs) and
568+
# the release-specific libicu are only available from the noble (24.04) archive onwards,
569+
# so fail fast on older bases rather than letting apt-get fail midway through the build.
570+
if image_config.base not in ARM_SUPPORTED_BASE_IMAGES:
571+
raise github_runner_image_builder.errors.UnsupportedArchitectureError(
572+
f"armhf images require a base image of "
573+
f"{[base.value for base in ARM_SUPPORTED_BASE_IMAGES]} or newer, "
574+
f"got: {image_config.base.value}."
575+
)
576+
# rustup provides the armhf/armv7 Rust toolchain and conflicts with the distro cargo/rustc
577+
# packages, so drop those from the default set. libicu's soname is release-specific, so
578+
# add the armhf build matching the base image.
579+
default_packages = [
580+
package
581+
for package in IMAGE_DEFAULT_APT_PACKAGES
582+
if package not in ARM_EXCLUDED_DEFAULT_APT_PACKAGES
583+
]
584+
apt_packages = (
585+
default_packages
586+
+ ARM_ADDITIONAL_APT_PACKAGES
587+
+ [ARM_LIBICU_APT_PACKAGE_BY_BASE[image_config.base]]
588+
)
555589
return template.render(
556590
PROXY=proxy,
557591
APT_PACKAGES=" ".join(apt_packages),

app/src/github_runner_image_builder/store.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ def upload_image(
7777
with openstack.connect(cloud=cloud_name) as connection:
7878
try:
7979
logger.info("Uploading image %s.", image_name)
80+
image_properties = {"architecture": arch.to_openstack()}
8081
# ignore type since the library does not provide correct type hinting but the docstring
8182
# does define the return type.
8283
image: Image = connection.create_image(
8384
name=image_name,
8485
filename=str(image_path),
85-
properties={"architecture": arch.to_openstack()},
86+
properties=image_properties,
8687
allow_duplicates=True,
8788
wait=True,
8889
) # type: ignore

app/src/github_runner_image_builder/templates/cloud-init.sh.j2

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,34 @@ EOF
5353
sleep 5
5454
}
5555

56+
function enable_armhf_multiarch() {
57+
echo "Enabling armhf multiarch"
58+
native_arch=$(/usr/bin/dpkg --print-architecture)
59+
codename=$(. /etc/os-release && echo "$VERSION_CODENAME")
60+
/usr/bin/dpkg --add-architecture armhf
61+
# The arm64 cloud image's apt mirrors only carry the native architecture; armhf is a "ports"
62+
# architecture served from ports.ubuntu.com. Pin the stock sources to the native architecture
63+
# and add a ports.ubuntu.com source for armhf so the 32-bit runner's dependencies resolve.
64+
/usr/bin/sed -i "/^Types: deb/a Architectures: $native_arch" /etc/apt/sources.list.d/ubuntu.sources
65+
/usr/bin/tee /etc/apt/sources.list.d/armhf-ports.sources >/dev/null <<EOF
66+
Types: deb
67+
URIs: http://ports.ubuntu.com/ubuntu-ports
68+
Suites: $codename $codename-updates $codename-security
69+
Components: main restricted universe multiverse
70+
Architectures: armhf
71+
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
72+
EOF
73+
# The runner VM boots from a snapshot whose cloud-init state was cleared (see the
74+
# "cloud-init clean" before snapshot), so cloud-init regenerates the stock apt sources on
75+
# first boot and drops the architecture pin above while the armhf dpkg architecture persists,
76+
# breaking "apt-get update". Tell cloud-init to preserve the pinned sources baked into the
77+
# image so the 32-bit multiarch configuration survives on the runner VM.
78+
/usr/bin/tee /etc/cloud/cloud.cfg.d/99-armhf-preserve-apt-sources.cfg >/dev/null <<EOF
79+
apt:
80+
preserve_sources_list: true
81+
EOF
82+
}
83+
5684
function install_apt_packages() {
5785
local packages="$1"
5886
local hwe_version="$2"
@@ -205,6 +233,11 @@ github_runner_arch="{{ RUNNER_ARCH }}"
205233
runner_binary_repo="{{ RUNNER_BINARY_REPO }}"
206234

207235
configure_proxy "$proxy"
236+
# Enable armhf multiarch before installing packages so the 32-bit linux-arm runner agent and
237+
# its armhf runtime dependencies can be installed and executed via native AArch32.
238+
if [ "$github_runner_arch" == "arm" ]; then
239+
enable_armhf_multiarch
240+
fi
208241
install_apt_packages "$apt_packages" "$hwe_version"
209242
disable_unattended_upgrades
210243
enable_network_fair_queuing_congestion
@@ -218,3 +251,9 @@ install_opentelemetry_collector_snap
218251
install_github_runner "$github_runner_version" "$github_runner_arch"
219252
chown_home
220253
configure_system_users
254+
255+
# Set the default Rust toolchain for the ubuntu user. rustup is only installed on
256+
# armhf images, so this block is a no-op on other architectures.
257+
if [ "$github_runner_arch" == "arm" ]; then
258+
sudo -u ubuntu rustup default stable
259+
fi

app/tests/integration/commands.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
import dataclasses
77

8-
from tests.integration.helpers import TESTDATA_TEST_SCRIPT_URL
8+
from github_runner_image_builder.config import Arch
9+
10+
TESTDATA_TEST_SCRIPT_URL = (
11+
"https://raw.githubusercontent.com/canonical/github-runner-image-builder-operator/"
12+
"be135aa505b37aae29aec0ab13805909c46b7903/app/tests/integration/testdata/test_script.sh"
13+
)
914

1015

1116
@dataclasses.dataclass
@@ -125,3 +130,34 @@ class Commands:
125130
"! grep '/home/ubuntu/secret.txt' /var/log/auth.log*",
126131
),
127132
)
133+
134+
135+
# armhf-specific assertions. These packages/binaries are only installed on armhf images
136+
# (see ARM_ADDITIONAL_APT_PACKAGES and the rustup cloud-init step), so they must not run on
137+
# other architectures.
138+
ARM_RUNNER_COMMANDS = (
139+
Commands(name="rustc version (rustup default stable)", command="rustc --version"),
140+
Commands(name="cargo version", command="cargo --version"),
141+
Commands(name="docker buildx version", command="docker buildx version"),
142+
Commands(
143+
name="github runner binary is 32-bit ARM",
144+
command=(
145+
"file /home/ubuntu/actions-runner/bin/Runner.Listener | grep -i 'ELF 32-bit' | "
146+
"grep -i 'ARM'"
147+
),
148+
),
149+
)
150+
151+
152+
def commands_for_arch(arch: Arch) -> tuple[Commands, ...]:
153+
"""Return the test commands to run for the given architecture.
154+
155+
Args:
156+
arch: The architecture under test.
157+
158+
Returns:
159+
The base test commands, plus armhf-specific commands when arch is ARM.
160+
"""
161+
if arch == Arch.ARM:
162+
return TEST_RUNNER_COMMANDS + ARM_RUNNER_COMMANDS
163+
return TEST_RUNNER_COMMANDS

app/tests/integration/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def arch_fixture(pytestconfig: pytest.Config):
3333
match arch:
3434
case "arm64":
3535
return config.Arch.ARM64
36+
case "armhf":
37+
return config.Arch.ARM
3638
case "amd64":
3739
return config.Arch.X64
3840
case "s390x":

0 commit comments

Comments
 (0)