构建脚本、音频运行时与资源维护
本页说明 Hydrogen Music 如何通过 GitHub Actions 构建音频专用 MPV、按平台归档运行时资源,并在 Electron 打包阶段把匹配平台的 MPV 文件放入最终应用包。重点是构建与资源交付链路,而不是 MPV 播放器内部实现或应用层播放控制。
Purpose and Scope
本页覆盖以下紧密耦合的能力:
.github/workflows/build-mpv-audio-only.yml定义的 Linux x64、macOS arm64、Windows x64 构建矩阵与跨平台 bundle;scripts/mpv-audio-only/download-artifacts.cjs暴露的资源下载参数、平台选择、artifact 命名和 GitHub API 认证约束;electron-builder.config.cjs如何发现resources/mpv下的平台目录,并通过extraResources将它们复制到安装包外部资源目录;- MPV 资源目录在构建产物中的目标路径和失败边界。
应用内播放服务、媒体通知、下载插件等属于其他目录和目录项;本页只在说明资源如何被打包时提及它们,不展开其运行时 API。部署发布流程若有独立页面,应以该页面为准。
Overview
该机制采用“CI 构建运行时 → Actions artifact → 本地下载/准备资源 → electron-builder 按目标平台注入”的分层方式。MPV 不是通过 npm 依赖直接塞进 asar,而是作为平台相关的额外资源放在 resources/mpv/<platform-arch>/,这样可以将体积较大的本地运行时与 JavaScript 应用代码分开,并让各平台使用自己的二进制布局。
工作流的输入是 mpv_ref 与 ffmpeg_ref,默认值均为 release;三个平台 job 分别生成平台 artifact,bundle job 等待它们完成后重新组装统一的 mpv-audio-only-all-platforms artifact。下载脚本可以选择当前平台、全部平台或显式平台,并将 artifact 解压/复制到仓库的 resources/mpv 树中。打包配置随后只选择当前目标平台目录:macOS 匹配 darwin,Windows 匹配 win32,Linux 匹配 linux,并把匹配项映射到包外的 mpv/<directory>。
Architecture
Source: build-mpv-audio-only.yml
Source: download-artifacts.cjs
Source: electron-builder.config.cjs
架构中的关键边界是 artifact 与打包配置之间的本地目录约定:下载脚本负责把资源准备到 resources/mpv,而 builder 不负责下载网络资源,只扫描该目录是否存在并生成 extraResources。因此,缺少资源时不会由 electron-builder.config.cjs 自动补齐。
CI 构建与 Artifact 拓扑
工作流只在三类事件下运行:手动 workflow_dispatch、相关 workflow/README/脚本发生 push,以及这些路径发生 pull request。它设置 contents: read,并用 concurrency 将同一 ref 的运行归为 mpv-audio-only-${{ github.ref }},但 cancel-in-progress: false 表示新运行不会取消旧运行。
三个平台 job 都有 120 分钟超时。Linux 使用 Ubuntu 24.04 和 apt 依赖;macOS 使用 macos-15 与 Homebrew;Windows 使用 MSYS2 的 MINGW64 环境。每个 job 调用对应的 build-*.sh 脚本,将结果放到 resources/mpv/<platform>,然后使用同名 artifact 上传,且 if-no-files-found: error 将空输出视为失败。
1concurrency:
2 group: mpv-audio-only-${{ github.ref }}
3 cancel-in-progress: false
4
5jobs:
6 linux-x64:
7 runs-on: ubuntu-24.04
8 timeout-minutes: 120
9 steps:
10 - name: Build
11 run: bash scripts/mpv-audio-only/build-linux-x64.sh
12
13 - uses: actions/upload-artifact@v7
14 with:
15 name: mpv-audio-only-linux-x64
16 path: resources/mpv/linux-x64/**
17 if-no-files-found: error
18 retention-days: 14Source: build-mpv-audio-only.yml
bundle job 的 needs 明确要求三个平台 job 全部完成。它先删除并重新创建三个资源目录,再下载三个平台 artifact,最后把整个 resources 目录暂存到 _mpv-audio-only-artifact 并上传统一 artifact。这样统一 artifact 的内容不会依赖 checkout 工作区中原先残留的二进制文件。
1bundle:
2 needs:
3 - linux-x64
4 - darwin-arm64
5 - win32-x64
6 steps:
7 - name: Prepare clean MPV resource directories
8 run: |
9 rm -rf resources/mpv/linux-x64 resources/mpv/darwin-arm64 resources/mpv/win32-x64
10 mkdir -p resources/mpv/linux-x64 resources/mpv/darwin-arm64 resources/mpv/win32-x64
11
12 - uses: actions/download-artifact@v8
13 with:
14 name: mpv-audio-only-linux-x64
15 path: resources/mpv/linux-x64
16
17 - name: Stage bundle
18 run: |
19 mkdir -p _mpv-audio-only-artifact
20 cp -R resources _mpv-audio-only-artifact/Source: build-mpv-audio-only.yml
下载脚本:平台解析、Artifact 选择与认证
scripts/mpv-audio-only/download-artifacts.cjs 将命令行输入归一化为一组下载选项。默认 workflow 是 build-mpv-audio-only.yml,默认 platform 是 current,默认启用清理;当 platform 为 all 时选择统一 artifact,否则使用 artifactByPlatform 中的显式映射。--keep-existing 将 clean 改为 false,用于保留目标目录中已有内容,而不是执行默认替换策略。
平台解析有两个有意的保护点:
current只接受process.platform与process.arch组合出的三个受支持值:win32-x64、darwin-arm64、linux-x64;其他机器会立即报错,而不会猜测兼容包。- 显式平台必须属于固定列表;拼写错误会得到包含合法值的错误消息。该约束使 artifact 名称与 CI job 名称保持一一对应。
1const defaultWorkflow = 'build-mpv-audio-only.yml';
2const defaultArtifact = 'mpv-audio-only-all-platforms';
3const platforms = ['win32-x64', 'darwin-arm64', 'linux-x64'];
4const artifactByPlatform = Object.freeze({
5 'win32-x64': 'mpv-audio-only-win32-x64',
6 'darwin-arm64': 'mpv-audio-only-darwin-arm64',
7 'linux-x64': 'mpv-audio-only-linux-x64',
8});
9
10function resolveRequestedPlatform(value) {
11 const platform = value || 'all';
12 if (platform === 'all') return platform;
13 if (platform === 'current') return resolveCurrentPlatform();
14 if (platforms.includes(platform)) return platform;
15 throw new Error(`Unsupported platform "${platform}". Use all, current, ${platforms.join(', ')}.`);
16}
17
18function resolveArtifactName(platform) {
19 if (platform === 'all') return defaultArtifact;
20 return artifactByPlatform[platform];
21}Source: download-artifacts.cjs
Source: download-artifacts.cjs
下载请求使用 GITHUB_API_URL(未设置时为 https://api.github.com),仓库可以通过 --repo、GITHUB_REPOSITORY 或本地 git remote 推断。脚本只在目标 host 与 API host 相同的情况下添加 GitHub API 版本头和 Bearer token;token 来源是 GH_TOKEN 或 GITHUB_TOKEN。这一区分避免把认证头发送到 artifact 下载重定向到的其他 host。
1function githubToken() {
2 return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '';
3}
4
5function requestHeaders(urlString) {
6 const headers = {
7 'User-Agent': 'hydrogen-music-mpv-downloader',
8 };
9
10 const apiHost = new URL(githubApiBase).hostname;
11 const targetHost = new URL(urlString).hostname;
12 if (targetHost === apiHost) {
13 headers.Accept = 'application/vnd.github+json';
14 headers['X-GitHub-Api-Version'] = '2022-11-28';
15 const token = githubToken();
16 if (token) headers.Authorization = `Bearer ${token}`;
17 }
18
19 return headers;
20}Source: download-artifacts.cjs
下载脚本选项
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
--workflow | string | build-mpv-audio-only.yml | 指定 Actions workflow 文件 |
--artifact | string | 按平台解析 | 覆盖 artifact 名称 |
--platform | current|all|win32-x64|darwin-arm64|linux-x64 | current | 选择当前平台、全部平台或单个平台 |
--current | flag | 未设置 | --platform current 的快捷方式 |
--run-id | string | 空 | 指定 workflow run |
--repo | owner/name | 自动推断 | 指定 GitHub 仓库 |
--keep-existing | flag | 清理目标目录 | 保留已有 resources/mpv 内容 |
GH_TOKEN / GITHUB_TOKEN | 环境变量 | 空 | 下载 Actions artifact zip 所需的 token |
脚本帮助文本明确指出:GitHub 即使允许查看 artifact 元数据,下载 Actions artifact zip 仍可能要求认证;401/403 会生成带认证提示的错误信息。脚本还限制重定向最多 8 次,防止下载链路无限跟随重定向。
Core Flow
Source: download-artifacts.cjs
Source: electron-builder.config.cjs
实际控制流中的关键顺序是:先确定 artifact,再处理下载和本地目录,最后才进入打包。builder 配置不会调用下载脚本,因此本地开发若未准备 resources/mpv,getMpvExtraResourcesForPlatform 会直接返回空数组;打包本身是否继续由 electron-builder 其他配置和具体目标决定,源码没有在此处提供自动修复或 fallback。
Electron 打包:平台目录发现与资源映射
electron-builder.config.cjs 将 resources/mpv 定义为 MPV 资源根目录。getMpvExtraResourcesForPlatform(platform) 先检查根目录是否存在;存在时只保留目录项,并接受两种命名形式:目录名等于目标平台,或以 ${platform}- 开头。每个匹配目录被映射为 { from, to },其中 from 是仓库中的绝对路径,to 是包外资源中的 POSIX 路径 mpv/<name>。
这种“按前缀匹配”而不是写死单一目录名的设计允许同一平台下保留带架构或变体后缀的资源目录,同时仍然阻止把其他操作系统目录放进当前安装包。
1const MPV_RESOURCE_ROOT = path.resolve(__dirname, 'resources', 'mpv');
2
3function getMpvExtraResourcesForPlatform(platform) {
4 if (!fs.existsSync(MPV_RESOURCE_ROOT)) return [];
5
6 const platformDirs = fs.readdirSync(MPV_RESOURCE_ROOT, { withFileTypes: true })
7 .filter((entry) => entry.isDirectory() && (entry.name === platform || entry.name.startsWith(`${platform}-`)))
8 .map((entry) => entry.name);
9
10 return platformDirs.map((name) => ({
11 from: path.join(MPV_RESOURCE_ROOT, name),
12 to: path.posix.join('mpv', name),
13 }));
14}Source: electron-builder.config.cjs
平台配置分别调用该函数:macOS 传入 darwin,Windows 传入 win32,Linux 传入 linux。因此 darwin-arm64 会被 macOS 规则匹配,win32-x64 会被 Windows 规则匹配,linux-x64 会被 Linux 规则匹配。最终资源位于包的 mpv/<directory>,而不是 asar 内部;这是由 extraResources 而非普通 files 条目完成的。
1mac: {
2 category: 'public.app-category.music',
3 icon: './src/assets/icon/icon.icns',
4 extraResources: getMpvExtraResourcesForPlatform('darwin'),
5 target: ['dmg'],
6},
7win: {
8 extraResources: getMpvExtraResourcesForPlatform('win32'),
9 target: ['nsis', 'portable', 'zip'],
10},
11linux: {
12 category: 'Audio',
13 extraResources: getMpvExtraResourcesForPlatform('linux'),
14 target: [
15 { target: 'AppImage', arch: ['x64'] },
16 { target: 'deb', arch: ['x64'] },
17 ],
18},Source: electron-builder.config.cjs
API Reference
parseArgs(argv)
将命令行数组解析为内部选项对象,并在解析完成后调用平台与 artifact 解析逻辑。源码中默认返回对象包含 workflow、artifact、platform、runId、repo 和 clean。未知参数抛出 Error;--keep-existing 将 clean 设为 false。
- 参数:
argv,命令行参数数组。 - 返回值:解析并归一化后的选项对象。
- 错误:未知参数、非法平台或当前平台不受支持时抛出
Error。
resolveRequestedPlatform(value)
校验并返回 all、当前受支持平台或显式平台名称。current 会进一步根据 Node.js 的 process.platform 与 process.arch 计算;不支持的组合会抛出错误。
- 参数:
value,平台字符串。 - 返回值:
all或规范化的平台标识。 - 错误:平台不在允许列表中时抛出
Error。
resolveArtifactName(platform)
根据平台选择统一 artifact 或平台专用 artifact。它不发起网络请求,只负责确定名称。
- 参数:
platform,已解析的平台标识。 - 返回值:artifact 名称字符串。
getMpvExtraResourcesForPlatform(platform)
扫描本地 MPV 资源根目录并生成 electron-builder 的 extraResources 映射。
- 参数:
platform,如darwin、win32或linux。 - 返回值:资源映射数组;根目录不存在时为空数组。
- 边界:只处理目录项,不处理根目录下的普通文件;只匹配精确平台名或平台前缀目录。
上述函数在源码中作为内部函数使用,当前未看到 module.exports 暴露它们作为独立公共库 API;扩展时应优先修改现有脚本调用链,而不是假定可以从其他模块直接导入这些函数。
Failure Modes, Edge Cases & Concurrency
构建与资源失败
- 空构建输出:每个平台 artifact 上传步骤使用
if-no-files-found: error,因此平台构建没有生成预期资源时,CI 不会产出看似成功的空 artifact。 - 错误平台:下载脚本拒绝未列入固定列表的平台,当前机器也不会自动降级到其他架构。
- 缺少认证:GitHub API 返回 401 或 403 时,错误文本明确提示设置
GH_TOKEN或GITHUB_TOKEN,且需要 Actions read 权限。 - 仓库无法推断:如果
--repo、GITHUB_REPOSITORY和 git remote 都无法提供有效owner/name,脚本会报错,而不是请求未知仓库。 - 资源根目录不存在:builder 的平台资源函数返回空数组。源码未显示额外下载、警告或回退逻辑,因此资源准备是打包前置条件。
- 重定向过多:HTTP 下载最多允许 8 次重定向,超出后拒绝请求。
并发与一致性
CI concurrency group 使用 ref 作为分组键,且 cancel-in-progress: false;同一 ref 的新旧运行可以并存完成,不会通过取消旧运行来强制只保留最新结果。bundle job 则通过 needs 等待三个平台结果,避免只打包部分平台。
本地下载脚本默认 clean: true,而 --keep-existing 显式改变这一行为。源码片段显示该选项的语义是“复制时保留已有资源目录”,但具体清理/复制实现位于已读取片段之外;因此不应把它描述为原子替换或并发安全操作。多个进程同时写入同一个 resources/mpv 目录时,源码没有提供锁或事务机制。
Performance and Operational Notes
- 三个平台 job 的超时均为 120 分钟;Windows 使用 MSYS2,Linux 与 macOS 分别安装原生构建依赖,构建成本主要集中在本地编译而不是打包阶段。
- bundle job 先删除平台目录再下载 artifact,优先保证内容干净一致,但也意味着它依赖三个上游 artifact 都成功。
- artifact 保留期为 14 天;超过该期限后,默认下载流程不能依赖旧 artifact,除非仓库或 workflow 另有存档策略。
- electron-builder 配置开启
asar: true与最大压缩,但 MPV 通过extraResources交付,不受 JavaScript asar 文件布局影响。ffmpeg-static另有asarUnpack规则;它与 MPV 资源目录是两个不同的打包路径,不能混为同一运行时来源。
Extension Points
扩展新平台或架构至少需要同步三层契约:
- 在 workflow 增加构建 job、对应依赖安装、输出目录与 artifact 名称;
- 在下载脚本的
platforms与artifactByPlatform中增加同名标识,并在resolveCurrentPlatform中定义当前平台如何映射; - 在 builder 配置中增加平台调用或确保现有前缀规则能够匹配,并确认 electron-builder target 支持该平台。
只修改下载脚本而不增加 CI artifact,会导致下载阶段找不到对应产物;只修改 CI 而不修改 builder,则资源可能存在于工作区但不会进入最终安装包。