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 .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
- name: Pack (Windows)
if: matrix.runtime == 'win-x64'
shell: pwsh
run: ./build.ps1 Pack PackMsi --skip Test --Runtime ${{ matrix.runtime }} --Version ${{ needs.compile.outputs.version }}
run: ./build.ps1 Pack --skip Test --Runtime ${{ matrix.runtime }} --Version ${{ needs.compile.outputs.version }}

- name: Pack (macOS)
if: matrix.runtime == 'osx-arm64'
Expand Down
1 change: 0 additions & 1 deletion .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"Clean",
"Mutate",
"Pack",
"PackMsi",
"ProbeTranslation",
"ProbeVoice",
"Publish",
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- The settings page showed the version `1.0.0` for every release: the build never stamped the release version into
the program. The version shown now is the release's (`--Version`, the tag); a build made any other way shows
`0.0.0-dev`.
- The update check no longer says "you are on the latest version" when it did not look. A copy that was not installed
by an installer of the release (a development build, or the `.msi` of 2.1.0 and earlier) cannot update itself, and
says so with a link to the download page.

### Changed
- One installer technology on Windows: the `.msi` is now built by Velopack (`vpk pack --msi`) like the Setup.exe, so an
application installed from it updates itself the same way. It installs per user (no elevation, also for updates). The
hand-made WiX `.msi`, which could not update, is gone along with the WiX tool.
- On Windows the settings, models and logs moved from `%LocalAppData%\LiveLingo` to `%LocalAppData%\AGIBuild\LiveLingo`.
Velopack owns the first folder: it empties it when an installer runs over it and when the application is uninstalled,
which would have deleted the models and the settings. The first start of this version moves `settings.json`,
`preferences.json` and `models` over (a rename, no copy).
**If you installed the old `.msi` (its data is in the old folder), uninstall it, move those three entries into
`%LocalAppData%\AGIBuild\LiveLingo` yourself, and only then run the new Setup.exe or `.msi`**: the installer asks
before it replaces a folder that is not empty, and replacing it deletes what is inside.
- Updates report where they are: *Settings → Models → Updates* shows the checking, the download with its progress, the
result with the time of the check, and a failure with its reason. The tray's *Check for Updates* opens it and starts
a check, instead of a dialog that vanished.

## [2.1.0] - 2026-11-02

Stopping a dictation now polishes the whole text (summary first), with editable polish requirements, a Clear button,
Expand Down
17 changes: 9 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ Thanks for your interest in contributing! This guide covers everything you need
| [.NET SDK](https://dotnet.microsoft.com/download) | 10.0+ | Required |
| Git | Any recent | Required |
| [Nuke](https://nuke.build/) | Via `dotnet tool restore` | Build automation |
| [Velopack](https://velopack.io/) | Via `dotnet tool restore` | Windows installer packaging |
| [WiX Toolset](https://wixtoolset.org/) | Via `dotnet tool restore` | Windows MSI packaging |
| [Velopack](https://velopack.io/) | Via `dotnet tool restore` | Windows (Setup.exe and .msi) and macOS packaging, updates |
| [Stryker.NET](https://stryker-mutator.io/) | Via `dotnet tool restore` | Mutation testing |

---
Expand All @@ -23,7 +22,7 @@ Thanks for your interest in contributing! This guide covers everything you need
git clone https://github.com/AGIBuild/LiveLingo.git
cd LiveLingo

# Restore CLI tools (Velopack, WiX, Stryker)
# Restore CLI tools (Velopack, Stryker)
dotnet tool restore

# Run in development mode
Expand Down Expand Up @@ -70,7 +69,6 @@ LiveLingo/
│
├── build/ # Nuke build automation
│ ├── BuildTask.cs # All build targets
│ ├── windows/LiveLingo.wxs # WiX MSI definition
│ └── macos/ # macOS packaging scripts
│
├── .github/workflows/ # CI/CD (compile → test → pack → release)
Expand Down Expand Up @@ -126,8 +124,7 @@ LiveLingo uses [Nuke](https://nuke.build/) for build automation. All targets are
./build.sh Publish --Runtime osx-arm64 # Publish for macOS
./build.ps1 Publish --Runtime win-x64 # Publish for Windows
./build.sh Pack --Runtime osx-arm64 --Version 1.0.0 # macOS: .pkg installer + update package and feed
./build.ps1 Pack --Runtime win-x64 --Version 1.0.0 # Windows: Setup.exe installer + update package and feed
./build.ps1 PackMsi --Runtime win-x64 --Version 1.0.0 # WiX .msi installer (does not update itself)
./build.ps1 Pack --Runtime win-x64 --Version 1.0.0 # Windows: Setup.exe and .msi installers + update package and feed
```

End-to-end probes download the pinned models (SHA256-verified) and use real inference; they are not part of the unit-test gate:
Expand Down Expand Up @@ -265,8 +262,12 @@ Installed applications update themselves from the GitHub releases of the reposit
publish a release whose feed does not name a package that is in it.
- The application checks at start and every `Update.CheckIntervalHours`, downloads a newer version in the background,
and then offers *Restart to Update* in the tray menu; quitting also installs it. Pre-releases are not offered.
- Only an application installed by the Setup.exe or the .pkg can update itself. A development build and the .msi cannot;
they skip the check.
*Settings → Models → Updates* shows the version, the progress of a check or download, and the result; the tray's
*Check for Updates* opens it and starts a check.
- Only an application installed by an installer of the release (Setup.exe, the .msi or the .pkg — all Velopack) can
update itself; the .msi is built by `vpk pack --msi` and installs per user, so updates never need elevation. A
development build and a copy installed by the hand-built `.msi` of 2.1.0 and earlier cannot; they skip the check, and
*Updates* says so and links to the download page.
- A release without a feed (the 1.x releases) is skipped. The application reads the latest 10 releases.
- Installers are not signed, so Windows SmartScreen and macOS Gatekeeper warn on the first install; updates run from the
installed application. Signing (`--signAppIdentity`, `--notaryProfile`) is not set up.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ exported as SRT.
endpoint must be `https`; the API key is kept in the system secret store, not in a file. A cloud failure is shown,
never silently retried on the local model.
- Models are downloaded only after you agree, from their original hosts, and verified against pinned SHA256 hashes.
- Settings, models and logs live in `%LocalAppData%\AGIBuild\LiveLingo` (Windows) or
`~/Library/Application Support/LiveLingo` (macOS), outside the installed program, so updating and uninstalling keep
them. Delete that folder to remove everything LiveLingo stored.
- Recent drafts are kept in memory only and are gone when the app exits.
- The installed app looks for updates on the GitHub releases of this repository (at start and every few hours) and
downloads a newer version in the background. Nothing you wrote or said is part of that request. Set
Expand Down
35 changes: 14 additions & 21 deletions build/BuildTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ private static void RunProcessLogger(OutputType type, string output)
.SetConfiguration(Configuration.Release)
.SetRuntime(Runtime)
.SetSelfContained(true)
.SetVersion(Version)
.SetOutput(PublishDir / Runtime)
.SetNoLogo(true));
});
Expand Down Expand Up @@ -331,6 +332,11 @@ double RunMutationTesting()
$"--outputDir {ReleasesDir} " +
$"--noPortable";

// The .msi is Velopack's too, so an application installed from it updates itself like the Setup.exe one.
// Per user, so that updates never need elevation.
if (windows)
arguments += " --msi --instLocation PerUser";

if (!windows)
{
// The bundle version has no pre-release suffix; the update feed carries the full semantic version.
Expand All @@ -349,34 +355,21 @@ double RunMutationTesting()

DotNet(arguments, environmentVariables: vpkEnv.AsReadOnly());

// The installer keeps the file name of earlier releases.
// The installers keep the file names of earlier releases.
var installer = ReleasesDir.GlobFiles(windows ? "*Setup.exe" : "*Setup.pkg").Single();
var renamed = ReleasesDir / (windows
? $"LiveLingo-{Version}-{Runtime}-Setup.exe"
: $"LiveLingo-{Version}-{Runtime}.pkg");
File.Move(installer, renamed, overwrite: true);
Log.Information("Installer: {Path}", renamed);
});

Target PackMsi => _ => _
.DependsOn(Publish)
.Requires(() => Runtime.StartsWith("win"))
.Executes(() =>
{
DotNetToolRestore();

ReleasesDir.CreateDirectory();

var wxsFile = RootDirectory / "build" / "windows" / "LiveLingo.wxs";
var msiOutput = ReleasesDir / $"LiveLingo-{Version}-{Runtime}.msi";

DotNet(
$"wix build \"{wxsFile}\" " +
$"-o \"{msiOutput}\" " +
$"-d Version={Version} " +
$"-d PublishDir={PublishDir / Runtime}");

Log.Information("Windows MSI created: {Path}", msiOutput);
if (windows)
{
var msi = ReleasesDir.GlobFiles("*.msi").Single();
var msiRenamed = ReleasesDir / $"LiveLingo-{Version}-{Runtime}.msi";
File.Move(msi, msiRenamed, overwrite: true);
Log.Information("MSI installer: {Path}", msiRenamed);
}
});

static bool IsCoverageClassIncluded(string packageName, string className)
Expand Down
58 changes: 0 additions & 58 deletions build/windows/LiveLingo.wxs

This file was deleted.

34 changes: 34 additions & 0 deletions docs/superpowers/specs/2026-11-03-windows-packaging-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Windows 打包与更新设计(2026-11-03)

## 问题

- Windows 发布了两种安装方式:Velopack `Setup.exe`(能自动更新)和自维护的 WiX `.msi`(不能)。用 `.msi` 的用户永远看不到更新,
界面还把"没有检查"说成"已是最新"。同一件事(安装与更新)由两套机制实现,违反"同一功能只由一个组件实现"。
- Windows 上用户数据(设置、模型、日志)放在 `%LocalAppData%\LiveLingo`,与 Velopack 的安装根目录(`packId` = `LiveLingo`)是同一个目录。

## 事实(已核对)

- [VERIFIED] Velopack 1.2.158 起支持 `vpk pack --msi`:MSI 与 Setup.exe 安装同样的目录结构,装完后更新方式一致
(docs.velopack.io/packaging/installer)。本仓库用 vpk 1.2.161。
- [VERIFIED] Velopack 卸载会删除整个 `%LocalAppData%\{packId}`(docs.velopack.io/integrating/uninstalling)。
- [VERIFIED] Velopack 安装器遇到非空目标目录时,用户确认后会把旧目录改名、重装、成功后**删除旧目录**
(velopack `src/bins/src/commands/install.rs`)。所以数据留在安装根目录里,首次安装和卸载都会丢模型与设置。
- [VERIFIED] 更新只替换 `current` 目录,根目录其他内容不动。

## 决策

| # | 决策 | 备选项 | 取舍 |
|---|------|--------|------|
| 1 | Windows 只用 Velopack 一条打包链:`Setup.exe`(个人)+ `.msi`(`vpk pack --msi`,面向部署工具) | 自维护 WiX MSI;MSIX;ClickOnce;只留 Setup.exe | 自维护 MSI 不能自更新,且要维护第二套工程和 WiX 许可(v7 的 OSMF)。MSIX 未签名无法安装,要 Store 或证书;ClickOnce 无增量更新、不跨平台。Velopack 同时覆盖 Windows 与 macOS,有增量更新。MSI 保留是因为已有用户使用且部署工具需要 |
| 2 | MSI 固定 `--instLocation PerUser` | `PerMachine` / `Either` | 与 Setup.exe 一致:后台更新不需要提权。按机器安装的更新是否弹 UAC 未验证,不冒险 |
| 3 | 数据目录移出安装根目录:Windows `%LocalAppData%\AGIBuild\LiveLingo`,macOS 不变;由 `LiveLingo.Core.UserData` 一处定义 | 数据留在原处;改 `packId` | 留在原处会在安装和卸载时被清空。改 `packId` 会切断已安装版本的更新链,macOS 也受影响。卸载后保留数据与 macOS 行为一致 |
| 4 | 首次启动一次性把 `settings.json`、`preferences.json`、`models` 从旧目录改名搬走;失败只记日志,不阻断启动 | 复制;不迁移 | 同一磁盘卷内改名,GB 级模型瞬间完成。不迁移会让已装 Setup.exe 的用户丢设置、重新下载模型 |

## 已知限制与未决项

- 旧 `.msi`(2.1.0 及以前)的用户在装新版前,必须自己把旧目录里的三项搬到新目录,否则安装器替换旧目录时会删掉它们
(旧版没有代码可以替他们搬)。已写入 CHANGELOG。
- 未签名:SmartScreen 会警告首次安装。微软推荐非 Store 分发使用 Azure Artifact Signing(约 10 美元/月,需组织或个人身份验证,
信誉随发布逐步积累)。Velopack 支持 `--signTemplate` / Azure Trusted Signing。需要账号和密钥,不在本次范围。
- `winget` 分发(`winget upgrade` 也能升级):需要先有签名与稳定的下载地址,之后再评估。
- `.msi` 的产物文件名、`--msi` 在 CI 上的输出需要在首次 Windows 发布时核对(本地 macOS 无法运行 MSI 打包)。
7 changes: 0 additions & 7 deletions dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,6 @@
"dotnet-stryker"
],
"rollForward": false
},
"wix": {
"version": "6.0.2",
"commands": [
"wix"
],
"rollForward": false
}
}
}
8 changes: 2 additions & 6 deletions src/LiveLingo.Core/CoreOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ namespace LiveLingo.Core;

public class CoreOptions
{
public string ModelStoragePath { get; set; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LiveLingo", "models");
public string ModelStoragePath { get; set; } = Path.Combine(UserData.Directory, "models");

/// <summary>Where short-lived runtime state (pid records of started servers) is kept.</summary>
public string RuntimeStatePath { get; set; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LiveLingo", "run");
public string RuntimeStatePath { get; set; } = Path.Combine(UserData.Directory, "run");

public int InferenceThreads { get; set; }

Expand Down
23 changes: 23 additions & 0 deletions src/LiveLingo.Core/UserData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace LiveLingo.Core;

/// <summary>
/// Where the user's data (settings, models, logs) lives: one place for the whole application. It is never inside a
/// folder an installer owns. Velopack empties <c>%LocalAppData%\LiveLingo</c> when an installer runs over it and when
/// the application is uninstalled, so on Windows the data sits under the publisher's folder next to it and survives
/// both, like the data under Application Support on macOS survives deleting the app.
/// </summary>
public static class UserData
{
public static string Directory { get; } = Resolve(
OperatingSystem.IsMacOS(),
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));

/// <summary>The folder earlier versions kept the data in on Windows; the installer owns it now.</summary>
public static string LegacyWindowsDirectory { get; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LiveLingo");

internal static string Resolve(bool macOS, string home, string localAppData) => macOS
? Path.Combine(home, "Library", "Application Support", "LiveLingo")
: Path.Combine(localAppData, "AGIBuild", "LiveLingo");
}
Loading
Loading