Merge pull request #1103 from Tokisaki-Galaxy/ci/dev-build-publish
ci: add the dev-build workflow for toolchain-free contributor builds
This commit is contained in:
12 files changed
+821
-16
No files matched your search
@@ -0,0 +1,380 @@
|
||||
name: Dev build
|
||||
|
||||
# Development builds for contributors. The point is that a newcomer does not install vcpkg,
|
||||
# Boost, Inno Setup and the Windows SDK to try a change; the runner builds, packages and signs,
|
||||
# and the artifact is a full installer with a fresh self-signed certificate plus the public key
|
||||
# to trust it.
|
||||
#
|
||||
# Three ways in. A feature-branch push builds automatically, so the artifact is ready when the
|
||||
# push finishes. A pull_request run gives reviewers and fork contributors an installer for real
|
||||
# device testing -- it uses pull_request, not pull_request_target, deliberately: building the
|
||||
# PR's code runs scripts from that code, and pull_request_target would hand those scripts a
|
||||
# writable token. workflow_dispatch stays for picking a single component or rebuilding without
|
||||
# a push. develop and main never trigger this; they have ci.yml and release.yml.
|
||||
#
|
||||
# This is a test channel, not a release: the version always carries -dev.<date>.<hash>, the
|
||||
# certificate is minted per run, and nothing here creates a release or touches the update
|
||||
# manifest. ci.yml remains the merge gate; this workflow deliberately skips the suite the CI
|
||||
# already runs and only builds what an installable artifact needs.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
component:
|
||||
description: Which component to build (all = full signed installer)
|
||||
required: true
|
||||
type: choice
|
||||
default: all
|
||||
options:
|
||||
- all
|
||||
- tip
|
||||
- server
|
||||
push:
|
||||
branches-ignore:
|
||||
- develop
|
||||
- main
|
||||
- 'release-please--*'
|
||||
# The Windows 7 port lives in its own repository with dev-build-win7.yml; branches named for
|
||||
# it are pushed here from that work and would only rebuild an artifact nobody asked for.
|
||||
- 'win7*'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# push and pull_request have no inputs; both build everything, since those events exist to
|
||||
# produce an installable artifact and a component filter nobody can set would only mislead.
|
||||
|
||||
# Artifacts carry a working installer plus its signing certificate. The default 90-day retention
|
||||
# would pile up hundreds of megabytes per push on an active branch, so every upload below passes
|
||||
# retention-days: 7 -- enough to notice a build and install it, and the next push replaces it.
|
||||
env:
|
||||
DEV_ARTIFACT_RETENTION: 7
|
||||
|
||||
# Same ref, newer run wins; an older run keeps burning a runner for nothing.
|
||||
concurrency:
|
||||
group: dev-build-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tip:
|
||||
name: TSF ${{ matrix.name }}
|
||||
if: ${{ (inputs.component || 'all') == 'all' || (inputs.component || 'all') == 'tip' }}
|
||||
runs-on: windows-2025
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: x64
|
||||
architecture: x64
|
||||
triplet: x64-windows-static
|
||||
build_dir: windows/build64-release
|
||||
- name: Win32
|
||||
architecture: Win32
|
||||
triplet: x86-windows-static
|
||||
build_dir: windows/build32-release
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Stamp the dev version into the resource
|
||||
shell: pwsh
|
||||
run: |
|
||||
# YYMMDD in the fourth VERSIONINFO slot: the quad must stay four 16-bit fields, and a
|
||||
# date-compressed value sorts a dev DLL above its own base and below the next release.
|
||||
$base = python scripts/dev-build-version.py --base
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$date = python scripts/dev-build-version.py --date
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
python scripts/apply_version.py --version "$base.$date"
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
- name: Restore dependency binaries
|
||||
id: vcpkg-cache
|
||||
uses: ./.github/actions/vcpkg-cache
|
||||
with:
|
||||
scope: tip-${{ matrix.triplet }}
|
||||
- name: Install dependencies
|
||||
shell: pwsh
|
||||
working-directory: windows
|
||||
run: vcpkg install --triplet ${{ matrix.triplet }}
|
||||
- name: Save dependency binaries before compiling
|
||||
if: steps.vcpkg-cache.outputs.matched-key != format('{0}-complete', steps.vcpkg-cache.outputs.key)
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.vcpkg-cache.outputs.path }}
|
||||
key: ${{ steps.vcpkg-cache.outputs.key }}-complete
|
||||
- name: Configure
|
||||
shell: pwsh
|
||||
run: >
|
||||
cmake -S windows -B ${{ matrix.build_dir }} -A ${{ matrix.architecture }}
|
||||
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||
-DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }}
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: cmake --build ${{ matrix.build_dir }} --config Release --parallel
|
||||
- name: Check the Release binary and symbols
|
||||
shell: pwsh
|
||||
run: ./scripts/ci/check-tsf-dll.ps1 -BuildDir '${{ matrix.build_dir }}'
|
||||
- name: Upload the TSF binary and symbols
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dev-tsf-${{ matrix.name }}
|
||||
retention-days: ${{ env.DEV_ARTIFACT_RETENTION }}
|
||||
# Same layout convention as the release workflow: download-artifact restores the
|
||||
# directory contents where Prepare-PackageFiles.ps1 expects them.
|
||||
path: ${{ matrix.build_dir }}/Release
|
||||
if-no-files-found: error
|
||||
|
||||
server:
|
||||
name: Server
|
||||
if: ${{ (inputs.component || 'all') == 'all' || (inputs.component || 'all') == 'server' }}
|
||||
runs-on: windows-2025
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Stamp the dev version into the resource
|
||||
# The TSF DLL carries the product's VERSIONINFO even in a server-only build, because the
|
||||
# resource is stamped before configure; keep every build of this run on one version.
|
||||
shell: pwsh
|
||||
run: |
|
||||
$base = python scripts/dev-build-version.py --base
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$date = python scripts/dev-build-version.py --date
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
python scripts/apply_version.py --version "$base.$date"
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
- name: Restore dependency binaries
|
||||
id: vcpkg-cache
|
||||
uses: ./.github/actions/vcpkg-cache
|
||||
with:
|
||||
scope: server-x64-windows-and-static-md
|
||||
- name: Set up MSVC environment
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
|
||||
- name: Set up sccache
|
||||
id: sccache
|
||||
continue-on-error: true
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
- name: Install Boost
|
||||
shell: pwsh
|
||||
run: ./scripts/ci/install-boost.ps1
|
||||
- name: Save Boost binaries before configuring
|
||||
if: steps.vcpkg-cache.outputs.matched-key != format('{0}-complete', steps.vcpkg-cache.outputs.key) && steps.vcpkg-cache.outputs.matched-key != format('{0}-boost', steps.vcpkg-cache.outputs.key)
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.vcpkg-cache.outputs.path }}
|
||||
key: ${{ steps.vcpkg-cache.outputs.key }}-boost
|
||||
# Ninja Multi-Config so sccache plugs in through CMAKE_<LANG>_COMPILER_LAUNCHER, mirroring
|
||||
# ci.yml's Server job; the bin/Release layout is what every later step reads. The override
|
||||
# embeds the full dev version string into the native settings page; server/CMakeLists.txt
|
||||
# accepts a semver prerelease suffix, and the about page's ParseVersion strips it for the
|
||||
# update check while displaying it verbatim.
|
||||
- name: Configure
|
||||
shell: pwsh
|
||||
run: |
|
||||
$display = python scripts/dev-build-version.py --display
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$cmakeArgs = @(
|
||||
'-S', 'server', '-B', 'server/build-release', '-G', 'Ninja Multi-Config',
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake",
|
||||
'-DVCPKG_TARGET_TRIPLET=x64-windows',
|
||||
"-DMSIME_PRODUCT_VERSION_OVERRIDE=$display"
|
||||
)
|
||||
$cmakeArgs += @(./scripts/ci/cmake-sccache-args.ps1 -SetupOutcome '${{ steps.sccache.outcome }}')
|
||||
cmake @cmakeArgs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
- name: Save dependency binaries before compiling
|
||||
if: steps.vcpkg-cache.outputs.matched-key != format('{0}-complete', steps.vcpkg-cache.outputs.key)
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.vcpkg-cache.outputs.path }}
|
||||
key: ${{ steps.vcpkg-cache.outputs.key }}-complete
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: cmake --build server/build-release --config Release --parallel
|
||||
- name: Check the Release binaries and symbols
|
||||
shell: pwsh
|
||||
run: ./scripts/ci/check-server-binaries.ps1 -BuildDir server/build-release/bin/Release
|
||||
- name: Test with the product's exact data
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./scripts/ci/test-server.ps1 -ServerRoot server -StagingRoot "$env:RUNNER_TEMP/product-data" -BuildDir build-release
|
||||
# uiAccess=true is ignored without a signature, so the manifest goes in before upload and
|
||||
# the whole payload gets signed in the package job.
|
||||
- name: Embed the Server uiAccess manifest
|
||||
shell: pwsh
|
||||
run: ./scripts/ci/embed-server-manifest.ps1 -BuildDir server/build-release/bin/Release
|
||||
# component=server uploads this directory as an installable artifact with no package job to
|
||||
# sign it. Windows ignores uiAccess=true without a trusted signature, so the binary would
|
||||
# install and then silently lose the privilege. component=all leaves it unsigned on purpose:
|
||||
# Prepare-PackageFiles.ps1 copies these files into the payload and sign-dev-build.ps1 signs
|
||||
# the copies, so signing here would only add a second signing pass over the same bytes.
|
||||
- name: Sign the Server build for the standalone artifact
|
||||
if: ${{ (inputs.component || 'all') == 'server' }}
|
||||
shell: pwsh
|
||||
env:
|
||||
DEV_CERT_PATH: ${{ runner.temp }}/dev-server-certificate.cer
|
||||
run: |
|
||||
# -Include 只有在 -Path 带通配符或同时给 -Recurse 时才生效,这里显式计算再交给参数,
|
||||
# 免得把一条空列表传进脚本后它才在内部报错。
|
||||
$targets = @(
|
||||
Get-ChildItem -LiteralPath 'server/build-release/bin/Release' -Recurse -File |
|
||||
Where-Object { $_.Extension -in @('.exe', '.dll') } |
|
||||
ForEach-Object FullName
|
||||
)
|
||||
if ($targets.Count -eq 0) { throw 'No EXE or DLL found to sign under server/build-release/bin/Release.' }
|
||||
./installer/sign-dev-build.ps1 -FilePath $targets
|
||||
- name: Upload the Server build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dev-server
|
||||
retention-days: ${{ env.DEV_ARTIFACT_RETENTION }}
|
||||
path: server/build-release/bin/Release
|
||||
if-no-files-found: error
|
||||
# The certificate has to travel with the standalone Server artifact too, or nobody can import
|
||||
# the trust that the signature above depends on.
|
||||
- name: Upload the signing certificate
|
||||
if: ${{ (inputs.component || 'all') == 'server' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dev-server-certificate
|
||||
retention-days: ${{ env.DEV_ARTIFACT_RETENTION }}
|
||||
path: ${{ runner.temp }}/dev-server-certificate.cer
|
||||
if-no-files-found: error
|
||||
|
||||
package:
|
||||
name: Package installer
|
||||
needs: [tip, server]
|
||||
# Only component=all produces an installer: packaging needs both TSF architectures and the
|
||||
# Server, so there is no meaningful way to package a single-component run.
|
||||
if: ${{ !cancelled() && needs.tip.result == 'success' && needs.server.result == 'success' && (inputs.component || 'all') == 'all' }}
|
||||
runs-on: windows-2025
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Read the dev version
|
||||
id: version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$display = python scripts/dev-build-version.py --display
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
"display=$display" >> $env:GITHUB_OUTPUT
|
||||
"installer=installer/Output/MetasequoiaIME_Setup_v$display.exe" >> $env:GITHUB_OUTPUT
|
||||
- name: Download the x64 TSF binary and symbols
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: dev-tsf-x64
|
||||
path: windows/build64-release/Release
|
||||
- name: Download the 32-bit TSF binary and symbols
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: dev-tsf-Win32
|
||||
path: windows/build32-release/Release
|
||||
- name: Download the Server build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: dev-server
|
||||
path: server/build-release/bin/Release
|
||||
- name: Download the dictionaries
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
STAGING_ROOT: .
|
||||
run: bash scripts/ci/download-dictionaries.sh
|
||||
- name: Build the word-lattice language model
|
||||
shell: pwsh
|
||||
run: ./scripts/build-language-model.ps1
|
||||
- name: Download the neural sentence models
|
||||
shell: pwsh
|
||||
run: ./scripts/fetch-neural-model.ps1
|
||||
- name: Collect the installer payload
|
||||
shell: pwsh
|
||||
working-directory: installer
|
||||
env:
|
||||
TARGET_VERSION: ${{ steps.version.outputs.display }}
|
||||
run: >
|
||||
pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion $env:TARGET_VERSION
|
||||
-RepoRoot (Resolve-Path '..').Path
|
||||
-TsfDirectory windows -ServerDirectory server -NoticesDirectory .
|
||||
-HelpCodeDirectory engine/helpcode
|
||||
- name: Sign the payload binaries
|
||||
shell: pwsh
|
||||
working-directory: installer
|
||||
env:
|
||||
DEV_CERT_PATH: ${{ runner.temp }}/dev-build.cer
|
||||
run: ./sign-dev-build.ps1
|
||||
- name: Install the Chinese language file for Inno Setup
|
||||
shell: pwsh
|
||||
run: ./scripts/ci/install-inno-language.ps1
|
||||
- name: Compile the installer
|
||||
shell: pwsh
|
||||
working-directory: installer
|
||||
run: pwsh -File ./Compile-Installer.ps1
|
||||
- name: Sign the installer
|
||||
shell: pwsh
|
||||
working-directory: installer
|
||||
env:
|
||||
DEV_CERT_PATH: ${{ runner.temp }}/dev-build.cer
|
||||
run: ./sign-dev-build.ps1 -Installer
|
||||
- name: Name the artifact and record its checksum
|
||||
id: asset
|
||||
shell: pwsh
|
||||
working-directory: installer
|
||||
env:
|
||||
TARGET_VERSION: ${{ steps.version.outputs.display }}
|
||||
run: |
|
||||
$built = "Output/MetasequoiaIME_Setup_v$env:TARGET_VERSION.exe"
|
||||
if (-not (Test-Path $built)) { throw "Inno Setup did not produce $built" }
|
||||
$hash = (Get-FileHash $built -Algorithm SHA256).Hash.ToLower()
|
||||
$size = [math]::Round((Get-Item $built).Length / 1MB, 1)
|
||||
Write-Host "$built ($size MB, sha256 $hash)"
|
||||
"installer_path=$((Resolve-Path $built).Path)" >> $env:GITHUB_OUTPUT
|
||||
"installer_sha256=$hash" >> $env:GITHUB_OUTPUT
|
||||
- name: Upload the installer and the signing certificate
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dev-installer
|
||||
retention-days: ${{ env.DEV_ARTIFACT_RETENTION }}
|
||||
path: |
|
||||
${{ steps.asset.outputs.installer_path }}
|
||||
${{ runner.temp }}/dev-build.cer
|
||||
installer/import-dev-trust.ps1
|
||||
if-no-files-found: error
|
||||
- name: Summarise the build
|
||||
shell: pwsh
|
||||
env:
|
||||
TARGET_VERSION: ${{ steps.version.outputs.display }}
|
||||
INSTALLER_SHA256: ${{ steps.asset.outputs.installer_sha256 }}
|
||||
run: |
|
||||
@(
|
||||
'### Dev build'
|
||||
''
|
||||
"| Field | Value |"
|
||||
"| --- | --- |"
|
||||
"| Version | $env:TARGET_VERSION |"
|
||||
"| Installer SHA256 | ``$env:INSTALLER_SHA256`` |"
|
||||
''
|
||||
'To install: download the artifact, unzip, then in an **admin** PowerShell run'
|
||||
'`import-dev-trust.ps1` and launch the installer. Re-run the trust import for every'
|
||||
'new build; the certificate is minted per run and the script replaces the old one.'
|
||||
) >> $env:GITHUB_STEP_SUMMARY
|
||||
@@ -39,6 +39,8 @@ Server 当前仍使用引擎的兼容 `InputSession`,尚未迁移公共 `Sessi
|
||||
|
||||
## 构建
|
||||
|
||||
**用户想编译验证但没装工具链时,优先用 dev-build workflow 而不是让用户装 vcpkg**:特性分支 push 自动触发,PR 会自动跑一次,或 `gh workflow run dev-build.yml --ref <分支> -f component=all` 手动触发,然后 `gh run watch` 等它,`gh run download <runId> -n dev-installer` 拿安装包与证书,安装步骤见 [CONTRIBUTING.md](CONTRIBUTING.md#没有本地工具链时dev-build-workflow)。develop 和 main 不触发;版本号带 `-dev.<日期>.<commit>`,与正式发布隔离。
|
||||
|
||||
每个组件是独立的 CMake 工程,各自带 `vcpkg.json` 和 preset,从仓库根目录指定 `-S`:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -15,6 +15,27 @@
|
||||
|
||||
Boost 按 `-DBoost_ROOT=...` → 环境变量 `BOOST_ROOT` → `%USERPROFILE%\scoop\apps\boost\current` 的顺序解析。preset 里的 vcpkg 路径取自 `$env{VCPKG_ROOT}`,不需要改文件。
|
||||
|
||||
### 没有本地工具链时:dev-build workflow
|
||||
|
||||
只想验证一个小改动、不想为它装一整套工具链的话,让 CI 编。`Dev build` workflow 平时不跑主分支(develop 和 main 有自己的 CI 和发布流),三种触发方式:
|
||||
|
||||
```powershell
|
||||
# 特性分支一推就自动编,不需要任何手动操作
|
||||
git push -u origin fix/xxx
|
||||
|
||||
# 或者手动触发,可选单个组件快速迭代
|
||||
gh workflow run dev-build.yml --ref <你的分支> -f component=all
|
||||
|
||||
# 等 run 结束拿产物;agent 也可以在这里等
|
||||
gh run list --workflow=dev-build.yml --branch <你的分支> --limit 1 --json databaseId,status
|
||||
gh run watch <runId> --exit-status
|
||||
gh run download <runId> -n dev-installer
|
||||
```
|
||||
|
||||
开向 develop 的 PR 也会自动跑一次,评审者和 fork 贡献者都能从 PR 的检查页拿到安装包。安装只有两步:管理员 PowerShell 里运行解压出来的 `import-dev-trust.ps1`(导入本次构建的签名证书并清掉上一张),然后双击安装包。版本号带 `-dev.<日期>.<commit>`,一眼可辨,设置页的更新检查不会提示它升级。每次新构建的证书都不同,重装前重跑一次导入脚本即可;产物保留 7 天。
|
||||
|
||||
几个边界:`workflow_dispatch` 需要仓库写权限;fork PR 的构建用 `pull_request` 触发(只读 token,无 secrets),首个 run 要维护者点一次批准;`component=tip` 或 `server` 只编译单个组件出裸二进制;这个 workflow 不出 release、不碰更新清单,合并门槛仍然由 PR 上的 CI 把守。
|
||||
|
||||
## 取代码
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -347,6 +347,17 @@ $updatedIss = [regex]::Replace(
|
||||
'(?m)^#define\s+MyAppVersion\s+"[^"]+"\s*$',
|
||||
"#define MyAppVersion `"$TargetVersion`""
|
||||
)
|
||||
# VersionInfoVersion 只接受数字和点号;从目标版本剥掉 -后缀(dev 构建的
|
||||
# 0.7.1-dev.20261019.<hash> → 0.7.1),正式版本剥了也原样。
|
||||
$versionInfo = ($TargetVersion -replace '-.*$', '')
|
||||
if ($versionInfo -notmatch '^[0-9]+(\.[0-9]+)*$') {
|
||||
throw "目标版本剥掉后缀后不是数字版本号,无法写进 VersionInfoVersion:$TargetVersion"
|
||||
}
|
||||
$updatedIss = [regex]::Replace(
|
||||
$updatedIss,
|
||||
'(?m)^#define\s+MyVersionInfo\s+"[^"]+"\s*$',
|
||||
"#define MyVersionInfo `"$versionInfo`""
|
||||
)
|
||||
# .gitattributes pins this repo to eol=lf; emit LF so packaging does not leave the file as a
|
||||
# CRLF-only diff that git flags as modified while `git diff` shows no textual change.
|
||||
$updatedIss = $updatedIss.TrimEnd("`r", "`n") + "`n"
|
||||
|
||||
+13
-2
@@ -1,8 +1,9 @@
|
||||
# Metasequoia IME Installer
|
||||
|
||||
本目录的脚本从 Windows 合仓目录收集产物、签名、用 Inno Setup 打成安装包。它服务两条流程:
|
||||
本目录的脚本从 Windows 合仓目录收集产物、签名、用 Inno Setup 打成安装包。它服务三条流程:
|
||||
|
||||
- **正式发布**:由 `MSIME-Windows` 的 release workflow 驱动,用真实证书签名包内全部 EXE/DLL 和最终安装包;产物是挂在 Release 上的 `MetasequoiaIME_Setup_v<版本>.exe`,并携带与 Release 二进制匹配的 PDB 符号文件。见下面「CI 契约」
|
||||
- **开发构建**:由 `dev-build.yml` workflow 驱动,runner 上现场生成自签名证书并签名,产物随 artifact 分发。见下面「Dev build」
|
||||
- **本地测试**:手工跑,用本机自签名证书,用来在自己机器上验证安装流程。本文其余部分讲的是这条
|
||||
|
||||
版本号不是固定的,由 `Prepare-PackageFiles.ps1` 的 `-TargetVersion` 决定,默认 `0.0.1`;正式发布时 CI 传入真实版本。
|
||||
@@ -11,6 +12,16 @@
|
||||
|
||||
签名脚本会在你自己的机器上生成并复用一张自签名测试证书。
|
||||
|
||||
## Dev build
|
||||
|
||||
`dev-build.yml` workflow 给没有本地工具链的贡献者出可安装的包。触发见 [CONTRIBUTING.md](../CONTRIBUTING.md#没有本地工具链时dev-build-workflow)。签名证书由 `sign-dev-build.ps1` 在 runner 上现场生成,subject 固定为 `CN=Metasequoia IME Dev Build Code Signing`,私钥只在 runner 内存里存在一次构建;公钥 `.cer` 随 artifact 分发,用户用 `import-dev-trust.ps1` 导入本机受信任存储(按 subject 清旧导新,重复构建不会膨胀存储)。
|
||||
|
||||
- `sign-dev-build.ps1` 复用同一次运行早先创建的证书,所以包内二进制和安装包总在同一签名下;复用的依据是本次运行写在 `RUNNER_TEMP` 里的指纹标记,不按 subject 猜,机器上残留的旧证书不会被误用。`DEV_CERT_PATH` 指出 `.cer` 的导出位置
|
||||
- 证书有效期 90 天:artifact 只留 7 天,但用户可能下载到本地后放很久才装,而安装器校验的是签名时刻证书是否有效;`import-dev-trust.ps1` 会打印这张 `.cer` 自带的到期时间,发现已过期就直接退出,不往受信任存储里写
|
||||
- `import-dev-trust.ps1` 也随 artifact 分发,用户解压后在管理员 PowerShell 里直接跑
|
||||
- 版本号带 `-dev.<日期>.<commit>`,由 `scripts/dev-build-version.py` 从 `version.txt` 派生;设置页的更新检查会剥掉 `-dev` 后缀做数字比较,dev 包不会提示升级
|
||||
- 不要把这两个脚本接进 release 路径:正式发布的签名在 `scripts/ci/sign-binaries.ps1` 或 SignPath,两套互不相干
|
||||
|
||||
## CI 契约
|
||||
|
||||
根 `.github/workflows/release.yml` **不加修改地调用本目录的 `Prepare-PackageFiles.ps1` 和 `Compile-Installer.ps1`**。源目录名是 `Prepare-PackageFiles.ps1` 的参数,不是写死在脚本里的字面量:默认值为 `windows/`、`server/` 和仓根授权文件;旧目录或自定义布局通过参数覆盖。回归检查同时执行默认路径和显式参数的打包。辅助码默认读取 `engine/helpcode/`,词库缓存位于 `MetasequoiaImeDict/out/`。
|
||||
@@ -71,7 +82,7 @@ Windows CI 运行 `installer/tests/cloud-choice.ps1`,用 Inno Setup 跑八个
|
||||
安装包不携带 VC 运行库,它是系统级共享组件。缺了它 Server 起不来,而这个故障出现在安装结束之后,用户看不到任何解释。所以 `msime_setup.iss` 的 `InitializeSetup` 在安装开始前先查一遍,缺了就当场说清楚,并给出下载地址。界面全部由 `msimeui` 以 Direct2D 绘制,不再依赖 WebView2 Runtime;旧版本留下的 WebView2 用户数据(`ProgramData\metasequoiaime\webview2`、`webview2-settings`,以及 `%LOCALAPPDATA%\metasequoiaime` 下的同名回退目录)由安装器在升级和卸载时清掉。
|
||||
|
||||
- **Visual C++ 2015–2022 可再发行组件包(x64)**:读 `Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64` 的 `Installed` / `Major` / `Minor`,`HKLM32` 和 `HKLM64` 两个视图都查。要求 **14.40 以上**(常量 `VCRuntimeMinMinor`):`server_exe\` 里的 exe 用 MSVC 14.40+(VS 2022 17.10 起)编译并导入 `msvcp140.dll`,这一版起 `std::mutex` 的构造函数是 constexpr,配上更旧的 `msvcp140.dll` 会在第一次加锁时访问冲突、无声闪退——设置页「点了没反应」就是这个现象。装了 2019 / 早期 2022 运行库的机器同样有 `Installed=1`,所以必须比版本号。构建工具链升级到要求更高 redist 时同步调高这个常量。32 位 TSF DLL 用静态 CRT(`windows/CMakeLists.txt` 的 `CMAKE_MSVC_RUNTIME_LIBRARY`),所以不检查 x86 redist
|
||||
- **不要改成查 `System32` 里的 DLL。** Setup.exe 是 32 位进程,Pascal Script 的 `FileExists` 会被 WOW64 重定向到 `SysWOW64`,查到的是 x86 运行库;只装了 x64 redist 的机器会被误判成缺少 x64 运行库。绕开重定向的办法不可移植:`EnableFsRedirection` 在 Inno Setup 7 里已被移除,而它的替代品在 CI 用的 6.x 里还不存在
|
||||
- **不要改成查 `System32` 里的 DLL,也不要在 Pascal Script 里用 `ApplyPathRedirRulesForCurrentProcess`。** 那是 Inno Setup 7 的 support function,CI 的 6.7.1 没有它,ISCC 会报 `Unknown identifier`(#925 引入后第一次在 hosted runner 上编安装包就炸了,#55e1127a 改回直接展开 `{sys}`)。Setup 在 x64/ARM64 上是 64 位进程,`{sys}` 本来就不经重定向;需要 32 位视图时用 `{syswow64}`
|
||||
- 缺组件或版本过旧时弹一次对话框,区分「没装」和「已装 x.y 但过旧」,列出直接下载链接(`https://aka.ms/vc14/vc_redist.x64.exe`)和说明页:选「是」直接下载安装包并结束安装,选「否」继续装。**不阻断安装**——用户可能马上就要去补组件,也可能只是在准备环境。静默安装(CI、批量部署)用 `SuppressibleMsgBox` 默认「否」直接继续,缺什么已经写进安装日志(`/LOG=`)
|
||||
|
||||
## 依赖
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# 信任一张 dev-build 签名证书并清理旧证书。
|
||||
#
|
||||
# CI 的 dev-build workflow 每次运行现场生成一张自签名代码签名证书,签完整包后把公钥
|
||||
# (.cer)放进 artifact。指纹每次都变,但 subject 固定,所以本脚本先把本地受信任存储里
|
||||
# 旧 subject 的证书清掉,再导入新证书,测试机的受信任根不会随构建次数膨胀。
|
||||
#
|
||||
# 用管理员 PowerShell 运行(写 LocalMachine 存储需要提升):
|
||||
# pwsh -File .\import-dev-trust.ps1 [-CertificatePath .\dev-build.cer] [-Keep]
|
||||
#
|
||||
# 导入完成就可以安装同目录下的 MetasequoiaIME_Setup_v*-dev.*.exe。
|
||||
# -Keep 保留旧证书不清理(排查签名问题时用)。
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# 默认取脚本同目录下唯一的 .cer;有多个时报错让用户指定,避免导错文件。
|
||||
[string]$CertificatePath,
|
||||
[switch]$Keep
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$Subject = 'CN=Metasequoia IME Dev Build Code Signing'
|
||||
$TrustStores = @('Cert:\LocalMachine\Root', 'Cert:\LocalMachine\TrustedPublisher')
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [System.Security.Principal.WindowsPrincipal]::new($identity)
|
||||
return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
if (-not $CertificatePath) {
|
||||
$candidates = @(
|
||||
Get-ChildItem -LiteralPath $PSScriptRoot -Filter '*.cer' -File -ErrorAction SilentlyContinue
|
||||
)
|
||||
if ($candidates.Count -eq 0) {
|
||||
throw "当前目录没有 .cer 文件。请把 dev-build artifact 解压后在本目录运行:$PSScriptRoot"
|
||||
}
|
||||
if ($candidates.Count -gt 1) {
|
||||
throw "发现多个 .cer 文件,请用 -CertificatePath 指定一个:$($candidates.Name -join ', ')"
|
||||
}
|
||||
$CertificatePath = $candidates[0].FullName
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $CertificatePath -PathType Leaf)) {
|
||||
throw "证书文件不存在:$CertificatePath"
|
||||
}
|
||||
|
||||
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertificatePath)
|
||||
if ($certificate.Subject -ne $Subject) {
|
||||
throw "这不是 dev-build 签名证书(subject:$($certificate.Subject))。预期:$Subject"
|
||||
}
|
||||
|
||||
# 到期时间读的是这张 .cer 自带的字段,不靠「构建过了几天」去推。签名在证书有效期内完成才能
|
||||
# 通过校验,所以证书过期后这份包就装不上了,导入得再干净也没用,必须换新构建。
|
||||
# 这一步必须排在写受信任存储之前:过期证书导入进去只会让本机多一条无用的信任记录。
|
||||
if ($certificate.NotAfter -le (Get-Date)) {
|
||||
Write-Warning "这张证书已经过期($($certificate.NotAfter.ToString('yyyy-MM-dd HH:mm'))),包内签名无法通过校验。"
|
||||
Write-Warning '请重新下载最新的 dev build,用它的证书和安装包;不要继续安装当前这份。'
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 权限检查放在体检之后:先让没提升权限的用户知道包本身已经过期,再告诉他去开管理员。
|
||||
if (-not (Test-Administrator)) {
|
||||
throw '需要管理员 PowerShell:写入 LocalMachine 受信任存储必须提升权限。'
|
||||
}
|
||||
|
||||
# 按固定 subject 清理所有旧证书,再导入新的一张。跳过与本次导入指纹相同的那份。
|
||||
if (-not $Keep) {
|
||||
foreach ($store in $TrustStores) {
|
||||
$stale = @(Get-ChildItem -Path $store -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Subject -eq $Subject -and $_.Thumbprint -ne $certificate.Thumbprint })
|
||||
foreach ($old in $stale) {
|
||||
Remove-Item -LiteralPath ("{0}\{1}" -f $store, $old.Thumbprint) -Force
|
||||
Write-Host "已移除旧 dev 证书:$($old.Thumbprint)($store)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$temporaryFile = Join-Path ([IO.Path]::GetTempPath()) ('msime-dev-trust-' + [Guid]::NewGuid() + '.cer')
|
||||
try {
|
||||
[IO.File]::Copy($CertificatePath, $temporaryFile, $true)
|
||||
foreach ($store in $TrustStores) {
|
||||
Import-Certificate -FilePath $temporaryFile -CertStoreLocation $store | Out-Null
|
||||
Write-Host "已信任:$store"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "Dev 证书已就绪:$($certificate.Subject)"
|
||||
Write-Host "指纹:$($certificate.Thumbprint)(本次构建专有,换新 build 需重跑本脚本)"
|
||||
Write-Host "有效期至:$($certificate.NotAfter.ToString('yyyy-MM-dd HH:mm'))(本机时间)"
|
||||
Write-Host '现在可以安装同目录下的 dev 安装包了。'
|
||||
@@ -20,6 +20,10 @@
|
||||
|
||||
#define MyAppName "Metasequoia IME 水杉输入法"
|
||||
#define MyAppVersion "0.0.1"
|
||||
; VersionInfoVersion 是 PE 的 VERSIONINFO,只接受数字和点号;dev 构建的 MyAppVersion 带
|
||||
; -dev.<日期>.<hash> 后缀(Prepare-PackageFiles.ps1 会把数字部分同步写进这个 define),
|
||||
; 文件名与 AppVersion 用完整串,这里只用数字段。
|
||||
#define MyVersionInfo "0.0.1"
|
||||
#define MyAppPublisher "Metasequoia"
|
||||
#define MyAppExeName "MetasequoiaImeServer.exe"
|
||||
#define MySettingsExeName "MetasequoiaImeSettings.exe"
|
||||
@@ -36,7 +40,11 @@
|
||||
#define MySourceRoot "."
|
||||
; TSF DLL 装在系统目录下:CS2 等开启 Trusted Mode 的游戏只放行「位于系统目录且有代码签名」的
|
||||
; 第三方 in-proc DLL,装在 Program Files 的 DLL 连加载都会被拒,输入法在游戏里根本切不出来。
|
||||
; 本安装器是 32 位进程,[Code] 里访问 64 位那份要先过 ApplyPathRedirRulesForCurrentProcess。
|
||||
; 本安装器在 x64/ARM64 上以 64 位模式运行(ArchitecturesInstallIn64BitMode),
|
||||
; Pascal Script 里访问 {sys} 就是真正的 System32,没有 WOW64 重定向可绕。
|
||||
; 不要在这里用 ApplyPathRedirRulesForCurrentProcess:那是 Inno Setup 7 的 support
|
||||
; function,windows-2025 runner 的 6.7.1 没有它,ISCC 直接报 Unknown identifier
|
||||
; (#925 引入后第一次在 hosted runner 上编安装包就炸了,见 installer/README.md)。
|
||||
#define MyTsfRoot64 "{sys}\IME\MetasequoiaIME"
|
||||
#define MyTsfRoot32 "{syswow64}\IME\MetasequoiaIME"
|
||||
#ifdef LightPackage
|
||||
@@ -69,7 +77,7 @@ ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
UninstallDisplayName={#MyAppName}
|
||||
UninstallDisplayIcon={commonpf64}\metasequoiaime\MetasequoiaIME.ico
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
VersionInfoVersion={#MyVersionInfo}
|
||||
|
||||
[Languages]
|
||||
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
|
||||
@@ -718,10 +726,10 @@ begin
|
||||
'现有数据将从这里迁移:' + NewLine + Space + ResolvePreviousDataDir;
|
||||
end;
|
||||
|
||||
{ 本进程能直接访问的 TSF 根目录。32 位安装器里不经改写的 System32 会被 WOW64 落到 SysWOW64。}
|
||||
// 本进程能直接访问的 TSF 根目录。64 位模式下 sys 常量不经重定向,直接就是 System32。
|
||||
function TsfRoot64: String;
|
||||
begin
|
||||
Result := ApplyPathRedirRulesForCurrentProcess(True, ExpandConstant('{#MyTsfRoot64}'));
|
||||
Result := ExpandConstant('{#MyTsfRoot64}');
|
||||
end;
|
||||
|
||||
function TsfRoot32: String;
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# 在 runner 上现场生成自签名代码签名证书,签 dev-build 的包内二进制与安装包。
|
||||
#
|
||||
# dev-build workflow 每次运行生成一张新证书,私钥只存在于 runner 内存中,运行结束即消失;
|
||||
# 公钥导出成 .cer 进 artifact,由用户用 import-dev-trust.ps1 导入本机受信任存储。
|
||||
# uiAccess=true 的 Server 没有可信签名会被 Windows 忽略,所以签名是 dev 包可用的前提。
|
||||
#
|
||||
# 环境变量:DEV_CERT_PATH(.cer 输出路径)。在 installer/ 目录下运行,与
|
||||
# Sign-PackageBinaries-Local.ps1 相同的待签集合(server_exe/ 与 tsf_dll/ 下全部 EXE/DLL)。
|
||||
#
|
||||
# pwsh -File .\sign-dev-build.ps1 # 签包内二进制
|
||||
# pwsh -File .\sign-dev-build.ps1 -Installer # 签编译完成的安装包
|
||||
# pwsh -File .\sign-dev-build.ps1 -FilePath a.exe,b.exe # 签显式列出的文件(server 单独构建用)
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Installer,
|
||||
[string[]]$FilePath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$CertificateSubject = 'CN=Metasequoia IME Dev Build Code Signing'
|
||||
|
||||
function Find-SignTool {
|
||||
$kitsBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
||||
$candidate = Get-ChildItem -LiteralPath $kitsBin -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Name } catch { [version]'0.0' } } -Descending |
|
||||
ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } |
|
||||
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
|
||||
Select-Object -First 1
|
||||
if (-not $candidate) {
|
||||
throw 'signtool.exe not found in the Windows SDK.'
|
||||
}
|
||||
return $candidate
|
||||
}
|
||||
|
||||
# 同一次运行里重复调用本脚本时复用签名步骤生成的证书,保持包内二进制与安装包同一签名。
|
||||
#
|
||||
# 复用的判断必须来自「本次运行写入的标记」,不能只靠「证书看起来像 dev 证书」:改了 subject
|
||||
# 之后,任何一台没被重置的机器上都会留下上次构建的证书,有效期 30 天,subject 和 EKU 全都
|
||||
# 对得上。只按属性筛就会把上一个任务生成的私钥拿来签名,产出的包属于别的流水线。标记文件
|
||||
# 放在 RUNNER_TEMP 下(托管 runner 每次都是新的临时目录),内容就是本次要复用的指纹。
|
||||
function Get-RunMarkerPath {
|
||||
$root = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [IO.Path]::GetTempPath() }
|
||||
return Join-Path $root 'msime-dev-build-certificate.thumbprint'
|
||||
}
|
||||
|
||||
function Get-RunCertificate {
|
||||
$marker = Get-RunMarkerPath
|
||||
if (-not (Test-Path -LiteralPath $marker -PathType Leaf)) {
|
||||
return $null
|
||||
}
|
||||
$thumbprint = (Get-Content -LiteralPath $marker -Raw -ErrorAction SilentlyContinue)
|
||||
if ($thumbprint) { $thumbprint = $thumbprint.Trim() }
|
||||
if (-not $thumbprint) {
|
||||
return $null
|
||||
}
|
||||
$certificate = Get-ChildItem -Path 'Cert:\CurrentUser\My' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Thumbprint -eq $thumbprint -and $_.HasPrivateKey } |
|
||||
Select-Object -First 1
|
||||
if (-not $certificate) {
|
||||
# 标记在但证书没了(比如被清理):当作本次没有证书,重新生成并覆盖标记。
|
||||
Write-Host "The certificate recorded for this run is gone from the store: $thumbprint"
|
||||
}
|
||||
return $certificate
|
||||
}
|
||||
|
||||
function New-RunCertificate {
|
||||
# -Type CodeSigningCert 负责代码签名 EKU。有效期权衡的是两件事:artifact 只留 7 天,
|
||||
# 但用户可能下载到本地后放很久才装,而安装器检查的是签名时刻证书是否有效;拉得太长又会
|
||||
# 让一张随构建轮换的证书变成长期可信身份。90 天覆盖到拖延的安装,又不至于成为长期凭据。
|
||||
# import-dev-trust.ps1 会打印实际的到期时间,安装器发现包内证书过期时也会明确报出来。
|
||||
$certificate = New-SelfSignedCertificate `
|
||||
-Type CodeSigningCert `
|
||||
-Subject $CertificateSubject `
|
||||
-FriendlyName 'Metasequoia IME Dev Build Code Signing' `
|
||||
-KeyAlgorithm RSA `
|
||||
-KeyLength 3072 `
|
||||
-HashAlgorithm SHA256 `
|
||||
-KeyUsage DigitalSignature `
|
||||
-CertStoreLocation 'Cert:\CurrentUser\My' `
|
||||
-NotBefore (Get-Date).AddHours(-1) `
|
||||
-NotAfter (Get-Date).AddDays(90)
|
||||
return $certificate
|
||||
}
|
||||
|
||||
$certificate = Get-RunCertificate
|
||||
if ($certificate) {
|
||||
Write-Host "Reusing the certificate created earlier in this run: $($certificate.Thumbprint)"
|
||||
}
|
||||
else {
|
||||
$certificate = New-RunCertificate
|
||||
# 先落标记再往下走:后面任何一步失败重跑,都应该复用这一张而不是再生成一张。
|
||||
Set-Content -LiteralPath (Get-RunMarkerPath) -Value $certificate.Thumbprint -NoNewline -Encoding utf8NoBOM
|
||||
Write-Host "Created the dev-build signing certificate: $($certificate.Thumbprint)"
|
||||
}
|
||||
|
||||
$certPath = $env:DEV_CERT_PATH
|
||||
if (-not $certPath) {
|
||||
throw 'DEV_CERT_PATH is not set; the workflow must say where to export the public key.'
|
||||
}
|
||||
$certDirectory = Split-Path -Parent $certPath
|
||||
if (-not (Test-Path -LiteralPath $certDirectory -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $certDirectory -Force | Out-Null
|
||||
}
|
||||
Export-Certificate -Cert $certificate -FilePath $certPath -Type CERT -Force | Out-Null
|
||||
Write-Host "Public key exported to $certPath"
|
||||
|
||||
$signTool = Find-SignTool
|
||||
|
||||
if ($Installer) {
|
||||
$candidates = @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot 'Output') -Filter '*.exe' -File -ErrorAction SilentlyContinue)
|
||||
if ($candidates.Count -eq 0) {
|
||||
throw 'No installer found under Output\. Compile it first.'
|
||||
}
|
||||
if ($candidates.Count -gt 1) {
|
||||
throw "Multiple installers under Output\, refusing to guess: $($candidates.Name -join ', ')"
|
||||
}
|
||||
$targets = @($candidates[0].FullName)
|
||||
}
|
||||
elseif ($FilePath) {
|
||||
$targets = @(
|
||||
foreach ($item in $FilePath) {
|
||||
if (-not (Test-Path -LiteralPath $item -PathType Leaf)) {
|
||||
throw "File to sign does not exist: $item"
|
||||
}
|
||||
(Resolve-Path -LiteralPath $item).Path
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
$packageDirectories = @(
|
||||
(Join-Path $PSScriptRoot 'server_exe'),
|
||||
(Join-Path $PSScriptRoot 'tsf_dll')
|
||||
)
|
||||
foreach ($directory in $packageDirectories) {
|
||||
if (-not (Test-Path -LiteralPath $directory -PathType Container)) {
|
||||
throw "Missing staging directory, run Prepare-PackageFiles.ps1 first: $directory"
|
||||
}
|
||||
}
|
||||
# 最外层的 @() 不能省:只有一个待签文件时管道结果是标量,.Count 在严格模式下会报错。
|
||||
$targets = @(
|
||||
@(
|
||||
foreach ($directory in $packageDirectories) {
|
||||
Get-ChildItem -LiteralPath $directory -Recurse -File |
|
||||
Where-Object { $_.Extension -in @('.exe', '.dll') }
|
||||
}
|
||||
) | Sort-Object FullName -Unique
|
||||
) | ForEach-Object { $_.FullName }
|
||||
if ($targets.Count -eq 0) {
|
||||
throw 'No EXE or DLL found to sign.'
|
||||
}
|
||||
}
|
||||
|
||||
$thumbprint = $certificate.Thumbprint
|
||||
Write-Host "Signing $($targets.Count) file(s) with $thumbprint"
|
||||
# 自签名证书的时间戳没有意义(测试机可能离线,且证书随 build 轮换),所以不加 /tr。
|
||||
& $signTool sign /sha1 $thumbprint /s My /fd sha256 /v @($targets)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "signtool.exe signing failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
foreach ($target in $targets) {
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $target
|
||||
if (-not $signature.SignerCertificate -or
|
||||
$signature.SignerCertificate.Thumbprint -ne $thumbprint) {
|
||||
throw "Signature missing or signed by the wrong certificate: $target"
|
||||
}
|
||||
}
|
||||
Write-Host "Signed $($targets.Count) file(s); every signature verified against the dev certificate."
|
||||
@@ -5,7 +5,8 @@ release-please owns version.txt. The VERSIONINFO block in windows/src/IME/Metase
|
||||
same number in two shapes, a comma-separated quad and a dotted string, so it cannot be driven by
|
||||
release-please's generic updater. The release workflow runs this before configuring CMake.
|
||||
|
||||
The fourth field of the quad stays 0; only MAJOR.MINOR.PATCH are release-managed.
|
||||
The fourth field of the quad stays 0 for releases; a dev build passes an explicit
|
||||
--version MAJOR.MINOR.PATCH.YYMMDD to stamp its date there.
|
||||
|
||||
python scripts/apply_version.py # stamp from version.txt
|
||||
python scripts/apply_version.py --check # exit 1 if the resource is out of date
|
||||
@@ -23,7 +24,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
VERSION_FILE = REPO_ROOT / "version.txt"
|
||||
RESOURCE_FILE = REPO_ROOT / "windows" / "src" / "IME" / "MetasequoiaIME.rc"
|
||||
|
||||
SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||
SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$")
|
||||
|
||||
# Each pattern captures the part before the version and the part after it, so only the digits move.
|
||||
SUBSTITUTIONS = (
|
||||
@@ -34,7 +35,7 @@ SUBSTITUTIONS = (
|
||||
)
|
||||
|
||||
|
||||
def read_version(explicit_version: str | None = None) -> tuple[str, str, str]:
|
||||
def read_version(explicit_version: str | None = None) -> tuple[str, str, str, str]:
|
||||
if explicit_version is None:
|
||||
if not VERSION_FILE.is_file():
|
||||
raise SystemExit(f"error: {VERSION_FILE} is missing")
|
||||
@@ -45,13 +46,17 @@ def read_version(explicit_version: str | None = None) -> tuple[str, str, str]:
|
||||
source = "--version"
|
||||
match = SEMVER.match(raw)
|
||||
if not match:
|
||||
raise SystemExit(f"error: {source} must hold MAJOR.MINOR.PATCH, got {raw!r}")
|
||||
return match.groups()
|
||||
raise SystemExit(
|
||||
f"error: {source} must hold MAJOR.MINOR.PATCH[.BUILD], got {raw!r}"
|
||||
)
|
||||
# The optional fourth field carries a dev build's YYMMDD stamp; releases leave it at 0.
|
||||
major, minor, patch, build_field = match.groups()
|
||||
return major, minor, patch, build_field or "0"
|
||||
|
||||
|
||||
def render(version: tuple[str, str, str]) -> tuple[str, str]:
|
||||
major, minor, patch = version
|
||||
return f"{major},{minor},{patch},0", f"{major}.{minor}.{patch}.0"
|
||||
def render(version: tuple[str, str, str, str]) -> tuple[str, str]:
|
||||
major, minor, patch, build_field = version
|
||||
return f"{major},{minor},{patch},{build_field}", f"{major}.{minor}.{patch}.{build_field}"
|
||||
|
||||
|
||||
def apply(text: str, quad: str, dotted: str) -> tuple[str, int]:
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Derive the dev-build version from version.txt.
|
||||
|
||||
The release owns version.txt (release-please), and dev builds must never masquerade as one.
|
||||
The dev number keeps the release base for human recognition and appends a date and the short
|
||||
commit: 0.7.1-dev.20261019.7fa6fb1a.
|
||||
|
||||
The strings go to the installer name and the settings page. The TSF DLL's VERSIONINFO quad has
|
||||
four 16-bit fields, so the date compresses to YYMMDD in the fourth slot: 0.7.1.261019. yy > 79
|
||||
sorts above every year this product exists, and any real release leaves that slot at 0, so a dev
|
||||
DLL always reads as newer than its own base and older than the next release. The settings page's
|
||||
parseVersion strips the -dev suffix and compares 0.7.1, which is why the update check stays quiet.
|
||||
|
||||
Windows filenames and Inno Setup accept the dots in the short hash; a detached HEAD (workflow
|
||||
dispatch on a raw SHA) falls back to the first twelve hex digits.
|
||||
|
||||
python scripts/dev-build-version.py # all three outputs, one per line
|
||||
python scripts/dev-build-version.py --display # 0.7.1-dev.20261019.7fa6fb1a
|
||||
python scripts/dev-build-version.py --base # 0.7.1
|
||||
python scripts/dev-build-version.py --date # 261019
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
VERSION_FILE = REPO_ROOT / "version.txt"
|
||||
|
||||
SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||
SHORT_SHA = re.compile(r"^[0-9a-f]{7,40}$")
|
||||
|
||||
|
||||
def read_base() -> tuple[str, str, str]:
|
||||
if not VERSION_FILE.is_file():
|
||||
raise SystemExit(f"error: {VERSION_FILE} is missing")
|
||||
raw = VERSION_FILE.read_text(encoding="utf-8").strip()
|
||||
match = SEMVER.match(raw)
|
||||
if not match:
|
||||
raise SystemExit(f"error: version.txt must hold MAJOR.MINOR.PATCH, got {raw!r}")
|
||||
return match.groups()
|
||||
|
||||
|
||||
def read_short_sha(explicit: str | None) -> str:
|
||||
if explicit:
|
||||
sha = explicit.strip().lower()
|
||||
if not SHORT_SHA.match(sha):
|
||||
raise SystemExit(f"error: --commit must be hex digits, got {explicit!r}")
|
||||
return sha[:12]
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--short=12", "HEAD"],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"error: cannot read the commit: {result.stderr.strip()}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def build(base: tuple[str, str, str], today: datetime.date, sha: str) -> tuple[str, str]:
|
||||
major, minor, patch = base
|
||||
display = f"{major}.{minor}.{patch}-dev.{today:%Y%m%d}.{sha}"
|
||||
# 四段 quad 的末位放 YYMMDD:YY > 79 恒大于本产品存在的一切年份,任何正式版该位是 0,
|
||||
# 所以 dev DLL 恒读作比基线新、比下一个发布旧。
|
||||
date = today.strftime("%y%m%d")
|
||||
quad = f"{major}.{minor}.{patch}.{date}"
|
||||
return display, date
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
output = parser.add_mutually_exclusive_group()
|
||||
output.add_argument("--display", action="store_true", help="print the display version only")
|
||||
output.add_argument("--base", action="store_true", help="print the base version only")
|
||||
output.add_argument("--date", action="store_true", help="print the YYMMDD stamp only")
|
||||
parser.add_argument("--commit", metavar="SHA", help="use this short commit instead of reading git")
|
||||
args = parser.parse_args()
|
||||
|
||||
base = read_base()
|
||||
display, date = build(base, datetime.date.today(), read_short_sha(args.commit))
|
||||
if args.display:
|
||||
print(display)
|
||||
elif args.base:
|
||||
print(".".join(base))
|
||||
elif args.date:
|
||||
print(date)
|
||||
else:
|
||||
print(display)
|
||||
print(date)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -43,7 +43,10 @@ else()
|
||||
file(STRINGS "${MSIME_VERSION_FILE}" MSIME_PRODUCT_VERSION LIMIT_COUNT 1)
|
||||
endif()
|
||||
string(STRIP "${MSIME_PRODUCT_VERSION}" MSIME_PRODUCT_VERSION)
|
||||
if(NOT MSIME_PRODUCT_VERSION MATCHES "^[0-9]+(\\.[0-9]+)*$")
|
||||
# 版本主体必须是数字段;允许一个可选的连字符后缀(semver 预发布段),dev-build 用它携带
|
||||
# 日期与 commit(0.7.1-dev.20261019.<hash>),设置窗口关于页原样显示。关于页的 ParseVersion
|
||||
# 会剥掉后缀做更新检查的数字比较,所以后缀不影响版本判定。
|
||||
if(NOT MSIME_PRODUCT_VERSION MATCHES "^[0-9]+(\\.[0-9]+)*(-[0-9A-Za-z.-]+)?$")
|
||||
message(FATAL_ERROR "Invalid product version in ${MSIME_VERSION_FILE}: '${MSIME_PRODUCT_VERSION}'")
|
||||
endif()
|
||||
configure_file(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ManifestTool = 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\mt.exe',
|
||||
[ValidatePattern('^\d+\.\d+\.\d+$')]
|
||||
[ValidatePattern('^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$')]
|
||||
[string]$ProductVersion = '',
|
||||
# ALL_BUILD also builds MetasequoiaImeServerTests and test_webview_contract, which the packaging
|
||||
# step deletes again. The server target already depends on the watchdog, the three panels,
|
||||
|
||||
Reference in new issue
Block a user