diff --git a/README.md b/README.md index 2c1d55e..942ef7f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Install these first on a new machine: 4. Install FastFlowLM from [fastflowlm.com](https://fastflowlm.com/) or directly with PowerShell: ```powershell -Invoke-WebRequest https://github.com/FastFlowLM/FastFlowLM/releases/latest/download/flm-setup.msi -OutFile flm-setup.msi +Invoke-WebRequest https://github.com/ROCm/FastFlowLM/releases/latest/download/flm-setup.msi -OutFile flm-setup.msi Start-Process msiexec.exe -ArgumentList '/i', 'flm-setup.msi', '/quiet', '/norestart' -Wait ``` diff --git a/SPEC.md b/SPEC.md index 1a2c498..095335b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -222,4 +222,6 @@ B45|2026-08-13|pre-release review: `_ensure_note_schema`'s migration write ran o B46|2026-08-13|pre-release review: `trash_note` was the only note-mutating fn ⊥ taking a `revision` param ∴ "Move to Trash" could silently act on a note that changed since the editor loaded it, unlike update/organize/archive; also `_act_note_archive`/`_act_notes_board_save` used a bare `int()` revision parse (raw 500 on bad input) unlike `_act_note_update`/`_act_note_organize`'s guarded parse|V54; add revision param + conflict check to `trash_note` + daemon action + app.js call site; guard the two bare `int()` parses to match the others B47|2026-08-13|`v2.5.0` tag build failed: upstream FastFlowLM/FastFlowLM → ROCm/FastFlowLM org move + v1.0.1 "Windows Installer Switch" replaced `flm-setup.exe` w/ `flm-setup.msi`; `build.ps1 -BundleFlm` 404'd on the old filename, blocking `release-installer.yml`|fetch/vendor/chain `flm-setup.msi` (build.ps1, install.ps1, installer.iss [Files]/[Run] → `msiexec /i ... /quiet /norestart`); docs updated. UNVERIFIED: `NeedsFLM`/`FlmUninstallCmd` match uninstall subkeys by NAME PREFIX `"flm version "`, which the old Inno .exe set — an MSI install registers under a product-code GUID instead, so detection/uninstall-chaining may silently no-op until validated on a real machine (needs T8 clean-VM test) B48|2026-08-13|self-caught, same retry: B47's fix note on `NeedsFLM` embedded a literal `{commonpf}` inside a Pascal `{}` comment → comment closed early at that `}`, `iscc` syntax error line 224 col 13; installer.iss already carries a standing NB at line ~320 warning exactly against this (missed it before writing the first comment)|reword the comment w/o a literal brace-constant; no code behavior change +B49|2026-08-24|`check_flm_update` matched release assets by `.exe` suffix only ∴ every check since FLM v1.0.1 (exe→msi, B47) returned `asset_url=""` — latent, ∵ ⊥ consumer today (dashboard uses `release_url`)|prefer `.msi` w/ `.exe` fallback (older/rolled-back releases still resolve); repoint `FLM_RELEASES_API`/`_PAGE` at ROCm/FastFlowLM (old org 301s, but ⊥ depend on a redirect); live-verified 0.9.45→1.0.2 w/ real asset URL. SELF-CORRECTION: first draft of this entry also claimed `vendor/.gitignore` lacking `flm/*.msi` made a 26MB installer commitable — FALSE, review-caught. Root `.gitignore:36 vendor/*` already excludes the subtree ∧ git ⊥ descends into an excluded dir ∴ ∀ rules inside `vendor/.gitignore` are unreachable (`git check-ignore -v` → `.gitignore:36`, `git add` refused). Added `flm/*.msi` kept as dead-but-consistent w/ its equally-dead `ahk/*.exe`/`flm/*.exe` neighbours; real guard = root `vendor/*`, ⊥ the vendor-local file +B50|2026-08-24|B47's UNVERIFIED flag resolved = BROKEN, ∧ worse than flagged: `NeedsFLM`/`FlmUninstallCmd` matched uninstall SUBKEY NAME vs prefix `"flm version "`, but real subkey names are `flm_is1` (Inno; `flm version 0.9.45` is only its *DisplayName*) ∨ a product-code GUID (MSI) ∴ matched NEITHER format — ⊥ just an MSI regression. Path fallback probed `PF\FastFlowLM\flm.exe`; real install dir = `PF\flm\`. Both signals fail ∴ `NeedsFLM`=True ("absent") on a machine w/ FLM installed+running ⇒ installer redundantly chain-installs FLM ∀ run ∧ ⊥ drops `.flm_installed_by_us` ∴ uninstall chain never fires. Verified on a real registry carrying BOTH entries|match `DisplayName` (⊥ subkey name) across HKLM64 ∧ HKLM32 (installer is 64-bit ∴ plain HKLM ⊥ sees the legacy 32-bit entry); probe `PF\flm\flm.exe` first; derive quiet uninstall from `UninstallString` when `QuietUninstallString` empty (MSI publishes it empty). Live-simulated vs real registry: NeedsFLM→False, cmd→`MsiExec.exe /X{GUID} /quiet /norestart`. iscc compile = CI (⊥ local Inno) ``` diff --git a/installer/build.ps1 b/installer/build.ps1 index d4cc742..459c96d 100644 --- a/installer/build.ps1 +++ b/installer/build.ps1 @@ -144,13 +144,13 @@ if ($BundleFlm) { if (-not (Test-Path $vendorDir)) { New-Item -ItemType Directory -Path $vendorDir -Force | Out-Null } # FastFlowLM moved from FastFlowLM/FastFlowLM to ROCm/FastFlowLM and, as of # v1.0.1, switched its Windows asset from an Inno-Setup .exe to an .msi - # (release title: "Windows Installer Switch"). GitHub redirects the old - # org's "latest" URL to the new one, so the URL below still works. + # (release title: "Windows Installer Switch"). Point at the canonical + # ROCm/FastFlowLM org directly rather than the old org's redirect. $flmDst = Join-Path $vendorDir "flm-setup.msi" if (Test-Path $flmDst) { "FLM installer already present: $flmDst" } else { - $flmUrl = "https://github.com/FastFlowLM/FastFlowLM/releases/latest/download/flm-setup.msi" + $flmUrl = "https://github.com/ROCm/FastFlowLM/releases/latest/download/flm-setup.msi" "Downloading FLM installer from $flmUrl ..." Invoke-WebRequest -Uri $flmUrl -OutFile $flmDst -UseBasicParsing "Got: $flmDst ($([math]::Round((Get-Item $flmDst).Length/1MB,1)) MB)" diff --git a/installer/install.ps1 b/installer/install.ps1 index a59e72f..840f7b2 100644 --- a/installer/install.ps1 +++ b/installer/install.ps1 @@ -216,13 +216,13 @@ if ($SkipFlm) { } else { # FastFlowLM moved from FastFlowLM/FastFlowLM to ROCm/FastFlowLM and, as of # v1.0.1, switched its Windows asset from an Inno-Setup .exe to an .msi - # (release title: "Windows Installer Switch"). GitHub redirects the old - # org's "latest" URL to the new one, so the URL below still works. + # (release title: "Windows Installer Switch"). Point at the canonical + # ROCm/FastFlowLM org directly rather than the old org's redirect. $flmSetup = Join-Path $releaseRoot "vendor\flm\flm-setup.msi" if (-not (Test-Path $flmSetup)) { Info "Downloading FastFlowLM installer (large -- hundreds of MB)..." $flmSetup = Join-Path $env:TEMP "ffp-flm-setup.msi" - Invoke-WebRequest -Uri "https://github.com/FastFlowLM/FastFlowLM/releases/latest/download/flm-setup.msi" ` + Invoke-WebRequest -Uri "https://github.com/ROCm/FastFlowLM/releases/latest/download/flm-setup.msi" ` -OutFile $flmSetup -UseBasicParsing } Info "Installing FastFlowLM (a UAC prompt is expected; install is silent after you accept)..." diff --git a/installer/installer.iss b/installer/installer.iss index 6f36a85..90bb4e6 100644 --- a/installer/installer.iss +++ b/installer/installer.iss @@ -212,70 +212,118 @@ Type: dirifempty; Name: "{app}" [Code] const - FLM_REG_PREFIX = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\flm version '; + FLM_UNINST_PATH = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'; -{ True if no FLM uninstall key is found AND no flm.exe exists in PF\FastFlowLM. +{ True if this uninstall entry's DisplayName looks like FastFlowLM. - CAUTION (unverified as of the v1.0.1 exe-to-msi switch): this scans for an - uninstall SUBKEY NAME starting with 'flm version ' -- how FastFlowLM's old - Inno-Setup .exe installer named its own entry. An MSI-based install - typically registers its uninstall key under a product-code GUID instead, - which this prefix match would never find, falling through to the common - Program-Files path check below. Needs validation on a real machine with - the new .msi installer (see SPEC.md B47 / T8 clean-VM test). } -function NeedsFLM(): Boolean; + FLM has shipped two installer formats with different registry shapes: the + old Inno build registered SUBKEY 'flm_is1' with DisplayName + 'flm version 0.9.45'; the v1.0.1+ MSI registers under a product-code GUID + with DisplayName 'flm'. The previous code matched the SUBKEY NAME against + 'flm version ' and so found NEITHER -- verified on a real machine carrying + both entries (SPEC.md B50). Match DisplayName instead. } +function IsFlmDisplayName(Value: String): Boolean; +var + Lowered: String; +begin + Lowered := Lowercase(Trim(Value)); + Result := (Lowered = 'flm') + or (Pos('flm version', Lowered) = 1) + or (Pos('fastflowlm', Lowered) = 1); +end; + +{ Search one registry view for FLM's uninstall entry. Sets FoundKey to the + full subkey path on success. } +function FindFlmUninstallKey(RootKey: Integer; var FoundKey: String): Boolean; var Names: TArrayOfString; i: Integer; - Dummy: String; + KeyPath, Disp: String; begin - Result := True; - - { Scan 32-bit uninstall hive for any 'flm version *' subkey. } - if RegGetSubkeyNames(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall', Names) then + Result := False; + FoundKey := ''; + if not RegGetSubkeyNames(RootKey, FLM_UNINST_PATH, Names) then + Exit; + for i := 0 to GetArrayLength(Names) - 1 do begin - for i := 0 to GetArrayLength(Names) - 1 do + KeyPath := FLM_UNINST_PATH + '\' + Names[i]; + if RegQueryStringValue(RootKey, KeyPath, 'DisplayName', Disp) then begin - if Pos('flm version ', Names[i]) = 1 then + if IsFlmDisplayName(Disp) then begin - Result := False; + FoundKey := KeyPath; + Result := True; Exit; end; end; end; +end; + +{ True if FLM appears absent. Checks BOTH registry views: the MSI product + lands in the 64-bit view while the legacy Inno entry sits in the 32-bit + one, and this installer runs 64-bit (ArchitecturesInstallIn64BitMode), so + a plain HKLM would miss the legacy entry entirely. } +function NeedsFLM(): Boolean; +var + FoundKey: String; +begin + Result := True; - { Fallback: probe the default install path. } - if FileExists(ExpandConstant('{commonpf}\FastFlowLM\flm.exe')) then + if FindFlmUninstallKey(HKLM64, FoundKey) then + begin Result := False; + Exit; + end; + if FindFlmUninstallKey(HKLM32, FoundKey) then + begin + Result := False; + Exit; + end; - { Avoid 'Dummy unused' warning. } - Dummy := ''; + { Fallback: probe the install path. FLM installs into an flm-named folder + under Program Files; the old code probed a FastFlowLM-named one that + never exists. Keep both, preferring the real one. } + if FileExists(ExpandConstant('{commonpf}\flm\flm.exe')) then + Result := False + else if FileExists(ExpandConstant('{commonpf}\FastFlowLM\flm.exe')) then + Result := False; end; -{ Locate the FLM QuietUninstallString from the 32-bit Uninstall hive. - Returns a cmd-runnable string, or '' if FLM isn't registered. - - Same 'flm version ' subkey-name assumption as NeedsFLM above, and the same - post-v1.0.1-msi caveat: unverified whether it still finds the entry. } +{ Return a cmd-runnable silent uninstall command for FLM, or a harmless echo + if it isn't registered. The MSI product publishes an UninstallString but an + EMPTY QuietUninstallString, so derive the quiet form when needed. } function FlmUninstallCmd(Param: String): String; var - Names: TArrayOfString; - i: Integer; - KeyPath, Quiet: String; + FoundKey, Quiet, Raw: String; + RootKey: Integer; begin Result := 'echo FLM not registered'; - if not RegGetSubkeyNames(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall', Names) then - Exit; - for i := 0 to GetArrayLength(Names) - 1 do + + RootKey := HKLM64; + if not FindFlmUninstallKey(RootKey, FoundKey) then + begin + RootKey := HKLM32; + if not FindFlmUninstallKey(RootKey, FoundKey) then + Exit; + end; + + if RegQueryStringValue(RootKey, FoundKey, 'QuietUninstallString', Quiet) then begin - if Pos('flm version ', Names[i]) = 1 then + if Trim(Quiet) <> '' then begin - KeyPath := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + Names[i]; - if RegQueryStringValue(HKLM, KeyPath, 'QuietUninstallString', Quiet) then - begin - Result := Quiet; - Exit; - end; + Result := Quiet; + Exit; + end; + end; + + if RegQueryStringValue(RootKey, FoundKey, 'UninstallString', Raw) then + begin + if Trim(Raw) <> '' then + begin + if Pos('msiexec', Lowercase(Raw)) > 0 then + Result := Raw + ' /quiet /norestart' + else + Result := Raw + ' /SILENT'; end; end; end; diff --git a/scripts/ffp_flm_server.py b/scripts/ffp_flm_server.py index 9562878..1fb7f14 100644 --- a/scripts/ffp_flm_server.py +++ b/scripts/ffp_flm_server.py @@ -21,8 +21,11 @@ # FastFlowLM upstream release feed. The HTML page is what we open in the # browser for a manual download; the API gives us the latest tag + asset URL. -FLM_RELEASES_API = "https://api.github.com/repos/FastFlowLM/FastFlowLM/releases/latest" -FLM_RELEASES_PAGE = "https://github.com/FastFlowLM/FastFlowLM/releases/" +# Repo moved FastFlowLM/FastFlowLM -> ROCm/FastFlowLM (SPEC.md B47); GitHub's +# API 301-redirects the old path so this kept working either way, but point +# at the canonical location rather than depend on that redirect indefinitely. +FLM_RELEASES_API = "https://api.github.com/repos/ROCm/FastFlowLM/releases/latest" +FLM_RELEASES_PAGE = "https://github.com/ROCm/FastFlowLM/releases/" @dataclass(frozen=True) @@ -420,13 +423,21 @@ def check_flm_update( tag = str(payload.get("tag_name") or "").strip() latest = tag.lstrip("vV") release_url = str(payload.get("html_url") or FLM_RELEASES_PAGE) + # FLM switched its Windows installer asset from .exe to .msi in v1.0.1 + # (SPEC.md B47/B49). Prefer .msi, but still accept .exe so a check against + # an older/rolled-back release resolves an asset instead of silently "". asset_url = "" + fallback_url = "" for asset in payload.get("assets") or []: if not isinstance(asset, dict): continue - if str(asset.get("name") or "").lower().endswith(".exe"): + name = str(asset.get("name") or "").lower() + if name.endswith(".msi"): asset_url = str(asset.get("browser_download_url") or "") break + if name.endswith(".exe") and not fallback_url: + fallback_url = str(asset.get("browser_download_url") or "") + asset_url = asset_url or fallback_url out["latest"] = latest out["release_url"] = release_url diff --git a/scripts/ui/web/index.html b/scripts/ui/web/index.html index 338df3c..1694364 100644 --- a/scripts/ui/web/index.html +++ b/scripts/ui/web/index.html @@ -510,7 +510,7 @@

FastFlowLM runtime

FastFlowLM: checking…

- +
diff --git a/tests/test_ffp_flm_server.py b/tests/test_ffp_flm_server.py index b904c8e..9f19bf2 100644 --- a/tests/test_ffp_flm_server.py +++ b/tests/test_ffp_flm_server.py @@ -149,3 +149,68 @@ def poll(): assert result == "started" assert stopped == [True] assert len(spawned) == 1 + + +def _fake_release(assets: list[str], tag: str = "v1.0.2"): + """Stand in for the GitHub releases API payload with the given asset names.""" + payload = { + "tag_name": tag, + "html_url": f"https://github.com/ROCm/FastFlowLM/releases/tag/{tag}", + "assets": [ + { + "name": name, + "browser_download_url": ( + f"https://github.com/ROCm/FastFlowLM/releases/download/{tag}/{name}" + ), + } + for name in assets + ], + } + + class _Resp: + def read(self): + return json.dumps(payload).encode("utf-8") + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + return lambda *_a, **_k: _Resp() + + +def test_b49_update_check_resolves_msi_asset(monkeypatch, tmp_path): + """FLM ships flm-setup.msi as of v1.0.1; the .exe-only matcher returned ''.""" + monkeypatch.setattr(ffp_flm_server, "flm_version", lambda _nw: "0.9.45") + monkeypatch.setattr( + ffp_flm_server.urllib.request, + "urlopen", + _fake_release(["fastflowlm_1.0.2_linux.tar.gz", "flm-setup.msi"]), + ) + + out = ffp_flm_server.check_flm_update(0, cache_path=tmp_path / "c.json", force=True) + + assert out["latest"] == "1.0.2" + assert out["has_update"] is True + assert out["asset_url"].endswith("/flm-setup.msi") + + +def test_b49_update_check_still_accepts_exe_asset(monkeypatch, tmp_path): + """Older/rolled-back releases only ship .exe — must still resolve, not ''.""" + monkeypatch.setattr(ffp_flm_server, "flm_version", lambda _nw: "0.9.45") + monkeypatch.setattr( + ffp_flm_server.urllib.request, + "urlopen", + _fake_release(["flm-setup.exe"], tag="v1.0.0"), + ) + + out = ffp_flm_server.check_flm_update(0, cache_path=tmp_path / "c.json", force=True) + + assert out["asset_url"].endswith("/flm-setup.exe") + + +def test_b49_release_feed_points_at_rocm_org(): + """Repo moved orgs; don't rely on GitHub's 301 redirect indefinitely (B47).""" + assert "ROCm/FastFlowLM" in ffp_flm_server.FLM_RELEASES_API + assert "ROCm/FastFlowLM" in ffp_flm_server.FLM_RELEASES_PAGE diff --git a/vendor/.gitignore b/vendor/.gitignore index 8643554..2950001 100644 --- a/vendor/.gitignore +++ b/vendor/.gitignore @@ -2,3 +2,4 @@ ahk/*.exe ahk/LICENSE.txt flm/*.exe +flm/*.msi diff --git a/vendor/README.md b/vendor/README.md index e6e17d8..810a886 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -8,7 +8,7 @@ in source control — `installer/build.ps1` downloads them on demand. | dir | what | source | license | |------------|---------------------------------------|-------------------------------------------------------------------------|--------------| | `ahk/` | AutoHotkey v2 64-bit interpreter | | GPLv2 | -| `flm/` | FastFlowLM official setup wrapper | | see FLM site | +| `flm/` | FastFlowLM official setup wrapper | | see FLM site | ## Refresh