electron-builder-config.mjs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { join } from 'node:path'
  2. import { fileURLToPath } from 'node:url'
  3. import { execFile } from 'node:child_process'
  4. import { promisify } from 'node:util'
  5. import {
  6. resolveDesktopAppId,
  7. resolveMacOSNotarizationEnvironment,
  8. resolveMacOSSigningEnvironment,
  9. } from './desktop-release-environment.mjs'
  10. import { notarizeMacOSDiskImageArtifact } from './notarize-macos-disk-images.mjs'
  11. import { verifyMacOSSignatureAfterSign } from './verify-macos-signature.mjs'
  12. import {
  13. createWindowsTokenSigner,
  14. installWindowsNsisBootstrapSigner,
  15. resolveWindowsUpdatePublisher,
  16. scrubWindowsSigningEnvironment,
  17. } from './windows-sign.mjs'
  18. import { resolveDesktopAutoUpdateConfig } from './desktop-auto-update-environment.mjs'
  19. import { resolveDesktopPolicyEnvironment } from './desktop-policy-environment.mjs'
  20. import { desktopTargetBuildPaths, resolveDesktopBuildTarget } from './desktop-build-paths.mjs'
  21. import { installWindowsDirectoryInstaller } from './windows-directory-installer.mjs'
  22. import { preserveWindowsRuntimeSignature } from './windows-runtime-signature.mjs'
  23. import {
  24. resolveMacOSAppUpdateFeed,
  25. verifyMacOSAppUpdateConfig,
  26. writeMacOSAppUpdateConfig,
  27. } from './macos-app-update-config.mjs'
  28. /**
  29. * Create electron-builder configuration from one release environment.
  30. * @param {NodeJS.ProcessEnv} env - Packaging environment.
  31. * @param {NodeJS.Platform} hostPlatform - Build-host platform used when no explicit target is present.
  32. * @param {string} hostArch - Build-host architecture used when no explicit target is present.
  33. * @param {string | undefined} preparedRuntime - Verified private dsh tree for installed-update qualification; ordinary releases use the target tree.
  34. * @returns {object} electron-builder configuration.
  35. */
  36. export function createElectronBuilderConfig(
  37. env = process.env,
  38. hostPlatform = process.platform,
  39. hostArch = process.arch,
  40. preparedRuntime = undefined,
  41. ) {
  42. const appId = resolveDesktopAppId(env)
  43. const policy = resolveDesktopPolicyEnvironment(env)
  44. const targetPlatform = env.DSH_DESKTOP_TARGET_PLATFORM
  45. const resolvedPlatform = targetPlatform ?? hostPlatform
  46. const resolvedArch = env.DSH_DESKTOP_TARGET_ARCH ?? hostArch
  47. if (env.DSH_DESKTOP_UNSIGNED !== undefined && !['0', '1'].includes(env.DSH_DESKTOP_UNSIGNED)) {
  48. throw new Error('desktop package: DSH_DESKTOP_UNSIGNED must be 0 or 1')
  49. }
  50. const unsigned = env.DSH_DESKTOP_UNSIGNED === '1'
  51. if (unsigned && resolvedPlatform !== 'win32') throw new Error('desktop package: unsigned builds require Windows')
  52. const packagesMacOS = targetPlatform === 'darwin' || (targetPlatform === undefined && hostPlatform === 'darwin')
  53. const packagesWindows = resolvedPlatform === 'win32'
  54. if (resolvedPlatform === 'win32') installWindowsDirectoryInstaller()
  55. const macOSSigning = packagesMacOS ? resolveMacOSSigningEnvironment(env) : undefined
  56. if (packagesMacOS) resolveMacOSNotarizationEnvironment(env)
  57. const buildPaths = desktopTargetBuildPaths(resolveDesktopBuildTarget(env, hostPlatform, hostArch))
  58. let primaryRuntimeDestination
  59. const windowsSigner = packagesWindows && !unsigned
  60. ? createWindowsTokenSigner({
  61. certificateFile: env.DSH_DESKTOP_WINDOWS_CER_FILE,
  62. signTool: env.DSH_DESKTOP_WINDOWS_SIGNTOOL,
  63. tokenPin: env.DSH_DESKTOP_WINDOWS_TOKEN_PIN,
  64. keyContainer: env.DSH_DESKTOP_WINDOWS_KEY_CONTAINER,
  65. preserveSignature: async path => primaryRuntimeDestination === undefined ? false : preserveWindowsRuntimeSignature(path, {
  66. sourceRoot: join(buildPaths.runtime, 'primary-runtime'),
  67. destinationRoot: primaryRuntimeDestination,
  68. runDirectory: env.DSH_DESKTOP_PACKAGING_RUN_DIR,
  69. }),
  70. })
  71. : undefined
  72. if (windowsSigner !== undefined) {
  73. installWindowsNsisBootstrapSigner({ sign: windowsSigner })
  74. }
  75. const update = unsigned ? undefined : resolveDesktopAutoUpdateConfig(env, resolvedPlatform, resolvedArch)
  76. if (preparedRuntime !== undefined) buildPaths.dsh = preparedRuntime
  77. return {
  78. appId,
  79. extraMetadata: { dshDesktopAppId: appId, dshMandatoryUpdatePolicy: policy },
  80. productName: 'DeepSeek Harness',
  81. artifactName: 'deepseek-harness-${version}-${os}-${arch}.${ext}',
  82. directories: { output: unsigned ? join(buildPaths.root, 'unsigned-artifacts') : buildPaths.artifacts },
  83. asar: true,
  84. electronDist: buildPaths.electron,
  85. electronFuses: { runAsNode: true },
  86. beforeBuild: async () => {
  87. if (resolvedPlatform !== 'win32') return true
  88. await promisify(execFile)('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
  89. fileURLToPath(new URL('./prepare-windows-installer.ps1', import.meta.url)),
  90. '-OutputDirectory', join(buildPaths.root, 'installer-ui')], {
  91. env: scrubWindowsSigningEnvironment(env), windowsHide: true,
  92. })
  93. if (windowsSigner !== undefined) {
  94. await windowsSigner({ path: join(buildPaths.root, 'installer-ui', 'window-frame.dll'), hash: 'sha256', isNest: false })
  95. }
  96. // A falsy result tells electron-builder to omit its production node_modules collection.
  97. return true
  98. },
  99. files: [
  100. 'lib/main.js',
  101. 'lib/preload-app.cjs',
  102. 'lib/preload-mandatory.cjs',
  103. 'lib/preload-update-dialog.cjs',
  104. 'renderer/**/*',
  105. 'package.json',
  106. { from: buildPaths.dsh, to: 'dsh', filter: ['**/*'] },
  107. // electron-builder excludes a source directory's root node_modules.
  108. { from: join(buildPaths.dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] },
  109. ],
  110. asarUnpack: [
  111. '**/*.{node,dylib,dll,so,exe}',
  112. '**/*.so.*',
  113. '**/spawn-helper',
  114. '**/@vscode/ripgrep/bin/rg',
  115. ],
  116. extraResources: [
  117. { from: buildPaths.runtime, to: 'runtime' },
  118. { from: fileURLToPath(new URL('../resources/icon-windows.png', import.meta.url)), to: 'icon.png' },
  119. ],
  120. mac: {
  121. icon: fileURLToPath(new URL('../resources/icon-macos.png', import.meta.url)),
  122. category: 'public.app-category.developer-tools',
  123. identity: macOSSigning?.signingIdentity,
  124. forceCodeSigning: true,
  125. hardenedRuntime: true,
  126. // ASAR-unpacked native runtime files are pre-signed; PAK resources are sealed by their enclosing bundle.
  127. signIgnore: ['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)', '/Contents/Resources/runtime/primary-runtime(?:/|$)', '\\.pak$'],
  128. notarize: true,
  129. target: ['dmg', 'zip'],
  130. },
  131. dmg: {
  132. sign: true,
  133. writeUpdateInfo: false,
  134. },
  135. beforePack: async context => {
  136. if (windowsSigner !== undefined) primaryRuntimeDestination = join(context.appOutDir, 'resources', 'runtime', 'primary-runtime')
  137. if (policy === undefined) return
  138. const { resolveDesktopPolicyConfig } = await import('../lib/types/mandatory-update-policy.js')
  139. resolveDesktopPolicyConfig(policy)
  140. },
  141. afterPack: async context => {
  142. const { verifyDesktopRuntime, writeDesktopRuntime } = await import('../lib/types/runtime-tree.js')
  143. const resourcesDir = context.packager.getResourcesDir(context.appOutDir)
  144. if (resolvedPlatform === 'darwin' && update !== undefined) {
  145. await writeMacOSAppUpdateConfig(resourcesDir, resolveMacOSAppUpdateFeed(context.packager.config.publish),
  146. context.packager.appInfo.updaterCacheDirName)
  147. }
  148. if (resolvedPlatform === 'win32' && !unsigned) {
  149. // Windows signs copied executable resources before afterPack runs.
  150. const prepared = await verifyDesktopRuntime(buildPaths.dsh,
  151. context.packager.appInfo.version, { platform: resolvedPlatform, arch: resolvedArch })
  152. writeDesktopRuntime(buildPaths.dsh, prepared.release, prepared.sharedPackages.map(entry => entry.name),
  153. { platform: resolvedPlatform, arch: resolvedArch })
  154. }
  155. await verifyDesktopRuntime(buildPaths.dsh,
  156. context.packager.appInfo.version, { platform: resolvedPlatform, arch: resolvedArch })
  157. },
  158. afterSign: async context => {
  159. if (context.electronPlatformName !== 'darwin') return
  160. const appPath = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
  161. if (update !== undefined) {
  162. await verifyMacOSAppUpdateConfig(appPath, resolveMacOSAppUpdateFeed(context.packager.config.publish),
  163. context.packager.appInfo.updaterCacheDirName)
  164. }
  165. verifyMacOSSignatureAfterSign(context, macOSSigning ?? resolveMacOSSigningEnvironment(env))
  166. },
  167. artifactBuildCompleted: artifact => {
  168. if (!artifact.file.endsWith('.dmg')) return
  169. return notarizeMacOSDiskImageArtifact(
  170. artifact,
  171. env,
  172. macOSSigning ?? resolveMacOSSigningEnvironment(env),
  173. )
  174. },
  175. win: {
  176. icon: fileURLToPath(new URL('../resources/icon-windows.png', import.meta.url)),
  177. forceCodeSigning: !unsigned,
  178. signtoolOptions: {
  179. sign: windowsSigner,
  180. publisherName: windowsSigner === undefined ? undefined : resolveWindowsUpdatePublisher(env.DSH_DESKTOP_WINDOWS_CER_FILE),
  181. signingHashAlgorithms: ['sha256'],
  182. },
  183. target: ['nsis'],
  184. },
  185. linux: {
  186. category: 'Development',
  187. target: ['AppImage'],
  188. },
  189. nsis: {
  190. installerSidebar: join(buildPaths.root, 'installer-ui', 'uninstaller-sidebar.bmp'),
  191. uninstallerSidebar: join(buildPaths.root, 'installer-ui', 'uninstaller-sidebar.bmp'),
  192. include: fileURLToPath(new URL('./installer.nsh', import.meta.url)),
  193. oneClick: false,
  194. perMachine: false,
  195. allowElevation: false,
  196. allowToChangeInstallationDirectory: false,
  197. installerLanguages: ['en_US', 'zh_CN'],
  198. differentialPackage: true,
  199. },
  200. detectUpdateChannel: false,
  201. publish: update === undefined ? null : [{ provider: 'generic', url: update.publicUrl, channel: 'nightly' }],
  202. }
  203. }