macos-app-update-config.mjs 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /** Write and verify the updater configuration sealed into a macOS application. */
  2. import { readFile, writeFile } from 'node:fs/promises'
  3. import { join } from 'node:path'
  4. import { dump, load } from 'js-yaml'
  5. const CONFIG_FILENAME = 'app-update.yml'
  6. const CHANNEL = 'nightly'
  7. function object(value, label) {
  8. if (typeof value !== 'object' || value === null || Array.isArray(value)) {
  9. throw new Error(`desktop macOS update config: ${label} must be an object`)
  10. }
  11. return value
  12. }
  13. function nonEmptyString(value, label) {
  14. if (typeof value !== 'string' || value === '') {
  15. throw new Error(`desktop macOS update config: ${label} must be a non-empty string`)
  16. }
  17. return value
  18. }
  19. /**
  20. * Resolve the one generic macOS feed from the final electron-builder configuration.
  21. * @param {unknown} publish - Final electron-builder publish setting.
  22. * @returns {{ publicUrl: string }} Resolved feed used by the packaged App.
  23. */
  24. export function resolveMacOSAppUpdateFeed(publish) {
  25. if (!Array.isArray(publish) || publish.length !== 1) {
  26. throw new Error('desktop macOS update config: publish must contain exactly one provider')
  27. }
  28. const provider = object(publish[0], 'publish provider')
  29. if (provider.provider !== 'generic' || provider.channel !== CHANNEL) {
  30. throw new Error('desktop macOS update config: publish provider must be generic Nightly')
  31. }
  32. return { publicUrl: nonEmptyString(provider.url, 'publish provider URL') }
  33. }
  34. /**
  35. * Create the electron-updater configuration embedded before code signing.
  36. * @param {{ publicUrl: string }} update - Resolved update feed.
  37. * @param {string} updaterCacheDirName - electron-builder application cache directory.
  38. * @returns {{ provider: 'generic', url: string, channel: 'nightly', updaterCacheDirName: string }} Packaged updater fields.
  39. */
  40. export function createMacOSAppUpdateConfig(update, updaterCacheDirName) {
  41. return {
  42. provider: 'generic',
  43. url: nonEmptyString(update.publicUrl, 'public URL'),
  44. channel: CHANNEL,
  45. updaterCacheDirName: nonEmptyString(updaterCacheDirName, 'updater cache directory'),
  46. }
  47. }
  48. /**
  49. * Write the updater configuration into an assembled App before signing.
  50. * @param {string} resourcesDir - App Contents/Resources directory.
  51. * @param {{ publicUrl: string }} update - Resolved update feed.
  52. * @param {string} updaterCacheDirName - electron-builder application cache directory.
  53. * @returns {Promise<void>} Resolves after the configuration is durable.
  54. */
  55. export async function writeMacOSAppUpdateConfig(resourcesDir, update, updaterCacheDirName) {
  56. const config = createMacOSAppUpdateConfig(update, updaterCacheDirName)
  57. await writeFile(join(resourcesDir, CONFIG_FILENAME), dump(config, { lineWidth: -1, noRefs: true }))
  58. }
  59. /**
  60. * Verify the updater configuration inside an assembled macOS App.
  61. * @param {string} appPath - Application bundle path.
  62. * @param {{ publicUrl: string }} update - Expected update feed.
  63. * @param {string | undefined} updaterCacheDirName - Exact cache directory when known.
  64. * @returns {Promise<void>} Resolves when the packaged configuration matches the release destination.
  65. */
  66. export async function verifyMacOSAppUpdateConfig(appPath, update, updaterCacheDirName = undefined) {
  67. const path = join(appPath, 'Contents', 'Resources', CONFIG_FILENAME)
  68. let parsed
  69. try {
  70. parsed = load(await readFile(path, 'utf8'))
  71. }
  72. catch (error) {
  73. throw new Error(`desktop macOS update config: cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`)
  74. }
  75. const config = object(parsed, CONFIG_FILENAME)
  76. if (config.provider !== 'generic' || config.url !== update.publicUrl || config.channel !== CHANNEL) {
  77. throw new Error(`desktop macOS update config: ${path} does not match ${update.publicUrl}`)
  78. }
  79. const actualCacheDirName = nonEmptyString(config.updaterCacheDirName, `${CONFIG_FILENAME}.updaterCacheDirName`)
  80. if (updaterCacheDirName !== undefined && actualCacheDirName !== updaterCacheDirName) {
  81. throw new Error(`desktop macOS update config: ${path} has updater cache directory ${actualCacheDirName}; expected ${updaterCacheDirName}`)
  82. }
  83. }