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: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ See [docs/mirror.md](docs/mirror.md) for details.

See [docs/ohos.md](docs/ohos.md) for details.

## Install from source
## FAQ

### About `flutter upgrade`

See [docs/flutter-upgrade.md](docs/flutter-upgrade.md) for details.

### Install from source

See [docs/install-from-source.md](docs/install-from-source.md) for details.

Expand Down
60 changes: 60 additions & 0 deletions docs/flutter-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# About `flutter upgrade`

The Flutter release archives this plugin installs contain a full `.git`
checkout, so `flutter upgrade` (and its siblings `flutter downgrade`,
`flutter channel`, and any `git checkout` / `git pull` run inside the SDK
directory) can move the SDK's HEAD to a newer commit in place. vfox manages
SDK versions by directory and does not track in-place mutations, so the next
`vfox use flutter@3.47.0` will silently hand you whatever the SDK has drifted
to, not 3.47.0.

Do not run `flutter upgrade` inside a vfox-managed Flutter SDK. Change
versions through vfox:

```bash
vfox install flutter@3.47.4
vfox use --global flutter@3.47.4
```

## Recovering a drifted SDK

If you already upgraded in place, this plugin prints a warning on every
`vfox use` of that version and shows the commands that restore it:

```bash
vfox uninstall flutter@3.47.0
vfox install flutter@3.47.0
vfox use flutter@3.47.0
```

## How the check works

On install the plugin writes a `.vfox-manifest` file next to the SDK's `.git`,
recording the expected git HEAD, and re-checks it on each `vfox use`.

The warning is only a warning: `vfox use` still selects the version you asked
for. SDKs without a `.git` directory are left alone.

## Disabling the check

Delete the `.vfox-manifest` file of the affected installation. The file is
written by a real install, so it comes back only after you uninstall and
reinstall that version; `vfox install` alone skips an already-installed
version and leaves it absent.

### Ubuntu 26.04.1

Execute in Bash,

```bash
find ~/.vfox/cache/flutter/v-3.47.0 -name .vfox-manifest -print -delete
```

### Windows 11

Assume that PowerShell 7 is already installed. Execute in PowerShell 7,

```powershell
Get-ChildItem "$env:USERPROFILE\.vfox\cache\flutter\v-3.47.0" -Recurse -Force -Filter .vfox-manifest |
Remove-Item
```
4 changes: 4 additions & 0 deletions hooks/pre_install.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local http = require("http")
local json = require("json")
local manifest = require("manifest")
local ohos = require("ohos")
local source = require("source")

Expand Down Expand Up @@ -62,6 +63,9 @@ function PLUGIN:PostInstall(ctx)
if sdk == nil then
return
end
if sdk.path ~= nil and sdk.path ~= "" then
pcall(manifest.write, sdk.path, sdk)
end
if ohos.isOhosVersion(sdk.version) then
ohos.clean(sdk.version)
return
Expand Down
54 changes: 54 additions & 0 deletions hooks/pre_use.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
local manifest = require("manifest")

local REASON = {
["head-drifted"] = "the SDK's git HEAD no longer matches the commit vfox installed",
["sdk-no-longer-git"] = "vfox can no longer read the SDK's git HEAD (its .git may be broken)"
}

local function printDriftWarning(sdkPath, version, manifestInfo, currentHead, reason)
local installedVersion = (manifestInfo and manifestInfo.version) or version
local expectedHead = (manifestInfo and manifestInfo.expected_head) or "unknown"
io.write(string.format(
[[
Warning: flutter SDK at %s has drifted from the version vfox installed.
vfox installed: flutter@%s (git HEAD %s)
current git HEAD: %s
reason: %s

This typically happens when `flutter upgrade` or a similar in-place command
is run inside a vfox-managed SDK. vfox manages SDK versions by directory and
does not track in-place mutations, so the version you get may differ from
the one you asked for.

To restore flutter@%s:
vfox uninstall flutter@%s
vfox install flutter@%s
vfox use flutter@%s

To upgrade to a newer version, use vfox directly:
vfox install flutter@<new-version>
vfox use flutter@<new-version>
]],
sdkPath,
installedVersion,
expectedHead,
currentHead or "unknown",
REASON[reason] or reason,
version,
version,
version,
version
))
end

function PLUGIN:PreUse(ctx)
local sdk = ctx.installedSdks and ctx.installedSdks[ctx.version]
if sdk == nil or sdk.path == nil or sdk.path == "" then
return { version = ctx.version }
end
local drifted, reason = manifest.checkDrift(sdk.path)
if drifted then
printDriftWarning(sdk.path, ctx.version, manifest.read(sdk.path), manifest.currentHead(sdk.path), reason)
end
return { version = ctx.version }
end
182 changes: 182 additions & 0 deletions lib/manifest.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
local json = require("json")

local MARKER = ".vfox-manifest"

local function sep()
if RUNTIME.osType == "windows" then
return "\\"
end
return "/"
end

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 function quote(value)
if RUNTIME.osType == "windows" then
return value
end
return "'" .. value:gsub("'", "'\\''") .. "'"
end

local function capture(command)
local function run()
local pipe = io.popen(command)
if pipe == nil then
return nil
end
local content = pipe:read("*a")
pipe:close()
if content == nil then
return nil
end
content = content:match("^(.-)%s*$")
if content == "" then
return nil
end
return content
end
local ok, content = pcall(run)
if not ok then
return nil
end
return content
end

local function hasGitDir(path)
local f = io.open(joinPath(path, ".git" .. sep() .. "HEAD"), "r")
if f == nil then
return false
end
f:close()
return true
end

local function isEmptyTable(table)
for _ in pairs(table) do
return false
end
return true
end

local M = {}

function M.gitRoot(sdkRoot)
if type(sdkRoot) ~= "string" or sdkRoot == "" then
return nil
end
if hasGitDir(sdkRoot) then
return sdkRoot
end
local listCmd
if RUNTIME.osType == "windows" then
listCmd = "dir /b /ad " .. quote(sdkRoot)
else
listCmd = "find " .. quote(sdkRoot) .. " -maxdepth 1 -mindepth 1 -type d -printf '%f\\n'"
end
local subdirs = capture(listCmd)
if subdirs == nil then
return nil
end
for line in (subdirs .. "\n"):gmatch("(.-)\n") do
line = line:match("^(.-)%s*$")
if line and line ~= "" then
local candidate = joinPath(sdkRoot, line)
if hasGitDir(candidate) then
return candidate
end
end
end
return nil
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")
end

function M.manifestPath(sdkRoot)
local root = M.gitRoot(sdkRoot)
if root == nil then
return nil
end
return joinPath(root, MARKER)
end

function M.write(sdkRoot, info)
local root = M.gitRoot(sdkRoot)
if root == nil then
return false
end
local head = capture("git -C " .. quote(root) .. " rev-parse HEAD")
local data = {
plugin = "vfox-flutter",
plugin_version = (PLUGIN and PLUGIN.version) or "unknown",
version = (info and info.version) or "unknown",
expected_head = head,
installed_at = os.time(),
}
local f = io.open(joinPath(root, MARKER), "w")
if f == nil then
return false
end
local ok, encoded = pcall(json.encode, data)
if not ok then
f:close()
return false
end
f:write(encoded)
f:close()
return true
end

function M.read(sdkRoot)
local path = M.manifestPath(sdkRoot)
if path == nil then
return nil
end
local f = io.open(path, "r")
if f == nil then
return nil
end
local content = f:read("*a")
f:close()
local ok, data = pcall(json.decode, content)
if not ok or type(data) ~= "table" or isEmptyTable(data) then
return nil
end
return data
end

function M.checkDrift(sdkRoot)
if type(sdkRoot) ~= "string" or sdkRoot == "" then
return false, "no-path"
end
local m = M.read(sdkRoot)
if m == nil then
return false, "no-manifest"
end
if m.expected_head == nil or m.expected_head == "" then
return false, "no-git-anchor"
end
local current = M.currentHead(sdkRoot)
if current == nil then
return true, "sdk-no-longer-git"
end
if current ~= m.expected_head then
return true, "head-drifted"
end
return false, "ok"
end

return M
1 change: 1 addition & 0 deletions metadata.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ NOTE:
PLUGIN.minRuntimeVersion = "0.3.0"
-- Some things that need user to be attention!
PLUGIN.notes = {
"Do NOT run `flutter upgrade` (or `flutter downgrade`, `flutter channel`, or any in-place git operation) inside a vfox-managed Flutter SDK. It mutates the SDK in place and vfox will silently hand you the wrong version on the next `vfox use`. Change versions with `vfox install flutter@<version>` and `vfox use flutter@<version>`. This plugin detects such drift and prints a warning with the restore commands on the next `vfox use`.",
}
Loading
Loading