yy-tauri-macos-dmg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # yy-tauri-macos-dmg
2
+
3
+ 在 **macOS** 上为 **Tauri 2** 项目生成带「隔离属性修复」`.command` 的 DMG(依赖本机已安装 [create-dmg](https://github.com/create-dmg/create-dmg))。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -D yy-tauri-macos-dmg
9
+ # 或
10
+ pnpm add -D yy-tauri-macos-dmg
11
+ ```
12
+
13
+ ## 使用
14
+
15
+ 在 Tauri 项目根目录(含 `src-tauri/tauri.conf.json`)执行:
16
+
17
+ ```bash
18
+ npx yy-tauri-macos-dmg
19
+ # 或指定目录
20
+ npx yy-tauri-macos-dmg /path/to/your-tauri-app
21
+ ```
22
+
23
+ 本机需已安装:`brew install create-dmg`,以及 `release.config.json` 里配置的构建命令所需工具(默认 `pnpm tauri build`)。
24
+
25
+ ## 程序化读取发布配置
26
+
27
+ 本包导出与 Tauri 项目根目录 `release.config.json` / `tauri.conf.json` 合并后的配置:
28
+
29
+ ```js
30
+ import { loadReleaseConfig } from 'yy-tauri-macos-dmg/release-config'
31
+
32
+ const c = loadReleaseConfig('/path/to/project')
33
+ console.log(c.productName, c.version)
34
+ ```
35
+
36
+ ## 许可
37
+
38
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 在任意 Tauri 项目根目录执行 macOS DMG 打包(含隔离修复 .command)
4
+ */
5
+
6
+ import { spawnSync } from 'node:child_process'
7
+ import { existsSync } from 'node:fs'
8
+ import { dirname, join, resolve } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url))
12
+
13
+ const args = process.argv.slice(2)
14
+ if (args[0] === '-h' || args[0] === '--help') {
15
+ console.log(`用法: yy-tauri-macos-dmg [项目根目录]
16
+
17
+ 未指定目录时使用当前工作目录。
18
+
19
+ 依赖(本机):
20
+ - macOS
21
+ - brew install create-dmg
22
+ - Node.js、pnpm 或 npm(与目标项目的 tauriBuildCommand 一致)
23
+
24
+ 目标项目需包含 src-tauri/tauri.conf.json,且 bundle 已启用。`)
25
+ process.exit(0)
26
+ }
27
+
28
+ const project = resolve(args[0] || process.cwd())
29
+ const tauriConf = join(project, 'src-tauri', 'tauri.conf.json')
30
+ if (!existsSync(tauriConf)) {
31
+ console.error(`错误:未找到 Tauri 配置:${tauriConf}`)
32
+ process.exit(1)
33
+ }
34
+
35
+ const script = join(__dirname, '..', 'scripts', 'build-macos-dmg.sh')
36
+ if (!existsSync(script)) {
37
+ console.error(`错误:未找到打包脚本:${script}`)
38
+ process.exit(1)
39
+ }
40
+
41
+ const r = spawnSync('bash', [script], {
42
+ env: { ...process.env, TAURI_PROJECT_ROOT: project },
43
+ stdio: 'inherit',
44
+ cwd: project,
45
+ })
46
+
47
+ process.exit(r.status ?? 1)
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "yy-tauri-macos-dmg",
3
+ "version": "0.1.0",
4
+ "description": "CLI:为 Tauri 2 项目生成带「隔离属性修复」脚本的 macOS DMG(create-dmg)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": "bin/cli.mjs",
8
+ "files": [
9
+ "bin",
10
+ "scripts",
11
+ "templates"
12
+ ],
13
+ "exports": {
14
+ "./release-config": "./scripts/lib/release-config.mjs"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "keywords": [
20
+ "tauri",
21
+ "tauri2",
22
+ "dmg",
23
+ "macos",
24
+ "create-dmg",
25
+ "quarantine"
26
+ ]
27
+ }
@@ -0,0 +1,167 @@
1
+ #!/bin/bash
2
+ # ============================================
3
+ # macOS 通用 DMG 打包(Tauri)
4
+ # 依赖目标项目根目录 release.config.json(可选)与 tauri.conf.json
5
+ # 流程:tauri build → 从 DMG 或 .app 取包 → 注入隔离修复脚本 → create-dmg
6
+ #
7
+ # 项目根目录:优先环境变量 TAURI_PROJECT_ROOT;否则自动探测(旧 scripts/ 布局或本 monorepo)
8
+ # ============================================
9
+
10
+ set -e
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ if [ -n "${TAURI_PROJECT_ROOT:-}" ]; then
14
+ PROJECT_DIR="$(cd "$TAURI_PROJECT_ROOT" && pwd)"
15
+ else
16
+ _parent="$(dirname "$SCRIPT_DIR")"
17
+ if [ -f "$_parent/src-tauri/tauri.conf.json" ]; then
18
+ PROJECT_DIR="$(cd "$_parent" && pwd)"
19
+ elif [ -f "$(cd "$SCRIPT_DIR/../../.." && pwd)/src-tauri/tauri.conf.json" ]; then
20
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
21
+ else
22
+ echo "错误:无法确定 Tauri 项目根目录。"
23
+ echo "请设置 TAURI_PROJECT_ROOT=/path/to/project后重试(目录下需有 src-tauri/tauri.conf.json)。"
24
+ exit 1
25
+ fi
26
+ fi
27
+
28
+ # 使用临时文件 source,避免 `source <(node …)` 在 sh / 部分环境下失效导致变量为空
29
+ DMG_ENV_FILE=$(mktemp)
30
+ trap 'rm -f "$DMG_ENV_FILE"' EXIT
31
+ node "$SCRIPT_DIR/emit-dmg-env.mjs" "$PROJECT_DIR" >"$DMG_ENV_FILE"
32
+ set -a
33
+ # shellcheck disable=SC1090
34
+ source "$DMG_ENV_FILE"
35
+ set +a
36
+ if [ -z "$APP_DISPLAY_NAME" ] || [ -z "$APP_VERSION" ]; then
37
+ echo "错误:未能从 Tauri / release 配置读取 APP_DISPLAY_NAME 或 APP_VERSION(请用 bash 执行本脚本)。"
38
+ exit 1
39
+ fi
40
+
41
+ ARCH=$(uname -m)
42
+ BUNDLE_ROOT="$PROJECT_DIR/src-tauri/target/release/bundle"
43
+ DMG_DIR="$BUNDLE_ROOT/dmg"
44
+ FINAL_DMG="$DMG_DIR/${APP_DISPLAY_NAME}_${APP_VERSION}_${ARCH}.dmg"
45
+ QUARANTINE_TEMPLATE="$SCRIPT_DIR/../templates/macos-quarantine-fix.command.in"
46
+
47
+ echo ""
48
+ echo "========================================="
49
+ echo " ${APP_DISPLAY_NAME} macOS 打包"
50
+ echo "========================================="
51
+ echo ""
52
+
53
+ if ! command -v create-dmg &>/dev/null; then
54
+ echo "错误:未安装 create-dmg,请先执行:brew install create-dmg"
55
+ exit 1
56
+ fi
57
+
58
+ if ! command -v node &>/dev/null; then
59
+ echo "错误:需要 Node.js 以读取 release 配置"
60
+ exit 1
61
+ fi
62
+
63
+ echo "步骤 1/4:执行构建 ..."
64
+ cd "$PROJECT_DIR"
65
+ eval "$TAURI_BUILD_CMD"
66
+
67
+ if [ ! -d "$BUNDLE_ROOT" ]; then
68
+ echo "错误:未找到打包目录:"
69
+ echo " $BUNDLE_ROOT"
70
+ echo "请确认 tauri build 已成功完成(含 bundle 阶段)。"
71
+ exit 1
72
+ fi
73
+
74
+ TAURI_DMG=$(find "$BUNDLE_ROOT" -name "*.dmg" -type f 2>/dev/null | head -n 1)
75
+ APP_FROM_BUNDLE=$(find "$BUNDLE_ROOT" -maxdepth 4 -name "*.app" -type d 2>/dev/null | head -n 1)
76
+
77
+ if [ -n "$TAURI_DMG" ]; then
78
+ echo "找到 Tauri DMG:$TAURI_DMG"
79
+ elif [ -n "$APP_FROM_BUNDLE" ]; then
80
+ echo "未找到 DMG,将使用 .app 继续打包:$APP_FROM_BUNDLE"
81
+ else
82
+ echo "错误:在 $BUNDLE_ROOT 下未找到 .dmg 或 .app。"
83
+ echo "目录内容:"
84
+ ls -laR "$BUNDLE_ROOT" || true
85
+ exit 1
86
+ fi
87
+
88
+ if [ ! -f "$QUARANTINE_TEMPLATE" ]; then
89
+ echo "错误:未找到隔离修复模板:$QUARANTINE_TEMPLATE"
90
+ exit 1
91
+ fi
92
+
93
+ echo ""
94
+ echo "步骤 2/4:准备 .app 与卷图标 ..."
95
+ TEMP_DIR=$(mktemp -d)
96
+ STAGING_DIR="$TEMP_DIR/staging"
97
+ MOUNT_POINT="$TEMP_DIR/mount"
98
+ mkdir -p "$STAGING_DIR" "$MOUNT_POINT"
99
+
100
+ APP_BUNDLE_NAME=""
101
+ QUARANTINE_APP_STEM=""
102
+ VOL_ICON=""
103
+
104
+ if [ -n "$TAURI_DMG" ]; then
105
+ hdiutil attach "$TAURI_DMG" -mountpoint "$MOUNT_POINT" -quiet -nobrowse
106
+ APP_IN_DMG=$(find "$MOUNT_POINT" -maxdepth 1 -name "*.app" -type d | head -n 1)
107
+ if [ -z "$APP_IN_DMG" ] || [ ! -d "$APP_IN_DMG" ]; then
108
+ echo "错误:挂载的 DMG 内未找到 .app。"
109
+ ls -la "$MOUNT_POINT" || true
110
+ hdiutil detach "$MOUNT_POINT" -quiet || true
111
+ exit 1
112
+ fi
113
+ APP_BUNDLE_NAME=$(basename "$APP_IN_DMG")
114
+ cp -R "$APP_IN_DMG" "$STAGING_DIR/"
115
+ if [ -f "$MOUNT_POINT/.VolumeIcon.icns" ]; then
116
+ cp "$MOUNT_POINT/.VolumeIcon.icns" "$TEMP_DIR/VolumeIcon.icns"
117
+ VOL_ICON="$TEMP_DIR/VolumeIcon.icns"
118
+ fi
119
+ hdiutil detach "$MOUNT_POINT" -quiet
120
+ else
121
+ APP_BUNDLE_NAME=$(basename "$APP_FROM_BUNDLE")
122
+ cp -R "$APP_FROM_BUNDLE" "$STAGING_DIR/"
123
+ fi
124
+
125
+ QUARANTINE_APP_STEM="${APP_BUNDLE_NAME%.app}"
126
+
127
+ echo "步骤 3/4:生成隔离修复脚本并构建 DMG ..."
128
+ GEN_FIX="$STAGING_DIR/$DMG_QUARANTINE_SCRIPT_NAME"
129
+ node "$SCRIPT_DIR/render-quarantine-script.mjs" "$QUARANTINE_TEMPLATE" "$GEN_FIX" "$QUARANTINE_APP_STEM"
130
+ chmod +x "$GEN_FIX"
131
+
132
+ CREATE_DMG_ARGS=(
133
+ --volname "$APP_DISPLAY_NAME"
134
+ --window-pos 200 120
135
+ --window-size 700 340
136
+ --icon-size 80
137
+ --text-size 13
138
+ --icon "$APP_BUNDLE_NAME" 120 150
139
+ --icon "Applications" 350 150
140
+ --icon "$DMG_QUARANTINE_SCRIPT_NAME" 570 150
141
+ --app-drop-link 350 150
142
+ --no-internet-enable
143
+ )
144
+
145
+ if [ -n "$VOL_ICON" ]; then
146
+ CREATE_DMG_ARGS+=(--volicon "$VOL_ICON")
147
+ fi
148
+
149
+ rm -f "$TEMP_DIR/output.dmg"
150
+ create-dmg "${CREATE_DMG_ARGS[@]}" "$TEMP_DIR/output.dmg" "$STAGING_DIR"
151
+
152
+ echo ""
153
+ echo "步骤 4/4:写入最终 DMG ..."
154
+ mkdir -p "$DMG_DIR"
155
+ mv "$TEMP_DIR/output.dmg" "$FINAL_DMG"
156
+ rm -rf "$TEMP_DIR"
157
+
158
+ echo ""
159
+ echo "========================================="
160
+ echo "打包成功"
161
+ echo ""
162
+ echo "DMG 路径:"
163
+ echo " $FINAL_DMG"
164
+ echo ""
165
+ echo "内容:$APP_BUNDLE_NAME | Applications | $DMG_QUARANTINE_SCRIPT_NAME"
166
+ echo "========================================="
167
+ echo ""
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 向 stdout 输出可被 bash `source` 的变量赋值(用于 build-macos-dmg.sh)
3
+ */
4
+
5
+ import { loadReleaseConfig, shellSingleQuote } from './lib/release-config.mjs'
6
+
7
+ /**
8
+ * @returns {void}
9
+ */
10
+ function main() {
11
+ const projectRoot = process.argv[2] || process.cwd()
12
+ const c = loadReleaseConfig(projectRoot)
13
+ const esc = shellSingleQuote
14
+ const folder = c.macos.applicationsAppFolderName
15
+ console.log(`APP_DISPLAY_NAME=${esc(c.productName)}`)
16
+ console.log(`APP_VERSION=${esc(c.version)}`)
17
+ console.log(`QUARANTINE_APP_FOLDER=${esc(folder)}`)
18
+ console.log(`TAURI_BUILD_CMD=${esc(c.tauriBuildCommand)}`)
19
+ console.log(`DMG_QUARANTINE_SCRIPT_NAME=${esc(c.macos.quarantineScriptDisplayName)}`)
20
+ }
21
+
22
+ main()
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 跨项目复用的发布/打包配置加载器。
3
+ * 默认约定 Tauri 2 工程结构;可通过仓库根目录 release.config.json 覆盖路径与命令。
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'fs'
7
+ import { resolve } from 'path'
8
+
9
+ const DEFAULTS = {
10
+ tauriConfigPath: 'src-tauri/tauri.conf.json',
11
+ changelogOutPath: 'src/data/changelog.ts',
12
+ cargoTomlPath: 'src-tauri/Cargo.toml',
13
+ /** @type {string[]} */
14
+ bumpExtraJsonVersionPaths: [],
15
+ tauriBuildCommand: 'pnpm tauri build',
16
+ genChangelogScript: 'scripts/gen-changelog.mjs',
17
+ macos: {
18
+ /** DMG 内「移除隔离属性」脚本在访达中显示的文件名 */
19
+ quarantineScriptDisplayName: '文件损坏修复.command',
20
+ },
21
+ }
22
+
23
+ /**
24
+ * 读取并合并发布配置(defaults + release.config.json + tauri.conf.json 中的 productName/version)
25
+ *
26
+ * @param {string} projectRoot 仓库根目录(绝对或相对 cwd)
27
+ * @returns {object} 合并后的配置(含 productName、version、路径与 macOS 选项)
28
+ */
29
+ export function loadReleaseConfig(projectRoot) {
30
+ const root = resolve(projectRoot)
31
+ const configPath = resolve(root, 'release.config.json')
32
+ /** @type {Record<string, unknown>} */
33
+ let fileCfg = {}
34
+ if (existsSync(configPath)) {
35
+ fileCfg = JSON.parse(readFileSync(configPath, 'utf-8'))
36
+ }
37
+
38
+ const tauriRel =
39
+ typeof fileCfg.tauriConfigPath === 'string' ? fileCfg.tauriConfigPath : DEFAULTS.tauriConfigPath
40
+ const tauriPath = resolve(root, tauriRel)
41
+ if (!existsSync(tauriPath)) {
42
+ throw new Error(`[release-config] 未找到 Tauri 配置:${tauriPath}`)
43
+ }
44
+
45
+ const tauri = JSON.parse(readFileSync(tauriPath, 'utf-8'))
46
+ const productName = tauri.productName
47
+ const version = tauri.version
48
+ if (!productName || !version) {
49
+ throw new Error(`[release-config] ${tauriRel} 缺少 productName 或 version`)
50
+ }
51
+
52
+ const macosFile = /** @type {Record<string, unknown>} */ (
53
+ fileCfg.macos && typeof fileCfg.macos === 'object' ? fileCfg.macos : {}
54
+ )
55
+
56
+ const applicationsAppFolderName =
57
+ typeof macosFile.applicationsAppFolderName === 'string' ? macosFile.applicationsAppFolderName : productName
58
+
59
+ return {
60
+ projectRoot: root,
61
+ tauriConfigPath: tauriRel,
62
+ changelogOutPath:
63
+ typeof fileCfg.changelogOutPath === 'string' ? fileCfg.changelogOutPath : DEFAULTS.changelogOutPath,
64
+ cargoTomlPath: typeof fileCfg.cargoTomlPath === 'string' ? fileCfg.cargoTomlPath : DEFAULTS.cargoTomlPath,
65
+ bumpExtraJsonVersionPaths: Array.isArray(fileCfg.bumpExtraJsonVersionPaths)
66
+ ? fileCfg.bumpExtraJsonVersionPaths.filter((p) => typeof p === 'string')
67
+ : DEFAULTS.bumpExtraJsonVersionPaths,
68
+ tauriBuildCommand:
69
+ typeof fileCfg.tauriBuildCommand === 'string' ? fileCfg.tauriBuildCommand : DEFAULTS.tauriBuildCommand,
70
+ genChangelogScript:
71
+ typeof fileCfg.genChangelogScript === 'string' ? fileCfg.genChangelogScript : DEFAULTS.genChangelogScript,
72
+ productName,
73
+ version,
74
+ macos: {
75
+ quarantineScriptDisplayName:
76
+ typeof macosFile.quarantineScriptDisplayName === 'string'
77
+ ? macosFile.quarantineScriptDisplayName
78
+ : DEFAULTS.macos.quarantineScriptDisplayName,
79
+ applicationsAppFolderName,
80
+ },
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 将字符串转为可安全嵌入单引号 shell 赋值语句的字面量
86
+ *
87
+ * @param {string} s
88
+ * @returns {string}
89
+ */
90
+ export function shellSingleQuote(s) {
91
+ return `'${String(s).replace(/'/g, `'\\''`)}'`
92
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 将模板中的占位符替换为实际「应用程序」目录名(不含 .app),写入 DMG 内 .command 文件
3
+ *
4
+ * @param {string} templatePath 模板路径
5
+ * @param {string} outputPath 输出路径
6
+ * @param {string} appFolderName Applications 下的 .app 目录名(不含后缀)
7
+ */
8
+ import { readFileSync, writeFileSync } from 'fs'
9
+
10
+ /**
11
+ * @returns {void}
12
+ */
13
+ function main() {
14
+ const [, , templatePath, outputPath, appFolderName] = process.argv
15
+ if (!templatePath || !outputPath || appFolderName === undefined) {
16
+ console.error('用法: node render-quarantine-script.mjs <模板> <输出> <App目录名>')
17
+ process.exit(1)
18
+ }
19
+ let text = readFileSync(templatePath, 'utf8')
20
+ text = text.replace(/__QUARANTINE_APP_FOLDER__/g, appFolderName)
21
+ writeFileSync(outputPath, text, 'utf8')
22
+ }
23
+
24
+ main()
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # 由 build-macos-dmg.sh 从模板生成;占位符 __QUARANTINE_APP_FOLDER__ 为「应用程序」内 .app 主名
3
+
4
+ APP_FOLDER="__QUARANTINE_APP_FOLDER__"
5
+ APP_PATH="/Applications/${APP_FOLDER}.app"
6
+
7
+ echo ""
8
+ echo "========================================="
9
+ echo " 移除隔离属性:${APP_FOLDER}"
10
+ echo "========================================="
11
+ echo ""
12
+
13
+ if [ ! -d "$APP_PATH" ]; then
14
+ echo "错误:未在 /Applications 目录找到 ${APP_FOLDER}.app"
15
+ echo ""
16
+ echo "请确认应用已拖入「应用程序」文件夹,或手动输入路径:"
17
+ read -r -p "应用路径(直接回车跳过): " CUSTOM_PATH
18
+ if [ -n "$CUSTOM_PATH" ]; then
19
+ APP_PATH="$CUSTOM_PATH"
20
+ else
21
+ echo ""
22
+ echo "已取消操作。"
23
+ echo ""
24
+ read -n 1 -s -r -p "按任意键关闭窗口..."
25
+ exit 1
26
+ fi
27
+ fi
28
+
29
+ echo "正在修复:$APP_PATH"
30
+ echo "(需要输入开机密码,输入时不会显示字符)"
31
+ echo ""
32
+
33
+ sudo xattr -rd com.apple.quarantine "$APP_PATH"
34
+
35
+ if [ $? -eq 0 ]; then
36
+ echo ""
37
+ echo "完成:已移除隔离属性,可尝试重新打开 ${APP_FOLDER}。"
38
+ else
39
+ echo ""
40
+ echo "失败:请检查密码是否正确或路径是否存在。"
41
+ fi
42
+
43
+ echo ""
44
+ read -n 1 -s -r -p "按任意键关闭窗口..."