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
43 changes: 43 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test

on:
push:
branches: [master]
pull_request:

jobs:
test:
name: bats (${{ matrix.os }} / ${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
# x86_64 Linux — the primary CI target. Also serves as the "stamps
# must be preserved" guard for the bundled x64 SDK.
# aarch64 Linux — guards the regression documented in ADR001: if the
# install script stops invalidating the bundled stamps on non-x86_64
# hosts, the "stamps are removed" tests fail here.
include:
- os: linux
arch: x86_64
runner: ubuntu-24.04
- os: linux
arch: arm64
runner: ubuntu-24.04-arm

steps:
- uses: actions/checkout@v4

- name: Install bats
run: |
git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The bats installation step clones from the repository's default branch without pinning to a specific tag or commit, which means upstream changes can break CI unexpectedly. Consider cloning a specific release tag for reproducible builds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/test.yml, line 33:

<comment>The bats installation step clones from the repository's default branch without pinning to a specific tag or commit, which means upstream changes can break CI unexpectedly. Consider cloning a specific release tag for reproducible builds.</comment>

<file context>
@@ -0,0 +1,43 @@
+
+      - name: Install bats
+        run: |
+          git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core
+          /tmp/bats-core/install.sh "$HOME/.local"
+          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
</file context>
Suggested change
git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core
git clone --depth 1 --branch v1.11.0 https://github.com/bats-core/bats-core.git /tmp/bats-core

/tmp/bats-core/install.sh "$HOME/.local"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Report architecture
run: |
echo "uname -s = $(uname -s)"
echo "uname -m = $(uname -m)"

- name: Run tests
run: bats test/
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,31 @@ Apple will prompt you to install Rosetta if you open a GUI application but not i
softwareupdate --install-rosetta
```

### `cannot execute binary file: Exec format error` on Linux ARM64

The official Linux release tarball ships with an **x86-64** `dart-sdk` bundled
under `bin/cache/dart-sdk/`, regardless of host architecture. On an `aarch64`
(or `riscv64`) host this binary cannot run, and the first `flutter` invocation
fails with:

```
.../bin/internal/shared.sh: line 273:
.../bin/cache/dart-sdk/bin/dart: cannot execute binary file: Exec format error
```

Note that this error surfaces from Flutter's own bootstrap script, not from the
plugin's `install` step — the install itself reports success. Flutter ships two
stamp files (`engine-dart-sdk.stamp` and `flutter_tools.stamp`) that match on
first run, which causes Flutter's self-heal (`update_dart_sdk.sh`, the script
that would normally detect the host architecture and fetch a matching
dart-sdk) to be skipped entirely.

The plugin now works around this by deleting both stamps after extraction on
non-x86_64 Linux hosts, so that `update_dart_sdk.sh` runs on first invocation
and fetches the correct dart-sdk (e.g. `dart-sdk-linux-arm64.zip`). The first
`flutter` command will incur a one-time download (~220 MB) to replace the
bundled dart-sdk.

See [ADR001: Linux ARM64 Support](./docs/adr/adr001-linux-arm64-support.md)
for the full diagnosis and rationale.

30 changes: 27 additions & 3 deletions bin/install
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash

currentDir="$(dirname $0)"
currentDir="$(dirname "${BASH_SOURCE[0]}")"

source "${currentDir}/jq-downloader"
source "${currentDir}/utils.sh"
Expand Down Expand Up @@ -54,7 +54,31 @@ install() {
tar xf "${fileName}" --strip 1
fi
rm -f "${fileName}"

invalidate_bundled_stamps_for_non_x64_linux
}

# The official Linux tarball ships with an x64 dart-sdk pre-bundled in
# bin/cache/dart-sdk/ plus matching engine-dart-sdk.stamp and
# flutter_tools.stamp. On non-x64 hosts (arm64, riscv64) the bundled dart
# binary is the wrong architecture. Because the stamps match, Flutter's
# self-heal in bin/internal/update_dart_sdk.sh (which is only invoked when
# flutter_tools.stamp is invalid) never runs. Removing both stamps forces
# update_dart_sdk.sh to run on first invocation; it detects the host arch
# and fetches the correct dart-sdk (e.g. dart-sdk-linux-arm64.zip).
invalidate_bundled_stamps_for_non_x64_linux() {
if [ "$(uname -s)" == "Linux" ]; then
case "$(uname -m)" in
x86_64) ;;
*)
rm -f "${ASDF_INSTALL_PATH}/bin/cache/engine-dart-sdk.stamp"
rm -f "${ASDF_INSTALL_PATH}/bin/cache/flutter_tools.stamp"
;;
esac
fi
}

download_jq_if_not_exists
install
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
download_jq_if_not_exists
install
fi
2 changes: 1 addition & 1 deletion bin/utils.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

${FLUTTER_STORAGE_BASE_URL:=https://storage.googleapis.com} > /dev/null 2>&1
: "${FLUTTER_STORAGE_BASE_URL:=https://storage.googleapis.com}" > /dev/null 2>&1
23 changes: 23 additions & 0 deletions docs/adr/_adr-XXXX-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
id: adrs-adr000
title: "ADR000: [TITLE]"
# prettier-ignore
description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION]
---

<!-- These documents have names that are short noun phrases. For example, "ADR001: Deployment on Ruby on Rails 3.0.10" or "ADR009: LDAP for Multitenant Integration" -->

## Context

<!--
This section describes the forces at play, including technological, political, social, and project local. These forces are probably in tension, and should be called out as such. The language in this section is value-neutral. It is simply describing facts. -->

## Decision

<!-- This section describes our response to these forces. It is stated in full sentences, with active voice. "We will ..." -->

## Consequences

<!-- This section describes the resulting context, after applying the decision. All consequences should be listed here, not just the "positive" ones. A particular decision may have positive, negative, and neutral consequences, but all of them affect the team and project in the future. -->

<!-- This template is taken from a blog post by Michael Nygard http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions -->
145 changes: 145 additions & 0 deletions docs/adr/adr001-linux-arm64-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
id: adrs-adr001
title: "ADR001: Linux ARM64 Support"
# prettier-ignore
description: Architecture Decision Record (ADR) for Linux ARM64 support, fixing an exec format error on aarch64 hosts caused by the bundled x64 dart-sdk
---

## Context

Flutter publishes a single Linux release tarball per version/channel. That tarball
bundles an **x86-64 dart-sdk** under `bin/cache/dart-sdk/` along with two stamp files
that record the engine revision the SDK was built against:

- `bin/cache/engine-dart-sdk.stamp` — records the engine hash for the dart-sdk
- `bin/cache/flutter_tools.stamp` — records the compile key for the flutter_tools
snapshot (revision + tool args)

Flutter's bootstrap flow (`bin/internal/shared.sh`) computes a `compilekey` from the
current git revision and compares it against `flutter_tools.stamp`. Only when the
stamp is missing, empty, or mismatched does `upgrade_flutter()` invoke
`bin/internal/update_dart_sdk.sh`, which:

1. Reads the engine hash from `bin/cache/engine.stamp` (and `bin/cache/engine.realm`,
used to construct the download URL).
2. Compares it against `engine-dart-sdk.stamp`; if they differ (or the stamp is
absent) it re-downloads the dart-sdk.
3. Detects the host architecture via `uname -m`, mapping:
- `x86_64` → `x64`
- `riscv64` → `riscv64`
- anything else (including `aarch64`) → `arm64`
4. Fetches the matching `dart-sdk-linux-<arch>.zip` from the Flutter engine
bucket and replaces `bin/cache/dart-sdk/`.

On a Linux aarch64 host, the asdf-flutter plugin (before this change) extracted
the official Linux tarball verbatim. Because both shipped stamps matched on
first run, `update_dart_sdk.sh` was never invoked, and the bundled x86-64
`dart` binary remained in place. The first `flutter` invocation then failed:

```
/home/.../bin/internal/shared.sh: line 273:
/home/.../bin/cache/dart-sdk/bin/dart: cannot execute binary file: Exec format error
```

The user-visible symptom — "Exec format error" originating from `shared.sh`
rather than from the plugin's `install` script — made the failure mode
difficult to diagnose: the install itself reported success, and the error only
surfaced on first run, pointing at Flutter internals rather than at the
architecture mismatch.

This was investigated and verified on:

- Host: `aarch64` Linux (kernel reports `aarch64` via `uname -m`)
- Version: `3.44.4-stable`
- Bundled dart binary: `ELF 64-bit LSB pie executable, x86-64` (confirmed via
`file` on `bin/cache/dart-sdk/bin/dart`)
- Engine bucket: `dart-sdk-linux-arm64.zip` for the same engine hash returns
HTTP 200, i.e. Flutter does publish an arm64 dart-sdk — it just isn't shipped
inside the Linux tarball.
(`dart-sdk-linux-riscv64.zip` returns 404 for the same engine hash, so
Flutter does not currently publish a riscv64 dart-sdk despite the script
mapping `riscv64` → that filename.)

Note that macOS is unaffected because the asdf-flutter `install` script already
filters `releases_macos.json` by `dart_sdk_arch` (`arm64` on Apple Silicon,
`x64` otherwise). `releases_linux.json` exposes no `dart_sdk_arch` field at all
— every entry has `dart_sdk_arch == "x64"` and the archive filename carries no
architecture suffix — so the same filtering approach cannot be reused for
Linux. (Verified against `releases_linux.json`: all 719 entries across all
channels have `dart_sdk_arch == "x64"` and no arch suffix in `archive`.)

## Decision

We will **not** attempt to pick an architecture-specific Linux archive (none
exists). Instead, after extracting the official Linux tarball, on any non-x86_64
host we will delete both `bin/cache/engine-dart-sdk.stamp` and
`bin/cache/flutter_tools.stamp` so that Flutter's own self-heal runs on first
invocation and replaces the bundled dart-sdk with one matching the host
architecture.

Concretely, a helper function `invalidate_bundled_stamps_for_non_x64_linux`
is added to `bin/install` and called at the end of the `install` function:

```bash
invalidate_bundled_stamps_for_non_x64_linux() {
if [ "$(uname -s)" == "Linux" ]; then
case "$(uname -m)" in
x86_64) ;;
*)
rm -f "${ASDF_INSTALL_PATH}/bin/cache/engine-dart-sdk.stamp"
rm -f "${ASDF_INSTALL_PATH}/bin/cache/flutter_tools.stamp"
;;
esac
fi
}
```

It is extracted into its own function so it can be unit-tested in isolation
(see `test/install.bats`) without performing a full 1.4 GB download.

Both stamps are removed (rather than only `engine-dart-sdk.stamp`) because
`update_dart_sdk.sh` is only reachable inside the `upgrade_flutter()` branch
that fires when `flutter_tools.stamp` is invalid. Removing only
`engine-dart-sdk.stamp` leaves the bootstrap short-circuit intact and the
x86-64 binary in place — verified during diagnosis.

This approach deliberately delegates architecture detection to Flutter's
`update_dart_sdk.sh`, which already handles `aarch64` and `riscv64` correctly,
rather than duplicating that logic in the plugin.

## Consequences

Positive:

- Linux aarch64 hosts now run `flutter` and `dart` natively on first invocation.
Verified end-to-end: `flutter --version` reports
`Flutter 3.44.4 • channel stable` and `dart --version` reports
`Dart SDK version: 3.12.2 (stable) (...) on "linux_arm64"` (the
`on "linux_arm64"` suffix confirms the native arm64 SDK is in use); the
bundled `dart` binary is replaced with an `ELF 64-bit ... ARM aarch64`
executable.
- Linux riscv64 hosts would benefit from the same fix if Flutter publishes a
`dart-sdk-linux-riscv64.zip` for the engine version in use —
`update_dart_sdk.sh` already maps `riscv64` → that filename. As of engine
`a10d8ac38d` (3.44.4-stable) that zip returns 404, so riscv64 support is
aspirational and depends on Flutter publishing the artifact.
- The plugin keeps using a single archive per Linux version — no need to parse a
non-existent `dart_sdk_arch` field on Linux or special-case channels.
- Architecture detection lives in exactly one place (Flutter's own script), so
future architectures Flutter adds support for are picked up automatically.

Negative:

- The first `flutter` invocation on a non-x86_64 Linux host incurs an extra
download (~220 MB for `dart-sdk-linux-arm64.zip`) plus a flutter_tools
rebuild before any command can run. This is a one-time cost per install but
is noticeable.
- The fix depends on Flutter's internal stamp protocol staying as described.
If a future Flutter release changes how `flutter_tools.stamp` is computed or
stops gating `update_dart_sdk.sh` behind it, this workaround may silently
regress back to shipping an x86-64 binary on arm64 hosts. The failure mode
would be the original "Exec format error" again.
- Deleting `flutter_tools.stamp` also forces a rebuild of `flutter_tools.snapshot`
from source on first run. This is unavoidable: the shipped snapshot was built
Comment on lines +139 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The ADR documents the fix incorrectly here. Since the plugin deletes engine-dart-sdk.stamp as well as flutter_tools.stamp, update_dart_sdk.sh running unconditionally would still see a missing engine-dart-sdk.stamp and trigger re-download of the correct architecture SDK — the workaround would still work. The more accurate regression risk would be if update_dart_sdk.sh itself stops checking engine-dart-sdk.stamp or is bypassed entirely, not simply if it's no longer gated behind flutter_tools.stamp. Consider rephrasing this consequence so future maintainers understand the real dependency on the engine-stamp check inside update_dart_sdk.sh rather than on the outer flutter_tools.stamp gate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/adr/adr001-linux-arm64-support.md, line 139:

<comment>The ADR documents the fix incorrectly here. Since the plugin deletes `engine-dart-sdk.stamp` as well as `flutter_tools.stamp`, `update_dart_sdk.sh` running unconditionally would still see a missing `engine-dart-sdk.stamp` and trigger re-download of the correct architecture SDK — the workaround would still work. The more accurate regression risk would be if `update_dart_sdk.sh` itself stops checking `engine-dart-sdk.stamp` or is bypassed entirely, not simply if it's no longer gated behind `flutter_tools.stamp`. Consider rephrasing this consequence so future maintainers understand the real dependency on the engine-stamp check inside `update_dart_sdk.sh` rather than on the outer `flutter_tools.stamp` gate.</comment>

<file context>
@@ -0,0 +1,145 @@
+  is noticeable.
+- The fix depends on Flutter's internal stamp protocol staying as described.
+  If a future Flutter release changes how `flutter_tools.stamp` is computed or
+  stops gating `update_dart_sdk.sh` behind it, this workaround may silently
+  regress back to shipping an x86-64 binary on arm64 hosts. The failure mode
+  would be the original "Exec format error" again.
</file context>
Suggested change
stops gating `update_dart_sdk.sh` behind it, this workaround may silently
regress back to shipping an x86-64 binary on arm64 hosts. The failure mode
would be the original "Exec format error" again.
- Deleting `flutter_tools.stamp` also forces a rebuild of `flutter_tools.snapshot`
from source on first run. This is unavoidable: the shipped snapshot was built
- The fix depends on Flutter's internal stamp protocol staying as described.
- If a future Flutter release changes `update_dart_sdk.sh` to not check
`engine-dart-sdk.stamp`, or bypasses `update_dart_sdk.sh` entirely,
this workaround may silently regress back to shipping an x86-64 binary
on arm64 hosts. The failure mode would be the original "Exec format
error" again.

for the x86-64 dart VM and cannot be reused once the dart-sdk is replaced
with the arm64 one.
Loading