From e34dc01d2256be873bbb91ab80acaf0878e59954 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Thu, 10 Sep 2026 14:22:57 +0000 Subject: [PATCH] =?UTF-8?q?fix(electron):=20=E4=BF=AE=E5=A4=8D=20macOS=20?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E9=92=A5=E5=8C=99=E4=B8=B2=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E4=BC=A0=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/package.json | 4 +- electron/scripts/patch-mac-keychain.js | 50 +++++++++++++++++++++ electron/scripts/patch-mac-keychain.test.js | 47 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 electron/scripts/patch-mac-keychain.js create mode 100644 electron/scripts/patch-mac-keychain.test.js diff --git a/electron/package.json b/electron/package.json index bdf6559b0..b8effe741 100755 --- a/electron/package.json +++ b/electron/package.json @@ -8,9 +8,9 @@ "start": "electron-forge start", "start-quiet": "sleep 3 && electron-forge start", "build": "electron-builder", - "build-mac": "electron-builder --mac", + "build-mac": "node scripts/patch-mac-keychain.js && electron-builder --mac", "build-win": "electron-builder --win", - "build-mac-publish": "electron-builder --publish always --mac", + "build-mac-publish": "node scripts/patch-mac-keychain.js && electron-builder --publish always --mac", "build-win-publish": "electron-builder --publish always --win", "package": "electron-forge package", "make": "electron-forge make", diff --git a/electron/scripts/patch-mac-keychain.js b/electron/scripts/patch-mac-keychain.js new file mode 100644 index 000000000..1de967df2 --- /dev/null +++ b/electron/scripts/patch-mac-keychain.js @@ -0,0 +1,50 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const {createRequire} = require('node:module'); + +// Remove this workaround once the supported builder uses the keychain password. +const replacements = [ + ['return await importCerts(keychainFile, certPaths, cscPasswords);', + 'return await importCerts(keychainFile, certPaths, cscPasswords, keychainPassword);'], + ['async function importCerts(keychainFile, paths, keyPasswords) {', + 'async function importCerts(keychainFile, paths, keyPasswords, keychainPassword) {'], + ['["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]', + '["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile]'], +]; + +function patchSource(source, version) { + if (!['26.8.1', '26.15.3'].includes(version)) { + throw new Error(`Unsupported app-builder-lib ${version}; review the macOS keychain workaround before building.`); + } + const count = (text) => source.split(text).length - 1; + if (replacements.every(([before, after]) => count(before) === 0 && count(after) === 1)) { + return source; + } + if (!replacements.every(([before, after]) => count(before) === 1 && count(after) === 0)) { + throw new Error('Unexpected macCodeSign.js source; refusing to apply the keychain workaround.'); + } + for (const [before, after] of replacements) { + source = source.replace(before, after); + } + return source; +} + +function patchInstalledBuilder() { + // Resolve from electron-builder so a nested dependency is patched correctly. + const builderRequire = createRequire(require.resolve('electron-builder/package.json')); + const packageFile = builderRequire.resolve('app-builder-lib/package.json'); + const {version} = JSON.parse(fs.readFileSync(packageFile, 'utf8')); + const file = path.join(path.dirname(packageFile), 'out/codeSign/macCodeSign.js'); + const source = fs.readFileSync(file, 'utf8'); + const patched = patchSource(source, version); + if (patched !== source) { + fs.writeFileSync(file, patched); + } + console.log(`macOS keychain workaround ready (app-builder-lib ${version}).`); +} + +if (require.main === module) { + patchInstalledBuilder(); +} + +module.exports = {patchSource}; diff --git a/electron/scripts/patch-mac-keychain.test.js b/electron/scripts/patch-mac-keychain.test.js new file mode 100644 index 000000000..3904533cb --- /dev/null +++ b/electron/scripts/patch-mac-keychain.test.js @@ -0,0 +1,47 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const {patchSource} = require('./patch-mac-keychain'); + +const source = ` +async function createKeychain(keychainFile, certPaths, cscPasswords) { + const keychainPassword = "random-keychain-password"; + return await importCerts(keychainFile, certPaths, cscPasswords); +} +async function importCerts(keychainFile, paths, keyPasswords) { + for (let i = 0; i < paths.length; i++) { + const password = keyPasswords[i] ?? ""; + await exec("/usr/bin/security", ["import", paths[i], "-k", keychainFile, "-P", password]); + await exec("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]); + } +} +`; + +for (const version of ['26.8.1', '26.15.3']) { + test(`patch is idempotent for ${version}`, () => { + const patched = patchSource(source, version); + assert.notEqual(patched, source); + assert.equal(patchSource(patched, version), patched); + }); +} + +for (const passwords of [[''], ['certificate-password'], ['distribution-password', 'installer-password']]) { + test(`keeps certificate passwords separate: ${passwords.length} certificate(s), empty=${passwords[0] === ''}`, async () => { + const calls = []; + const context = vm.createContext({exec: async (command, args) => calls.push({command, args})}); + vm.runInContext(patchSource(source, '26.15.3'), context); + await context.createKeychain('test.keychain', passwords.map((_, i) => `cert-${i}.p12`), passwords); + assert.equal(calls.length, passwords.length * 2); + for (let i = 0; i < passwords.length; i++) { + assert.equal(calls[i * 2].args.at(-1), passwords[i]); + assert.equal(calls[i * 2 + 1].args.at(-2), 'random-keychain-password'); + } + }); +} + +test('rejects unknown versions and changed or partially patched sources', () => { + assert.throws(() => patchSource(source, '27.0.0'), /Unsupported/); + assert.throws(() => patchSource(source.replace('return await importCerts', 'return importCerts'), '26.15.3'), /Unexpected/); + assert.throws(() => patchSource(source + source, '26.15.3'), /Unexpected/); + assert.throws(() => patchSource(source.replace('cscPasswords);', 'cscPasswords, keychainPassword);'), '26.15.3'), /Unexpected/); +});