diff --git a/README.md b/README.md index b6929c6..4b74bfd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ sudo apt install --assume-yes curl git unzip xz-utils zip libglu1-mesa vfox use --global flutter@3.47.4 ``` +`sudo apt install --assume-yes curl git unzip xz-utils zip libglu1-mesa` comes from https://docs.flutter.dev/install/manual#download-prerequisites , +these are prerequisite packages not included with the Flutter SDK. You can verify this using the following command: ```bash diff --git a/docs/ohos.md b/docs/ohos.md index a5d36c5..3144fbf 100644 --- a/docs/ohos.md +++ b/docs/ohos.md @@ -33,8 +33,6 @@ vfox search flutter - **`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 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 from Huawei's OBS, so it needs a network connection. - **No checksum is verified.** Git history is the integrity check, and this is a diff --git a/lib/git.lua b/lib/git.lua index 45e024c..1b665a9 100644 --- a/lib/git.lua +++ b/lib/git.lua @@ -17,30 +17,16 @@ local function quote(value) return "'" .. value:gsub("'", "'\\''") .. "'" end +-- PowerShell single-quoted strings are literal: 'text' escapes ' as ''. +local function psQuote(value) + 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 - -local function isGithubUrl(url) - return type(url) == "string" and url:sub(1, #GH_BASE) == GH_BASE -end - local B64CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" local function base64(data) @@ -70,6 +56,35 @@ local function utf16le(data) return data:gsub("(.)", "%1" .. string.char(0)) end +-- Run a PowerShell script via -EncodedCommand (base64 UTF-16LE). The encoded +-- string has no spaces or quotes, so Go's argv re-quoting cannot mangle it. +local function execPS(script) + return exec("powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -EncodedCommand " + .. base64(utf16le(script))) +end + +local function git(root, args) + if RUNTIME.osType == "windows" then + return execPS("& git -C " .. psQuote(root) .. " " .. args) + end + 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 + +local function isGithubUrl(url) + return type(url) == "string" and url:sub(1, #GH_BASE) == GH_BASE +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. @@ -147,8 +162,7 @@ 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))) + return execPS(script) end local function probeWithTimeout(remote, ref, timeout) @@ -162,10 +176,9 @@ end function M.removeDir(path) if RUNTIME.osType == "windows" then - exec('if exist "' .. path .. '" rmdir /s /q "' .. path .. '"') - else - exec("rm -rf " .. quote(path)) + return execPS("Remove-Item -Recurse -Force " .. psQuote(path) .. " -ErrorAction SilentlyContinue") end + return exec("rm -rf " .. quote(path)) end local function vfoxHome() @@ -197,13 +210,14 @@ local function parentDir(dir) end local function makeParentDir(dir) - if RUNTIME.osType == "windows" then - return - end local parent = parentDir(dir) if parent == nil then return end + if RUNTIME.osType == "windows" then + execPS("New-Item -ItemType Directory -Force " .. psQuote(parent) .. " | Out-Null") + return + end exec("mkdir -p " .. quote(parent)) end @@ -213,6 +227,15 @@ function M.resetDir(dir) end function M.init(root) + if RUNTIME.osType == "windows" then + local script = string.format([[ +& git init -q %s +if ($LASTEXITCODE -ne 0) { exit 1 } +& git -C %s config core.longpaths true +exit $LASTEXITCODE +]], psQuote(root), psQuote(root)) + return execPS(script) + end if not exec("git init -q " .. quote(root)) then return false end diff --git a/lib/manifest.lua b/lib/manifest.lua index 841d0fc..72c73ed 100644 --- a/lib/manifest.lua +++ b/lib/manifest.lua @@ -13,12 +13,40 @@ local function joinPath(a, b) return a .. sep() .. b end --- vfox feeds command strings to cmd.exe /c on Windows. Go has to re-quote the --- whole command line, which turns a double quote into \" so cmd.exe reads a --- quoted path back as the volume-relative path \path\ and rejects it with --- "The filename, directory name, or volume label syntax is incorrect." Match --- lib/git.lua and pass paths unquoted there; a path with a space then simply --- skips the drift check instead of breaking vfox use. +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 + +local function utf16le(data) + return data:gsub("(.)", "%1" .. string.char(0)) +end + +-- PowerShell single-quoted strings are literal: 'text' escapes ' as ''. +local function psQuote(value) + return "'" .. value:gsub("'", "''") .. "'" +end + +-- Shell single-quote quoting. On Windows, capture wraps commands in PowerShell +-- with psQuote instead; this function is for Unix only. local function quote(value) if RUNTIME.osType == "windows" then return value @@ -28,7 +56,12 @@ end local function capture(command) local function run() - local pipe = io.popen(command) + local shellCmd = command + if RUNTIME.osType == "windows" then + shellCmd = "powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -EncodedCommand " + .. base64(utf16le(command)) + end + local pipe = io.popen(shellCmd) if pipe == nil then return nil end @@ -77,7 +110,7 @@ function M.gitRoot(sdkRoot) end local listCmd if RUNTIME.osType == "windows" then - listCmd = "dir /b /ad " .. quote(sdkRoot) + listCmd = "Get-ChildItem -Directory " .. psQuote(sdkRoot) .. " | Select-Object -ExpandProperty Name" else listCmd = "find " .. quote(sdkRoot) .. " -maxdepth 1 -mindepth 1 -type d -printf '%f\\n'" end @@ -97,12 +130,19 @@ function M.gitRoot(sdkRoot) return nil end +local function gitRevParse(root) + if RUNTIME.osType == "windows" then + return capture("& git -C " .. psQuote(root) .. " rev-parse HEAD") + end + return capture("git -C " .. quote(root) .. " rev-parse HEAD") +end + function M.currentHead(sdkRoot) local root = M.gitRoot(sdkRoot) if root == nil then return nil end - return capture("git -C " .. quote(root) .. " rev-parse HEAD") + return gitRevParse(root) end function M.manifestPath(sdkRoot) @@ -118,7 +158,7 @@ function M.write(sdkRoot, info) if root == nil then return false end - local head = capture("git -C " .. quote(root) .. " rev-parse HEAD") + local head = gitRevParse(root) local data = { plugin = "vfox-flutter", plugin_version = (PLUGIN and PLUGIN.version) or "unknown", diff --git a/tests/e2e/windows/e2e.ps1 b/tests/e2e/windows/e2e.ps1 index cd168b3..3569fe3 100644 --- a/tests/e2e/windows/e2e.ps1 +++ b/tests/e2e/windows/e2e.ps1 @@ -32,10 +32,16 @@ foreach ($vfox in $foxes) { foreach ($flavor in $flavours) { foreach ($mirror in $mirrorList) { if ((-not $mirrorExplicit) -and ($mirror -ne 'default') -and ($vfox -ne 'latest')) { continue } - $combos.Add([pscustomobject]@{ Vfox = $vfox; Flavor = $flavor; Mirror = $mirror }) + $combos.Add([pscustomobject]@{ Vfox = $vfox; Flavor = $flavor; Mirror = $mirror; Spaces = 'normal' }) } } } +# One combo with a VFOX_HOME path containing a space, to exercise the +# PowerShell -EncodedCommand quoting fix. Only on x64: arm64 runs official +# only, which downloads a prebuilt archive and never touches git paths. +if ($arch -eq 'amd64') { + $combos.Add([pscustomobject]@{ Vfox = 'latest'; Flavor = 'ohos'; Mirror = 'default'; Spaces = 'yes' }) +} $maxJobs = 2 if ($env:E2E_MAX_JOBS) { $maxJobs = [int]$env:E2E_MAX_JOBS } @@ -43,7 +49,9 @@ if ($maxJobs -lt 1) { $maxJobs = 1 } function Get-ComboSlug { param([object] $Combo) - return ($Combo.Vfox + '-' + $Combo.Flavor + '-' + ($Combo.Mirror -replace '[^A-Za-z0-9]', '_')) + $slug = ($Combo.Vfox + '-' + $Combo.Flavor + '-' + ($Combo.Mirror -replace '[^A-Za-z0-9]', '_')) + if ($Combo.Spaces -eq 'yes') { $slug += '-spaces' } + return $slug } function Get-ContainerExitCode { @@ -65,6 +73,7 @@ for ($start = 0; $start -lt $combos.Count; $start += $maxJobs) { $flavor = $combo.Flavor $mirror = $combo.Mirror $prefix = "vfox $vfox, $flavor, mirror $mirror, $platform" + if ($combo.Spaces -eq 'yes') { $prefix += ', spaces' } Write-Output "[$prefix] === start ===" $slug = Get-ComboSlug $combo @@ -72,6 +81,7 @@ for ($start = 0; $start -lt $combos.Count; $start += $maxJobs) { 'run', '-d', '--platform', $platform, '-e', "VFOX_VERSION=$vfox", '-e', "FLAVOR=$flavor", '-e', "VFOX_E2E_SLOT=$slug") + if ($combo.Spaces -eq 'yes') { $runArgs += @('-e', "VFOX_E2E_SPACES=yes") } if ($mirror -ne 'default') { $runArgs += @('-e', "FLUTTER_STORAGE_BASE_URL=$mirror") } $runArgs += $Image diff --git a/tests/e2e/windows/setup.ps1 b/tests/e2e/windows/setup.ps1 index 4968e79..6fa0ad5 100644 --- a/tests/e2e/windows/setup.ps1 +++ b/tests/e2e/windows/setup.ps1 @@ -8,7 +8,11 @@ $slot = if ($env:VFOX_E2E_SLOT) { $env:VFOX_E2E_SLOT } else { 'default' } $slotRoot = Join-Path $env:USERPROFILE "vfox-e2e-runs\$slot" New-Item -ItemType Directory -Force -Path (Join-Path $slotRoot 'tmp') | Out-Null $env:USERPROFILE = $slotRoot -$env:VFOX_HOME = Join-Path $slotRoot '.vfox' +if ($env:VFOX_E2E_SPACES -eq 'yes') { + $env:VFOX_HOME = "$slotRoot\.vfox with space" +} else { + $env:VFOX_HOME = Join-Path $slotRoot '.vfox' +} $env:TEMP = Join-Path $slotRoot 'tmp' $env:TMP = $env:TEMP