|
| 1 | +// The MSIX ships unsigned. The Store signs it on ingestion. |
| 2 | +import { execFileSync } from 'node:child_process'; |
| 3 | +import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; |
| 4 | +import { createRequire } from 'node:module'; |
| 5 | +import { basename, join } from 'node:path'; |
| 6 | +import { fileURLToPath } from 'node:url'; |
| 7 | +import { manifestExecutable, packageVersion, readManifest, stampVersion, storeVersion } from './msix.mjs'; |
| 8 | + |
| 9 | +const ROOT = fileURLToPath(new URL('..', import.meta.url)); |
| 10 | +const TAURI = join(ROOT, 'src-tauri'); |
| 11 | +const TAURI_CLI = createRequire(import.meta.url).resolve('@tauri-apps/cli/tauri.js'); |
| 12 | +// Pinned so an ARM64 host cannot pack its own binaries into the x64 package. |
| 13 | +const TARGET = 'x86_64-pc-windows-msvc'; |
| 14 | +const UNPLATED_SIZES = [16, 24, 32, 48, 256]; |
| 15 | + |
| 16 | +const run = (file, args) => execFileSync(file, args, { cwd: ROOT, stdio: 'inherit' }); |
| 17 | + |
| 18 | +/** Where the SDK installer put the Windows Kits, since its install path is choosable. */ |
| 19 | +function kitsRoot() { |
| 20 | + let out = ''; |
| 21 | + try { |
| 22 | + out = execFileSync( |
| 23 | + 'reg', |
| 24 | + ['query', 'HKLM\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots', '/v', 'KitsRoot10', '/reg:32'], |
| 25 | + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }, |
| 26 | + ); |
| 27 | + } catch { |
| 28 | + // reg exits non-zero when the key is absent. |
| 29 | + } |
| 30 | + const root = /KitsRoot10\s+REG_SZ\s+(.+)/.exec(out)?.[1]?.trim(); |
| 31 | + if (!root) throw new Error('KitsRoot10 not found in the registry, install the Windows SDK'); |
| 32 | + return root; |
| 33 | +} |
| 34 | + |
| 35 | +function sdkTools() { |
| 36 | + const bin = join(kitsRoot(), 'bin'); |
| 37 | + const versions = existsSync(bin) ? readdirSync(bin).filter((v) => /^\d+\.\d+\.\d+\.\d+$/.test(v)) : []; |
| 38 | + versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); |
| 39 | + for (const v of versions) { |
| 40 | + const tools = { makeappx: join(bin, v, 'x64', 'makeappx.exe'), makepri: join(bin, v, 'x64', 'makepri.exe') }; |
| 41 | + if (existsSync(tools.makeappx) && existsSync(tools.makepri)) return tools; |
| 42 | + } |
| 43 | + throw new Error(`makeappx.exe/makepri.exe not found under ${bin}, install the Windows SDK`); |
| 44 | +} |
| 45 | + |
| 46 | +// Read from the config, so a new sidecar ships in the MSIX as in every Tauri bundle. |
| 47 | +function shippedExecutables() { |
| 48 | + const readConf = (file) => JSON.parse(readFileSync(join(TAURI, file), 'utf8')); |
| 49 | + const conf = readConf('tauri.conf.json'); |
| 50 | + const overlay = readConf('tauri.msix.conf.json'); |
| 51 | + if (overlay.mainBinaryName || overlay.bundle?.externalBin || overlay.bundle?.resources || conf.bundle.resources) { |
| 52 | + throw new Error('build-msix packs only mainBinaryName and externalBin from tauri.conf.json'); |
| 53 | + } |
| 54 | + return [conf.mainBinaryName, ...(conf.bundle.externalBin ?? []).map((bin) => basename(bin))].map((name) => `${name}.exe`); |
| 55 | +} |
| 56 | + |
| 57 | +if (process.platform !== 'win32') throw new Error('build-msix runs on Windows only'); |
| 58 | + |
| 59 | +// Validated before the release compile, so a broken input fails in seconds. |
| 60 | +const { makeappx, makepri } = sdkTools(); |
| 61 | +const version = storeVersion(JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version); |
| 62 | +const manifest = stampVersion(readManifest(), packageVersion(version)); |
| 63 | +const executables = shippedExecutables(); |
| 64 | +if (manifestExecutable(manifest) !== executables[0]) throw new Error(`AppxManifest must launch ${executables[0]}`); |
| 65 | +// cargo, not a fixed path: CARGO_TARGET_DIR or a config target-dir moves it. |
| 66 | +const targetDir = JSON.parse( |
| 67 | + execFileSync('cargo', ['metadata', '--format-version', '1', '--no-deps'], { cwd: TAURI, encoding: 'utf8' }), |
| 68 | +).target_directory; |
| 69 | +const out = join(targetDir, 'msix'); |
| 70 | +const layout = join(out, 'layout'); |
| 71 | +const assets = join(layout, 'Assets'); |
| 72 | + |
| 73 | +rmSync(out, { recursive: true, force: true }); |
| 74 | +mkdirSync(assets, { recursive: true }); |
| 75 | +writeFileSync(join(layout, 'AppxManifest.xml'), manifest); |
| 76 | +for (const icon of ['StoreLogo.png', 'Square44x44Logo.png', 'Square150x150Logo.png']) { |
| 77 | + copyFileSync(join(TAURI, 'icons', icon), join(assets, icon)); |
| 78 | +} |
| 79 | +// Taskbar and Start look up these variants through resources.pri. Without |
| 80 | +// them Windows draws the plated, upscaled 44px tile. |
| 81 | +const sized = join(out, 'sized'); |
| 82 | +run(process.execPath, [TAURI_CLI, 'icon', join(TAURI, 'icons', 'icon.png'), '--png', UNPLATED_SIZES.join(','), '-o', sized]); |
| 83 | +for (const size of UNPLATED_SIZES) { |
| 84 | + copyFileSync(join(sized, `${size}x${size}.png`), join(assets, `Square44x44Logo.targetsize-${size}_altform-unplated.png`)); |
| 85 | +} |
| 86 | +const priconfig = join(out, 'priconfig.xml'); |
| 87 | +run(makepri, ['createconfig', '/cf', priconfig, '/dq', 'en-US_scale-100', '/o']); |
| 88 | + |
| 89 | +// The overlay turns the Tauri bundler off: makeappx packages this variant. |
| 90 | +// The binary reports the same version the Store lists. |
| 91 | +run(process.execPath, [TAURI_CLI, 'build', '--target', TARGET, '--config', join(TAURI, 'tauri.msix.conf.json'), '--config', JSON.stringify({ version })]); |
| 92 | + |
| 93 | +const release = join(targetDir, TARGET, 'release'); |
| 94 | +for (const exe of executables) copyFileSync(join(release, exe), join(layout, exe)); |
| 95 | +run(makepri, ['new', '/pr', layout, '/cf', priconfig, '/mn', join(layout, 'AppxManifest.xml'), '/of', join(layout, 'resources.pri'), '/o']); |
| 96 | + |
| 97 | +const msix = join(out, `ZPLab_${version}_x64.msix`); |
| 98 | +run(makeappx, ['pack', '/d', layout, '/p', msix, '/o']); |
| 99 | +console.log(`Packed ${msix}`); |
| 100 | +// Only this script knows the dir the workflow installs and uploads from. |
| 101 | +if (process.env.GITHUB_ENV) appendFileSync(process.env.GITHUB_ENV, `MSIX_DIR=${out}\n`); |
0 commit comments