import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import { resolveDesktopAppId, resolveMacOSNotarizationEnvironment, resolveMacOSSigningEnvironment, } from './desktop-release-environment.mjs' import { notarizeMacOSDiskImageArtifact } from './notarize-macos-disk-images.mjs' import { verifyMacOSSignatureAfterSign } from './verify-macos-signature.mjs' import { createWindowsTokenSigner, installWindowsNsisBootstrapSigner, resolveWindowsUpdatePublisher, scrubWindowsSigningEnvironment, } from './windows-sign.mjs' import { resolveDesktopAutoUpdateConfig } from './desktop-auto-update-environment.mjs' import { resolveDesktopPolicyEnvironment } from './desktop-policy-environment.mjs' import { desktopTargetBuildPaths, resolveDesktopBuildTarget } from './desktop-build-paths.mjs' import { installWindowsDirectoryInstaller } from './windows-directory-installer.mjs' import { preserveWindowsRuntimeSignature } from './windows-runtime-signature.mjs' import { resolveMacOSAppUpdateFeed, verifyMacOSAppUpdateConfig, writeMacOSAppUpdateConfig, } from './macos-app-update-config.mjs' /** * Create electron-builder configuration from one release environment. * @param {NodeJS.ProcessEnv} env - Packaging environment. * @param {NodeJS.Platform} hostPlatform - Build-host platform used when no explicit target is present. * @param {string} hostArch - Build-host architecture used when no explicit target is present. * @param {string | undefined} preparedRuntime - Verified private dsh tree for installed-update qualification; ordinary releases use the target tree. * @returns {object} electron-builder configuration. */ export function createElectronBuilderConfig( env = process.env, hostPlatform = process.platform, hostArch = process.arch, preparedRuntime = undefined, ) { const appId = resolveDesktopAppId(env) const policy = resolveDesktopPolicyEnvironment(env) const targetPlatform = env.DSH_DESKTOP_TARGET_PLATFORM const resolvedPlatform = targetPlatform ?? hostPlatform const resolvedArch = env.DSH_DESKTOP_TARGET_ARCH ?? hostArch if (env.DSH_DESKTOP_UNSIGNED !== undefined && !['0', '1'].includes(env.DSH_DESKTOP_UNSIGNED)) { throw new Error('desktop package: DSH_DESKTOP_UNSIGNED must be 0 or 1') } const unsigned = env.DSH_DESKTOP_UNSIGNED === '1' if (unsigned && resolvedPlatform !== 'win32') throw new Error('desktop package: unsigned builds require Windows') const packagesMacOS = targetPlatform === 'darwin' || (targetPlatform === undefined && hostPlatform === 'darwin') const packagesWindows = resolvedPlatform === 'win32' if (resolvedPlatform === 'win32') installWindowsDirectoryInstaller() const macOSSigning = packagesMacOS ? resolveMacOSSigningEnvironment(env) : undefined if (packagesMacOS) resolveMacOSNotarizationEnvironment(env) const buildPaths = desktopTargetBuildPaths(resolveDesktopBuildTarget(env, hostPlatform, hostArch)) let primaryRuntimeDestination const windowsSigner = packagesWindows && !unsigned ? createWindowsTokenSigner({ certificateFile: env.DSH_DESKTOP_WINDOWS_CER_FILE, signTool: env.DSH_DESKTOP_WINDOWS_SIGNTOOL, tokenPin: env.DSH_DESKTOP_WINDOWS_TOKEN_PIN, keyContainer: env.DSH_DESKTOP_WINDOWS_KEY_CONTAINER, preserveSignature: async path => primaryRuntimeDestination === undefined ? false : preserveWindowsRuntimeSignature(path, { sourceRoot: join(buildPaths.runtime, 'primary-runtime'), destinationRoot: primaryRuntimeDestination, runDirectory: env.DSH_DESKTOP_PACKAGING_RUN_DIR, }), }) : undefined if (windowsSigner !== undefined) { installWindowsNsisBootstrapSigner({ sign: windowsSigner }) } const update = unsigned ? undefined : resolveDesktopAutoUpdateConfig(env, resolvedPlatform, resolvedArch) if (preparedRuntime !== undefined) buildPaths.dsh = preparedRuntime return { appId, extraMetadata: { dshDesktopAppId: appId, dshMandatoryUpdatePolicy: policy }, productName: 'DeepSeek Harness', artifactName: 'deepseek-harness-${version}-${os}-${arch}.${ext}', directories: { output: unsigned ? join(buildPaths.root, 'unsigned-artifacts') : buildPaths.artifacts }, asar: true, electronDist: buildPaths.electron, electronFuses: { runAsNode: true }, beforeBuild: async () => { if (resolvedPlatform !== 'win32') return true await promisify(execFile)('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', fileURLToPath(new URL('./prepare-windows-installer.ps1', import.meta.url)), '-OutputDirectory', join(buildPaths.root, 'installer-ui')], { env: scrubWindowsSigningEnvironment(env), windowsHide: true, }) if (windowsSigner !== undefined) { await windowsSigner({ path: join(buildPaths.root, 'installer-ui', 'window-frame.dll'), hash: 'sha256', isNest: false }) } // A falsy result tells electron-builder to omit its production node_modules collection. return true }, files: [ 'lib/main.js', 'lib/preload-app.cjs', 'lib/preload-mandatory.cjs', 'lib/preload-update-dialog.cjs', 'renderer/**/*', 'package.json', { from: buildPaths.dsh, to: 'dsh', filter: ['**/*'] }, // electron-builder excludes a source directory's root node_modules. { from: join(buildPaths.dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] }, ], asarUnpack: [ '**/*.{node,dylib,dll,so,exe}', '**/*.so.*', '**/spawn-helper', '**/@vscode/ripgrep/bin/rg', ], extraResources: [ { from: buildPaths.runtime, to: 'runtime' }, { from: fileURLToPath(new URL('../resources/icon-windows.png', import.meta.url)), to: 'icon.png' }, ], mac: { icon: fileURLToPath(new URL('../resources/icon-macos.png', import.meta.url)), category: 'public.app-category.developer-tools', identity: macOSSigning?.signingIdentity, forceCodeSigning: true, hardenedRuntime: true, // ASAR-unpacked native runtime files are pre-signed; PAK resources are sealed by their enclosing bundle. signIgnore: ['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)', '/Contents/Resources/runtime/primary-runtime(?:/|$)', '\\.pak$'], notarize: true, target: ['dmg', 'zip'], }, dmg: { sign: true, writeUpdateInfo: false, }, beforePack: async context => { if (windowsSigner !== undefined) primaryRuntimeDestination = join(context.appOutDir, 'resources', 'runtime', 'primary-runtime') if (policy === undefined) return const { resolveDesktopPolicyConfig } = await import('../lib/types/mandatory-update-policy.js') resolveDesktopPolicyConfig(policy) }, afterPack: async context => { const { verifyDesktopRuntime, writeDesktopRuntime } = await import('../lib/types/runtime-tree.js') const resourcesDir = context.packager.getResourcesDir(context.appOutDir) if (resolvedPlatform === 'darwin' && update !== undefined) { await writeMacOSAppUpdateConfig(resourcesDir, resolveMacOSAppUpdateFeed(context.packager.config.publish), context.packager.appInfo.updaterCacheDirName) } if (resolvedPlatform === 'win32' && !unsigned) { // Windows signs copied executable resources before afterPack runs. const prepared = await verifyDesktopRuntime(buildPaths.dsh, context.packager.appInfo.version, { platform: resolvedPlatform, arch: resolvedArch }) writeDesktopRuntime(buildPaths.dsh, prepared.release, prepared.sharedPackages.map(entry => entry.name), { platform: resolvedPlatform, arch: resolvedArch }) } await verifyDesktopRuntime(buildPaths.dsh, context.packager.appInfo.version, { platform: resolvedPlatform, arch: resolvedArch }) }, afterSign: async context => { if (context.electronPlatformName !== 'darwin') return const appPath = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`) if (update !== undefined) { await verifyMacOSAppUpdateConfig(appPath, resolveMacOSAppUpdateFeed(context.packager.config.publish), context.packager.appInfo.updaterCacheDirName) } verifyMacOSSignatureAfterSign(context, macOSSigning ?? resolveMacOSSigningEnvironment(env)) }, artifactBuildCompleted: artifact => { if (!artifact.file.endsWith('.dmg')) return return notarizeMacOSDiskImageArtifact( artifact, env, macOSSigning ?? resolveMacOSSigningEnvironment(env), ) }, win: { icon: fileURLToPath(new URL('../resources/icon-windows.png', import.meta.url)), forceCodeSigning: !unsigned, signtoolOptions: { sign: windowsSigner, publisherName: windowsSigner === undefined ? undefined : resolveWindowsUpdatePublisher(env.DSH_DESKTOP_WINDOWS_CER_FILE), signingHashAlgorithms: ['sha256'], }, target: ['nsis'], }, linux: { category: 'Development', target: ['AppImage'], }, nsis: { installerSidebar: join(buildPaths.root, 'installer-ui', 'uninstaller-sidebar.bmp'), uninstallerSidebar: join(buildPaths.root, 'installer-ui', 'uninstaller-sidebar.bmp'), include: fileURLToPath(new URL('./installer.nsh', import.meta.url)), oneClick: false, perMachine: false, allowElevation: false, allowToChangeInstallationDirectory: false, installerLanguages: ['en_US', 'zh_CN'], differentialPackage: true, }, detectUpdateChannel: false, publish: update === undefined ? null : [{ provider: 'generic', url: update.publicUrl, channel: 'nightly' }], } }