mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-07 00:02:06 +00:00
Merge main, and finish two loose ends
Conflict resolution: main's updater work (#330) already localized every update string this branch was adding, with its own key names, so the duplicate `SettingsUpdate*` keys and the inline `phase_text` match are dropped in favour of main's `localized_update_phase`. The About page keeps neither the Explorer section nor the CLI toggle, which is the point of this branch. Two fixes on top: - The Explorer flag handling moves below `logfile::install`. A GUI-subsystem process has no console and Inno does not surface a [Run] entry's exit code, so the log was the only place a failed registration could have been reported -- and it ran before the log existed. - Document the feature, including the two flags. Windows also ships a portable zip, and with the Settings buttons gone that install had no way left to add or remove the verbs. Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
This commit is contained in:
@@ -23,9 +23,6 @@ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
PACKAGE_UPDATE_ZIP="${TTY7_PACKAGE_UPDATE_ZIP:-1}"
|
||||
if [[ "$VERSION" == *-nightly.* ]]; then
|
||||
PACKAGE_UPDATE_ZIP=0
|
||||
fi
|
||||
APP="dist/tty7.app"
|
||||
|
||||
rm -rf dist
|
||||
@@ -42,9 +39,10 @@ chmod +x "$APP/Contents/MacOS/tty7"
|
||||
if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then
|
||||
# A focused out-of-process updater can replace the bundle after the GUI
|
||||
# exits, then relaunch or roll back without teaching the GUI to mutate
|
||||
# itself. Stable macOS builds carry it beside the app/CLI so its signature
|
||||
# is covered by the outer bundle; Nightly remains byte-for-byte on its old
|
||||
# packaging path for the first updater release.
|
||||
# itself. Every macOS build carries it beside the app/CLI so its signature
|
||||
# is covered by the outer bundle — including Nightly, whose users are
|
||||
# offered the stable release that supersedes their prerelease and need a
|
||||
# working helper to get there.
|
||||
cp "target/${TARGET}/release/tty7-updater" "$APP/Contents/MacOS/tty7-updater"
|
||||
chmod +x "$APP/Contents/MacOS/tty7-updater"
|
||||
fi
|
||||
@@ -155,10 +153,10 @@ else
|
||||
codesign --force --deep --sign - "$APP"
|
||||
fi
|
||||
|
||||
# The stable-channel in-app updater needs the signed, notarized .app itself
|
||||
# rather than a disk image that requires Finder interaction. Nightly versions
|
||||
# skip this path above: their rolling release remains unchanged until the stable
|
||||
# updater has shipped and been exercised.
|
||||
# The in-app updater needs the signed, notarized .app itself rather than a disk
|
||||
# image that requires Finder interaction. The helper re-reads the full embedded
|
||||
# version out of the staged bundle and refuses anything that is not the release
|
||||
# it was told to install.
|
||||
ZIP=""
|
||||
if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then
|
||||
ZIP="dist/tty7-${VERSION}-macos-${ARCH}.zip"
|
||||
|
||||
@@ -7,15 +7,22 @@
|
||||
# Fonts are embedded via include_bytes! and the app icon is compiled into the
|
||||
# executable as a resource (see build.rs). So the payload is tty7-app.exe plus a
|
||||
# sibling completions\ dir (loaded at runtime — see terminal::signature) and the
|
||||
# license/readme. Both artifacts are unsigned builds — SmartScreen will
|
||||
# warn on first launch.
|
||||
# license/readme. Windows release artifacts are intentionally unsigned. The
|
||||
# in-app updater verifies the published SHA-256 checksum and PE file version
|
||||
# before and after waiting for the GUI to exit.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Target = $args[0]
|
||||
$Arch = $args[1]
|
||||
$Version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
|
||||
# Inno accepts the full semantic version for AppVersion, but the PE version
|
||||
# resource only accepts numeric components. Keep both values so Nightly and
|
||||
# other prerelease builds retain their display version without breaking ISCC.
|
||||
$VersionCore = ($Version -split '[-+]', 2)[0]
|
||||
$VersionInfoVersion = "${VersionCore}.0"
|
||||
$Name = "tty7-$Version-windows-$Arch"
|
||||
$Stage = "dist/$Name"
|
||||
$PackageUpdater = $env:TTY7_PACKAGE_UPDATE_HELPER -ne '0'
|
||||
|
||||
Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
|
||||
@@ -25,6 +32,12 @@ Copy-Item "target/$Target/release/tty7-app.exe" "$Stage/tty7-app.exe"
|
||||
# `core::cli_install` resolves it relative to tty7-app.exe and puts that
|
||||
# directory on the user's PATH.
|
||||
Copy-Item "target/$Target/release/tty7.exe" "$Stage/tty7.exe"
|
||||
if ($PackageUpdater) {
|
||||
# The installed copy is never executed in place during an update. The GUI
|
||||
# first copies it to a private staging directory so Inno can replace every
|
||||
# installed executable without colliding with Windows image locks.
|
||||
Copy-Item "target/$Target/release/tty7-updater.exe" "$Stage/tty7-updater.exe"
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path "$Stage/completions" | Out-Null
|
||||
Copy-Item "assets/completions/*.json" "$Stage/completions/"
|
||||
Copy-Item LICENSE "$Stage/LICENSE.txt"
|
||||
@@ -51,7 +64,19 @@ if (Test-Path $ServerSrc) {
|
||||
Write-Warning "no $ServerAsset to bundle - this build cannot serve WSL distros"
|
||||
}
|
||||
|
||||
# The marker tells the in-app updater which of the two Windows layouts it is
|
||||
# running from, and therefore which release asset can replace it. It says
|
||||
# nothing about where updates come from: that is always the latest stable
|
||||
# release. The Inno payload gets the mutually exclusive marker below.
|
||||
if ($PackageUpdater) {
|
||||
Set-Content -Path "$Stage/.tty7-portable" -Value 'portable-v1' -NoNewline -Encoding ascii
|
||||
}
|
||||
Compress-Archive -Path "$Stage/*" -DestinationPath "dist/$Name.zip" -Force
|
||||
if ($PackageUpdater) {
|
||||
# The Inno payload must never retain the mutually exclusive portable marker.
|
||||
Remove-Item -LiteralPath "$Stage/.tty7-portable" -Force
|
||||
Set-Content -Path "$Stage/.tty7-inno-install" -Value 'inno-v1' -NoNewline -Encoding ascii
|
||||
}
|
||||
|
||||
# Installer, built from the same staged payload. ISCC is on PATH on GitHub's
|
||||
# windows-latest image; fall back to the default install location.
|
||||
@@ -59,6 +84,7 @@ $Iscc = (Get-Command ISCC.exe -ErrorAction SilentlyContinue).Source
|
||||
if (-not $Iscc) { $Iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" }
|
||||
& $Iscc `
|
||||
"/DAppVersion=$Version" `
|
||||
"/DVersionInfoVersion=$VersionInfoVersion" `
|
||||
"/DStageDir=$((Resolve-Path $Stage).Path)" `
|
||||
"/DOutputDir=$((Resolve-Path dist).Path)" `
|
||||
"/DOutputName=$Name-setup" `
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Verifies that the Windows release artifacts carry everything the in-app
|
||||
# updater requires, immediately after bundle-windows.ps1 produces them.
|
||||
#
|
||||
# The updater refuses to install a package it cannot recognise, and it does so
|
||||
# on the user's machine, after the download, after the GUI has exited. Every
|
||||
# fact it checks there is checked here instead, so a packaging mistake fails
|
||||
# the release build rather than every user's next update.
|
||||
#
|
||||
# Mirrors, in order:
|
||||
# core::update::windows_update_layout_for — the install marker
|
||||
# core::update::package_for_current_install — tty7-updater.exe beside the app
|
||||
# tty7-updater `windows::verify_portable_payload` — portable layout + versions
|
||||
# tty7-updater `windows::extract_portable_archive` — ZIP entry rules
|
||||
# tty7-updater `windows::verify_file_version` — setup.exe PE version
|
||||
#
|
||||
# Usage: verify-windows-package.ps1 <arch> [version]
|
||||
# `version` defaults to the version in Cargo.toml, which is what the bundle
|
||||
# script stamped into the artifact names.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Arch = $args[0]
|
||||
if (-not $Arch) { throw "usage: verify-windows-package.ps1 <arch> [version]" }
|
||||
$Version = $args[1]
|
||||
if (-not $Version) {
|
||||
$Version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
|
||||
}
|
||||
# The PE fixed-version resource carries only numeric components, so the updater
|
||||
# compares the release version's numeric core against it. Keep the same split.
|
||||
$VersionCore = ($Version -split '[-+]', 2)[0]
|
||||
|
||||
$Name = "tty7-$Version-windows-$Arch"
|
||||
$Zip = "dist/$Name.zip"
|
||||
$Setup = "dist/$Name-setup.exe"
|
||||
$Stage = "dist/$Name"
|
||||
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
function Fail([string]$message) { $failures.Add($message) }
|
||||
|
||||
function Get-ProductVersion([string]$path) {
|
||||
# The same string the updater reads back with VerQueryValueW
|
||||
# (\StringFileInfo\<lang><cp>\ProductVersion).
|
||||
(Get-Item -LiteralPath $path).VersionInfo.ProductVersion
|
||||
}
|
||||
|
||||
function Assert-BinaryVersion([string]$path, [string]$label) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { Fail "$label is missing: $path"; return }
|
||||
$actual = Get-ProductVersion $path
|
||||
if ($actual -ne $Version) {
|
||||
Fail "$label reports ProductVersion '$actual', expected '$Version'"
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Portable ZIP --------------------------------------------------------
|
||||
# Update rules live in the updater's extractor; the ones that can be broken by
|
||||
# packaging alone are re-stated here.
|
||||
if (-not (Test-Path -LiteralPath $Zip)) {
|
||||
Fail "the portable archive is missing: $Zip"
|
||||
} else {
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $Zip).Path)
|
||||
try {
|
||||
$entries = @($archive.Entries | ForEach-Object { $_.FullName })
|
||||
} finally {
|
||||
$archive.Dispose()
|
||||
}
|
||||
|
||||
# `extract_portable_archive` rejects a backslash outright: the ZIP spec
|
||||
# names '/' as the separator, and a mixed archive is one the updater will
|
||||
# not unpack. PowerShell's archive writer has emitted both over the years.
|
||||
$backslashed = @($entries | Where-Object { $_.Contains('\') })
|
||||
if ($backslashed.Count -gt 0) {
|
||||
Fail ("the portable archive uses backslash separators the updater rejects: " +
|
||||
($backslashed -join ', '))
|
||||
}
|
||||
|
||||
# `validate_portable_relative_path` allows only these top-level names.
|
||||
$managed = @(
|
||||
'tty7-app.exe', 'tty7.exe', 'tty7-updater.exe', '.tty7-portable',
|
||||
'completions', 'server', 'LICENSE.txt', 'README.md'
|
||||
)
|
||||
$roots = @($entries |
|
||||
ForEach-Object { ($_ -split '[\\/]', 2)[0] } |
|
||||
Sort-Object -Unique)
|
||||
foreach ($root in $roots) {
|
||||
if ($managed -notcontains $root) {
|
||||
Fail "the portable archive has a top-level entry the updater rejects: $root"
|
||||
}
|
||||
}
|
||||
|
||||
# The Inno marker and the portable marker are mutually exclusive: whichever
|
||||
# one is present decides how the updater replaces this installation.
|
||||
if ($entries -contains '.tty7-inno-install') {
|
||||
Fail "the portable archive carries the Inno install marker"
|
||||
}
|
||||
|
||||
$unzipped = Join-Path ([System.IO.Path]::GetTempPath()) "tty7-verify-portable-$([guid]::NewGuid())"
|
||||
New-Item -ItemType Directory -Force -Path $unzipped | Out-Null
|
||||
try {
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory(
|
||||
(Resolve-Path $Zip).Path, $unzipped)
|
||||
|
||||
# `verify_portable_payload`: every required member, then the marker
|
||||
# content, then the complete version of both executables.
|
||||
foreach ($required in @('tty7-app.exe', 'tty7.exe', 'tty7-updater.exe',
|
||||
'.tty7-portable', 'LICENSE.txt', 'README.md')) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $unzipped $required) -PathType Leaf)) {
|
||||
Fail "the portable archive is missing the required file $required"
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $unzipped 'completions') -PathType Container)) {
|
||||
Fail "the portable archive is missing the required directory completions"
|
||||
}
|
||||
|
||||
$markerPath = Join-Path $unzipped '.tty7-portable'
|
||||
if (Test-Path -LiteralPath $markerPath) {
|
||||
$marker = [System.IO.File]::ReadAllBytes($markerPath)
|
||||
$expected = [System.Text.Encoding]::ASCII.GetBytes('portable-v1')
|
||||
if (@(Compare-Object $marker $expected -SyncWindow 0).Count -ne 0) {
|
||||
Fail "the portable marker does not contain exactly 'portable-v1'"
|
||||
}
|
||||
}
|
||||
|
||||
Assert-BinaryVersion (Join-Path $unzipped 'tty7-app.exe') 'the portable tty7-app.exe'
|
||||
Assert-BinaryVersion (Join-Path $unzipped 'tty7-updater.exe') 'the portable tty7-updater.exe'
|
||||
} finally {
|
||||
Remove-Item -Recurse -Force $unzipped -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Inno payload --------------------------------------------------------
|
||||
# ISCC compiled the installer from this staging directory, so what it holds is
|
||||
# what lands in {app}. Reading the compiled setup.exe back would need
|
||||
# innoextract, which the runners do not carry.
|
||||
if (-not (Test-Path -LiteralPath $Stage -PathType Container)) {
|
||||
Fail "the Inno staging directory is missing: $Stage"
|
||||
} else {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Stage '.tty7-inno-install') -PathType Leaf)) {
|
||||
Fail "the Inno payload is missing the .tty7-inno-install marker; installed copies would never be offered an in-app update"
|
||||
}
|
||||
if (Test-Path -LiteralPath (Join-Path $Stage '.tty7-portable')) {
|
||||
Fail "the Inno payload carries the portable marker, which would misroute the updater"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Stage 'tty7-updater.exe') -PathType Leaf)) {
|
||||
Fail "the Inno payload is missing tty7-updater.exe"
|
||||
}
|
||||
Assert-BinaryVersion (Join-Path $Stage 'tty7-app.exe') 'the installed tty7-app.exe'
|
||||
Assert-BinaryVersion (Join-Path $Stage 'tty7-updater.exe') 'the installed tty7-updater.exe'
|
||||
}
|
||||
|
||||
# ---- Setup executable ----------------------------------------------------
|
||||
# `verify_update` re-reads this numeric version after the GUI exits and before
|
||||
# it runs the installer, so a mis-stamped VersionInfoVersion is an update that
|
||||
# aborts on the user's machine.
|
||||
if (-not (Test-Path -LiteralPath $Setup -PathType Leaf)) {
|
||||
Fail "the Windows installer is missing: $Setup"
|
||||
} else {
|
||||
$info = (Get-Item -LiteralPath $Setup).VersionInfo
|
||||
$actual = "$($info.FileMajorPart).$($info.FileMinorPart).$($info.FileBuildPart)"
|
||||
if ($actual -ne $VersionCore) {
|
||||
Fail "$Setup reports file version '$actual', expected '$VersionCore'"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
foreach ($failure in $failures) { Write-Output "::error::$failure" }
|
||||
throw "the Windows release package would not be updatable in place ($($failures.Count) problem(s))"
|
||||
}
|
||||
|
||||
Write-Output "Windows package verified: markers, tty7-updater.exe and versions match $Version"
|
||||
@@ -2,8 +2,9 @@
|
||||
; windows-latest runners). Compiled by bundle-windows.ps1, which stages the
|
||||
; payload and passes every path in via /D defines:
|
||||
;
|
||||
; /DAppVersion=<semver> version parsed from Cargo.toml
|
||||
; /DStageDir=<abs path> staged payload (tty7-app.exe, completions\, LICENSE.txt, README.md)
|
||||
; /DAppVersion=<semver> display version parsed from Cargo.toml
|
||||
; /DVersionInfoVersion=<numeric version> PE-compatible file version
|
||||
; /DStageDir=<abs path> staged payload (app, CLI, updater, marker, resources)
|
||||
; /DOutputDir=<abs path> where the setup exe is written
|
||||
; /DOutputName=<basename> setup exe filename, without ".exe"
|
||||
;
|
||||
@@ -15,6 +16,9 @@
|
||||
#ifndef AppVersion
|
||||
#error Missing /DAppVersion — this script is meant to be compiled via bundle-windows.ps1
|
||||
#endif
|
||||
#ifndef VersionInfoVersion
|
||||
#error Missing /DVersionInfoVersion — this script is meant to be compiled via bundle-windows.ps1
|
||||
#endif
|
||||
|
||||
[Setup]
|
||||
; Never change AppId: it is how Windows ties upgrades + the uninstall entry
|
||||
@@ -22,6 +26,7 @@
|
||||
AppId={{9A3F6C1E-4B7D-4E2A-8C5F-D01B92E64A37}
|
||||
AppName=tty7
|
||||
AppVersion={#AppVersion}
|
||||
VersionInfoVersion={#VersionInfoVersion}
|
||||
AppPublisher=tty7 contributors
|
||||
AppPublisherURL=https://github.com/l0ng-ai/tty7
|
||||
AppSupportURL=https://github.com/l0ng-ai/tty7/issues
|
||||
@@ -82,6 +87,10 @@ Source: "{#StageDir}\tty7-app.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; installer to do it, and one code path serving both is one behaviour to debug.
|
||||
; The uninstaller takes that entry back out; see RemoveAppDirFromUserPath below.
|
||||
Source: "{#StageDir}\tty7.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#StageDir}\tty7-updater.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
|
||||
; This installer-only marker is the authority for enabling automatic Windows
|
||||
; updates. The portable archive is created before the marker enters the stage.
|
||||
Source: "{#StageDir}\.tty7-inno-install"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
|
||||
Source: "{#StageDir}\completions\*"; DestDir: "{app}\completions"; Flags: ignoreversion recursesubdirs
|
||||
Source: "{#StageDir}\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#StageDir}\README.md"; DestDir: "{app}"; Flags: ignoreversion
|
||||
@@ -90,9 +99,18 @@ Source: "{#StageDir}\README.md"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; still has to produce an installer. See bundle-windows.ps1.
|
||||
Source: "{#StageDir}\server\*"; DestDir: "{app}\server"; Flags: ignoreversion recursesubdirs skipifsourcedoesntexist
|
||||
|
||||
; AppUserModelID is what lets toast notifications carry the tty7 name and icon
|
||||
; instead of the notify-rust PowerShell fallback: Windows only honors an
|
||||
; unpackaged app's toast identity when a shortcut stamps it. Must match
|
||||
; `core::aumid::AUMID` (src/core/aumid.rs), which at startup stamps the
|
||||
; per-user shortcut below if some older installer left it unstamped, and
|
||||
; writes one from scratch for the portable zip. It deliberately leaves an
|
||||
; all-users install alone — it cannot write {commonprograms} unelevated, and a
|
||||
; per-user twin would both duplicate the Start Menu entry and outlive this
|
||||
; uninstaller — so an elevated install depends on the stamp right here.
|
||||
[Icons]
|
||||
Name: "{autoprograms}\tty7"; Filename: "{app}\tty7-app.exe"
|
||||
Name: "{autodesktop}\tty7"; Filename: "{app}\tty7-app.exe"; Tasks: desktopicon
|
||||
Name: "{autoprograms}\tty7"; Filename: "{app}\tty7-app.exe"; AppUserModelID: "com.github.tty7"
|
||||
Name: "{autodesktop}\tty7"; Filename: "{app}\tty7-app.exe"; Tasks: desktopicon; AppUserModelID: "com.github.tty7"
|
||||
|
||||
[Run]
|
||||
; The registry shape lives in core::explorer_context_menu, not here: the app
|
||||
|
||||
Reference in New Issue
Block a user