/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import fs from 'fs'; import path from 'path'; import { sign, type SignOptions } from '@electron/osx-sign'; import { spawn } from '@malept/cross-spawn-promise'; const root = path.dirname(path.dirname(import.meta.dirname)); const baseDir = path.dirname(import.meta.dirname); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; function getElectronVersion(): string { const npmrc = fs.readFileSync(path.join(root, '.npmrc'), 'utf8'); const target = /^target="(.*)"$/m.exec(npmrc)![1]; return target; } function getEntitlementsForFile(filePath: string): string { if (filePath.includes(gpuHelperAppName)) { return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'); } else if (filePath.includes(rendererHelperAppName)) { return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'); } else if (filePath.includes(pluginHelperAppName)) { return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'); } return path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'); } async function retrySignOnKeychainError(fn: () => Promise, maxRetries: number = 3): Promise { let lastError: Error | undefined; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; // Check if this is the specific keychain error we want to retry const errorMessage = error instanceof Error ? error.message : String(error); const isKeychainError = errorMessage.includes('The specified item could not be found in the keychain.'); if (!isKeychainError || attempt === maxRetries) { throw error; } console.log(`Signing attempt ${attempt} failed with keychain error, retrying...`); console.log(`Error: ${errorMessage}`); const delay = 1000 * Math.pow(2, attempt - 1); console.log(`Waiting ${Math.round(delay)}ms before retry ${attempt}/${maxRetries}...`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } async function main(buildDir?: string): Promise { const tempDir = process.env['AGENT_TEMPDIRECTORY']; const arch = process.env['VSCODE_ARCH']; const identity = process.env['CODESIGN_IDENTITY']; if (!buildDir) { throw new Error('$AGENT_BUILDDIRECTORY not set'); } if (!tempDir) { throw new Error('$AGENT_TEMPDIRECTORY not set'); } const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); const appName = product.nameLong + '.app'; const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); const appOpts: SignOptions = { app: path.join(appRoot, appName), platform: 'darwin', optionsForFile: (filePath) => ({ entitlements: getEntitlementsForFile(filePath), hardenedRuntime: true, }), preAutoEntitlements: false, preEmbedProvisioningProfile: false, keychain: path.join(tempDir, 'buildagent.keychain'), version: getElectronVersion(), identity, }; // Only overwrite plist entries for x64 and arm64 builds, // universal will get its copy from the x64 build. if (arch !== 'universal') { await spawn('plutil', [ '-insert', 'NSAppleEventsUsageDescription', '-string', 'An application in Visual Studio Code wants to use AppleScript.', `${infoPlistPath}` ]); await spawn('plutil', [ '-replace', 'NSMicrophoneUsageDescription', '-string', 'An application in Visual Studio Code wants to use the Microphone.', `${infoPlistPath}` ]); await spawn('plutil', [ '-replace', 'NSCameraUsageDescription', '-string', 'An application in Visual Studio Code wants to use the Camera.', `${infoPlistPath}` ]); } await retrySignOnKeychainError(() => sign(appOpts)); } if (import.meta.main) { main(process.argv[2]).catch(async err => { console.error(err); const tempDir = process.env['AGENT_TEMPDIRECTORY']; if (tempDir) { const keychain = path.join(tempDir, 'buildagent.keychain'); const identities = await spawn('security', ['find-identity', '-p', 'codesigning', '-v', keychain]); console.error(`Available identities:\n${identities}`); const dump = await spawn('security', ['dump-keychain', keychain]); console.error(`Keychain dump:\n${dump}`); } process.exit(1); }); }