diff --git a/.github/workflows/linux-packages.yml b/.github/workflows/linux-packages.yml deleted file mode 100644 index 24952bc..0000000 --- a/.github/workflows/linux-packages.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: Linux 安装包 - -# 仓库本身在 Gitea,这个工作流是给 GitHub 镜像用的: -# 推送、打 tag(v*)、手动触发、PR 检查时编译并打出 .deb / .rpm。 -on: - push: - branches: [main, master] - tags: ["v*"] - pull_request: - workflow_dispatch: - inputs: - publish: - description: "发布 Release(用 v<版本> 当 tag,不必先打 tag)" - type: boolean - default: false - -permissions: - contents: write - -concurrency: - group: packages-${{ github.ref }} - cancel-in-progress: true - -jobs: - package: - name: ${{ matrix.arch }} · deb + rpm - runs-on: ${{ matrix.runs-on }} - # ARM runner 只有公共仓库(或自建 arm64 runner)才有;私有仓库里这一步失败 - # 不算整体失败,x86_64 的包照常产出。 - continue-on-error: ${{ matrix.optional }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - arch: x86_64 - runs-on: ubuntu-22.04 - deb_arch: amd64 - rpm_arch: x86_64 - optional: false - - arch: aarch64 - runs-on: ubuntu-24.04-arm - deb_arch: arm64 - rpm_arch: aarch64 - optional: true - - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 安装构建依赖 - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - build-essential file \ - qtbase5-dev qtbase5-dev-tools qt5-qmake \ - rpm dpkg-dev - - - name: 读取版本号 - id: meta - run: | - version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') - if [ -z "$version" ]; then - echo "VoiletMark.pro 里没有 VERSION" >&2 - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: 编译 - run: | - qmake_bin=$(command -v qmake-qt5 || command -v qmake || true) - if [ -z "$qmake_bin" ]; then - echo "找不到 qmake(确认装了 qtbase5-dev / qt5-qmake)" >&2 - exit 1 - fi - "$qmake_bin" -v - mkdir -p build - cd build - "$qmake_bin" ../VoiletMark.pro PREFIX=/usr CONFIG+=release - make -j"$(nproc)" - make install INSTALL_ROOT="$PWD/stage" - find stage -type f | sort - - - name: 打 .deb - run: | - ./packaging/build-deb.sh build/stage "${{ steps.meta.outputs.version }}" "${{ matrix.deb_arch }}" dist - - - name: 打 .rpm - run: | - ./packaging/build-rpm.sh build/stage "${{ steps.meta.outputs.version }}" "${{ matrix.rpm_arch }}" dist - - - name: 检查产物 - run: | - ls -lh dist - dpkg-deb -I dist/*.deb - rpm -qpi dist/*.rpm - rpm -qpl dist/*.rpm - - - name: 上传构建产物 - uses: actions/upload-artifact@v4 - with: - name: voiletmark-${{ matrix.arch }} - path: dist/* - if-no-files-found: error - - release: - name: 发布到 Release - needs: package - # 只有两种情况发 Release: - # 1. 推了 v* tag - # 2. 手动 Run workflow 并勾上 publish - # 普通推送(main / PR)只会留下 artifact,这一步显示 skipped 是正常的。 - # 用 always() 是为了 ARM 那个 job 失败(拿不到 arm runner)时这里照常收尾, - # 下面会检查 x86_64 的包在不在,缺了就不发。 - if: >- - always() && ( - startsWith(github.ref, 'refs/tags/') || - (github.event_name == 'workflow_dispatch' && inputs.publish) - ) - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 读取版本号 - id: meta - run: | - version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: 下载所有产物 - uses: actions/download-artifact@v4 - with: - path: dist - merge-multiple: true - - - name: 确认 x86_64 的包存在 - run: | - ls -lh dist - test -n "$(ls dist/*amd64.deb 2>/dev/null)" || { - echo "没有 x86_64 的 deb,不发布" >&2 - exit 1 - } - - - name: 生成 SHA256SUMS - run: | - cd dist - sha256sum ./*.deb ./*.rpm > SHA256SUMS - cat SHA256SUMS - - - name: 挂到 Release - uses: softprops/action-gh-release@v2 - with: - # 打 tag 时用那个 tag;手动发布时用 v<版本>(不存在就按当前提交新建) - tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', steps.meta.outputs.version) }} - target_commitish: ${{ github.sha }} - name: VoiletMark ${{ steps.meta.outputs.version }} - files: dist/* - generate_release_notes: true - fail_on_unmatched_files: true diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml new file mode 100644 index 0000000..74624eb --- /dev/null +++ b/.github/workflows/packages.yml @@ -0,0 +1,366 @@ +name: 安装包 + +# Gitea 不跑这个工作流;同步到 GitHub 后,这里编译并打出: +# Linux deb / rpm(x86_64、aarch64) +# Windows 安装包(关联 .md)+ 便携 zip +# macOS arm64 / x86_64 两个 dmg(未签名,不是公证版) +on: + push: + branches: [main, master] + tags: ["v*"] + pull_request: + workflow_dispatch: + inputs: + publish: + description: "发布 Release(用 v<版本> 当 tag,不必先打 tag)" + type: boolean + default: false + +permissions: + contents: write + +concurrency: + group: packages-${{ github.ref }} + cancel-in-progress: true + +env: + QT_VERSION: "5.15.2" + +jobs: + linux: + name: Linux ${{ matrix.arch }} + runs-on: ${{ matrix.runs-on }} + continue-on-error: ${{ matrix.optional }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runs-on: ubuntu-22.04 + deb_arch: amd64 + rpm_arch: x86_64 + optional: false + - arch: aarch64 + runs-on: ubuntu-24.04-arm + deb_arch: arm64 + rpm_arch: aarch64 + optional: true + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装构建依赖 + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential file \ + qtbase5-dev qtbase5-dev-tools qt5-qmake \ + rpm dpkg-dev + + - name: 读取版本号 + id: meta + run: | + version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') + if [ -z "$version" ]; then + echo "VoiletMark.pro 里没有 VERSION" >&2 + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: 编译 + run: | + qmake_bin=$(command -v qmake-qt5 || command -v qmake || true) + if [ -z "$qmake_bin" ]; then + echo "找不到 qmake(确认装了 qtbase5-dev / qt5-qmake)" >&2 + exit 1 + fi + "$qmake_bin" -v + mkdir -p build + cd build + "$qmake_bin" ../VoiletMark.pro PREFIX=/usr CONFIG+=release + make -j"$(nproc)" + make install INSTALL_ROOT="$PWD/stage" + find stage -type f | sort + + - name: 打 .deb + run: ./packaging/build-deb.sh build/stage "${{ steps.meta.outputs.version }}" "${{ matrix.deb_arch }}" dist + + - name: 打 .rpm + run: ./packaging/build-rpm.sh build/stage "${{ steps.meta.outputs.version }}" "${{ matrix.rpm_arch }}" dist + + - name: 检查产物 + run: | + ls -lh dist + dpkg-deb -I dist/*.deb + rpm -qpi dist/*.rpm + + - name: 上传产物 + uses: actions/upload-artifact@v4 + with: + name: voiletmark-linux-${{ matrix.arch }} + path: dist/* + if-no-files-found: error + + windows: + name: Windows x86_64 + runs-on: windows-2022 + timeout-minutes: 40 + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 准备 MSVC 环境 + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: 安装 Qt ${{ env.QT_VERSION }} + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + arch: win64_msvc2019_64 + cache: true + + - name: 编译并收集 Qt 依赖 + shell: bash + run: | + set -euo pipefail + version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') + if [ -z "$version" ]; then + echo "VoiletMark.pro 里没有 VERSION" >&2 + exit 1 + fi + echo "APP_VERSION=$version" >> "$GITHUB_ENV" + + mkdir -p build + cd build + qmake ../VoiletMark.pro CONFIG+=release + nmake release || nmake + if [ -f release/VoiletMark.exe ]; then + exe=release/VoiletMark.exe + else + exe=$(find . -name VoiletMark.exe | head -n1) + fi + if [ -z "$exe" ]; then + echo "没找到 VoiletMark.exe" >&2 + exit 1 + fi + mkdir -p ../deploy + cp "$exe" ../deploy/VoiletMark.exe + cd .. + + windeployqt --release --no-translations --compiler-runtime deploy/VoiletMark.exe + find deploy -maxdepth 1 -type f | sort + + - name: 打 NSIS 安装包(关联 .md) + shell: bash + run: | + set -euo pipefail + mkdir -p dist + + makensis='/c/Program Files (x86)/NSIS/makensis.exe' + if [ ! -x "$makensis" ]; then + makensis=$(find '/c/Program Files (x86)' '/c/Program Files' -maxdepth 3 -iname 'makensis.exe' 2>/dev/null | head -n1) + fi + if [ -z "$makensis" ]; then + choco install nsis -y --no-progress + makensis=$(find '/c/Program Files (x86)' '/c/Program Files' -maxdepth 3 -iname 'makensis.exe' 2>/dev/null | head -n1) + fi + if [ -z "$makensis" ]; then + echo "找不到 makensis(NSIS)" >&2 + exit 1 + fi + + output=$(cygpath -w "$PWD/dist/VoiletMark-$APP_VERSION-win64-setup.exe") + deploy=$(cygpath -w "$PWD/deploy") + icon=$(cygpath -w "$PWD/packaging/voiletmark.ico") + script=$(cygpath -w "$PWD/packaging/windows/installer.nsi") + + "$makensis" \ + "/DAPP_VERSION=$APP_VERSION" \ + "/DDEPLOY_DIR=$deploy" \ + "/DICON_FILE=$icon" \ + "/DOUTPUT_FILE=$output" \ + "$script" + + ls -lh dist + + - name: 便携版 zip + shell: pwsh + run: | + Compress-Archive -Path deploy/* -DestinationPath "dist/VoiletMark-$env:APP_VERSION-win64-portable.zip" + Get-ChildItem dist | Select-Object Name, Length + + - name: 上传产物 + uses: actions/upload-artifact@v4 + with: + name: voiletmark-windows-x86_64 + path: dist/* + if-no-files-found: error + + macos-arm64: + name: macOS arm64 (Apple Silicon) + runs-on: macos-15 + timeout-minutes: 50 + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Qt 5(Homebrew,原生 arm64) + run: | + set -euo pipefail + brew install qt@5 + qt_prefix=$(brew --prefix qt@5) + echo "QT_PREFIX=$qt_prefix" >> "$GITHUB_ENV" + "$qt_prefix/bin/qmake" -v + + - name: 编译并打包 Qt 框架 + run: | + set -euo pipefail + version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') + if [ -z "$version" ]; then + echo "VoiletMark.pro 里没有 VERSION" >&2 + exit 1 + fi + echo "APP_VERSION=$version" >> "$GITHUB_ENV" + + mkdir -p build + cd build + "$QT_PREFIX/bin/qmake" ../VoiletMark.pro CONFIG+=release + make -j"$(sysctl -n hw.ncpu)" + + file VoiletMark.app/Contents/MacOS/VoiletMark | tee /tmp/arch.txt + grep -q arm64 /tmp/arch.txt + + "$QT_PREFIX/bin/macdeployqt" VoiletMark.app -always-overwrite + otool -L VoiletMark.app/Contents/MacOS/VoiletMark | head -30 + + - name: 收 Homebrew 依赖 + ad-hoc 签名 + run: | + set -euo pipefail + ./packaging/macos/bundle-libs.sh build/VoiletMark.app + xattr -cr build/VoiletMark.app + codesign --force --deep --sign - build/VoiletMark.app + codesign --verify --deep --strict build/VoiletMark.app + + - name: 打 dmg + run: ./packaging/macos/build-dmg.sh build/VoiletMark.app "$APP_VERSION" arm64 dist + + - name: 上传产物 + uses: actions/upload-artifact@v4 + with: + name: voiletmark-macos-arm64 + path: dist/* + if-no-files-found: error + + macos-x86_64: + name: macOS x86_64 (Intel) + # Intel 的 GitHub runner;拿不到这个 runner 时这一步会失败,不影响其它产物 + runs-on: macos-15-intel + timeout-minutes: 40 + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Qt ${{ env.QT_VERSION }} + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + arch: clang_64 + cache: true + + - name: 编译并打包 Qt 框架 + run: | + set -euo pipefail + version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') + if [ -z "$version" ]; then + echo "VoiletMark.pro 里没有 VERSION" >&2 + exit 1 + fi + echo "APP_VERSION=$version" >> "$GITHUB_ENV" + + mkdir -p build + cd build + qmake ../VoiletMark.pro CONFIG+=release + make -j"$(sysctl -n hw.ncpu)" + + file VoiletMark.app/Contents/MacOS/VoiletMark | tee /tmp/arch.txt + grep -q x86_64 /tmp/arch.txt + + macdeployqt VoiletMark.app -always-overwrite + + - name: 收依赖 + ad-hoc 签名 + run: | + set -euo pipefail + ./packaging/macos/bundle-libs.sh build/VoiletMark.app + xattr -cr build/VoiletMark.app + codesign --force --deep --sign - build/VoiletMark.app + codesign --verify --deep --strict build/VoiletMark.app + + - name: 打 dmg + run: ./packaging/macos/build-dmg.sh build/VoiletMark.app "$APP_VERSION" x86_64 dist + + - name: 上传产物 + uses: actions/upload-artifact@v4 + with: + name: voiletmark-macos-x86_64 + path: dist/* + if-no-files-found: error + + release: + name: 发布到 Release + needs: [linux, windows, macos-arm64, macos-x86_64] + # 打 tag 或手动勾 publish 才发;普通推送/PR 只留 artifact。 + # always() 是为了某个平台失败时其它产物还能发出来。 + if: >- + always() && ( + startsWith(github.ref, 'refs/tags/') || + (github.event_name == 'workflow_dispatch' && inputs.publish) + ) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 读取版本号 + id: meta + run: | + version=$(sed -n 's/^VERSION[[:space:]]*=[[:space:]]*//p' VoiletMark.pro | head -n1 | tr -d '[:space:]') + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: 下载所有产物 + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: 检查产物 + run: | + ls -lh dist + count=$(find dist -maxdepth 1 -type f | wc -l) + if [ "$count" -eq 0 ]; then + echo "一个安装包都没有,不发布" >&2 + exit 1 + fi + + - name: 生成 SHA256SUMS + run: | + cd dist + find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' | sort > /tmp/files.txt + xargs sha256sum < /tmp/files.txt > SHA256SUMS + cat SHA256SUMS + + - name: 挂到 Release + uses: softprops/action-gh-release@v2 + with: + # 打 tag 时用那个 tag;手动发布时用 v<版本>(不存在就按当前提交新建) + tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', steps.meta.outputs.version) }} + target_commitish: ${{ github.sha }} + name: VoiletMark ${{ steps.meta.outputs.version }} + files: dist/* + generate_release_notes: true + fail_on_unmatched_files: true diff --git a/README.md b/README.md index 07edbe8..41a48e9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 -当前版本 **0.12.3**。 +当前版本 **0.12.4**。 ## 界面 @@ -205,24 +205,39 @@ make -C build install INSTALL_ROOT=/tmp/pkg # 打包用,装到 /tmp/pkg/usr/lo 会一起装上可执行文件、`voiletmark.desktop` 和 256×256 图标, 装完就能在应用菜单里搜到 VoiletMark。 -## 打包(Linux) +## 打包(安装包) -`.github/workflows/linux-packages.yml` 是给 GitHub 镜像用的:它会编译代码并打出 -`.deb` 和 `.rpm`(本仓库在 Gitea 上,Gitea 不跑这个工作流)。 +`.github/workflows/packages.yml` 是给 GitHub 镜像用的(Gitea 上不跑):推代码、打 tag、 +PR 或手动运行时编译,并打出下面这些安装包。 -- 触发方式:推送到 `main`、打 tag(`v*`)、手动 **Actions → Linux 安装包 → Run workflow**、PR 检查 -- x86_64(`ubuntu-22.04`):`voiletmark_<版本>_amd64.deb` 和 `voiletmark-<版本>-1.x86_64.rpm` -- aarch64(`ubuntu-24.04-arm`):`..._arm64.deb` 和 `...aarch64.rpm`。这个 runner 公共仓库才有; - 私有仓库里 ARM 那一步设成了 `continue-on-error`,失败不会影响 x86_64 的包, - 也可以把 `runs-on` 换成自建的 arm64 runner -- 每次构建都会上传 artifact -- 发 Release 的两种方式: - - 打 tag:`git tag v0.12.3 && git push origin v0.12.3`(同步到 GitHub 时把 tag 一起推过去) - - 不打 tag:**Actions → Linux 安装包 → Run workflow**,把 **publish** 勾上, - 它会用 `v<版本>` 当 tag(不存在就按当前提交新建)并发出 Release -- 普通推送到 main / PR 只上传 artifact,不发 Release——「发布到 Release」显示 skipped 是正常的 +| 平台 | runner | 产物 | +| --- | --- | --- | +| Linux x86_64 | `ubuntu-22.04` | `voiletmark_<版本>_amd64.deb`、`voiletmark-<版本>-1.x86_64.rpm` | +| Linux aarch64 | `ubuntu-24.04-arm` | `..._arm64.deb`、`...aarch64.rpm` | +| Windows x86_64 | `windows-2022` | `VoiletMark-<版本>-win64-setup.exe`、`...-win64-portable.zip` | +| macOS arm64 | `macos-15` | `VoiletMark-<版本>-macos-arm64.dmg` | +| macOS x86_64 | `macos-15-intel` | `VoiletMark-<版本>-macos-x86_64.dmg` | -本地打包(版本号从 `VoiletMark.pro` 里取): +说明: + +- Windows 安装包按当前用户装到 `%LOCALAPPDATA%\Programs\VoiletMark`,不需要管理员权限; + 会把 `.md` 关联到 VoiletMark,卸载时清理快捷方式、注册表和(仍指向本程序的)关联 +- macOS 的 arm64 包是**原生**的,不依赖 Rosetta;Intel 包在 `macos-15-intel` 上用 Qt 5.15.2 官方二进制打 +- 两个 dmg 都没有签名 / 公证:第一次打开要右键 →「打开」,或去「系统设置 → 隐私与安全性」点「仍要打开」 +- Windows 安装包也没签名,SmartScreen 会拦一下(更多信息 → 仍要运行) +- 如果 .md 在资源管理器里还指向别的程序:右键 →「打开方式」→「选择其他应用」→ 勾选用 VoiletMark 打开(Win8 之后 UserChoice 不允许安装包直接改) +- Linux ARM 和 macOS Intel 的 runner 不是所有仓库都有;拿不到时那几个 job 会失败,不影响其它产物 +- 某个平台失败时 Release 仍会带上其它平台成功的包 + +### 发 Release + +- 打 tag:`git tag v0.12.4 && git push origin v0.12.4`(同步到 GitHub 时把 tag 一起推过去) +- 不打 tag:**Actions → 安装包 → Run workflow**,勾上 **publish**,会用 `v<版本>` 当 tag +- 普通推送 / PR 只上传 artifact,不发 Release——「发布到 Release」显示 skipped 是正常的 + +### 本地打包(Linux) + +版本号从 `VoiletMark.pro` 里取: ```bash mkdir -p build && cd build @@ -243,6 +258,7 @@ sudo dnf install ./voiletmark-*.x86_64.rpm 打 rpm 需要 `rpmbuild`(Ubuntu 上 `apt install rpm`)。rpm 的 `License` 字段现在写的是 `Proprietary`(仓库里还没放 LICENSE 文件),要换许可证改 `packaging/build-rpm.sh` 里那一行即可。 +Windows / macOS 的安装包建议直接用 CI 打(本地需要 NSIS、Xcode / Homebrew Qt),这里不再展开。 ## 图标 @@ -304,8 +320,8 @@ src/ logo.png 图标源文件 resources.qrc 窗口图标与翻译资源 translations/ 中英文翻译(.ts / .qm) -packaging/ 图标生成、.desktop、deb / rpm 打包脚本 -.github/workflows/ GitHub Actions:编译 + 打 deb / rpm +packaging/ 图标、.desktop、deb/rpm、NSIS、macOS dmg 打包脚本 +.github/workflows/ GitHub Actions:编译 + 打三平台安装包 ``` ## 下一版计划 diff --git a/VoiletMark.pro b/VoiletMark.pro index 50f1b58..e13eb06 100644 --- a/VoiletMark.pro +++ b/VoiletMark.pro @@ -1,6 +1,6 @@ TEMPLATE = app TARGET = VoiletMark -VERSION = 0.12.3 +VERSION = 0.12.4 QT += core gui widgets @@ -16,6 +16,8 @@ RESOURCES += resources.qrc # 各平台的可执行文件图标 win32: RC_ICONS = packaging/voiletmark.ico macx: ICON = packaging/voiletmark.icns +macx: QMAKE_TARGET_BUNDLE_PREFIX = com.linuxacme +macx: QMAKE_TARGET_DESCRIPTION = "VoiletMark Markdown Editor" SOURCES += \ src/main.cpp \ diff --git a/packaging/macos/build-dmg.sh b/packaging/macos/build-dmg.sh new file mode 100755 index 0000000..3cfaa25 --- /dev/null +++ b/packaging/macos/build-dmg.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# 用 hdiutil 打一个拖拽式 dmg(里面带 /Applications 快捷方式) +# 用法: build-dmg.sh <版本> <输出目录> +set -euo pipefail + +app=$1 +version=$2 +arch=$3 +out=$4 + +if [ ! -d "$app" ]; then + echo "找不到 $app" >&2 + exit 1 +fi + +mkdir -p "$out" +stage=$(mktemp -d) +trap 'rm -rf "$stage"' EXIT + +ditto "$app" "$stage/VoiletMark.app" +ln -s /Applications "$stage/Applications" + +name="VoiletMark-$version-macos-$arch.dmg" +hdiutil create -volname "VoiletMark $version" -srcfolder "$stage" -ov -format UDZO "$out/$name" >/dev/null +echo "已生成 $out/$name" diff --git a/packaging/macos/bundle-libs.sh b/packaging/macos/bundle-libs.sh new file mode 100755 index 0000000..2cda616 --- /dev/null +++ b/packaging/macos/bundle-libs.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# 把 .app 里引用到的 Homebrew 动态库收进 Contents/Frameworks,并把引用改成包内相对路径。 +# Homebrew 的 Qt 会链到 /opt/homebrew 下的 libpng / freetype 等,macdeployqt 不会带它们。 +# 用法: bundle-libs.sh +set -euo pipefail + +app=$1 +if [ ! -d "$app" ]; then + echo "找不到 $app" >&2 + exit 1 +fi + +frameworks=$app/Contents/Frameworks +mkdir -p "$frameworks" + +round=0 +while [ "$round" -lt 12 ]; do + round=$((round + 1)) + changed=0 + while IFS= read -r file; do + [ -f "$file" ] || continue + while IFS= read -r dep; do + case "$dep" in + /opt/homebrew/*|/usr/local/*) + base=$(basename "$dep") + if [ ! -f "$frameworks/$base" ]; then + cp "$dep" "$frameworks/$base" + chmod 755 "$frameworks/$base" + install_name_tool -id "@executable_path/../Frameworks/$base" "$frameworks/$base" >/dev/null 2>&1 || true + changed=1 + fi + install_name_tool -change "$dep" "@executable_path/../Frameworks/$base" "$file" >/dev/null 2>&1 || true + ;; + esac + done < <(otool -L "$file" | tail -n +2 | awk '{print $1}') + done < <(find "$app" -type f \( -perm -111 -o -name '*.dylib' \) -print) + + [ "$changed" -eq 1 ] || break +done + +left=$(find "$app" -type f \( -perm -111 -o -name '*.dylib' \) -exec otool -L {} + 2>/dev/null | grep -E '^[[:space:]]*(/opt/homebrew|/usr/local)/' | wc -l | tr -d ' ') +if [ "$left" -ne 0 ]; then + echo "还有 $left 个 Homebrew 依赖没处理:" >&2 + find "$app" -type f \( -perm -111 -o -name '*.dylib' \) -exec otool -L {} + 2>/dev/null | grep -E '^[[:space:]]*(/opt/homebrew|/usr/local)/' | head -20 >&2 + exit 1 +fi diff --git a/packaging/windows/installer.nsi b/packaging/windows/installer.nsi new file mode 100644 index 0000000..4b42463 --- /dev/null +++ b/packaging/windows/installer.nsi @@ -0,0 +1,116 @@ +; VoiletMark Windows 安装包(未签名) +; 按当前用户安装(不需要管理员权限),默认装到 %LOCALAPPDATA%\Programs\VoiletMark +Unicode true +!include "MUI2.nsh" +!include "LogicLib.nsh" + +!ifndef APP_VERSION + !define APP_VERSION "0.0.0" +!endif +!ifndef DEPLOY_DIR + !error "需要 /DDEPLOY_DIR=" +!endif +!ifndef ICON_FILE + !define ICON_FILE "packaging\voiletmark.ico" +!endif +!ifndef OUTPUT_FILE + !define OUTPUT_FILE "dist\VoiletMark-setup.exe" +!endif + +!define APP_NAME "VoiletMark" +!define APP_EXE "VoiletMark.exe" +!define APP_PUBLISHER "VoiletMark" +!define APP_ID "VoiletMark" +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_ID}" + +Name "${APP_NAME} ${APP_VERSION}" +OutFile "${OUTPUT_FILE}" +InstallDir "$LOCALAPPDATA\Programs\${APP_NAME}" +InstallDirRegKey HKCU "Software\${APP_ID}" "InstallDir" +RequestExecutionLevel user +SetCompressor /SOLID lzma +CRCCheck on +BrandingText "${APP_NAME} ${APP_VERSION}" + +!define MUI_ICON "${ICON_FILE}" +!define MUI_UNICON "${ICON_FILE}" +!define MUI_ABORTWARNING +!define MUI_WELCOMEPAGE_TITLE "${APP_NAME} ${APP_VERSION}" +!define MUI_WELCOMEPAGE_TEXT "把 ${APP_NAME} 装到当前用户目录,不需要管理员权限。$\r$\n$\r$\n安装后会把 .md 文件关联到 ${APP_NAME},随时可以在 Windows 设置里改回去。" +!define MUI_FINISHPAGE_RUN "$INSTDIR\${APP_EXE}" +!define MUI_FINISHPAGE_RUN_TEXT "立即运行 ${APP_NAME}" + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "English" + +Section "VoiletMark" SecMain + SetShellVarContext current + SetOutPath "$INSTDIR" + File /r "${DEPLOY_DIR}\*.*" + WriteUninstaller "$INSTDIR\uninstall.exe" + + ; 开始菜单 + 桌面快捷方式 + CreateDirectory "$SMPROGRAMS\${APP_NAME}" + CreateShortcut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" + CreateShortcut "$SMPROGRAMS\${APP_NAME}\卸载 ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" + CreateShortcut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" + + ; .md 关联(只写 HKCU,不动系统级关联) + WriteRegStr HKCU "Software\Classes\.md" "" "${APP_ID}.md" + WriteRegStr HKCU "Software\Classes\.md" "Content Type" "text/markdown" + WriteRegStr HKCU "Software\Classes\${APP_ID}.md" "" "Markdown 文档" + WriteRegStr HKCU "Software\Classes\${APP_ID}.md" "FriendlyTypeName" "Markdown 文档" + WriteRegStr HKCU "Software\Classes\${APP_ID}.md\DefaultIcon" "" "$INSTDIR\${APP_EXE},0" + WriteRegStr HKCU "Software\Classes\${APP_ID}.md\shell\open\command" "" '"$INSTDIR\${APP_EXE}" "%1"' + + ; 「打开方式」里也出现一份 + WriteRegStr HKCU "Software\Classes\Applications\${APP_EXE}" "FriendlyAppName" "${APP_NAME}" + WriteRegStr HKCU "Software\Classes\Applications\${APP_EXE}\shell\open\command" "" '"$INSTDIR\${APP_EXE}" "%1"' + WriteRegStr HKCU "Software\Classes\Applications\${APP_EXE}\SupportedTypes" ".md" "" + + ; 让资源管理器立刻刷新图标 / 关联 + System::Call 'shell32::SHChangeNotify(i 0x8000000, i 0, i 0, i 0)' + + ; 控制面板「卸载程序」 + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${APP_NAME}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${APP_VERSION}" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${APP_PUBLISHER}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${APP_EXE}" + WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' + WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 + WriteRegStr HKCU "Software\${APP_ID}" "InstallDir" "$INSTDIR" +SectionEnd + +Section "Uninstall" + SetShellVarContext current + + Delete "$DESKTOP\${APP_NAME}.lnk" + Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" + Delete "$SMPROGRAMS\${APP_NAME}\卸载 ${APP_NAME}.lnk" + RMDir "$SMPROGRAMS\${APP_NAME}" + + ; 只在 .md 仍指向我们时才清掉默认值,别破坏用户后来改的关联 + ReadRegStr $0 HKCU "Software\Classes\.md" "" + ${If} $0 == "${APP_ID}.md" + DeleteRegValue HKCU "Software\Classes\.md" "" + ${EndIf} + DeleteRegKey HKCU "Software\Classes\${APP_ID}.md" + DeleteRegKey HKCU "Software\Classes\Applications\${APP_EXE}" + + DeleteRegKey HKCU "${UNINST_KEY}" + DeleteRegKey HKCU "Software\${APP_ID}" + + RMDir /r "$INSTDIR" + + System::Call 'shell32::SHChangeNotify(i 0x8000000, i 0, i 0, i 0)' +SectionEnd