From ac135a5fe5f66ecf62f856306aabcc9a625b137e Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Tue, 22 Sep 2026 21:21:38 +0800 Subject: [PATCH 1/3] Add support for Windows ARM64 --- .github/workflows/e2e.yml | 11 ++- docs/arm64.md | 25 +++--- docs/e2e.md | 29 ++++++- docs/ohos.md | 4 +- lib/git.lua | 115 ++++++++++++++++++++++++++ lib/ohos.lua | 111 ++----------------------- lib/source.lua | 112 ++++--------------------- tests/e2e/linux/e2e.sh | 8 +- tests/e2e/linux/install.sh | 12 ++- tests/e2e/windows/Dockerfile | 9 +- tests/e2e/windows/bootstrap-tools.ps1 | 8 +- tests/e2e/windows/e2e.ps1 | 94 ++++++++++++++++----- tests/e2e/windows/install-docker.ps1 | 102 +++++++++++++++++++++++ tests/e2e/windows/install.ps1 | 12 ++- tests/e2e/windows/setup.ps1 | 6 +- 15 files changed, 407 insertions(+), 251 deletions(-) create mode 100644 lib/git.lua create mode 100644 tests/e2e/windows/install-docker.ps1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a57a933..680c13a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -28,9 +28,14 @@ jobs: e2e-windows: if: github.repository == 'version-fox/vfox-flutter' - name: Windows container (amd64) - runs-on: windows-latest - timeout-minutes: 90 + name: Windows container (${{ matrix.runs-on }}) + strategy: + fail-fast: false + matrix: + runs-on: [windows-latest, windows-11-arm] + runs-on: ${{ matrix.runs-on }} + timeout-minutes: 60 steps: - uses: actions/checkout@v7 + - run: pwsh -NoProfile -File tests/e2e/windows/install-docker.ps1 - run: pwsh -NoProfile -File tests/e2e/windows/e2e.ps1 diff --git a/docs/arm64.md b/docs/arm64.md index 6ef9ebc..915c7ba 100644 --- a/docs/arm64.md +++ b/docs/arm64.md @@ -16,31 +16,36 @@ their existing version names. Channels also accept a suffix, such as first, followed by other architectures, with each group ordered by version. This keeps `@latest` on the default architecture. -Only architectures provided by Flutter for the current operating system are -listed. On Apple Silicon, running an x64 build requires Rosetta. +Only architectures provided by Flutter for the current operating system are listed. + +## MacOS ARM64 + +On Apple Silicon, running an x64 build requires Rosetta. ## Linux ARM64 On Linux ARM64 the plugin installs the official `flutter/flutter` git tag instead of a prebuilt bundle. The Flutter tool bootstraps the matching native `dart-sdk-linux-arm64` itself on first run, so the result is a native SDK. -This follows the same pattern as the OpenHarmony builds (`lib/ohos.lua`): -`Available` advertises one `-arm64` entry per `x64` release whose `url` is the -clone URL, and `PreInstall` materializes the tag into a local directory under -`~/.vfox/tmp` that vfox consumes. `PostInstall` removes that throwaway -checkout. Explicit `-x64` requests still resolve to the upstream archive, which cannot run on Linux ARM64. ## Windows ARM64 -Not supported: upstream publishes no `windows-arm64` archives, so no `-arm64` -entries are listed and installs fail instead of handing out an unusable `x64` -bundle. +On Windows ARM64 the plugin installs the official `flutter/flutter` git tag +instead of a prebuilt bundle, for the same reason as Linux ARM64: upstream +publishes no `windows-arm64` archives. The Flutter tool bootstraps the matching +native `dart-sdk-windows-arm64` itself on first run, so the result is a native +SDK. + +Explicit `-x64` requests still resolve to the upstream archive, which cannot +run on Windows ARM64. # Limits +## Linux ARM64 and Windows ARM64 + - **`git` is required.** The checkout runs `git init` / `fetch` / `checkout` against `https://github.com/flutter/flutter.git`. - **The first `flutter` run is slow.** It downloads the native Dart SDK and diff --git a/docs/e2e.md b/docs/e2e.md index 73ddfbf..7bd9227 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -1,9 +1,8 @@ # E2E The offline Lua hook tests (`tests/hooks_test.lua`) run in the **Test Plugin** workflow. -The end-to-end suite runs in containers against real vfox (`latest` and `main`) on Linux -and Windows; each of the four `vfox` x `flavor` combinations gets its own throwaway -container. +The end-to-end suite runs in containers against real vfox on Linux and Windows; +each `vfox` x `flavor` x `mirror` combination gets its own throwaway container. ## Ubuntu 26.04.1 @@ -32,7 +31,12 @@ docker run --privileged --rm tonistiigi/binfmt --install all ARCH=arm64 bash tests/e2e/linux/e2e.sh ``` -## Running on Windows 11 Pro +## Windows 11 Pro + +Windows containers cannot be emulated across architectures, so the ARM64 suite +needs an ARM64 host (a Copilot+ PC or the `windows-11-arm` CI runner). + +### x64 1. Execute in Windows PowerShell 5.1, @@ -63,3 +67,20 @@ git clone git@github.com:version-fox/vfox-flutter.git cd ./vfox-flutter/ pwsh -NoProfile -File .\tests\e2e\windows\e2e.ps1 ``` + +### arm64 + +1. Execute in Windows PowerShell 5.1, + +```powershell +winget install --id Microsoft.PowerShell --source winget --exact +``` + +2. Execute in PowerShell 7, + +```powershell +git clone git@github.com:version-fox/vfox-flutter.git +cd ./vfox-flutter/ +pwsh -NoProfile -File .\tests\e2e\windows\install-docker.ps1 +pwsh -NoProfile -File .\tests\e2e\windows\e2e.ps1 +``` diff --git a/docs/ohos.md b/docs/ohos.md index 0923841..a5d36c5 100644 --- a/docs/ohos.md +++ b/docs/ohos.md @@ -30,9 +30,9 @@ vfox search flutter - **Install by exact version.** OpenHarmony versions sort after the official releases, so `@latest` and the `stable`/`beta`/`dev` channels never select one. - **No architecture variants.** `flutter@3.41.10-ohos-1.0.0-x64` is not supported. -- **`linux-arm64`.** Upstream publishes only x64 Dart SDKs, so the first +- **`linux-arm64` and `windows-arm64`.** Upstream publishes only x64 Dart SDKs, so the first `flutter` run fails there (`It appears that the downloaded file is corrupt`). - The Linux ARM64 e2e runs the official flavor only. + The ARM64 e2e runs the official flavor only. - **A Windows home containing a space** (`C:\Users\John Doe\.vfox`) is not supported; point vfox at a path without spaces with `$env:VFOX_HOME = 'D:\vfox'`. - **The first `flutter` command is slow.** It builds the tool and downloads the Dart SDK diff --git a/lib/git.lua b/lib/git.lua new file mode 100644 index 0000000..7a7d0b8 --- /dev/null +++ b/lib/git.lua @@ -0,0 +1,115 @@ +local HOME_ENV = "VFOX_HOME" +local FALLBACK_HOME_ENV = { "HOME", "USERPROFILE" } +local VFOX_DIR = ".vfox" +local TMP_DIR = "tmp" +local ENGINE_PIN = "bin/internal/engine.version" +local FETCH_ATTEMPTS = 3 + +local M = {} + +local function quote(value) + if RUNTIME.osType == "windows" then + return value + end + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function exec(command) + local result = os.execute(command) + return result == 0 or result == true +end + +local function git(root, args) + return exec("git -C " .. quote(root) .. " " .. args) +end + +local function sep() + if RUNTIME.osType == "windows" then + return "\\" + end + return "/" +end + +local function localPath(path) + return path:gsub("/", sep()) +end + +function M.removeDir(path) + if RUNTIME.osType == "windows" then + exec('if exist "' .. path .. '" rmdir /s /q "' .. path .. '"') + else + exec("rm -rf " .. quote(path)) + end +end + +local function vfoxHome() + local home = os.getenv(HOME_ENV) + if home == nil or home == "" then + for _, name in ipairs(FALLBACK_HOME_ENV) do + home = os.getenv(name) + if home ~= nil and home ~= "" then + break + end + end + end + if home == nil or home == "" then + return nil + end + return localPath(home .. "/" .. VFOX_DIR) +end + +function M.workDir(version) + local home = vfoxHome() + if home == nil then + return nil + end + return localPath(home .. "/" .. TMP_DIR .. "/" .. version) +end + +local function parentDir(dir) + return dir:match("^(.+)[" .. sep() .. "][^" .. sep() .. "]+$") +end + +local function makeParentDir(dir) + if RUNTIME.osType == "windows" then + return + end + local parent = parentDir(dir) + if parent == nil then + return + end + exec("mkdir -p " .. quote(parent)) +end + +function M.resetDir(dir) + M.removeDir(dir) + makeParentDir(dir) +end + +function M.init(root) + return exec("git init -q " .. quote(root)) +end + +function M.fetch(root, remote, ref) + for _ = 1, FETCH_ATTEMPTS do + if git(root, "fetch -q --depth 1 " .. remote .. " " .. ref) then + return true + end + end + return false +end + +function M.checkoutHead(root) + return git(root, "checkout -q FETCH_HEAD") +end + +function M.hasEnginePin(root) + local pin = io.open(localPath(root .. "/" .. ENGINE_PIN), "r") + if pin == nil then + return false + end + pin:close() + return true +end + +return M diff --git a/lib/ohos.lua b/lib/ohos.lua index 47ef635..3b52fd2 100644 --- a/lib/ohos.lua +++ b/lib/ohos.lua @@ -1,16 +1,12 @@ local http = require("http") local json = require("json") +local git = require("git") local MARKER = "-ohos-" local REPO = "CPF-Flutter/flutter_flutter" local RELEASES_URL = "https://gitcode.com/api/v5/repos/%s/releases?per_page=100" local CLONE_URL = "https://gitcode.com/%s.git" local NOTE = "OpenHarmony" -local HOME_ENV = "VFOX_HOME" -local FALLBACK_HOME_ENV = { "HOME", "USERPROFILE" } -local VFOX_DIR = ".vfox" -local TMP_DIR = "tmp" -local ENGINE_PIN = "bin/internal/engine.version" local M = {} @@ -18,42 +14,6 @@ function M.isOhosVersion(version) return type(version) == "string" and version:find(MARKER, 1, true) ~= nil end -local function quote(value) - if RUNTIME.osType == "windows" then - return value - end - return "'" .. value:gsub("'", "'\\''") .. "'" -end - -local function run(command) - local result = os.execute(command) - return result == 0 or result == true -end - -local function git(root, args) - return run("git -C " .. quote(root) .. " " .. args) -end - -local function removeDir(path) - if RUNTIME.osType == "windows" then - os.execute('if exist "' .. path .. '" rmdir /s /q "' .. path .. '"') - else - os.execute("rm -rf " .. quote(path)) - end -end - -local function sep() - if RUNTIME.osType == "windows" then - return "\\" - end - return "/" -end - -local function localPath(path) - local translated = path:gsub("/", sep()) - return translated -end - local function releases() local resp, err = http.get({ url = RELEASES_URL:format(REPO) }) if err ~= nil or resp.status_code ~= 200 then @@ -82,50 +42,6 @@ function M.list() return result end -local function vfoxHome() - local home = os.getenv(HOME_ENV) - if home == nil or home == "" then - for _, name in ipairs(FALLBACK_HOME_ENV) do - home = os.getenv(name) - if home ~= nil and home ~= "" then - break - end - end - end - if home == nil or home == "" then - return nil - end - return localPath(home .. "/" .. VFOX_DIR) -end - -local function workDir(version) - local home = vfoxHome() - if home == nil then - return nil - end - return localPath(home .. "/" .. TMP_DIR .. "/" .. version) -end - -local function parentDir(dir) - return dir:match("^(.+)[" .. sep() .. "][^" .. sep() .. "]+$") -end - -local function makeParentDir(dir) - if RUNTIME.osType == "windows" then - return - end - local parent = parentDir(dir) - if parent == nil then - return - end - os.execute("mkdir -p " .. quote(parent)) -end - -local function resetDir(dir) - removeDir(dir) - makeParentDir(dir) -end - function M.checkout(version, requestedArch) if requestedArch ~= nil then return nil @@ -140,33 +56,24 @@ function M.checkout(version, requestedArch) if commit == nil or commit == "" then return nil end - local dir = workDir(version) + local dir = git.workDir(version) if dir == nil then error("cannot resolve the vfox home directory") end local cloneUrl = CLONE_URL:format(REPO) - resetDir(dir) - if not run("git init -q " .. quote(dir)) then + git.resetDir(dir) + if not git.init(dir) then error("failed to initialize git in " .. dir .. " (is git installed?)") end - local fetched = false - for _ = 1, 3 do - if git(dir, "fetch -q --depth 1 " .. cloneUrl .. " " .. commit) then - fetched = true - break - end - end - if not fetched then + if not git.fetch(dir, cloneUrl, commit) then error("failed to fetch " .. commit .. " from " .. cloneUrl) end - if not git(dir, "checkout -q FETCH_HEAD") then + if not git.checkoutHead(dir) then error("failed to check out " .. commit .. " in " .. dir) end - local pin = io.open(localPath(dir .. "/" .. ENGINE_PIN), "r") - if pin == nil then + if not git.hasEnginePin(dir) then error("the checkout at " .. commit .. " has no engine version pin") end - pin:close() return { version = version, url = dir, @@ -175,9 +82,9 @@ function M.checkout(version, requestedArch) end function M.clean(version) - local dir = workDir(version) + local dir = git.workDir(version) if dir ~= nil then - removeDir(dir) + git.removeDir(dir) end end diff --git a/lib/source.lua b/lib/source.lua index 0061bb6..3d0d897 100644 --- a/lib/source.lua +++ b/lib/source.lua @@ -1,10 +1,12 @@ +local git = require("git") + local REPO = "flutter/flutter" local CLONE_URL = "https://github.com/%s.git" -local HOME_ENV = "VFOX_HOME" -local FALLBACK_HOME_ENV = { "HOME", "USERPROFILE" } -local VFOX_DIR = ".vfox" -local TMP_DIR = "tmp" -local ENGINE_PIN = "bin/internal/engine.version" + +local SOURCE_INSTALL_PLATFORMS = { + ["linux/arm64"] = true, + ["windows/arm64"] = true, +} local M = {} @@ -13,87 +15,7 @@ function M.repoUrl() end function M.supports(osType, archType) - return osType == "linux" and archType == "arm64" -end - -local function quote(value) - if RUNTIME.osType == "windows" then - return value - end - return "'" .. value:gsub("'", "'\\''") .. "'" -end - -local function run(command) - local result = os.execute(command) - return result == 0 or result == true -end - -local function git(root, args) - return run("git -C " .. quote(root) .. " " .. args) -end - -local function removeDir(path) - if RUNTIME.osType == "windows" then - os.execute('if exist "' .. path .. '" rmdir /s /q "' .. path .. '"') - else - os.execute("rm -rf " .. quote(path)) - end -end - -local function sep() - if RUNTIME.osType == "windows" then - return "\\" - end - return "/" -end - -local function localPath(path) - local translated = path:gsub("/", sep()) - return translated -end - -local function vfoxHome() - local home = os.getenv(HOME_ENV) - if home == nil or home == "" then - for _, name in ipairs(FALLBACK_HOME_ENV) do - home = os.getenv(name) - if home ~= nil and home ~= "" then - break - end - end - end - if home == nil or home == "" then - return nil - end - return localPath(home .. "/" .. VFOX_DIR) -end - -local function workDir(version) - local home = vfoxHome() - if home == nil then - return nil - end - return localPath(home .. "/" .. TMP_DIR .. "/" .. version) -end - -local function parentDir(dir) - return dir:match("^(.+)[" .. sep() .. "][^" .. sep() .. "]+$") -end - -local function makeParentDir(dir) - if RUNTIME.osType == "windows" then - return - end - local parent = parentDir(dir) - if parent == nil then - return - end - os.execute("mkdir -p " .. quote(parent)) -end - -local function resetDir(dir) - removeDir(dir) - makeParentDir(dir) + return SOURCE_INSTALL_PLATFORMS[osType .. "/" .. archType] == true end function M.list(releases) @@ -126,26 +48,24 @@ function M.checkout(baseVersion, versionName) if type(versionName) ~= "string" or versionName == "" then versionName = baseVersion end - local dir = workDir(versionName) + local dir = git.workDir(versionName) if dir == nil then error("cannot resolve the vfox home directory") end local cloneUrl = M.repoUrl() - resetDir(dir) - if not run("git init -q " .. quote(dir)) then + git.resetDir(dir) + if not git.init(dir) then error("failed to initialize git in " .. dir .. " (is git installed?)") end - if not git(dir, "fetch -q --depth 1 " .. cloneUrl .. " refs/tags/" .. baseVersion) then + if not git.fetch(dir, cloneUrl, "refs/tags/" .. baseVersion) then error("failed to fetch tag " .. baseVersion .. " from " .. cloneUrl) end - if not git(dir, "checkout -q FETCH_HEAD") then + if not git.checkoutHead(dir) then error("failed to check out " .. baseVersion .. " in " .. dir) end - local pin = io.open(localPath(dir .. "/" .. ENGINE_PIN), "r") - if pin == nil then + if not git.hasEnginePin(dir) then error("the checkout at " .. baseVersion .. " has no engine version pin") end - pin:close() return { version = versionName, url = dir @@ -153,9 +73,9 @@ function M.checkout(baseVersion, versionName) end function M.clean(version) - local dir = workDir(version) + local dir = git.workDir(version) if dir ~= nil then - removeDir(dir) + git.removeDir(dir) end end diff --git a/tests/e2e/linux/e2e.sh b/tests/e2e/linux/e2e.sh index 74b6094..fd69079 100755 --- a/tests/e2e/linux/e2e.sh +++ b/tests/e2e/linux/e2e.sh @@ -34,11 +34,11 @@ run_one() { local flavor="$2" local mirror="$3" local prefix="vfox $vfox, $flavor, mirror $mirror, $platform" - if [ "$mirror" = default ]; then - docker run --rm --platform "$platform" -e VFOX_VERSION="$vfox" -e FLAVOR="$flavor" "$image" 2>&1 | sed -e "s|^|[$prefix] |" - else - docker run --rm --platform "$platform" -e VFOX_VERSION="$vfox" -e FLAVOR="$flavor" -e FLUTTER_STORAGE_BASE_URL="$mirror" "$image" 2>&1 | sed -e "s|^|[$prefix] |" + local env=("-e" "VFOX_VERSION=$vfox" "-e" "FLAVOR=$flavor" "-e" "GITHUB_TOKEN=${GITHUB_TOKEN:-}") + if [ "$mirror" != default ]; then + env+=("-e" "FLUTTER_STORAGE_BASE_URL=$mirror") fi + docker run --rm --platform "$platform" "${env[@]}" "$image" 2>&1 | sed -e "s|^|[$prefix] |" } for vfox in $foxes; do diff --git a/tests/e2e/linux/install.sh b/tests/e2e/linux/install.sh index a9b2338..6802f5b 100755 --- a/tests/e2e/linux/install.sh +++ b/tests/e2e/linux/install.sh @@ -4,6 +4,16 @@ set -eu version="${1:?missing version}" eval "$(vfox activate bash)" -vfox install flutter@"$version" + +attempt=1 +while ! vfox install flutter@"$version"; do + if [ "$attempt" -ge 5 ]; then + echo "FAIL vfox install flutter@$version" >&2 + exit 1 + fi + attempt=$((attempt + 1)) + sleep 10 +done + vfox use --global flutter@"$version" eval "$(vfox activate bash)" diff --git a/tests/e2e/windows/Dockerfile b/tests/e2e/windows/Dockerfile index a1b82c4..b6ff25c 100644 --- a/tests/e2e/windows/Dockerfile +++ b/tests/e2e/windows/Dockerfile @@ -1,10 +1,10 @@ # escape=` -FROM mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871 +ARG BASE_IMAGE=mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 +FROM ${BASE_IMAGE} SHELL ["powershell", "-NoProfile", "-Command"] COPY tests/e2e/windows/bootstrap-tools.ps1 C:\bootstrap-tools.ps1 -RUN powershell -NoProfile -ExecutionPolicy Bypass -File 'C:\bootstrap-tools.ps1' WORKDIR C:\e2e @@ -13,5 +13,6 @@ COPY hooks C:\e2e\hooks COPY lib C:\e2e\lib COPY tests/e2e/windows C:\e2e\tests\e2e\windows -ENTRYPOINT ["C:/Program Files/PowerShell/7/pwsh.exe", "-File"] -CMD ["C:/e2e/tests/e2e/windows/run.ps1"] +# No ENTRYPOINT/CMD here: e2e.ps1 installs pwsh and git in a container and then +# commits it. docker commit bakes that container's entrypoint into the image, so +# e2e.ps1 sets the entrypoint explicitly for every test run. diff --git a/tests/e2e/windows/bootstrap-tools.ps1 b/tests/e2e/windows/bootstrap-tools.ps1 index 8e7b82a..91467c5 100644 --- a/tests/e2e/windows/bootstrap-tools.ps1 +++ b/tests/e2e/windows/bootstrap-tools.ps1 @@ -1,10 +1,14 @@ $ErrorActionPreference = 'Stop' +$IsArm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' +$pwshArch = if ($IsArm64) { 'arm64' } else { 'x64' } +$gitAsset = if ($IsArm64) { 'Git-2.55.0.5-arm64.exe' } else { 'Git-2.55.0.5-64-bit.exe' } + $msi = "$env:TEMP\pwsh.msi" -& curl.exe -fsSL -o $msi 'https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-x64.msi' +& curl.exe -fsSL -o $msi "https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-$pwshArch.msi" & msiexec.exe /i $msi /quiet /norestart if ($LASTEXITCODE -notin 0, 3010) { throw "PowerShell MSI install failed (exit $LASTEXITCODE)" } $git = "$env:TEMP\git-installer.exe" -& curl.exe -fsSL -o $git 'https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/Git-2.55.0.5-64-bit.exe' +& curl.exe -fsSL -o $git "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/$gitAsset" Start-Process $git -Wait -ArgumentList '/VERYSILENT', '/NORESTART', '/SP-', '/SUPPRESSMSGBOXES' diff --git a/tests/e2e/windows/e2e.ps1 b/tests/e2e/windows/e2e.ps1 index cd78b57..0e10952 100644 --- a/tests/e2e/windows/e2e.ps1 +++ b/tests/e2e/windows/e2e.ps1 @@ -1,15 +1,57 @@ $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true +function Write-DockerLog { + param([string]$log) + if (-not $log -or -not (Test-Path $log)) { return } + Get-Content $log | Select-String 'level=error|exit code|failed|Windows default isolation mode' | Select-Object -Last 40 | ForEach-Object { "dockerd: $($_.Line)" } +} + +$dockerLog = $env:DOCKER_LOG + $Repo = (Resolve-Path "$PSScriptRoot\..\..\..").Path -$Image = 'vfox-flutter-e2e:windows' -& docker build --pull -f "$PSScriptRoot\Dockerfile" -t $Image $Repo -if ($LASTEXITCODE -ne 0) { throw "FAIL docker build exited with code $LASTEXITCODE" } +$hostArch = $env:PROCESSOR_ARCHITECTURE +if ($env:ARCH) { $hostArch = $env:ARCH } +$arch = $hostArch.ToLowerInvariant() +if ($arch -eq 'aarch64') { $arch = 'arm64' } +if ($arch -eq 'x86_64' -or $arch -eq 'x64') { $arch = 'amd64' } +if (($arch -ne 'amd64') -and ($arch -ne 'arm64')) { throw "FAIL unknown architecture $hostArch" } +$platform = "windows/$arch" +$Image = "vfox-flutter-e2e:windows-$arch" +$baseImage = "mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-$arch" +$pwshExe = 'C:/Program Files/PowerShell/7/pwsh.exe' +$runScript = 'C:/e2e/tests/e2e/windows/run.ps1' + +try { + & docker build --pull --platform $platform --build-arg "BASE_IMAGE=$baseImage" -f "$PSScriptRoot\Dockerfile" -t $Image $Repo +} +catch { + Write-DockerLog $dockerLog + throw +} + +$PSNativeCommandUseErrorActionPreference = $false + +$cid = & docker create --platform $platform --entrypoint 'powershell' $Image -NoProfile -ExecutionPolicy Bypass -File C:\bootstrap-tools.ps1 +if ($LASTEXITCODE -ne 0) { throw 'FAIL creating the bootstrap container' } +& docker start -ai $cid +$code = $LASTEXITCODE +# `docker commit` bakes the container's own entrypoint into the new image, so +# the combos below must set it explicitly with --entrypoint. +& docker commit $cid $Image +& docker rm $cid +if ($code -ne 0) { throw "FAIL bootstrap-tools.ps1 exited with code $code" } $foxes = if ($env:VFOX_VERSION) { @($env:VFOX_VERSION) } else { @('latest', 'main') } -$flavours = if ($env:FLAVOR) { @($env:FLAVOR) } else { @('official', 'ohos') } -$mirrorList = if ($env:MIRROR) { @($env:MIRROR -split '\s+' | Where-Object { $_ -ne '' }) } else { @('default', 'https://storage.flutter-io.cn') } +$defaultFlavours = @('official', 'ohos') +$defaultMirrors = @('default', 'https://storage.flutter-io.cn') +if ($arch -eq 'arm64') { + $defaultFlavours = @('official') + $defaultMirrors = @('default') +} +$flavours = if ($env:FLAVOR) { @($env:FLAVOR) } else { $defaultFlavours } +$mirrorList = if ($env:MIRROR) { @($env:MIRROR -split '\s+' | Where-Object { $_ -ne '' }) } else { $defaultMirrors } $mirrorExplicit = -not [string]::IsNullOrWhiteSpace($env:MIRROR) $combos = foreach ($vfox in $foxes) { @@ -21,17 +63,31 @@ $combos = foreach ($vfox in $foxes) { } } -$combos | ForEach-Object -Parallel { - $ErrorActionPreference = 'Stop' - $PSNativeCommandUseErrorActionPreference = $false - $vfox = $_.Vfox - $flavor = $_.Flavor - $mirror = $_.Mirror - $prefix = "vfox $vfox, $flavor, mirror $mirror" - Write-Output "[$prefix] === start ===" - $dockerArgs = @('run', '--rm', '-e', "VFOX_VERSION=$vfox", '-e', "FLAVOR=$flavor") - if ($mirror -ne 'default') { $dockerArgs += @('-e', "FLUTTER_STORAGE_BASE_URL=$mirror") } - $dockerArgs += $using:Image - & docker @dockerArgs 2>&1 | ForEach-Object { "[$prefix] $_" } - if ($LASTEXITCODE -ne 0) { throw ("FAIL docker run ({0}) exited with code {1}" -f $prefix, $LASTEXITCODE) } -} -ThrottleLimit 3 +try { + # Run detached and read the daemon-captured output, so a failed run still + # leaves its logs on the console. + foreach ($combo in $combos) { + $vfox = $combo.Vfox + $flavor = $combo.Flavor + $mirror = $combo.Mirror + $prefix = "vfox $vfox, $flavor, mirror $mirror, $platform" + Write-Output "[$prefix] === start ===" + $runArgs = @('run', '-d', '--platform', $platform, '--entrypoint', $pwshExe, '-e', "VFOX_VERSION=$vfox", '-e', "FLAVOR=$flavor") + if ($mirror -ne 'default') { $runArgs += @('-e', "FLUTTER_STORAGE_BASE_URL=$mirror") } + $runArgs += @($Image, '-File', $runScript) + $cid = & docker @runArgs + if ($LASTEXITCODE -ne 0) { throw ("FAIL docker run ({0}) failed to start" -f $prefix) } + $exitCode = & docker wait $cid + $logs = ((& docker logs $cid 2>&1) -join "`r`n") + Write-Output $logs + & docker rm $cid + if ([int]$exitCode -ne 0) { throw ("FAIL docker run ({0}) exited with code {1}" -f $prefix, $exitCode) } + # A misconfigured entrypoint re-runs the silent bootstrap and exits 0, + # so require the test banner to prove run.ps1 actually ran. + if ($logs -notmatch '=== vfox') { throw "FAIL docker run ($prefix) produced no test output" } + } +} +catch { + Write-DockerLog $dockerLog + throw +} diff --git a/tests/e2e/windows/install-docker.ps1 b/tests/e2e/windows/install-docker.ps1 new file mode 100644 index 0000000..9060ddd --- /dev/null +++ b/tests/e2e/windows/install-docker.ps1 @@ -0,0 +1,102 @@ +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +function Test-Engine { + if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { return $false } + $PSNativeCommandUseErrorActionPreference = $false + & docker version 2>&1 | Out-Null + $code = $LASTEXITCODE + $PSNativeCommandUseErrorActionPreference = $true + return $code -eq 0 +} + +function Wait-Engine { + for ($attempt = 1; $attempt -le 60; $attempt++) { + if (Test-Engine) { return $true } + Start-Sleep -Seconds 5 + } + return $false +} + +function Probe-Containers { + $operatingSystem = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue + $computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue + $hypervisor = $computerSystem.HypervisorPresent + Write-Output "host os build : $($operatingSystem.Version)" + Write-Output "hypervisor present : $(if ($null -eq $hypervisor) { 'not reported' } else { $hypervisor })" + $features = @{} + foreach ($feature in (Get-WindowsOptionalFeature -Online -ErrorAction SilentlyContinue)) { + $features[$feature.FeatureName] = $feature.State + } + if ($features.Count -eq 0) { Write-Output 'feature enumeration failed' } + foreach ($name in @('Containers', 'Containers-Optional', 'Microsoft-Hyper-V-Client', 'Microsoft-Hyper-V-All', 'VirtualMachinePlatform', 'WindowsHypervisorPlatform')) { + $state = if ($features.ContainsKey($name)) { $features[$name] } else { 'not found' } + Write-Output "feature $name : $state" + } + foreach ($name in @('Containers', 'Containers-Optional')) { + if ($features.ContainsKey($name) -and $features[$name] -eq 'Disabled') { + $result = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction SilentlyContinue + Write-Output "enabled $name : success=$($result.Success) restartNeeded=$($result.RestartNeeded)" + } + } +} + +$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } + +if (Test-Engine) { + Write-Output 'PASS docker is already installed' +} +else { + Probe-Containers + $goVersion = '1.27.1' + $root = 'C:\vfox-docker' + New-Item -ItemType Directory -Force -Path $root | Out-Null + Write-Output "Building docker from source for windows/$arch ..." + + $env:GOPATH = Join-Path $root 'gopath' + New-Item -ItemType Directory -Force -Path $env:GOPATH | Out-Null + $goZip = Join-Path $root "go$goVersion.windows-$arch.zip" + & curl.exe -fsSL -o $goZip "https://go.dev/dl/go$goVersion.windows-$arch.zip" + if ($LASTEXITCODE -ne 0) { throw "FAIL the Go download exited with code $LASTEXITCODE" } + Expand-Archive -Path $goZip -DestinationPath $root + $env:PATH = "$root\go\bin;$env:GOPATH\bin;$env:PATH" + + $moby = Join-Path $root 'moby' + git clone --depth 1 https://github.com/moby/moby.git $moby + if ($LASTEXITCODE -ne 0) { throw "FAIL the moby clone exited with code $LASTEXITCODE" } + go install github.com/tc-hib/go-winres@latest + if ($LASTEXITCODE -ne 0) { throw "FAIL the go-winres install exited with code $LASTEXITCODE" } + $env:DOCKERCLI_VERSION = '29.8.1' + Push-Location $moby + try { + & .\hack\make.ps1 -Client -Daemon + } + finally { + Pop-Location + } + + $bin = Join-Path $moby 'bundles' + if (-not (Test-Path (Join-Path $bin 'dockerd.exe'))) { throw 'FAIL the moby build produced no dockerd.exe' } + if (-not (Test-Path (Join-Path $bin 'docker.exe'))) { throw 'FAIL the moby build produced no docker.exe' } + $env:PATH = "$bin;$env:PATH" + if ($env:GITHUB_PATH) { Add-Content -Path $env:GITHUB_PATH -Value $bin } + + $dataRoot = Join-Path $root 'data' + $log = Join-Path $root 'dockerd.log' + # Windows client SKUs (e.g. windows-11-arm) default to Hyper-V isolation, + # which needs a hypervisor feature this runner does not have. Process + # isolation only needs the "Containers" feature, which is enabled. + $dockerdArgs = "--data-root `"$dataRoot`" --debug --exec-opt isolation=process" + Start-Process -FilePath (Join-Path $bin 'dockerd.exe') -ArgumentList $dockerdArgs -RedirectStandardOutput $log -RedirectStandardError (Join-Path $root 'dockerd.err.log') + if ($env:GITHUB_ENV) { Add-Content -Path $env:GITHUB_ENV -Value "DOCKER_LOG=$log" } + if (-not (Wait-Engine)) { + if (Test-Path $log) { Get-Content $log -Tail 40 | ForEach-Object { "dockerd: $_" } } + throw 'FAIL the docker engine never became ready' + } +} + +$info = ((& docker info --format '{{.OSType}}|{{.Architecture}}') -join '').Trim().ToLowerInvariant() +$os, $rawArch = $info -split '\|' +$engineArch = if ($rawArch -eq 'x86_64') { 'amd64' } else { $rawArch } +if ($os -ne 'windows' -or $engineArch -ne $arch) { throw "FAIL the docker engine runs ${os}/${rawArch} containers, expected windows/$arch" } +Write-Output "PASS the docker engine runs windows/$arch containers" diff --git a/tests/e2e/windows/install.ps1 b/tests/e2e/windows/install.ps1 index 2668327..f33db47 100644 --- a/tests/e2e/windows/install.ps1 +++ b/tests/e2e/windows/install.ps1 @@ -5,7 +5,15 @@ param( $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true -vfox install flutter@$Version -if ($LASTEXITCODE -ne 0) { throw "FAIL vfox install flutter@$Version exited with code $LASTEXITCODE" } +for ($attempt = 1; $attempt -le 5; $attempt++) { + $PSNativeCommandUseErrorActionPreference = $false + vfox install flutter@$Version + $code = $LASTEXITCODE + $PSNativeCommandUseErrorActionPreference = $true + if ($code -eq 0) { break } + if ($attempt -eq 5) { throw "FAIL vfox install flutter@$Version exited with code $code" } + Start-Sleep -Seconds 10 +} + vfox use --global flutter@$Version if ($LASTEXITCODE -ne 0) { throw "FAIL vfox use --global flutter@$Version exited with code $LASTEXITCODE" } diff --git a/tests/e2e/windows/setup.ps1 b/tests/e2e/windows/setup.ps1 index e04e1f0..c483216 100644 --- a/tests/e2e/windows/setup.ps1 +++ b/tests/e2e/windows/setup.ps1 @@ -12,16 +12,18 @@ $env:PATH = "$WorkDir;$env:PATH" function Install-VfoxRelease { $tag = ((& curl.exe -fsSLI -o NUL -w '%{url_effective}' 'https://github.com/version-fox/vfox/releases/latest') -split '/tag/')[-1].Trim() + $vfoxArch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'aarch64' } else { 'x86_64' } $zip = "$WorkDir\vfox.zip" - & curl.exe -fsSL -o $zip "https://github.com/version-fox/vfox/releases/download/$tag/vfox_$($tag.TrimStart('v'))_windows_x86_64.zip" + & curl.exe -fsSL -o $zip "https://github.com/version-fox/vfox/releases/download/$tag/vfox_$($tag.TrimStart('v'))_windows_$vfoxArch.zip" tar -xf $zip -C $WorkDir Remove-Item $zip Copy-Item -Path (Get-ChildItem -Path $WorkDir -Recurse -Filter 'vfox.exe') -Destination $VfoxExe } function Install-VfoxMain { + $goArch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } $goMsi = "$env:TEMP\go.msi" - & curl.exe -fsSL -o $goMsi 'https://go.dev/dl/go1.27.1.windows-amd64.msi' + & curl.exe -fsSL -o $goMsi "https://go.dev/dl/go1.27.1.windows-$goArch.msi" Start-Process msiexec.exe -Wait -ArgumentList '/i', "`"$goMsi`"", '/quiet', '/norestart' Remove-Item $goMsi $src = "$WorkDir\vfox-src" From 2b96fb58ccde9b31d950107ab940360432267070 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Wed, 23 Sep 2026 16:54:02 +0800 Subject: [PATCH 2/3] Simplified design --- .github/workflows/e2e.yml | 3 +- docs/e2e.md | 5 +- hooks/available.lua | 25 ++++++--- lib/util.lua | 8 +++ tests/e2e/linux/install.sh | 12 +---- tests/e2e/windows/Dockerfile | 6 +-- tests/e2e/windows/e2e.ps1 | 77 ++++++++-------------------- tests/e2e/windows/install-docker.ps1 | 59 ++++++--------------- tests/e2e/windows/install.ps1 | 12 +---- tests/hooks_test.lua | 20 +++++++- 10 files changed, 92 insertions(+), 135 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 680c13a..c634c40 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,5 +37,6 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7 - - run: pwsh -NoProfile -File tests/e2e/windows/install-docker.ps1 + - if: matrix.runs-on == 'windows-11-arm' + run: pwsh -NoProfile -File tests/e2e/windows/install-docker.ps1 - run: pwsh -NoProfile -File tests/e2e/windows/e2e.ps1 diff --git a/docs/e2e.md b/docs/e2e.md index 7bd9227..64a5920 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -1,7 +1,7 @@ # E2E The offline Lua hook tests (`tests/hooks_test.lua`) run in the **Test Plugin** workflow. -The end-to-end suite runs in containers against real vfox on Linux and Windows; +The end-to-end suite runs in containers against real vfox on Linux and Windows; each `vfox` x `flavor` x `mirror` combination gets its own throwaway container. ## Ubuntu 26.04.1 @@ -36,6 +36,9 @@ ARCH=arm64 bash tests/e2e/linux/e2e.sh Windows containers cannot be emulated across architectures, so the ARM64 suite needs an ARM64 host (a Copilot+ PC or the `windows-11-arm` CI runner). +The `windows-11-arm` runner has no Windows Hypervisor Platform feature, +so Hyper-V isolation is unavailable; process isolation needs only the Containers feature. + ### x64 1. Execute in Windows PowerShell 5.1, diff --git a/hooks/available.lua b/hooks/available.lua index 0eaae1c..b121c85 100644 --- a/hooks/available.lua +++ b/hooks/available.lua @@ -3,16 +3,27 @@ local json = require("json") local ohos = require("ohos") local source = require("source") +local MAX_ATTEMPTS = 3 +local RETRY_DELAY = 5 + require("util") +local function releases(type) + local resp, err + for attempt = 1, MAX_ATTEMPTS do + resp, err = http.get({ url = BASE_URL:format(type.osType) }) + if resp ~= nil and resp.status_code == 200 then + return json.decode(resp.body) + end + if attempt < MAX_ATTEMPTS then + sleep(RETRY_DELAY) + end + end + error("get version failed: " .. tostring(err) .. " (status " .. tostring(resp and resp.status_code) .. ")") +end + function PLUGIN:Available(ctx) local type = getOsTypeAndArch() - local resp, err = http.get({ - url = BASE_URL:format(type.osType) - }) - if err ~= nil or resp.status_code ~= 200 then - error("get version failed: " .. tostring(err) .. " (status " .. tostring(resp and resp.status_code) .. ")") - end - local body = json.decode(resp.body) + local body = releases(type) local result = {} for _, info in ipairs(body.releases) do local version = info.version diff --git a/lib/util.lua b/lib/util.lua index 8d648ec..82c440f 100644 --- a/lib/util.lua +++ b/lib/util.lua @@ -1,5 +1,13 @@ local DEFAULT_STORAGE_BASE_URL = "https://storage.googleapis.com" +function sleep(seconds) + if RUNTIME.osType == "windows" then + os.execute("timeout /t " .. seconds .. " >NUL") + else + os.execute("sleep " .. seconds) + end +end + function getStorageBaseUrl() local envUrl = os.getenv("FLUTTER_STORAGE_BASE_URL") if envUrl ~= nil and envUrl ~= "" then diff --git a/tests/e2e/linux/install.sh b/tests/e2e/linux/install.sh index 6802f5b..a9b2338 100755 --- a/tests/e2e/linux/install.sh +++ b/tests/e2e/linux/install.sh @@ -4,16 +4,6 @@ set -eu version="${1:?missing version}" eval "$(vfox activate bash)" - -attempt=1 -while ! vfox install flutter@"$version"; do - if [ "$attempt" -ge 5 ]; then - echo "FAIL vfox install flutter@$version" >&2 - exit 1 - fi - attempt=$((attempt + 1)) - sleep 10 -done - +vfox install flutter@"$version" vfox use --global flutter@"$version" eval "$(vfox activate bash)" diff --git a/tests/e2e/windows/Dockerfile b/tests/e2e/windows/Dockerfile index b6ff25c..2bc74f4 100644 --- a/tests/e2e/windows/Dockerfile +++ b/tests/e2e/windows/Dockerfile @@ -5,6 +5,7 @@ FROM ${BASE_IMAGE} SHELL ["powershell", "-NoProfile", "-Command"] COPY tests/e2e/windows/bootstrap-tools.ps1 C:\bootstrap-tools.ps1 +RUN powershell -NoProfile -ExecutionPolicy Bypass -File 'C:\bootstrap-tools.ps1' WORKDIR C:\e2e @@ -13,6 +14,5 @@ COPY hooks C:\e2e\hooks COPY lib C:\e2e\lib COPY tests/e2e/windows C:\e2e\tests\e2e\windows -# No ENTRYPOINT/CMD here: e2e.ps1 installs pwsh and git in a container and then -# commits it. docker commit bakes that container's entrypoint into the image, so -# e2e.ps1 sets the entrypoint explicitly for every test run. +ENTRYPOINT ["C:/Program Files/PowerShell/7/pwsh.exe", "-File"] +CMD ["C:/e2e/tests/e2e/windows/run.ps1"] diff --git a/tests/e2e/windows/e2e.ps1 b/tests/e2e/windows/e2e.ps1 index 0e10952..5657676 100644 --- a/tests/e2e/windows/e2e.ps1 +++ b/tests/e2e/windows/e2e.ps1 @@ -1,13 +1,5 @@ $ErrorActionPreference = 'Stop' -$PSNativeCommandUseErrorActionPreference = $true - -function Write-DockerLog { - param([string]$log) - if (-not $log -or -not (Test-Path $log)) { return } - Get-Content $log | Select-String 'level=error|exit code|failed|Windows default isolation mode' | Select-Object -Last 40 | ForEach-Object { "dockerd: $($_.Line)" } -} - -$dockerLog = $env:DOCKER_LOG +$PSNativeCommandUseErrorActionPreference = $false $Repo = (Resolve-Path "$PSScriptRoot\..\..\..").Path @@ -20,28 +12,9 @@ if (($arch -ne 'amd64') -and ($arch -ne 'arm64')) { throw "FAIL unknown architec $platform = "windows/$arch" $Image = "vfox-flutter-e2e:windows-$arch" $baseImage = "mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-$arch" -$pwshExe = 'C:/Program Files/PowerShell/7/pwsh.exe' -$runScript = 'C:/e2e/tests/e2e/windows/run.ps1' -try { - & docker build --pull --platform $platform --build-arg "BASE_IMAGE=$baseImage" -f "$PSScriptRoot\Dockerfile" -t $Image $Repo -} -catch { - Write-DockerLog $dockerLog - throw -} - -$PSNativeCommandUseErrorActionPreference = $false - -$cid = & docker create --platform $platform --entrypoint 'powershell' $Image -NoProfile -ExecutionPolicy Bypass -File C:\bootstrap-tools.ps1 -if ($LASTEXITCODE -ne 0) { throw 'FAIL creating the bootstrap container' } -& docker start -ai $cid -$code = $LASTEXITCODE -# `docker commit` bakes the container's own entrypoint into the new image, so -# the combos below must set it explicitly with --entrypoint. -& docker commit $cid $Image -& docker rm $cid -if ($code -ne 0) { throw "FAIL bootstrap-tools.ps1 exited with code $code" } +& docker build --pull --platform $platform --build-arg "BASE_IMAGE=$baseImage" -f "$PSScriptRoot\Dockerfile" -t $Image $Repo +if ($LASTEXITCODE -ne 0) { throw "FAIL docker build exited with code $LASTEXITCODE" } $foxes = if ($env:VFOX_VERSION) { @($env:VFOX_VERSION) } else { @('latest', 'main') } $defaultFlavours = @('official', 'ohos') @@ -63,31 +36,21 @@ $combos = foreach ($vfox in $foxes) { } } -try { - # Run detached and read the daemon-captured output, so a failed run still - # leaves its logs on the console. - foreach ($combo in $combos) { - $vfox = $combo.Vfox - $flavor = $combo.Flavor - $mirror = $combo.Mirror - $prefix = "vfox $vfox, $flavor, mirror $mirror, $platform" - Write-Output "[$prefix] === start ===" - $runArgs = @('run', '-d', '--platform', $platform, '--entrypoint', $pwshExe, '-e', "VFOX_VERSION=$vfox", '-e', "FLAVOR=$flavor") - if ($mirror -ne 'default') { $runArgs += @('-e', "FLUTTER_STORAGE_BASE_URL=$mirror") } - $runArgs += @($Image, '-File', $runScript) - $cid = & docker @runArgs - if ($LASTEXITCODE -ne 0) { throw ("FAIL docker run ({0}) failed to start" -f $prefix) } - $exitCode = & docker wait $cid - $logs = ((& docker logs $cid 2>&1) -join "`r`n") - Write-Output $logs - & docker rm $cid - if ([int]$exitCode -ne 0) { throw ("FAIL docker run ({0}) exited with code {1}" -f $prefix, $exitCode) } - # A misconfigured entrypoint re-runs the silent bootstrap and exits 0, - # so require the test banner to prove run.ps1 actually ran. - if ($logs -notmatch '=== vfox') { throw "FAIL docker run ($prefix) produced no test output" } - } -} -catch { - Write-DockerLog $dockerLog - throw +foreach ($combo in $combos) { + $vfox = $combo.Vfox + $flavor = $combo.Flavor + $mirror = $combo.Mirror + $prefix = "vfox $vfox, $flavor, mirror $mirror, $platform" + Write-Output "[$prefix] === start ===" + $runArgs = @('run', '-d', '--platform', $platform, '-e', "VFOX_VERSION=$vfox", '-e', "FLAVOR=$flavor") + if ($mirror -ne 'default') { $runArgs += @('-e', "FLUTTER_STORAGE_BASE_URL=$mirror") } + $runArgs += $Image + $cid = & docker @runArgs + if ($LASTEXITCODE -ne 0) { throw ("FAIL docker run ({0}) failed to start" -f $prefix) } + $exitCode = & docker wait $cid + $logs = (& docker logs $cid) -join "`r`n" + Write-Output $logs + & docker rm $cid + if ([int]$exitCode -ne 0) { throw ("FAIL docker run ({0}) exited with code {1}" -f $prefix, $exitCode) } + if ($logs -notmatch '=== vfox') { throw "FAIL docker run ($prefix) produced no test output" } } diff --git a/tests/e2e/windows/install-docker.ps1 b/tests/e2e/windows/install-docker.ps1 index 9060ddd..932260f 100644 --- a/tests/e2e/windows/install-docker.ps1 +++ b/tests/e2e/windows/install-docker.ps1 @@ -18,55 +18,34 @@ function Wait-Engine { return $false } -function Probe-Containers { - $operatingSystem = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue - $computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue - $hypervisor = $computerSystem.HypervisorPresent - Write-Output "host os build : $($operatingSystem.Version)" - Write-Output "hypervisor present : $(if ($null -eq $hypervisor) { 'not reported' } else { $hypervisor })" - $features = @{} - foreach ($feature in (Get-WindowsOptionalFeature -Online -ErrorAction SilentlyContinue)) { - $features[$feature.FeatureName] = $feature.State - } - if ($features.Count -eq 0) { Write-Output 'feature enumeration failed' } - foreach ($name in @('Containers', 'Containers-Optional', 'Microsoft-Hyper-V-Client', 'Microsoft-Hyper-V-All', 'VirtualMachinePlatform', 'WindowsHypervisorPlatform')) { - $state = if ($features.ContainsKey($name)) { $features[$name] } else { 'not found' } - Write-Output "feature $name : $state" - } - foreach ($name in @('Containers', 'Containers-Optional')) { - if ($features.ContainsKey($name) -and $features[$name] -eq 'Disabled') { - $result = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction SilentlyContinue - Write-Output "enabled $name : success=$($result.Success) restartNeeded=$($result.RestartNeeded)" - } - } -} - -$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } +if ($env:PROCESSOR_ARCHITECTURE -ne 'ARM64') { throw "FAIL this script builds a windows/arm64 engine and needs an arm64 host" } if (Test-Engine) { Write-Output 'PASS docker is already installed' } else { - Probe-Containers - $goVersion = '1.27.1' + $mobyVersion = '29.8.1' $root = 'C:\vfox-docker' New-Item -ItemType Directory -Force -Path $root | Out-Null - Write-Output "Building docker from source for windows/$arch ..." + Write-Output 'Building docker from source for windows/arm64 ...' + $goMsi = "$env:TEMP\go.msi" + & curl.exe -fsSL -o $goMsi 'https://go.dev/dl/go1.27.1.windows-arm64.msi' + if ($LASTEXITCODE -ne 0) { throw "FAIL the Go download exited with code $LASTEXITCODE" } + & msiexec.exe /i $goMsi /quiet /norestart + if ($LASTEXITCODE -notin 0, 3010) { throw "FAIL the Go MSI install exited with code $LASTEXITCODE" } + Remove-Item $goMsi $env:GOPATH = Join-Path $root 'gopath' New-Item -ItemType Directory -Force -Path $env:GOPATH | Out-Null - $goZip = Join-Path $root "go$goVersion.windows-$arch.zip" - & curl.exe -fsSL -o $goZip "https://go.dev/dl/go$goVersion.windows-$arch.zip" - if ($LASTEXITCODE -ne 0) { throw "FAIL the Go download exited with code $LASTEXITCODE" } - Expand-Archive -Path $goZip -DestinationPath $root - $env:PATH = "$root\go\bin;$env:GOPATH\bin;$env:PATH" + $env:PATH = "C:\Program Files\Go\bin;$env:GOPATH\bin;$env:PATH" $moby = Join-Path $root 'moby' - git clone --depth 1 https://github.com/moby/moby.git $moby + git clone --depth 1 --branch "docker-v$mobyVersion" https://github.com/moby/moby.git $moby if ($LASTEXITCODE -ne 0) { throw "FAIL the moby clone exited with code $LASTEXITCODE" } - go install github.com/tc-hib/go-winres@latest + go install github.com/tc-hib/go-winres@v0.3.1 if ($LASTEXITCODE -ne 0) { throw "FAIL the go-winres install exited with code $LASTEXITCODE" } - $env:DOCKERCLI_VERSION = '29.8.1' + $env:DOCKERCLI_VERSION = $mobyVersion + $env:VERSION = $mobyVersion Push-Location $moby try { & .\hack\make.ps1 -Client -Daemon @@ -83,12 +62,8 @@ else { $dataRoot = Join-Path $root 'data' $log = Join-Path $root 'dockerd.log' - # Windows client SKUs (e.g. windows-11-arm) default to Hyper-V isolation, - # which needs a hypervisor feature this runner does not have. Process - # isolation only needs the "Containers" feature, which is enabled. $dockerdArgs = "--data-root `"$dataRoot`" --debug --exec-opt isolation=process" Start-Process -FilePath (Join-Path $bin 'dockerd.exe') -ArgumentList $dockerdArgs -RedirectStandardOutput $log -RedirectStandardError (Join-Path $root 'dockerd.err.log') - if ($env:GITHUB_ENV) { Add-Content -Path $env:GITHUB_ENV -Value "DOCKER_LOG=$log" } if (-not (Wait-Engine)) { if (Test-Path $log) { Get-Content $log -Tail 40 | ForEach-Object { "dockerd: $_" } } throw 'FAIL the docker engine never became ready' @@ -96,7 +71,5 @@ else { } $info = ((& docker info --format '{{.OSType}}|{{.Architecture}}') -join '').Trim().ToLowerInvariant() -$os, $rawArch = $info -split '\|' -$engineArch = if ($rawArch -eq 'x86_64') { 'amd64' } else { $rawArch } -if ($os -ne 'windows' -or $engineArch -ne $arch) { throw "FAIL the docker engine runs ${os}/${rawArch} containers, expected windows/$arch" } -Write-Output "PASS the docker engine runs windows/$arch containers" +if ($info -ne 'windows|arm64') { throw "FAIL the docker engine runs $info containers" } +Write-Output 'PASS the docker engine runs windows/arm64 containers' diff --git a/tests/e2e/windows/install.ps1 b/tests/e2e/windows/install.ps1 index f33db47..2668327 100644 --- a/tests/e2e/windows/install.ps1 +++ b/tests/e2e/windows/install.ps1 @@ -5,15 +5,7 @@ param( $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true -for ($attempt = 1; $attempt -le 5; $attempt++) { - $PSNativeCommandUseErrorActionPreference = $false - vfox install flutter@$Version - $code = $LASTEXITCODE - $PSNativeCommandUseErrorActionPreference = $true - if ($code -eq 0) { break } - if ($attempt -eq 5) { throw "FAIL vfox install flutter@$Version exited with code $code" } - Start-Sleep -Seconds 10 -} - +vfox install flutter@$Version +if ($LASTEXITCODE -ne 0) { throw "FAIL vfox install flutter@$Version exited with code $LASTEXITCODE" } vfox use --global flutter@$Version if ($LASTEXITCODE -ne 0) { throw "FAIL vfox use --global flutter@$Version exited with code $LASTEXITCODE" } diff --git a/tests/hooks_test.lua b/tests/hooks_test.lua index b8dbf1c..1304244 100644 --- a/tests/hooks_test.lua +++ b/tests/hooks_test.lua @@ -1,6 +1,6 @@ package.path = "./lib/?.lua;" .. package.path -local fixture, requests, mirror +local fixture, requests, mirror, failures local originalGetenv = os.getenv os.getenv = function(name) if name == "FLUTTER_STORAGE_BASE_URL" then return mirror end @@ -9,6 +9,10 @@ end package.preload.http = function() return { get = function(request) table.insert(requests, request.url) + if failures > 0 then + failures = failures - 1 + return { status_code = 502 }, nil + end return { status_code = 200, body = "fixture" }, nil end } end @@ -29,7 +33,7 @@ local function release(version, arch, channel, hash) end local function setup(osType, archType, storage) - mirror, requests = storage, {} + mirror, requests, failures = storage, {}, 0 RUNTIME = { osType = osType, archType = archType } -- Both architectures share the same Flutter commit. Put x64 first to catch -- channel selection that relies on the upstream array's order. @@ -122,6 +126,18 @@ tests[#tests + 1] = { "mirror is used for index and every architecture's archive equal(requests[1], "https://mirror.example/flutter/flutter_infra_release/releases/releases_macos.json") equal(result.url, "https://mirror.example/flutter/flutter_infra_release/releases/sdk/3.44.0-x64.zip") end } +tests[#tests + 1] = { "transient mirror failures are retried", function() + setup("darwin", "arm64") + failures, sleep = 2, function() end + equal(#PLUGIN:Available({}), 7) + equal(failures, 0) +end } +tests[#tests + 1] = { "a persistent mirror failure is reported", function() + setup("darwin", "arm64") + failures, sleep = 99, function() end + local _, err = pcall(function() PLUGIN:Available({}) end) + assert(err:find("status 502"), tostring(err)) +end } tests[#tests + 1] = { "legacy releases remain installable without inventing architecture variants", function() setup("darwin", "arm64") installed("2.10.0", "2.10.0", "legacy") From f6d231e41c41b41b38e5c49ce7f4191199fd68d2 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Wed, 23 Sep 2026 20:20:08 +0800 Subject: [PATCH 3/3] Update doc --- .github/workflows/e2e.yml | 9 +++++++++ README.md | 2 +- lib/ohos.lua | 15 ++++++++++----- tests/e2e/linux/e2e.sh | 2 +- tests/hooks_test.lua | 20 ++------------------ 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c634c40..ab3bf45 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,6 +37,15 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7 + # TODO See https://github.com/actions/runner-images/issues/13729 + - if: matrix.runs-on == 'windows-latest' + run: | + for ($i = 1; $i -le 18; $i++) { + docker info *> $null + if ($LASTEXITCODE -eq 0) { exit 0 } + Start-Sleep -Seconds 10 + } + throw 'FAIL the docker daemon never became ready' - if: matrix.runs-on == 'windows-11-arm' run: pwsh -NoProfile -File tests/e2e/windows/install-docker.ps1 - run: pwsh -NoProfile -File tests/e2e/windows/e2e.ps1 diff --git a/README.md b/README.md index 296ae10..5c9caa0 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ See [docs/ohos.md](docs/ohos.md) for details. See [docs/install-from-source.md](docs/install-from-source.md) for details. -## Testing +### Testing See [docs/e2e.md](docs/e2e.md) for details. diff --git a/lib/ohos.lua b/lib/ohos.lua index 3b52fd2..671f7fc 100644 --- a/lib/ohos.lua +++ b/lib/ohos.lua @@ -17,11 +17,11 @@ end local function releases() local resp, err = http.get({ url = RELEASES_URL:format(REPO) }) if err ~= nil or resp.status_code ~= 200 then - return nil + return nil, err or ("HTTP " .. tostring(resp.status_code)) end local body = json.decode(resp.body) if type(body) ~= "table" then - return nil + return nil, "the releases response is not a JSON array" end return body end @@ -44,17 +44,22 @@ end function M.checkout(version, requestedArch) if requestedArch ~= nil then - return nil + error("flutter " .. version .. " has no architecture variants") end local commit - for _, info in ipairs(releases() or {}) do + local body, err = releases() + if body == nil then + error("OpenHarmony releases are unavailable from " .. RELEASES_URL:format(REPO) + .. ": " .. tostring(err)) + end + for _, info in ipairs(body) do if info.tag_name == version then commit = info.target_commitish break end end if commit == nil or commit == "" then - return nil + error("flutter " .. version .. " is not an OpenHarmony release") end local dir = git.workDir(version) if dir == nil then diff --git a/tests/e2e/linux/e2e.sh b/tests/e2e/linux/e2e.sh index fd69079..eae451c 100755 --- a/tests/e2e/linux/e2e.sh +++ b/tests/e2e/linux/e2e.sh @@ -34,7 +34,7 @@ run_one() { local flavor="$2" local mirror="$3" local prefix="vfox $vfox, $flavor, mirror $mirror, $platform" - local env=("-e" "VFOX_VERSION=$vfox" "-e" "FLAVOR=$flavor" "-e" "GITHUB_TOKEN=${GITHUB_TOKEN:-}") + local env=("-e" "VFOX_VERSION=$vfox" "-e" "FLAVOR=$flavor") if [ "$mirror" != default ]; then env+=("-e" "FLUTTER_STORAGE_BASE_URL=$mirror") fi diff --git a/tests/hooks_test.lua b/tests/hooks_test.lua index 1304244..b8dbf1c 100644 --- a/tests/hooks_test.lua +++ b/tests/hooks_test.lua @@ -1,6 +1,6 @@ package.path = "./lib/?.lua;" .. package.path -local fixture, requests, mirror, failures +local fixture, requests, mirror local originalGetenv = os.getenv os.getenv = function(name) if name == "FLUTTER_STORAGE_BASE_URL" then return mirror end @@ -9,10 +9,6 @@ end package.preload.http = function() return { get = function(request) table.insert(requests, request.url) - if failures > 0 then - failures = failures - 1 - return { status_code = 502 }, nil - end return { status_code = 200, body = "fixture" }, nil end } end @@ -33,7 +29,7 @@ local function release(version, arch, channel, hash) end local function setup(osType, archType, storage) - mirror, requests, failures = storage, {}, 0 + mirror, requests = storage, {} RUNTIME = { osType = osType, archType = archType } -- Both architectures share the same Flutter commit. Put x64 first to catch -- channel selection that relies on the upstream array's order. @@ -126,18 +122,6 @@ tests[#tests + 1] = { "mirror is used for index and every architecture's archive equal(requests[1], "https://mirror.example/flutter/flutter_infra_release/releases/releases_macos.json") equal(result.url, "https://mirror.example/flutter/flutter_infra_release/releases/sdk/3.44.0-x64.zip") end } -tests[#tests + 1] = { "transient mirror failures are retried", function() - setup("darwin", "arm64") - failures, sleep = 2, function() end - equal(#PLUGIN:Available({}), 7) - equal(failures, 0) -end } -tests[#tests + 1] = { "a persistent mirror failure is reported", function() - setup("darwin", "arm64") - failures, sleep = 99, function() end - local _, err = pcall(function() PLUGIN:Available({}) end) - assert(err:find("status 502"), tostring(err)) -end } tests[#tests + 1] = { "legacy releases remain installable without inventing architecture variants", function() setup("darwin", "arm64") installed("2.10.0", "2.10.0", "legacy")