diff --git a/docs/arm64.md b/docs/arm64.md index 915c7ba..78cd841 100644 --- a/docs/arm64.md +++ b/docs/arm64.md @@ -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. diff --git a/docs/mirror.md b/docs/mirror.md index 57f297a..ab91bd8 100644 --- a/docs/mirror.md +++ b/docs/mirror.md @@ -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. diff --git a/hooks/available.lua b/hooks/available.lua index b121c85..ed60371 100644 --- a/hooks/available.lua +++ b/hooks/available.lua @@ -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 diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index 3b1efd8..2c963ac 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -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) .. ")") diff --git a/lib/git.lua b/lib/git.lua index 4affec2..45e024c 100644 --- a/lib/git.lua +++ b/lib/git.lua @@ -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 = {} @@ -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 .. '"') @@ -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 @@ -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 diff --git a/lib/ohos.lua b/lib/ohos.lua index 671f7fc..69e5642 100644 --- a/lib/ohos.lua +++ b/lib/ohos.lua @@ -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 = {} @@ -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() diff --git a/lib/util.lua b/lib/util.lua index 82c440f..0137d4c 100644 --- a/lib/util.lua +++ b/lib/util.lua @@ -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()