From 85f33c85e374e56e0f58e3ee2543cd17af8a9566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chojnacki?= Date: Fri, 31 Jul 2026 15:51:24 +0200 Subject: [PATCH] feat(flutter): add a linux_arm64 toolchain Flutter publishes no linux-arm64 SDK tarball, so `linux` is registered as x86_64-only and there is no way to build on arm64 Linux -- increasingly common on CI (AWS CodeBuild arm images, Ampere runners) and on developer machines. Flutter does publish the architecture-specific pieces per engine revision: dart-sdk-linux-arm64.zip and linux-arm64/artifacts.zip (flutter_tester, gen_snapshot, impellerc). Only bin/cache holds native code -- the framework sources, the flutter_tools kernel snapshot and the templates in the x64 archive are architecture-independent. So linux_arm64 downloads the x64 release archive, deletes the native pieces of bin/cache along with the stamps that mark them current, and lets the existing fetch-time precache refetch them for the host: the launcher's update_dart_sdk.sh selects the Dart SDK by `uname -m`, and flutter_tools resolves engine artifacts through its own host-platform detection. This is the same bootstrap path a plain `git clone` of the SDK takes on arm64. Consequences worth reviewing: - The linux_arm64 repository can only be *fetched* on an arm64 Linux host, since completing it runs the launcher. Bazel only fetches the host platform's SDK repository, so this is not a practical restriction, but it is enforced with an explicit error rather than a confusing failure later. - _verify_rearchitected fails at fetch time if the replacements did not appear, so a future release that stops refetching them as a side effect of precache surfaces here instead of as an exec-format error inside a build action. - _host_matches_platform is now architecture-aware, which would have silently disabled the `linux` artifact group on arm64 hosts. Artifact groups are named after operating systems, so they now route through a separate _host_is_os helper. The x64, macOS and Windows paths are unchanged: _archive_platform and _platform_arch are identity mappings for every platform except linux_arm64. Tested on x86_64 (no regression: a full web build, test suite and analyze run of a real app all pass). The arm64 path has been verified against the published artifact URLs but not yet run end to end on arm64 hardware -- hence the draft. --- flutter/private/sdk_repo.bzl | 22 ++-- flutter/private/toolchains_repo.bzl | 12 +++ flutter/repositories.bzl | 151 +++++++++++++++++++++++++--- 3 files changed, 166 insertions(+), 19 deletions(-) diff --git a/flutter/private/sdk_repo.bzl b/flutter/private/sdk_repo.bzl index f3c2b93..18e3635 100644 --- a/flutter/private/sdk_repo.bzl +++ b/flutter/private/sdk_repo.bzl @@ -6,19 +6,27 @@ on `@_sdk` (e.g. `@flutter_sdk`) without caring about the underlying platform repository name. """ -def _host_platform(os_name): - """Normalize Bazel host OS strings to Flutter platform suffixes.""" - lower_name = os_name.lower() +def _is_arm64(repository_ctx): + """True when the host CPU is 64-bit ARM.""" + return repository_ctx.os.arch.lower() in ["aarch64", "arm64"] + +def _host_platform(repository_ctx): + """Normalize the Bazel host OS/CPU to a Flutter platform suffix. + + Only Linux distinguishes architectures: the macOS archive is a universal + binary and Flutter ships no Windows-on-ARM release. + """ + lower_name = repository_ctx.os.name.lower() if lower_name.startswith("mac"): return "macos" if lower_name.startswith("linux"): - return "linux" + return "linux_arm64" if _is_arm64(repository_ctx) else "linux" if lower_name.startswith("windows"): return "windows" - fail("Unsupported host platform '{}'. Expected macOS, Linux, or Windows.".format(os_name)) + fail("Unsupported host platform '{}'. Expected macOS, Linux, or Windows.".format(repository_ctx.os.name)) def _sdk_repo_impl(repository_ctx): - platform = _host_platform(repository_ctx.os.name) + platform = _host_platform(repository_ctx) target_repo = "{}_{}".format(repository_ctx.attr.user_repository_name, platform) canonical_target_repo = _canonical_target_repo(repository_ctx.name, target_repo) @@ -51,7 +59,7 @@ def _write_binary_symlinks(repository_ctx, canonical_target_repo): - rules_flutter++flutter+flutter_sdk/bin/dart - rules_flutter++flutter+flutter_sdk/bin/flutter """ - platform = _host_platform(repository_ctx.os.name) + platform = _host_platform(repository_ctx) if platform == "windows": dart_name = "dart.exe" diff --git a/flutter/private/toolchains_repo.bzl b/flutter/private/toolchains_repo.bzl index 79f0e32..ccb4158 100644 --- a/flutter/private/toolchains_repo.bzl +++ b/flutter/private/toolchains_repo.bzl @@ -31,6 +31,18 @@ PLATFORMS = { "@platforms//cpu:x86_64", ], ), + # Flutter publishes no linux-arm64 SDK archive, but it does publish + # linux-arm64 engine artifacts and a linux-arm64 Dart SDK. This platform + # reuses the x64 release archive (framework sources, flutter_tools, and + # templates are architecture-independent) and swaps the architecture- + # specific parts of bin/cache. See _rearchitect_linux_arm64 in + # repositories.bzl. + "linux_arm64": struct( + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:arm64", + ], + ), "windows": struct( compatible_with = [ "@platforms//os:windows", diff --git a/flutter/repositories.bzl b/flutter/repositories.bzl index 4b4a57d..68a005e 100644 --- a/flutter/repositories.bzl +++ b/flutter/repositories.bzl @@ -35,16 +35,42 @@ time (network is available to repository rules).""", } # Sentinel paths (relative to flutter/bin/cache) proving an artifact group is -# already present in the extracted archive. +# already present in the extracted archive. "{arch}" is substituted with the +# repository platform's Flutter arch suffix (x64 or arm64). _PRECACHE_SENTINELS = { "android": "artifacts/engine/android-arm64-release", "ios": "artifacts/engine/ios-release", - "linux": "artifacts/engine/linux-x64-release", + "linux": "artifacts/engine/linux-{arch}-release", "macos": "artifacts/engine/darwin-x64-release", "web": "flutter_web_sdk", "windows": "artifacts/engine/windows-x64-release", } +# Platforms that reuse another platform's release archive. Flutter publishes no +# linux-arm64 SDK tarball, but the framework sources, flutter_tools snapshot and +# templates in the x64 archive are architecture-independent — only bin/cache +# holds native code, and that is replaced by _strip_foreign_arch_artifacts plus +# the fetch-time precache below. +_ARCHIVE_ALIASES = { + "linux_arm64": "linux", +} + +# Flutter's own arch suffix for each platform (see bin/internal/update_dart_sdk.sh). +_PLATFORM_ARCH = { + "linux_arm64": "arm64", +} + +def _archive_platform(platform): + """Release-archive platform whose tarball this platform downloads.""" + return _ARCHIVE_ALIASES.get(platform, platform) + +def _platform_arch(platform): + """Flutter arch suffix used in bin/cache artifact paths.""" + return _PLATFORM_ARCH.get(platform, "x64") + +def _sentinel_path(group, platform): + return _PRECACHE_SENTINELS[group].replace("{arch}", _platform_arch(platform)) + # The tail of bin/internal/update_engine_version.sh that unconditionally # rewrites engine.stamp/engine.realm on every launcher invocation. Replaced at # fetch time so `flutter` never writes into the Bazel external repository. @@ -91,18 +117,44 @@ def _patch_engine_version_script(repository_ctx): legacy_utf8 = False, ) -def _host_matches_platform(repository_ctx, platform): +def _host_is_arm64(repository_ctx): + return repository_ctx.os.arch.lower() in ["aarch64", "arm64"] + +def _host_is_os(repository_ctx, os_family): + """True when the host OS is os_family, ignoring architecture. + + Distinct from _host_matches_platform: artifact *groups* (see + _PRECACHE_GROUP_HOSTS) are named after operating systems, so routing them + through the arch-aware platform check would disable e.g. the `linux` group + on an arm64 Linux host. + """ os_name = repository_ctx.os.name.lower() - if platform == "macos": + if os_family == "macos": return os_name.startswith("mac") or os_name.startswith("darwin") - return os_name.startswith(platform) + return os_name.startswith(os_family) + +def _host_matches_platform(repository_ctx, platform): + if platform == "macos": + return _host_is_os(repository_ctx, "macos") + if not _host_is_os(repository_ctx, _archive_platform(platform)): + return False + + # Linux distinguishes architectures: an arm64 SDK repo can only be + # completed on an arm64 host (the fetch-time precache below runs the + # launcher, which downloads native artifacts for the machine it is on), + # and the x64 repo must not claim an arm64 host. + if _archive_platform(platform) == "linux": + return _host_is_arm64(repository_ctx) == (_platform_arch(platform) == "arm64") + return True def _ensure_precached_artifacts(repository_ctx): """Verify requested artifact groups exist; precache them if fetchable.""" missing = [ group for group in repository_ctx.attr.precache - if not repository_ctx.path("flutter/bin/cache/" + _PRECACHE_SENTINELS[group]).exists + if not repository_ctx.path( + "flutter/bin/cache/" + _sentinel_path(group, repository_ctx.attr.platform), + ).exists ] if not missing: return @@ -172,7 +224,7 @@ def _warm_first_run_stamps(repository_ctx): # files before the warm-up: precache then records matching stamps, the # up-to-date check passes forever after, and the sealed cache stays # untouched. (On macOS the real artifacts exist; nothing to do.) - if not _host_matches_platform(repository_ctx, "macos"): + if not _host_is_os(repository_ctx, "macos"): # Union of the artifact names across Flutter generations: older # releases (e.g. 3.24) name the usbmuxd artifact without the lib # prefix and probe iproxy inside it; newer releases use libusbmuxd @@ -197,7 +249,7 @@ def _warm_first_run_stamps(repository_ctx): group for group in repository_ctx.attr.precache if _PRECACHE_GROUP_HOSTS.get(group) == None or - _host_matches_platform(repository_ctx, _PRECACHE_GROUP_HOSTS[group]) + _host_is_os(repository_ctx, _PRECACHE_GROUP_HOSTS[group]) ] args = ["flutter/bin/flutter", "--no-version-check", "precache"] for group in _PRECACHE_GROUP_HOSTS: @@ -257,7 +309,9 @@ def _resolve_integrity(repository_ctx): platform — this runs lazily per-platform, so cross-OS repos that are never fetched never trip on it. """ - platform = repository_ctx.attr.platform + # Platforms that reuse another platform's archive verify against that + # archive's integrity — it is literally the same download. + platform = _archive_platform(repository_ctx.attr.platform) version = repository_ctx.attr.flutter_version override = repository_ctx.attr.integrity if override: @@ -274,12 +328,75 @@ def _resolve_integrity(repository_ctx): platform = platform, )) +def _strip_foreign_arch_artifacts(repository_ctx): + """Remove native artifacts belonging to the archive's architecture. + + Only reached for platforms in _ARCHIVE_ALIASES, which download another + architecture's release archive. Deleting the native pieces (and the stamps + that mark them current) makes the fetch-time precache in + _warm_first_run_stamps refetch them for the host: the launcher's + update_dart_sdk.sh picks the Dart SDK by `uname -m`, and flutter_tools + resolves engine artifacts through its own host-platform detection. Both + then land on the arm64 downloads Flutter publishes per engine revision, + even though it ships no arm64 SDK tarball. + """ + archive_arch = _platform_arch(_archive_platform(repository_ctx.attr.platform)) + + stale = [ + # Native Dart SDK (dart, dartaotruntime, and the AOT snapshots). + "flutter/bin/cache/dart-sdk", + "flutter/bin/cache/engine-dart-sdk.stamp", + # Host engine artifacts: flutter_tester, gen_snapshot, impellerc. + "flutter/bin/cache/artifacts/engine/linux-{}".format(archive_arch), + "flutter/bin/cache/artifacts/engine/linux-{}-profile".format(archive_arch), + "flutter/bin/cache/artifacts/engine/linux-{}-release".format(archive_arch), + # font-subset ships per-arch alongside the engine artifacts. + "flutter/bin/cache/artifacts/engine/font-subset.stamp", + "flutter/bin/cache/font-subset.stamp", + ] + for path in stale: + target = repository_ctx.path(path) + if target.exists: + repository_ctx.delete(target) + +def _verify_rearchitected(repository_ctx): + """Fail at fetch time if the host-arch native artifacts are still missing. + + _strip_foreign_arch_artifacts relies on the fetch-time precache to refetch + the Dart SDK and host engine artifacts for the host architecture. If a + future Flutter release stops doing that as a side effect of `precache`, the + SDK would still assemble and only break much later inside a build action, + with an exec-format error or a missing flutter_tester. Check here instead. + """ + arch = _platform_arch(repository_ctx.attr.platform) + required = [ + ("Dart SDK", "flutter/bin/cache/dart-sdk/bin/dart"), + ("engine artifacts", "flutter/bin/cache/artifacts/engine/linux-{}/flutter_tester".format(arch)), + ] + missing = [name for name, path in required if not repository_ctx.path(path).exists] + if missing: + fail( + ("rules_flutter: the {} SDK is missing {} after the fetch-time precache. " + + "The x64 release archive was re-architected but Flutter did not refetch the " + + "host-arch replacements. Check that Flutter {} still publishes " + + "dart-sdk-linux-{}.zip and linux-{}/artifacts.zip for its engine revision.").format( + repository_ctx.attr.platform, + " and ".join(missing), + repository_ctx.attr.flutter_version, + arch, + arch, + ), + ) + def _flutter_repo_impl(repository_ctx): - # Flutter SDK download URLs from Google Cloud Storage + # Flutter SDK download URLs from Google Cloud Storage. Platforms without + # their own release archive (see _ARCHIVE_ALIASES) download a sibling + # architecture's and are re-architected below. platform = repository_ctx.attr.platform - extension = "zip" if platform == "windows" else ("zip" if platform == "macos" else "tar.xz") + archive_platform = _archive_platform(platform) + extension = "zip" if archive_platform == "windows" else ("zip" if archive_platform == "macos" else "tar.xz") url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/{0}/flutter_{0}_{1}-stable.{2}".format( - platform, + archive_platform, repository_ctx.attr.flutter_version, extension, ) @@ -291,8 +408,18 @@ def _flutter_repo_impl(repository_ctx): ) _patch_engine_version_script(repository_ctx) + if platform in _ARCHIVE_ALIASES: + if not _host_matches_platform(repository_ctx, platform): + fail( + ("rules_flutter: the {} SDK reuses the {} release archive and must refetch native " + + "artifacts for the host, so it can only be fetched on a matching host. " + + "Build this target on a {} machine.").format(platform, archive_platform, platform), + ) + _strip_foreign_arch_artifacts(repository_ctx) _ensure_precached_artifacts(repository_ctx) _warm_first_run_stamps(repository_ctx) + if platform in _ARCHIVE_ALIASES: + _verify_rearchitected(repository_ctx) # Drop transient download staging shipped in (or created by) the archive. downloads = repository_ctx.path("flutter/bin/cache/downloads")