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
8 changes: 6 additions & 2 deletions docs/arm64.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,9 @@ run on Windows ARM64.
- **No checksum is verified.** Git history is the integrity check.
- **No `-arm64` variants for legacy releases.** Versions without architecture
metadata (such as `2.10.0`) keep their existing x64-only behavior.
- **Mirrors don't apply.** `FLUTTER_STORAGE_BASE_URL` (see `mirror.md`) only
affects archive downloads; source installs clone from GitHub.
- **Mirrors don't apply to archive downloads.** `FLUTTER_STORAGE_BASE_URL`
(see `mirror.md`) only affects archive downloads; source installs clone
from GitHub. If GitHub sends no data within 10s, the plugin
automatically falls back to the `https://gh-proxy.org/` mirror and shows
a countdown while probing. Once the transfer starts it is never
interrupted.
8 changes: 8 additions & 0 deletions docs/mirror.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@ $env:FLUTTER_STORAGE_BASE_URL = "https://storage.flutter-io.cn"
```powershell
[Environment]::SetEnvironmentVariable('FLUTTER_STORAGE_BASE_URL', 'https://storage.flutter-io.cn', 'Machine')
```

## GitHub mirror for source installs

On Linux ARM64 and Windows ARM64,
the plugin installs Flutter from git source instead of a prebuilt archive.
If `https://github.com` sends no data within 10s,
the plugin automatically falls back to `https://gh-proxy.org/`.
Once the transfer starts it is never interrupted.
2 changes: 1 addition & 1 deletion hooks/available.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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) })
resp, err = http.get({ url = getReleasesUrl(type.osType) })
if resp ~= nil and resp.status_code == 200 then
return json.decode(resp.body)
end
Expand Down
2 changes: 1 addition & 1 deletion hooks/pre_install.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function PLUGIN:PreInstall(ctx)
local channelKey
if arg == "beta" or arg == "dev" or arg == "stable" then
local resp, err = http.get({
url = BASE_URL:format(platform.osType)
url = getReleasesUrl(platform.osType)
})
if err ~= nil or resp.status_code ~= 200 then
error("get version failed: " .. tostring(err) .. " (status " .. tostring(resp and resp.status_code) .. ")")
Expand Down
147 changes: 146 additions & 1 deletion lib/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ local FALLBACK_HOME_ENV = { "HOME", "USERPROFILE" }
local VFOX_DIR = ".vfox"
local TMP_DIR = "tmp"
local ENGINE_PIN = "bin/internal/engine.version"
local FETCH_TIMEOUT = 10
local FETCH_ATTEMPTS = 3
local GH_PROXY_PREFIX = "https://gh-proxy.org/"
local GH_BASE = "https://github.com/"

local M = {}

Expand Down Expand Up @@ -34,6 +37,129 @@ local function localPath(path)
return path:gsub("/", sep())
end

local function isGithubUrl(url)
return type(url) == "string" and url:sub(1, #GH_BASE) == GH_BASE
end

local B64CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

local function base64(data)
local out = {}
for i = 1, #data, 3 do
local a = data:byte(i)
local b = data:byte(i + 1) or 0
local c = data:byte(i + 2) or 0
local n = a * 65536 + b * 256 + c
out[#out + 1] = B64CHARS:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
.. B64CHARS:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
if i + 1 > #data then
out[#out + 1] = "=="
elseif i + 2 > #data then
out[#out + 1] = B64CHARS:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1) .. "="
else
out[#out + 1] = B64CHARS:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1)
.. B64CHARS:sub(n % 64 + 1, n % 64 + 1)
end
end
return table.concat(out)
end

-- PowerShell -EncodedCommand needs UTF-16LE. The script is ASCII-only, so
-- interleaving NUL bytes is sufficient.
local function utf16le(data)
return data:gsub("(.)", "%1" .. string.char(0))
end

-- Probe whether the remote answers within the timeout. Success means the
-- first byte arrived: once the transfer starts, the real fetch below runs
-- without a timeout and is never interrupted.
local function probeWithTimeoutUnix(remote, ref, timeout)
local gitCmd = "git ls-remote --exit-code " .. quote(remote) .. " " .. quote(ref)
-- Note: %%s becomes %s for the shell, %d is the Lua format arg for timeout
local script = string.format(
[[tmp=$(mktemp /tmp/vfox_probe_XXXXXX)
start=$(date +%%s)
end=$((start + %d))
%s >"$tmp" 2>/dev/null &
pid=$!
while kill -0 $pid 2>/dev/null; do
if [ -s "$tmp" ]; then
kill $pid 2>/dev/null
wait $pid 2>/dev/null
rm -f "$tmp"
printf "\r%%*s\r" 40 ""
printf "\n"
exit 0
fi
now=$(date +%%s)
remaining=$((end - now))
if [ $remaining -le 0 ]; then
kill $pid 2>/dev/null
wait $pid 2>/dev/null
rm -f "$tmp"
printf "\r%%*s\r" 40 ""
printf "\n"
exit 124
fi
printf "\rTimeout in %%3ds... " $remaining
sleep 1
done
wait $pid
rc=$?
rm -f "$tmp"
printf "\r%%*s\r" 40 ""
printf "\n"
exit $rc
]], timeout, gitCmd)
return exec(script)
end

local function probeWithTimeoutWindows(remote, ref, timeout)
-- The plugin runtime does not process shell quotes on Windows (a quoted
-- -File path arrives literally and fails with "Illegal characters in
-- path"), so the script is passed via -EncodedCommand instead: base64
-- has no spaces or quotes and survives any argv splitting.
local function psArg(value)
return '"' .. value:gsub('"', '""') .. '"'
end
local script = string.format([[
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("vfox_probe_" + $PID + ".out")
$proc = Start-Process -FilePath "git" -ArgumentList @("ls-remote", "--exit-code", %s, %s) -RedirectStandardOutput $tmp -RedirectStandardError "NUL" -PassThru -NoNewWindow
$deadline = (Get-Date).AddSeconds(%d)
while (-not $proc.HasExited) {
if ((Test-Path $tmp) -and ((Get-Item $tmp).Length -gt 0)) {
try { $proc.Kill() } catch {}
Remove-Item -Force $tmp
Write-Host ""
exit 0
}
$remaining = [int]($deadline - (Get-Date)).TotalSeconds
if ($remaining -le 0) {
try { $proc.Kill() } catch {}
Remove-Item -Force $tmp -ErrorAction SilentlyContinue
Write-Host ""
exit 124
}
Write-Host -NoNewline ("`rTimeout in " + $remaining + " s... ")
Start-Sleep -Seconds 1
}
Remove-Item -Force $tmp -ErrorAction SilentlyContinue
Write-Host ""
exit $proc.ExitCode
]], psArg(remote), psArg(ref), timeout)
return exec("powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -EncodedCommand "
.. base64(utf16le(script)))
end

local function probeWithTimeout(remote, ref, timeout)
io.write(string.format("Connecting to %s (timeout: %ds)...\n", remote, timeout))
io.flush()
if RUNTIME.osType == "windows" then
return probeWithTimeoutWindows(remote, ref, timeout)
end
return probeWithTimeoutUnix(remote, ref, timeout)
end

function M.removeDir(path)
if RUNTIME.osType == "windows" then
exec('if exist "' .. path .. '" rmdir /s /q "' .. path .. '"')
Expand Down Expand Up @@ -93,7 +219,7 @@ function M.init(root)
return git(root, "config core.longpaths true")
end

function M.fetch(root, remote, ref)
local function fetchPlain(root, remote, ref)
for _ = 1, FETCH_ATTEMPTS do
if git(root, "fetch -q --depth 1 " .. remote .. " " .. ref) then
return true
Expand All @@ -102,6 +228,25 @@ function M.fetch(root, remote, ref)
return false
end

function M.fetch(root, remote, ref)
if not isGithubUrl(remote) then
return fetchPlain(root, remote, ref)
end
local remotes = { remote, GH_PROXY_PREFIX .. remote }
for i, url in ipairs(remotes) do
if probeWithTimeout(url, ref, FETCH_TIMEOUT) then
if fetchPlain(root, url, ref) then
return true
end
end
if i < #remotes then
io.write(string.format("Cannot reach %s, trying %s...\n", url, remotes[i + 1]))
io.flush()
end
end
return false
end

function M.checkoutHead(root)
return git(root, "checkout -q FETCH_HEAD")
end
Expand Down
28 changes: 20 additions & 8 deletions lib/ohos.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ local http = require("http")
local json = require("json")
local git = require("git")

require("util")

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 RELEASES_ATTEMPTS = 3
local RETRY_DELAY = 10

local M = {}

Expand All @@ -15,15 +19,23 @@ function M.isOhosVersion(version)
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, err or ("HTTP " .. tostring(resp.status_code))
end
local body = json.decode(resp.body)
if type(body) ~= "table" then
return nil, "the releases response is not a JSON array"
local lastErr = "unknown error"
for attempt = 1, RELEASES_ATTEMPTS do
local resp, err = http.get({ url = RELEASES_URL:format(REPO) })
if err == nil and resp ~= nil and resp.status_code == 200 then
local body = json.decode(resp.body)
if type(body) == "table" then
return body
end
lastErr = "the releases response is not a JSON array"
else
lastErr = err or ("HTTP " .. tostring(resp and resp.status_code))
end
if attempt < RELEASES_ATTEMPTS then
sleep(RETRY_DELAY)
end
end
return body
return nil, lastErr
end

function M.list()
Expand Down
4 changes: 3 additions & 1 deletion lib/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ function getStorageBaseUrl()
return DEFAULT_STORAGE_BASE_URL
end

BASE_URL = getStorageBaseUrl() .. "/flutter_infra_release/releases/releases_%s.json"
function getReleasesUrl(osType)
return getStorageBaseUrl() .. "/flutter_infra_release/releases/releases_" .. osType .. ".json"
end


function getOsTypeAndArch()
Expand Down
Loading