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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
2 changes: 2 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
6 changes: 3 additions & 3 deletions installer/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
6 changes: 3 additions & 3 deletions installer/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)..."
Expand Down
128 changes: 88 additions & 40 deletions installer/installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +272 to +275

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve pre-existing FastFlowLM installations

When FastFlowLM is already installed, this new registry lookup makes NeedsFLM() return false, so the [Files] and [Run] entries correctly skip installation; however, CurStepChanged(ssPostInstall) calls NeedsFLM() again and creates .flm_installed_by_us whenever it returns false. Consequently, installing Flowkey alongside a user-managed FLM now marks that runtime as Flowkey-owned, and [UninstallRun] later removes it when Flowkey is uninstalled. Capture whether FLM was absent before the install and create the marker only when this setup actually installed it successfully.

Useful? React with 👍 / 👎.

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;
Expand Down
17 changes: 14 additions & 3 deletions scripts/ffp_flm_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/ui/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ <h3 class="subhead">FastFlowLM runtime</h3>
<p id="flm-version">FastFlowLM: checking…</p>
<div class="card-actions">
<button class="btn" id="flm-check">Check for updates</button>
<a class="btn" id="flm-download" href="https://github.com/FastFlowLM/FastFlowLM/releases/" target="_blank" rel="noopener" hidden>Download update…</a>
<a class="btn" id="flm-download" href="https://github.com/ROCm/FastFlowLM/releases/" target="_blank" rel="noopener" hidden>Download update…</a>
</div>
</div>
</div>
Expand Down
65 changes: 65 additions & 0 deletions tests/test_ffp_flm_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions vendor/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
ahk/*.exe
ahk/LICENSE.txt
flm/*.exe
flm/*.msi
2 changes: 1 addition & 1 deletion vendor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ in source control — `installer/build.ps1` downloads them on demand.
| dir | what | source | license |
|------------|---------------------------------------|-------------------------------------------------------------------------|--------------|
| `ahk/` | AutoHotkey v2 64-bit interpreter | <https://www.autohotkey.com/download/ahk-v2.zip> | GPLv2 |
| `flm/` | FastFlowLM official setup wrapper | <https://github.com/FastFlowLM/FastFlowLM/releases/latest> | see FLM site |
| `flm/` | FastFlowLM official setup wrapper | <https://github.com/ROCm/FastFlowLM/releases/latest> | see FLM site |

## Refresh

Expand Down