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

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: '1.24.0'
cache: false
- name: Test architecture selection with vfox's Lua interpreter
run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/hooks_test.lua
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,30 @@ sudo apt install --assume-yes curl git unzip xz-utils zip libglu1-mesa
vfox use --global flutter@3.44.0
```

## Architecture selection

Available releases include architecture suffixes such as `3.44.0-arm64` and
`3.44.0-x64`. You can install both builds and switch between them:

```bash
vfox install flutter@3.44.0-arm64
vfox install flutter@3.44.0-x64
vfox use --global flutter@3.44.0-x64
```

On Apple Silicon, running an x64 build requires Rosetta. Only architectures
provided by Flutter for the current operating system are listed.

Commands without an architecture, such as `vfox install flutter@3.44.0` or
`vfox install flutter@stable`, continue to select the host architecture and keep
their existing version names. Channels also accept a suffix, such as
`vfox install flutter@stable-x64`. The version list shows the default architecture
first, followed by other architectures, with each group ordered by version.
This keeps `@latest` on the default architecture.

## Mirror

By default, Flutter SDK is downloaded from `https://storage.googleapis.com`.
By default, Flutter SDK is downloaded from `https://storage.googleapis.com`.
If you have difficulty accessing it, you can set the `FLUTTER_STORAGE_BASE_URL` environment variable to use a mirror.

```bash
Expand Down
53 changes: 32 additions & 21 deletions hooks/available.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,41 @@ function PLUGIN:Available(ctx)
break
end
local dartArch = info.dart_sdk_arch
local includeVersion = true
-- only flutter for macos supports different archs
if (type.osType == "macos") then
-- if dartArch is null, this means that the version does not support different archs
includeVersion = (dartArch == nil or dartArch == type.archType)
end
if (includeVersion) then
table.insert(result, {
version = info.version,
url = getStorageBaseUrl() .. "/flutter_infra_release/releases/" .. info.archive,
sha256 = info.sha256,
key = info.hash,
note = info.channel,
addition = {
{
name = "dart",
version = info.dart_sdk_version
}
version = dartArch and (version .. "-" .. dartArch) or version
table.insert(result, {
version = version,
url = getStorageBaseUrl() .. "/flutter_infra_release/releases/" .. info.archive,
sha256 = info.sha256,
key = info.hash,
note = info.channel,
addition = {
{
name = "dart",
version = info.dart_sdk_version
}
})
end
}
})
end
table.sort(result, function(a, b)
return compare_versions(a.version, b.version) > 0
-- Keep the default architecture first, including legacy releases with
-- no architecture metadata, so @latest cannot select a foreign build.
local _, aArch = splitVersionAndArch(a.version)
local _, bArch = splitVersionAndArch(b.version)
local aDefault = aArch == nil or aArch == type.archType
local bDefault = bArch == nil or bArch == type.archType
if aDefault ~= bDefault then
return aDefault
end
local order = compare_versions(a.version, b.version)
if order ~= 0 then
return order > 0
end
if aArch == type.archType then
return bArch ~= type.archType
elseif bArch == type.archType then
return false
end
return a.version < b.version
end)
return result
end
59 changes: 35 additions & 24 deletions hooks/pre_install.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,50 @@ local http = require("http")
local json = require("json")

require("util")

function PLUGIN:PreInstall(ctx)
local arg = ctx.version
local arg, requestedArch = splitVersionAndArch(ctx.version)
local platform = getOsTypeAndArch()
local targetArch = requestedArch or platform.archType
local channelKey
if arg == "beta" or arg == "dev" or arg == "stable" then
local type = getOsTypeAndArch()
local resp, err = http.get({
url = BASE_URL:format(type.osType)
url = BASE_URL:format(platform.osType)
})
if err ~= nil or resp.status_code ~= 200 then
error("get version failed" .. err)
end
local body = json.decode(resp.body)
local cr = body.current_release
local key = cr[arg]
local releases = self:Available({})
for _, info in ipairs(releases) do
if info.key == key then
return {
version = info.version,
url = info.url,
sha256 = info.sha256
}
end
channelKey = body.current_release[arg]
if channelKey == nil then
return nil
end
end
local releases = self:Available({})
for _, info in ipairs(releases) do
if info.version == arg then
return {
version = info.version,
url = info.url,
sha256 = info.sha256
}

local function package(info, version)
return {
-- Keep existing unqualified install/use commands and directory
-- names. Explicit architecture requests get distinct versions.
version = requestedArch and info.version or version,
url = info.url,
sha256 = info.sha256
}
end

local legacy
for _, info in ipairs(self:Available({})) do
local version, arch = splitVersionAndArch(info.version)
local matches = channelKey and info.key == channelKey or
(channelKey == nil and version == arg)
if matches then
if arch == targetArch then
return package(info, version)
elseif arch == nil and requestedArch == nil then
-- Older releases have no architecture metadata. Preserve their
-- existing behavior only for requests without an architecture.
legacy = package(info, version)
end
end
end
return nil
end
return legacy
end
12 changes: 11 additions & 1 deletion lib/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,17 @@ function getOsTypeAndArch()
}
end

function splitVersionAndArch(version)
local base, arch = version:match("^(.+)%-(%w+)$")
if arch == "x64" or arch == "arm64" then
return base, arch
end
return version, nil
end

function compare_versions(v1, v2)
v1 = splitVersionAndArch(v1)
v2 = splitVersionAndArch(v2)
local v1_parts = {}
for part in string.gmatch(v1, "%d+") do
table.insert(v1_parts, tonumber(part))
Expand All @@ -51,4 +61,4 @@ function compare_versions(v1, v2)
end

return 0
end
end
149 changes: 149 additions & 0 deletions tests/hooks_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package.path = "./lib/?.lua;" .. package.path

local fixture, requests, mirror
local originalGetenv = os.getenv
os.getenv = function(name)
if name == "FLUTTER_STORAGE_BASE_URL" then return mirror end
return originalGetenv(name)
end
package.preload.http = function()
return { get = function(request)
table.insert(requests, request.url)
return { status_code = 200, body = "fixture" }, nil
end }
end
package.preload.json = function()
return { decode = function() return fixture end }
end

local function equal(actual, expected)
assert(actual == expected, "expected " .. tostring(expected) .. ", got " .. tostring(actual))
end

local function release(version, arch, channel, hash)
return {
version = version, dart_sdk_arch = arch, channel = channel, hash = hash,
archive = "sdk/" .. version .. "-" .. (arch or "legacy") .. ".zip",
sha256 = version .. ":" .. (arch or "legacy"), dart_sdk_version = "3.12.0"
}
end

local function setup(osType, archType, storage)
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.
fixture = {
current_release = { stable = "stable", beta = "beta", dev = "dev" },
base_url = "https://upstream.example/releases",
releases = {
release("3.44.0", "x64", "stable", "stable"),
release("3.44.0", "arm64", "stable", "stable"),
release("3.44.0-0.1.pre", "x64", "beta", "beta"),
release("3.44.0-0.1.pre", "arm64", "beta", "beta"),
release("3.43.0-0.1.pre", "x64", "dev", "dev"),
release("3.43.0-0.1.pre", "arm64", "dev", "dev"),
release("2.10.0", nil, "stable", "legacy")
}
}
package.loaded.util = nil
dofile("metadata.lua")
dofile("hooks/available.lua")
dofile("hooks/pre_install.lua")
end

local function installed(request, version, arch)
local result = PLUGIN:PreInstall({ version = request })
assert(result, "no package selected for " .. request)
equal(result.version, version)
assert(result.url:find("-" .. arch .. ".zip", 1, true), result.url)
assert(result.sha256:find(":" .. arch, 1, true), result.sha256)
return result
end

local tests = {}
tests[#tests + 1] = { "all variants are listed with native architecture first", function()
for _, arch in ipairs({ "arm64", "amd64" }) do
setup("darwin", arch)
local versions = PLUGIN:Available({})
equal(#versions, 7)
equal(versions[1].version, "3.44.0-" .. (arch == "amd64" and "x64" or arch))
local found = {}
for _, version in ipairs(versions) do found[version.version] = true end
assert(found["3.44.0-x64"] and found["3.44.0-arm64"] and found["2.10.0"])
equal(versions[1].addition[1].version, "3.12.0")
end
end }
tests[#tests + 1] = { "explicit architectures select matching archives", function()
setup("darwin", "arm64")
installed("3.44.0-x64", "3.44.0-x64", "x64")
installed("3.44.0-arm64", "3.44.0-arm64", "arm64")
end }
tests[#tests + 1] = { "latest keeps the default architecture when a foreign build is newer", function()
setup("darwin", "arm64")
table.insert(fixture.releases, 1, release("3.45.0", "x64", "beta", "newer"))
local versions = PLUGIN:Available({})
equal(versions[1].version, "3.44.0-arm64")
installed(versions[1].version, "3.44.0-arm64", "arm64")
end }
tests[#tests + 1] = { "existing version commands retain native selection and version names", function()
for _, platform in ipairs({ "darwin", "linux", "windows" }) do
for _, arch in ipairs({ "arm64", "amd64" }) do
setup(platform, arch)
installed("3.44.0", "3.44.0", arch == "amd64" and "x64" or arch)
end
end
end }
tests[#tests + 1] = { "channels use the native architecture even when hashes are shared", function()
for _, arch in ipairs({ "arm64", "amd64" }) do
setup("darwin", arch)
for channel, version in pairs({ stable = "3.44.0", beta = "3.44.0-0.1.pre", dev = "3.43.0-0.1.pre" }) do
installed(channel, version, arch == "amd64" and "x64" or arch)
installed(channel .. "-x64", version .. "-x64", "x64")
installed(channel .. "-arm64", version .. "-arm64", "arm64")
end
end
end }
tests[#tests + 1] = { "pre-release versions retain their full version strings", function()
setup("darwin", "arm64")
installed("3.44.0-0.1.pre", "3.44.0-0.1.pre", "arm64")
installed("3.44.0-0.1.pre-x64", "3.44.0-0.1.pre-x64", "x64")
end }
tests[#tests + 1] = { "architecture suffixes do not change version ordering", function()
setup("darwin", "arm64")
equal(compare_versions("3.44.0-arm64", "3.44.0-x64"), 0)
equal(compare_versions("3.44.0", "3.44.0-arm64"), 0)
assert(compare_versions("3.44.0-arm64", "3.44.0-0.1.pre-x64") > 0)
assert(compare_versions("3.44.0-0.2.pre-x64", "3.44.0-0.1.pre-arm64") > 0)
end }
tests[#tests + 1] = { "mirror is used for index and every architecture's archive", function()
setup("darwin", "arm64", "https://mirror.example/flutter/")
local result = installed("3.44.0-x64", "3.44.0-x64", "x64")
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] = { "legacy releases remain installable without inventing architecture variants", function()
setup("darwin", "arm64")
installed("2.10.0", "2.10.0", "legacy")
equal(PLUGIN:PreInstall({ version = "2.10.0-arm64" }), nil)
end }
tests[#tests + 1] = { "missing architecture never falls back to an incompatible archive", function()
setup("darwin", "arm64")
fixture.releases = { release("3.44.0", "x64", "stable", "stable") }
equal(PLUGIN:PreInstall({ version = "3.44.0" }), nil)
equal(PLUGIN:PreInstall({ version = "stable" }), nil)
equal(PLUGIN:PreInstall({ version = "stable-arm64" }), nil)
installed("stable-x64", "3.44.0-x64", "x64")
end }

local failures = 0
for _, test in ipairs(tests) do
local ok, err = pcall(test[2])
if ok then
print("PASS " .. test[1])
else
failures = failures + 1
print("FAIL " .. test[1] .. ": " .. tostring(err))
end
end
assert(failures == 0, tostring(failures) .. " test groups failed")