Merge pull request #560 from fanlusky/feat/bundle-skins
feat(installer): 随包安装外部皮肤并保留覆盖备份
This commit is contained in:
12 files changed
+342
-3
No files matched your search
@@ -347,6 +347,8 @@ jobs:
|
||||
run: ./installer/tests/package-files.ps1
|
||||
- name: Test that the installer preserves user data on upgrade
|
||||
run: ./installer/tests/preserve-user-data.ps1
|
||||
- name: Test that bundled skins back up changed user copies before overwriting
|
||||
run: ./installer/tests/bundled-skins.ps1
|
||||
- name: Stop local packaging after failed component builds
|
||||
run: ./installer/tests/build-failures.ps1
|
||||
- name: Test local Server test entrypoints in a moved checkout
|
||||
|
||||
@@ -17,7 +17,7 @@ Windows 端的全部一方源码在本仓。**合仓改变的是仓库数量,
|
||||
| `ui-html/` | 候选窗、工具栏、菜单、设置页的 HTML / CSS / JS | [ui-html/README.md](ui-html/README.md) |
|
||||
| `installer/` | 收集产物、自签名、Inno Setup 打包 | [installer/README.md](installer/README.md) |
|
||||
| `log/` | 日志采集库 | — |
|
||||
| `skins/` | 外部皮肤合集,每个子目录一个 `skin.toml` 皮肤包;不打进安装包 | [skins/README.md](skins/README.md) |
|
||||
| `skins/` | 外部皮肤合集,每个子目录一个 `skin.toml` 皮肤包;除素材授权未核实的外随安装包装到数据目录,同名且改过的旧版改名 `.bak` 保留 | [skins/README.md](skins/README.md) |
|
||||
| `examples/skin-examples/` | 外部皮肤制作样例、字段说明、预览页与安装脚本;本目录单独采用 MIT 授权 | [examples/skin-examples/README.md](examples/skin-examples/README.md) |
|
||||
| `experiments/tsf-edit-control/` | TSF 编辑控件实验工程,不参与产品构建 | — |
|
||||
| `vendor/` | submodule:`opencc`、`cpp-pinyin` | 上游仓库 |
|
||||
|
||||
@@ -5,6 +5,8 @@ server_exe/
|
||||
tsf_dll/
|
||||
app_data/
|
||||
default_skins/
|
||||
bundled_skins/
|
||||
bundled_skins.manifest
|
||||
MetasequoiaIME.ico
|
||||
/THIRD_PARTY_NOTICES.txt
|
||||
/LICENSE.txt
|
||||
|
||||
@@ -64,6 +64,7 @@ $tsf32Pdb = Join-Path $RepoRoot (Join-Path $TsfDirectory 'build32-release\Releas
|
||||
$tsf64Pdb = Join-Path $RepoRoot (Join-Path $TsfDirectory 'build64-release\Release\MetasequoiaImeTsf.pdb')
|
||||
$webviewRoot = Join-Path $RepoRoot (Join-Path $UiHtmlDirectory 'webview2')
|
||||
$defaultSkinsSource = Join-Path $RepoRoot 'skins\default'
|
||||
$bundledSkinsSource = Join-Path $RepoRoot 'skins'
|
||||
$serverConfig = Join-Path $RepoRoot (Join-Path $ServerDirectory 'assets\config\config.toml')
|
||||
$factoryConfig = Join-Path $PSScriptRoot 'default_config\config.default.toml'
|
||||
$pinyinTable = Join-Path $RepoRoot (Join-Path $ServerDirectory 'assets\tables\pinyin.txt')
|
||||
@@ -243,6 +244,50 @@ $targetDefaultSkins = Join-Path $PSScriptRoot 'default_skins'
|
||||
Reset-Directory -LiteralPath $targetDefaultSkins
|
||||
Copy-DirectoryContents -Source $defaultSkinsSource -Destination $targetDefaultSkins
|
||||
|
||||
# 仓库 skins\ 下的外部皮肤随包装到数据目录的 skins\<id>,同名的直接覆盖。覆盖前安装程序拿
|
||||
# bundled_skins.manifest 核对用户机器上的同名目录:内容与随包的一模一样就不动,否则先改名成
|
||||
# <id>.bak 备份(Server 扫描时跳过 .bak 目录)。清单每行是「id|相对路径|sha256」,
|
||||
# 安装程序按字节比较,所以路径限定 ASCII、不能含 |。
|
||||
# 素材授权没核实的皮肤([license] assets = "UNVERIFIED...")只能放在仓库里演示,不进安装包。
|
||||
$targetBundledSkins = Join-Path $PSScriptRoot 'bundled_skins'
|
||||
$bundledSkinsManifest = Join-Path $PSScriptRoot 'bundled_skins.manifest'
|
||||
Reset-Directory -LiteralPath $targetBundledSkins
|
||||
$manifestLines = [Collections.Generic.List[string]]::new()
|
||||
$bundledSkinIds = [Collections.Generic.List[string]]::new()
|
||||
foreach ($skinDir in @(Get-ChildItem -LiteralPath $bundledSkinsSource -Directory | Sort-Object Name)) {
|
||||
$id = $skinDir.Name
|
||||
if ($id -ceq 'default') { continue }
|
||||
if ($id -cnotmatch '^[a-z0-9][a-z0-9._-]{0,63}$' -or $id -match '\.bak$') {
|
||||
throw "皮肤目录名不是有效的外部皮肤 ID:$($skinDir.FullName)"
|
||||
}
|
||||
$skinManifest = Join-Path $skinDir.FullName 'skin.toml'
|
||||
Assert-PathExists -LiteralPath $skinManifest -Description "皮肤 $id 的 skin.toml"
|
||||
$skinText = [IO.File]::ReadAllText($skinManifest)
|
||||
if ($skinText -notmatch "(?m)^\s*id\s*=\s*`"$([regex]::Escape($id))`"\s*(#.*)?$") {
|
||||
throw "皮肤 $id 的 skin.toml 里 id 与目录名不一致,Server 不会加载它:$skinManifest"
|
||||
}
|
||||
if ($skinText -match '(?m)^\s*assets\s*=\s*"UNVERIFIED') {
|
||||
Write-Host "跳过皮肤 ${id}:素材授权未核实,不随安装包分发。"
|
||||
continue
|
||||
}
|
||||
$destination = Join-Path $targetBundledSkins $id
|
||||
Copy-DirectoryContents -Source $skinDir.FullName -Destination $destination
|
||||
foreach ($file in @(Get-ChildItem -LiteralPath $destination -Recurse -File -Force | Sort-Object FullName)) {
|
||||
$relative = $file.FullName.Substring($destination.Length + 1)
|
||||
if ($relative -notmatch '^[\x20-\x7E]+$' -or $relative.Contains('|')) {
|
||||
throw "皮肤文件名只能用 ASCII 且不能含 |(安装程序按清单逐字节比对):$($file.FullName)"
|
||||
}
|
||||
$hash = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$manifestLines.Add("$id|$relative|$hash")
|
||||
}
|
||||
$bundledSkinIds.Add($id)
|
||||
}
|
||||
if ($bundledSkinIds.Count -eq 0) {
|
||||
throw "没有可随包分发的外部皮肤:$bundledSkinsSource"
|
||||
}
|
||||
[IO.File]::WriteAllLines($bundledSkinsManifest, $manifestLines, [Text.UTF8Encoding]::new($false))
|
||||
Write-Host "随包皮肤:$($bundledSkinIds -join ', ')"
|
||||
|
||||
# Server Release 输出整体复制,但测试程序及其 PDB 绝不能进入安装包。
|
||||
# 带 -IncludeSymbols 时其他 PDB 保留在对应 EXE 旁边,方便安装后直接进行崩溃分析。
|
||||
Reset-Directory -LiteralPath $targetServer
|
||||
|
||||
@@ -42,6 +42,7 @@ pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion 1.2.3 -RepoRoot .. `
|
||||
- **数据目录必须整个归输入法**([#537](https://github.com/metasequoiaime/MSIME-Windows/issues/537))。用户选的目录里已经有别的文件、或者选了驱动器根目录时,安装器改用其中的 `metasequoiaime` 子目录,并在向导里把实际位置告诉用户;连那个子目录也不空就拒绝。输入法用过的目录(有标记,或历史默认位置)原样沿用
|
||||
- 安装器在数据目录里放一个 `.metasequoiaime-data` 标记文件。只有标记里带 `exclusive: adopted empty by the installer` 这一行(安装时目录是空的),或目录就是历史默认位置,覆盖安装才整目录清理、卸载才整目录删除
|
||||
- 旧版安装器不管目录里原来有什么都写标记,所以只有旧标记(没有那一行)的目录不能再信:覆盖安装、换目录和卸载都只按 `IsShippedAppDataItem` / `IsRuntimeAppDataItem` / `IsPreservedAppDataItem` 名单删输入法自己的条目,`html`、`logs`、`skins` 这种通用目录名只删输入法写进去的部分,最后目录空了才删目录。包里新增顶层文件要同步 `IsShippedAppDataItem`,`tests/package-files.ps1` 会拿真实打出的 `app_data` 核对;Server 在数据目录根下新写文件要同步 `IsRuntimeAppDataItem`
|
||||
- 仓库 `skins\` 下的外部皮肤(除 `default\` 和素材授权为 `UNVERIFIED*` 的)随包装到 `skins\<id>`,完整包和轻量包都带,卸载不删。`Prepare-PackageFiles.ps1` 同时写出 `bundled_skins.manifest`(每行 `id|相对路径|sha256`),安装器在 `PrepareToInstall` 里拿它核对用户机器上的同名目录:文件集合和内容完全一致就直接覆盖,否则先改名成 `<id>.bak`(已有就 `<id>.2.bak`……,不覆盖旧备份)再装。Server 扫描皮肤时跳过 `*.bak` 目录。`tests/bundled-skins.ps1` 把这段 Pascal 编进探针安装器真跑一遍
|
||||
- 静默安装用 `/DATADIR="D:\MetasequoiaIME"` 指定;该值在 `PrepareToInstall` 里和向导页走同一套换子目录和校验
|
||||
|
||||
## 运行时依赖检查
|
||||
|
||||
@@ -132,6 +132,13 @@ Source: "{#MySourceRoot}\default_skins\*"; \
|
||||
DestDir: "{code:GetDataDir}\skins\default"; \
|
||||
Flags: onlyifdoesntexist recursesubdirs createallsubdirs uninsneveruninstall
|
||||
|
||||
; 仓库 skins\ 下的外部皮肤,同名目录整目录覆盖。用户机器上的同名皮肤和随包的不一样时,
|
||||
; PrepareToInstall 里的 BackupChangedBundledSkins 先把它改名成 <id>.bak 再复制。
|
||||
Source: "{#MySourceRoot}\bundled_skins\*"; \
|
||||
DestDir: "{code:GetDataDir}\skins"; \
|
||||
Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall
|
||||
Source: "{#MySourceRoot}\bundled_skins.manifest"; Flags: dontcopy
|
||||
|
||||
#ifdef LightPackage
|
||||
; 轻量包只覆盖前端 HTML,不带词库/辅助码/出厂配置。
|
||||
Source: "{#MySourceRoot}\app_data\html\*"; \
|
||||
@@ -1398,6 +1405,115 @@ begin
|
||||
RemoveDataDir(OldDir);
|
||||
end;
|
||||
|
||||
function CountFilesUnder(const Directory: String): Integer;
|
||||
var
|
||||
FindRec: TFindRec;
|
||||
begin
|
||||
Result := 0;
|
||||
if FindFirst(AddBackslash(Directory) + '*', FindRec) then
|
||||
begin
|
||||
try
|
||||
repeat
|
||||
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
|
||||
begin
|
||||
if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
|
||||
Result := Result + CountFilesUnder(AddBackslash(Directory) + FindRec.Name)
|
||||
else
|
||||
Result := Result + 1;
|
||||
end;
|
||||
until not FindNext(FindRec);
|
||||
finally
|
||||
FindClose(FindRec);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ 数据目录里的同名皮肤与随包的文件集合、内容完全一致时返回 True。
|
||||
清单每行「id|相对路径|sha256」,由 Prepare-PackageFiles.ps1 按 id 分组写出。}
|
||||
function SkinMatchesBundle(const SkinDir, Id: String; const Lines: TArrayOfString): Boolean;
|
||||
var
|
||||
Index, P, Expected: Integer;
|
||||
Rest, FilePath: String;
|
||||
begin
|
||||
Result := False;
|
||||
Expected := 0;
|
||||
for Index := 0 to GetArrayLength(Lines) - 1 do
|
||||
begin
|
||||
P := Pos('|', Lines[Index]);
|
||||
if (P = 0) or (Copy(Lines[Index], 1, P - 1) <> Id) then
|
||||
continue;
|
||||
Rest := Copy(Lines[Index], P + 1, Length(Lines[Index]));
|
||||
P := Pos('|', Rest);
|
||||
if P = 0 then
|
||||
exit;
|
||||
Expected := Expected + 1;
|
||||
FilePath := AddBackslash(SkinDir) + Copy(Rest, 1, P - 1);
|
||||
if not FileExists(FilePath) then
|
||||
exit;
|
||||
try
|
||||
if CompareText(GetSHA256OfFile(FilePath), Copy(Rest, P + 1, Length(Rest))) <> 0 then
|
||||
exit;
|
||||
except
|
||||
exit;
|
||||
end;
|
||||
end;
|
||||
{ 多出来的文件也算改过:整目录覆盖不会删它们,留着就和随包的不是一个皮肤了。}
|
||||
Result := CountFilesUnder(SkinDir) = Expected;
|
||||
end;
|
||||
|
||||
{ 覆盖随包皮肤之前,把用户机器上内容不同的同名目录改名成 <id>.bak;
|
||||
已经有 .bak 就用 <id>.2.bak、<id>.3.bak……,不覆盖上一次的备份——那可能正是用户改过的版本。
|
||||
内容一样的不备份,否则每次升级都会多一份一模一样的 .bak。Server 扫描皮肤时跳过 .bak 目录。}
|
||||
function BackupChangedBundledSkins: String;
|
||||
var
|
||||
Lines: TArrayOfString;
|
||||
Index, P, N: Integer;
|
||||
Id, LastId, SkinsDir, SkinDir, BackupDir: String;
|
||||
begin
|
||||
Result := '';
|
||||
ExtractTemporaryFile('bundled_skins.manifest');
|
||||
if not LoadStringsFromFile(ExpandConstant('{tmp}\bundled_skins.manifest'), Lines) then
|
||||
begin
|
||||
Result := '无法读取随包皮肤清单,安装文件可能不完整。';
|
||||
exit;
|
||||
end;
|
||||
SkinsDir := AddBackslash(GetDataDir('')) + 'skins';
|
||||
LastId := '';
|
||||
for Index := 0 to GetArrayLength(Lines) - 1 do
|
||||
begin
|
||||
P := Pos('|', Lines[Index]);
|
||||
if P = 0 then
|
||||
continue;
|
||||
Id := Copy(Lines[Index], 1, P - 1);
|
||||
if Id = LastId then
|
||||
continue;
|
||||
LastId := Id;
|
||||
SkinDir := AddBackslash(SkinsDir) + Id;
|
||||
if not DirExists(SkinDir) then
|
||||
continue;
|
||||
if SkinMatchesBundle(SkinDir, Id, Lines) then
|
||||
begin
|
||||
Log('Bundled skin ' + Id + ' is unchanged; no backup needed.');
|
||||
continue;
|
||||
end;
|
||||
BackupDir := SkinDir + '.bak';
|
||||
N := 2;
|
||||
while DirExists(BackupDir) or FileExists(BackupDir) do
|
||||
begin
|
||||
BackupDir := AddBackslash(SkinsDir) + Id + '.' + IntToStr(N) + '.bak';
|
||||
N := N + 1;
|
||||
end;
|
||||
if not RenameFile(SkinDir, BackupDir) then
|
||||
begin
|
||||
Result :=
|
||||
'无法备份皮肤目录 ' + SkinDir + '。' + #13#10 +
|
||||
'请关闭正在打开该目录或其中图片的程序后重试安装。';
|
||||
exit;
|
||||
end;
|
||||
Log('Backed up skin ' + SkinDir + ' to ' + BackupDir);
|
||||
end;
|
||||
end;
|
||||
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
DataDirProblem: String;
|
||||
@@ -1428,6 +1544,13 @@ begin
|
||||
Result := DataDirProblem;
|
||||
exit;
|
||||
end;
|
||||
{ 迁移之后再备份:换了数据目录时,要比对的是已经搬到新目录的皮肤。}
|
||||
DataDirProblem := BackupChangedBundledSkins;
|
||||
if DataDirProblem <> '' then
|
||||
begin
|
||||
Result := DataDirProblem;
|
||||
exit;
|
||||
end;
|
||||
#ifdef LightPackage
|
||||
{ 轻量包不替换词库:只清 HTML 和 Server/TSF,保留本机 msime.db 等。}
|
||||
TryDeleteTree(AddBackslash(GetDataDir('')) + 'html');
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# 覆盖随包皮肤之前的备份逻辑跑在安装器的 Pascal 脚本里,只看 .iss 文本证明不了它对。
|
||||
# 这里把 msime_setup.iss 里的那几个函数原样抠出来,编进一个只在 InitializeSetup 里调用它们、
|
||||
# 数据目录指向临时目录的探针安装器,真跑一遍再看目录。
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Resolve-Iscc {
|
||||
$command = Get-Command 'ISCC.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($command) { return $command.Source }
|
||||
foreach ($version in @(7, 6)) {
|
||||
foreach ($root in @(${env:ProgramFiles(x86)}, $env:ProgramFiles, (Join-Path $env:LOCALAPPDATA 'Programs'))) {
|
||||
if (-not $root) { continue }
|
||||
$candidate = Join-Path $root "Inno Setup $version\ISCC.exe"
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
|
||||
}
|
||||
}
|
||||
throw '找不到 ISCC.exe'
|
||||
}
|
||||
|
||||
$work = Join-Path ([IO.Path]::GetTempPath()) ('msime-bundled-skins-' + [Guid]::NewGuid())
|
||||
function Write-Text([string]$Path, [string]$Text) {
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $Path) | Out-Null
|
||||
[IO.File]::WriteAllText($Path, $Text)
|
||||
}
|
||||
try {
|
||||
$bundle = Join-Path $work 'bundled_skins'
|
||||
$skins = Join-Path $work 'data\skins'
|
||||
$ids = @('same', 'changed', 'extra', 'missing', 'twice', 'absent')
|
||||
foreach ($id in $ids) {
|
||||
Write-Text (Join-Path $bundle "$id\skin.toml") "id = `"$id`"`n"
|
||||
Write-Text (Join-Path $bundle "$id\assets\bg.png") "$id png"
|
||||
}
|
||||
$manifest = foreach ($id in $ids) {
|
||||
foreach ($file in @('assets\bg.png', 'skin.toml')) {
|
||||
$hash = (Get-FileHash (Join-Path $bundle "$id\$file") -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
"$id|$file|$hash"
|
||||
}
|
||||
}
|
||||
$manifestPath = Join-Path $work 'bundled_skins.manifest'
|
||||
[IO.File]::WriteAllLines($manifestPath, [string[]]$manifest, [Text.UTF8Encoding]::new($false))
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $skins | Out-Null
|
||||
foreach ($id in @('same', 'changed', 'extra', 'missing', 'twice')) {
|
||||
Copy-Item (Join-Path $bundle $id) $skins -Recurse
|
||||
}
|
||||
Write-Text (Join-Path $skins 'changed\assets\bg.png') 'user edit'
|
||||
Write-Text (Join-Path $skins 'extra\assets\mine.png') 'user file'
|
||||
Remove-Item (Join-Path $skins 'missing\assets\bg.png')
|
||||
Write-Text (Join-Path $skins 'twice\skin.toml') 'user edit'
|
||||
Write-Text (Join-Path $skins 'twice.bak\skin.toml') 'earlier backup'
|
||||
|
||||
$iss = [IO.File]::ReadAllText((Join-Path $PSScriptRoot '../msime_setup.iss'))
|
||||
$functions = [regex]::Match($iss, '(?s)function CountFilesUnder\(.*?(?=\r?\nfunction PrepareToInstall\()').Value
|
||||
if (-not $functions) { throw 'msime_setup.iss is missing the bundled skin backup functions' }
|
||||
$resultPath = Join-Path $work 'result.txt'
|
||||
$probe = Join-Path $work 'probe.iss'
|
||||
@"
|
||||
[Setup]
|
||||
AppName=Bundled skin probe
|
||||
AppVersion=1.0
|
||||
CreateAppDir=no
|
||||
PrivilegesRequired=lowest
|
||||
Uninstallable=no
|
||||
OutputDir=$work
|
||||
OutputBaseFilename=bundled-skin-probe
|
||||
[Files]
|
||||
Source: "$manifestPath"; Flags: dontcopy
|
||||
[Code]
|
||||
function GetDataDir(Param: String): String;
|
||||
begin
|
||||
Result := '$(Join-Path $work 'data')';
|
||||
end;
|
||||
|
||||
$functions
|
||||
|
||||
function InitializeSetup: Boolean;
|
||||
var
|
||||
Problem: String;
|
||||
begin
|
||||
Problem := BackupChangedBundledSkins;
|
||||
if Problem = '' then
|
||||
SaveStringToFile('$resultPath', 'ok', False)
|
||||
else
|
||||
SaveStringToFile('$resultPath', 'error', False);
|
||||
Result := False;
|
||||
end;
|
||||
"@ | Set-Content -Path $probe -Encoding UTF8
|
||||
|
||||
& (Resolve-Iscc) /Q $probe
|
||||
if ($LASTEXITCODE -ne 0) { throw "ISCC failed to compile the probe (exit code $LASTEXITCODE)" }
|
||||
$process = Start-Process (Join-Path $work 'bundled-skin-probe.exe') -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES' -PassThru -Wait
|
||||
if (-not (Test-Path $resultPath)) { throw "Probe did not run (exit code $($process.ExitCode))" }
|
||||
if ([IO.File]::ReadAllText($resultPath) -ne 'ok') { throw 'BackupChangedBundledSkins reported an error' }
|
||||
|
||||
$actual = @(Get-ChildItem -LiteralPath $skins -Directory | ForEach-Object Name | Sort-Object)
|
||||
$expected = @('changed.bak', 'extra.bak', 'missing.bak', 'same', 'twice.2.bak', 'twice.bak' | Sort-Object)
|
||||
if (($actual -join ',') -cne ($expected -join ',')) {
|
||||
throw "Unexpected skins directory after backup: $($actual -join ', ')"
|
||||
}
|
||||
if ([IO.File]::ReadAllText((Join-Path $skins 'changed.bak\assets\bg.png')) -ne 'user edit') { throw 'changed.bak lost the user edit' }
|
||||
if (-not (Test-Path (Join-Path $skins 'extra.bak\assets\mine.png'))) { throw 'extra.bak lost the user file' }
|
||||
if ([IO.File]::ReadAllText((Join-Path $skins 'twice.bak\skin.toml')) -ne 'earlier backup') { throw 'An earlier backup was overwritten' }
|
||||
if ([IO.File]::ReadAllText((Join-Path $skins 'twice.2.bak\skin.toml')) -ne 'user edit') { throw 'twice.2.bak lost the user edit' }
|
||||
Write-Host 'Bundled skins: unchanged kept, changed/extra/missing backed up, earlier backups preserved'
|
||||
} finally {
|
||||
if (Test-Path $work) { Remove-Item $work -Recurse -Force }
|
||||
}
|
||||
@@ -47,6 +47,9 @@ try {
|
||||
'ui-html/webview2/settings/ime-settings/dist/index.html',
|
||||
'skins/default/fluent/skin.toml'
|
||||
)) { Write-Fixture $file }
|
||||
Write-Fixture 'skins/demo-skin/skin.toml' "schema_version = 1`nid = `"demo-skin`"`n"
|
||||
Write-Fixture 'skins/demo-skin/assets/background.png' 'png'
|
||||
Write-Fixture 'skins/unverified/skin.toml' "schema_version = 1`nid = `"unverified`"`n[license]`nassets = `"UNVERIFIED-DEMO-ONLY`"`n"
|
||||
Write-Fixture 'server/assets/tables/pinyin.txt' 'xing'
|
||||
Write-Fixture 'MetasequoiaImeDict/out/dictionary-manifest.json' '{"manifest_version":1}'
|
||||
$english = Join-Path $fixture 'MetasequoiaImeDict/out/english.db'
|
||||
@@ -72,6 +75,24 @@ try {
|
||||
if ($issText -notmatch '(?s)Source: "\{#MySourceRoot\}\\default_skins\\\*";[^\r\n]*\\\s*DestDir: "\{code:GetDataDir\}\\skins\\default";[^\r\n]*\\\s*Flags: onlyifdoesntexist') {
|
||||
throw 'Default skin settings must be installed to skins\default with onlyifdoesntexist'
|
||||
}
|
||||
# 仓库 skins\ 下的外部皮肤随包分发;素材授权未核实的不进包,skins\default 走上面那条。
|
||||
foreach ($file in @('bundled_skins/demo-skin/skin.toml', 'bundled_skins/demo-skin/assets/background.png')) {
|
||||
if (-not (Test-Path (Join-Path $installer $file))) { throw "Missing bundled skin file: $file" }
|
||||
}
|
||||
foreach ($excluded in @('bundled_skins/unverified', 'bundled_skins/default')) {
|
||||
if (Test-Path (Join-Path $installer $excluded)) { throw "Bundled a skin that must not ship: $excluded" }
|
||||
}
|
||||
$expectedManifest = @(
|
||||
"demo-skin|assets\background.png|$((Get-FileHash (Join-Path $fixture 'skins/demo-skin/assets/background.png') -Algorithm SHA256).Hash.ToLowerInvariant())",
|
||||
"demo-skin|skin.toml|$((Get-FileHash (Join-Path $fixture 'skins/demo-skin/skin.toml') -Algorithm SHA256).Hash.ToLowerInvariant())"
|
||||
)
|
||||
$actualManifest = @(Get-Content -LiteralPath (Join-Path $installer 'bundled_skins.manifest'))
|
||||
if (($actualManifest -join "`n") -cne ($expectedManifest -join "`n")) {
|
||||
throw "Unexpected bundled skin manifest:`n$($actualManifest -join "`n")"
|
||||
}
|
||||
if ($issText -notmatch '(?s)Source: "\{#MySourceRoot\}\\bundled_skins\\\*";[^\r\n]*\\\s*DestDir: "\{code:GetDataDir\}\\skins";[^\r\n]*\\\s*Flags: ignoreversion') {
|
||||
throw 'Bundled skins must overwrite skins\<id> in the data directory'
|
||||
}
|
||||
# 数据目录不整个归输入法时(旧版安装器把标记写进了用户原有的文件夹,#537),卸载只按
|
||||
# IsShippedAppDataItem 等名单删。包里新增了顶层条目而名单没跟上,卸载就会把它漏在用户目录里。
|
||||
$iss = [IO.File]::ReadAllText((Join-Path $installer 'msime_setup.iss'))
|
||||
|
||||
@@ -291,6 +291,17 @@ bool IsSafeId(const std::string &id)
|
||||
});
|
||||
}
|
||||
|
||||
bool IsBackupFolder(const std::string &folder)
|
||||
{
|
||||
constexpr std::string_view kSuffix = ".bak";
|
||||
if (folder.size() < kSuffix.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return std::equal(kSuffix.begin(), kSuffix.end(), folder.end() - kSuffix.size(),
|
||||
[](char a, char b) { return a == std::tolower(static_cast<unsigned char>(b)); });
|
||||
}
|
||||
|
||||
bool Supports(const Package &package, const std::string &layout, const std::string &theme)
|
||||
{
|
||||
return std::find(package.layouts.begin(), package.layouts.end(), layout) != package.layouts.end() &&
|
||||
@@ -493,7 +504,9 @@ ScanResult Scan(const std::filesystem::path &skinsRoot)
|
||||
continue;
|
||||
}
|
||||
const std::string folder = it->path().filename().u8string();
|
||||
if (folder == kDefaultSkinsFolder)
|
||||
// <id>.bak 是安装包覆盖随包皮肤前留下的旧版本:manifest 的 id 与目录名对不上,
|
||||
// 不跳过就会在设置页被列成一条损坏的皮肤。
|
||||
if (folder == kDefaultSkinsFolder || IsBackupFolder(folder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ inline constexpr bool kDefaultPageArrows = false;
|
||||
const std::vector<std::string> &BuiltInIds();
|
||||
bool IsBuiltIn(const std::string &id);
|
||||
bool IsSafeId(const std::string &id);
|
||||
// 安装包覆盖同名随包皮肤前,把旧目录改名成 <id>.bak(重名时 <id>.2.bak …);Scan 跳过它们。
|
||||
bool IsBackupFolder(const std::string &folder);
|
||||
bool Supports(const Package &package, const std::string &layout, const std::string &theme);
|
||||
std::optional<Package> Load(const std::filesystem::path &skinsRoot, const std::string &id,
|
||||
std::string *error = nullptr);
|
||||
|
||||
@@ -130,3 +130,24 @@ TEST_CASE(candidate_skin_scan_skips_default_settings_folder)
|
||||
REQUIRE(!CandidateSkinCatalog::Load(root, "default").has_value());
|
||||
Cleanup(root);
|
||||
}
|
||||
|
||||
// The installer renames a bundled skin the user already had to <id>.bak (or <id>.2.bak) before overwriting it.
|
||||
// Those backups keep the original manifest id, so without skipping them the settings page would list each one as
|
||||
// a broken skin.
|
||||
TEST_CASE(candidate_skin_scan_skips_installer_backups)
|
||||
{
|
||||
const auto root = MakeSkinsRoot(L"backup");
|
||||
WriteExternalSkin(root, "ext", "fluent", "");
|
||||
fs::copy(root / L"ext", root / L"ext.bak", fs::copy_options::recursive);
|
||||
fs::copy(root / L"ext", root / L"ext.2.bak", fs::copy_options::recursive);
|
||||
fs::copy(root / L"ext", root / L"ext.3.BAK", fs::copy_options::recursive);
|
||||
|
||||
const auto scan = CandidateSkinCatalog::Scan(root);
|
||||
REQUIRE(scan.issues.empty());
|
||||
REQUIRE(scan.packages.size() == 1);
|
||||
REQUIRE(scan.packages[0].id == "ext");
|
||||
REQUIRE(CandidateSkinCatalog::IsBackupFolder("ext.bak"));
|
||||
REQUIRE(!CandidateSkinCatalog::IsBackupFolder("bak"));
|
||||
REQUIRE(!CandidateSkinCatalog::IsBackupFolder("ext.backup"));
|
||||
Cleanup(root);
|
||||
}
|
||||
+3
-1
@@ -31,7 +31,9 @@
|
||||
|
||||
## 安装
|
||||
|
||||
把想用的皮肤目录整个复制到:
|
||||
除 `niya-demo`(素材授权未核实)外,这里的皮肤都随安装包装到数据目录的 `skins\<id>`。用户那边已有同名皮肤且内容和随包的不一样(改过、多了或少了文件)时,安装器先把它改名成 `<id>.bak` 保留下来(已有 `.bak` 则用 `<id>.2.bak`……),再装新版本;输入法不会把 `.bak` 目录列成皮肤。
|
||||
|
||||
手动安装时,把想用的皮肤目录整个复制到:
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\metasequoiaime\skins\
|
||||
|
||||
Reference in new issue
Block a user