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
178 changes: 89 additions & 89 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
type: string
required: true
publish_assets:
description: "Publish after builds and selected validation pass"
description: "Publish installers and application update resources"
type: boolean
default: false
validate_gpu:
Expand All @@ -23,7 +23,7 @@ permissions:
contents: write

concurrency:
group: desktop-release-${{ github.ref }}
group: desktop-release-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false

env:
Expand All @@ -32,7 +32,34 @@ env:
PYTHONIOENCODING: utf-8

jobs:
prepare-release:
runs-on: ubuntu-24.04
outputs:
publish: ${{ steps.release.outputs.publish }}
steps:
- uses: actions/checkout@v6
- name: Prepare release
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
version="$(tr -d '[:space:]' < VERSION)"
case "$RELEASE_TAG" in
"v$version"|"v$version-LTS") ;;
*) echo 'Release tag must match VERSION' >&2; exit 1 ;;
esac
publish='${{ startsWith(github.ref, 'refs/tags/v') || inputs.publish_assets }}'
echo "publish=$publish" >> "$GITHUB_OUTPUT"
if [ "$publish" = true ]; then
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" --draft
else
gh release create "$RELEASE_TAG" --draft --target '${{ github.sha }}' --generate-notes
fi
fi

models:
needs: prepare-release
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
Expand All @@ -42,14 +69,19 @@ jobs:
- run: python -m pip install requests
- name: Assemble common, verified models
run: python scripts/build_desktop_release.py models --tag "$RELEASE_TAG"
- name: Upload model component
if: needs.prepare-release.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$RELEASE_TAG" build/common-models/*.zip --clobber
- uses: actions/upload-artifact@v7
with:
name: common-models
path: build/common-models/**
if-no-files-found: error

desktop:
needs: models
needs: [prepare-release, models]
strategy:
fail-fast: false
matrix:
Expand All @@ -60,15 +92,9 @@ jobs:
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v6
with: {lfs: true, fetch-depth: 0}
with: {lfs: true}
- uses: actions/setup-python@v6
with: {python-version: "3.11"}
- name: Validate tag and source locks
shell: python
run: |
import os, pathlib
version = pathlib.Path('VERSION').read_text().strip()
assert os.environ['RELEASE_TAG'] in {f'v{version}', f'v{version}-LTS'}, 'Release tag must match VERSION'
- run: python scripts/desktop_distribution.py sources
- run: python -m pip install -r ${{ matrix.requirements }} -r requirements-build.txt
- name: Install Linux build and system libraries
Expand All @@ -91,33 +117,24 @@ jobs:
- name: Calculate component build inputs
id: inputs
run: python scripts/build_desktop_release.py inputs
- uses: actions/cache@v5
with:
path: build/desktop/application
key: desktop-application-${{ matrix.system }}-${{ steps.inputs.outputs.application }}
- uses: actions/cache@v5
with:
path: |
build/desktop/inference-cpu
build/desktop/inference-gpu
key: desktop-inference-${{ matrix.system }}-${{ steps.inputs.outputs.inference }}
build/desktop/application/stamp.json
build/desktop/application/*.zip
key: desktop-application-zip-${{ matrix.system }}-${{ steps.inputs.outputs.application }}
- uses: actions/cache@v5
with:
path: build/downloads
key: desktop-downloads-${{ matrix.system }}-${{ hashFiles('packaging/runtime/*.lock.json') }}
path: |
build/desktop/inference-cpu/stamp.json
build/desktop/inference-cpu/*.zip
build/desktop/inference-gpu/stamp.json
build/desktop/inference-gpu/*.zip
key: desktop-inference-zip-${{ matrix.system }}-${{ steps.inputs.outputs.inference }}
- name: Assemble native components
id: assemble
run: python scripts/build_desktop_release.py assemble
- name: Package components and offline installers
run: python scripts/build_desktop_release.py package --tag "${{ env.RELEASE_TAG }}"
- uses: actions/upload-artifact@v7
id: unsigned-installers
if: runner.os == 'Windows'
with:
name: unsigned-desktop-installers
path: dist/desktop/LaTeXSnipper_*.exe
if-no-files-found: error
compression-level: 0
- name: Check optional Windows signing
id: signpath-config
if: runner.os == 'Windows'
Expand All @@ -136,6 +153,14 @@ jobs:
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
output.write(f'enabled={str(enabled).lower()}\n')
print('SignPath configured.' if enabled else 'SignPath unavailable; keeping unsigned installers.')
- uses: actions/upload-artifact@v7
id: unsigned-installers
if: runner.os == 'Windows' && steps.signpath-config.outputs.enabled == 'true'
with:
name: unsigned-desktop-installers
path: dist/desktop/LaTeXSnipper_*.exe
if-no-files-found: error
compression-level: 0
- name: Sign Windows first-install carriers
id: sign-installers
if: runner.os == 'Windows' && steps.signpath-config.outputs.enabled == 'true'
Expand All @@ -153,7 +178,19 @@ jobs:
- name: Compute final Windows installer identities
if: runner.os == 'Windows' && steps.sign-installers.outcome == 'success'
run: python scripts/build_desktop_release.py finalize-windows --signed signed-installers
- name: Upload desktop installers and update components
if: needs.prepare-release.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: python scripts/build_desktop_release.py upload --tag "${{ env.RELEASE_TAG }}"
- uses: actions/upload-artifact@v7
if: needs.prepare-release.outputs.publish == 'true'
with:
name: release-metadata-${{ matrix.system }}
path: dist/desktop/distribution-*.json
if-no-files-found: error
- uses: actions/upload-artifact@v7
if: needs.prepare-release.outputs.publish != 'true' || inputs.validate_gpu
with:
name: desktop-${{ matrix.system }}
path: dist/desktop/**
Expand All @@ -167,34 +204,6 @@ jobs:
path: |
build/desktop/*validation.json

native:
needs: desktop
strategy:
fail-fast: false
matrix:
include:
- {system: windows, runner: windows-2025}
- {system: linux, runner: ubuntu-22.04}
- {system: macos, runner: macos-15}
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v6
with: {lfs: true}
- uses: actions/setup-python@v6
with: {python-version: "3.11"}
- run: python -m pip install requests Pillow
- uses: ./.github/actions/desktop-system-libraries
- uses: actions/download-artifact@v7
with: {name: 'desktop-${{ matrix.system }}', path: build/native-release}
- name: Native installation, edition replacement, repair and uninstall
run: python scripts/validate_desktop_release.py --directory build/native-release --output build/native-acceptance.json
- uses: actions/upload-artifact@v7
if: always()
with:
name: native-validation-${{ matrix.system }}
path: build/native-acceptance.json
if-no-files-found: error

gpu:
if: github.event_name == 'workflow_dispatch' && inputs.validate_gpu
needs: desktop
Expand Down Expand Up @@ -222,50 +231,26 @@ jobs:
if-no-files-found: error

publish-release-assets:
needs: [desktop, native, gpu, build-office-plugin-installer, build-user-manual]
needs: [prepare-release, models, desktop, gpu, build-office-plugin-installer, build-user-manual]
if: >-
always() && (startsWith(github.ref, 'refs/tags/v') || inputs.publish_assets) &&
needs.desktop.result == 'success' && needs.native.result == 'success' &&
always() && needs.prepare-release.outputs.publish == 'true' &&
needs.models.result == 'success' && needs.desktop.result == 'success' &&
(needs.gpu.result == 'success' || needs.gpu.result == 'skipped') &&
needs.build-office-plugin-installer.result == 'success' && needs.build-user-manual.result == 'success'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with: {python-version: "3.11"}
- run: python -m pip install requests Pillow
- uses: actions/download-artifact@v7
with: {pattern: 'desktop-*', merge-multiple: false, path: release-assets}
- uses: actions/download-artifact@v7
with: {name: office-plugin-installer, path: publish-assets}
- uses: actions/download-artifact@v7
with: {name: user-manual, path: publish-assets}
- name: Stage exact declared artifacts
shell: python
run: |
import pathlib, shutil
output = pathlib.Path('publish-assets')
for system in ('windows', 'linux', 'macos'):
source = pathlib.Path('release-assets') / f'desktop-{system}'
assert source.is_dir(), f'Missing desktop target: {system}'
for path in source.iterdir():
if path.is_file():
destination = output / path.name
if destination.exists():
assert destination.read_bytes() == path.read_bytes(), f'Conflicting artifact: {path.name}'
else:
shutil.copy2(path, destination)
- name: Validate every payload before publishing the complete catalog
run: python scripts/build_desktop_release.py catalog --output publish-assets
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ env.RELEASE_TAG }}
generate_release_notes: true
overwrite_files: false
fail_on_unmatched_files: true
files: publish-assets/*
with: {pattern: 'release-metadata-*', merge-multiple: true, path: release-metadata}
- name: Publish update catalog and complete release
env:
GH_TOKEN: ${{ github.token }}
run: python scripts/release_artifacts.py --tag "$RELEASE_TAG" --version "$(cat VERSION)" --directory release-metadata

build-office-plugin-installer:
needs: prepare-release
name: Validate Office Plugin Installer
runs-on: ubuntu-24.04
steps:
Expand Down Expand Up @@ -309,7 +294,14 @@ jobs:
echo "Validated $installer ($actual)"
echo "path=$installer" >> "$GITHUB_OUTPUT"

- name: Publish Office plugin installer
if: needs.prepare-release.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$RELEASE_TAG" '${{ steps.installer.outputs.path }}' --clobber

- name: Upload Office plugin installer artifact
if: needs.prepare-release.outputs.publish != 'true'
uses: actions/upload-artifact@v7
with:
name: office-plugin-installer
Expand All @@ -318,6 +310,7 @@ jobs:
retention-days: 30

build-user-manual:
needs: prepare-release
name: Build User Manual
runs-on: windows-2025
steps:
Expand All @@ -341,7 +334,14 @@ jobs:
run: |
& $env:TYPST compile --root $env:GITHUB_WORKSPACE user_manual\user_manual.typ user_manual\LaTeXSnipper_User_Manual.pdf

- name: Publish user manual
if: needs.prepare-release.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$env:RELEASE_TAG" user_manual/LaTeXSnipper_User_Manual.pdf --clobber

- name: Upload user manual artifact
if: needs.prepare-release.outputs.publish != 'true'
uses: actions/upload-artifact@v7
with:
name: user-manual
Expand Down
6 changes: 4 additions & 2 deletions docs/desktop_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

## 构建与验收

发行来源、依赖和资源锁定在 `packaging/runtime/`。应用与推理组件分别计算输入指纹,覆盖源码、实际平台依赖和构建辅助脚本;缓存命中还必须通过载荷完整性校验。
发行来源、依赖和资源锁定在 `packaging/runtime/`。应用与推理组件分别计算输入指纹,覆盖源码、实际平台依赖和构建辅助脚本。构建缓存保存组件 ZIP 与身份清单;命中后校验并解压到构建目录,打包时复用 ZIP。

`scripts/payload_pruning.py` 统一维护三平台载荷裁剪:推理环境移除开发工具、交互式 Python/Tk 工具和 OpenCV 非识别资源,保留 `unittest` 等依赖可能导入的标准库。Windows 的独立 FFmpeg 视频插件可移除;Linux/macOS 保留 OpenCV 扩展直接链接的编解码及图形库。Qt 按相同模块清单处理 DLL、ELF、dylib 和 framework 布局,保留 WebEngine 核心资源及中英文本地化。GPU 私有运行库仍由原有白名单提取。裁剪后在目标平台执行导入、推理与应用原生验收。

Expand All @@ -64,6 +64,8 @@ Bandit 通过 `security` extra 提供,用于网络、文件及进程边界的

安装器只在 GitHub Actions 构建。Windows 默认生成未签名安装包;SignPath 配置完整时尝试签名,未配置或签名失败仍使用未签名产物。macOS 使用不需要开发者证书的本地 ad-hoc 签名,不要求 Apple 公证。

常规构建使用 GitHub 托管 runner 完成装配与原生安装检查。CUDA 硬件验收仅在手动选择 `validate_gpu` 时使用自托管 runner;未选择时不阻塞构建或发布。平台系统调用的模拟测试不能替代原生安装与硬件测试。
常规发布由各平台 runner 构建并直接上传安装包和更新组件。用户手册与 Office 安装包独立上传;最终任务仅汇总发行元数据并公开草稿,不重新下载载荷。发行页保留安装包、用户手册、`desktop-release.json`、组件 ZIP 和 macOS 签名资源;构建旁文件不公开。Linux 离线安装包使用 `.tar.xz`,发行资源须小于 GitHub 单文件 2 GiB 限制。

完整安装、修复、版本类型替换和卸载验收使用 `scripts/validate_desktop_release.py` 单独运行,不作为每次发布的强制关卡。CUDA 硬件验收仅在手动选择 `validate_gpu` 时使用自托管 runner。平台系统调用的模拟测试不能替代原生安装与硬件测试。

MathJax 原生检查入口为 `tools/mathjax/smoke_qt.py`,覆盖预览、字体和 SVG/MathML 转换。冻结应用验收还检查 PDF、XML、MathJax 转换与网络速度计所需模块。
4 changes: 2 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ The main differences are platform integration details:
| Window close / background behavior | Closing the main window hides it to the system tray; use the tray menu to exit. | Closing the main window hides it to the system tray when a tray is available; without a tray, the app asks before exiting. | Closing the main window minimizes it while the app keeps running; Dock/menu Quit exits the app. |
| Permission model | No explicit screenshot permission is required for the normal capture path. | Wayland compositors can restrict global shortcuts or screenshot capture. | Screen Recording permission is required for screenshots. The native Carbon hotkey path normally does not require Accessibility permission. |
| Inference runtime | Installed independent Python; CPU or NVIDIA GPU edition. | Installed independent Python; CPU or NVIDIA GPU edition. | Installed independent Python; Apple Silicon CPU edition. |
| Packaging | Inno installer with optional SignPath signing. | Offline `.tar.gz` carrier installing dpkg-owned components on Debian/Ubuntu. | `.dmg` / `.app` with ad-hoc signing. |
| Packaging | Inno installer with optional SignPath signing. | Offline `.tar.xz` carrier installing dpkg-owned components on Debian/Ubuntu. | `.dmg` / `.app` with ad-hoc signing. |

The shortcut UI uses the platform's primary modifier: `Ctrl` on Windows/Linux and `Command` on macOS. macOS system and editing shortcuts such as `Command+Q`, `Command+H`, `Command+M`, `Command+W`, `Command+A`, `Command+C`, `Command+V`, `Command+X`, `Command+Z`, `Command+Space`, `Command+Tab`, and screenshot shortcuts are intentionally rejected; use an `Option+Command` combination when a plain Command shortcut is reserved.

## Which installer should I use?

- Windows x86_64: choose the CPU or NVIDIA GPU `.exe` installer.
- Debian/Ubuntu-compatible Linux x86_64: choose the CPU or NVIDIA GPU offline `.tar.gz`, extract it, and run `install.sh`.
- Debian/Ubuntu-compatible Linux x86_64: choose the CPU or NVIDIA GPU offline `.tar.xz`, extract it, and run `install.sh`.
- macOS 14.0 or later on Apple Silicon: use the CPU `.dmg`, copy the app to Applications, then launch that copy.

Windows installers are unsigned unless optional SignPath signing succeeds.
Expand Down
Loading