electron-builder 27.0.0-alpha.7 → 27.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,47 +1,34 @@
1
1
  import { createRequire } from "node:module";
2
+ import { AZURE_KNOWN_FIELDS, ELECTRON_DOWNLOAD_DROPPED, MAC_SIGN_FIELDS, MAC_SIGN_REMOVED_FIELDS, MAC_UNIVERSAL_FIELDS } from "app-builder-lib/internal";
2
3
  import { log, orNullIfFileNotExist } from "builder-util";
3
4
  import { promises as fs } from "fs";
4
5
  import * as path from "path";
5
6
  import { loadTypeScript, migrateProgrammaticSource } from "./migrate-schema-programmatic.js";
6
7
  const _require = createRequire(import.meta.url);
7
8
  // ─── Pure migration logic ─────────────────────────────────────────────────────
8
- // Azure Trusted Signing typed fields in v27 (everything else is an extra key → additionalMetadata).
9
- // "type" is included so it is not mistakenly moved to additionalMetadata if already present.
10
- export const AZURE_KNOWN_FIELDS = new Set([
11
- "type",
12
- "endpoint",
13
- "codeSigningAccountName",
14
- "certificateProfileName",
15
- "publisherName",
16
- "fileDigest",
17
- "timestampRfc3161",
18
- "timestampDigest",
19
- "additionalMetadata",
20
- ]);
21
- // macOS signing fields that moved from the platform root into the `sign` (ElectronSignOptions) bag.
22
- // `signIgnore` is renamed to `sign.ignore` separately (the @electron/osx-sign canonical name).
23
- export const MAC_SIGN_FIELDS = [
24
- "identity",
25
- "entitlements",
26
- "entitlementsInherit",
27
- "entitlementsLoginHelper",
28
- "provisioningProfile",
29
- "type",
30
- "binaries",
31
- "requirements",
32
- "hardenedRuntime",
33
- "gatekeeperAssess",
34
- "strictVerify",
35
- "preAutoEntitlements",
36
- "timestamp",
37
- "additionalArguments",
38
- ];
39
- // Universal-build fields that moved from the platform root into the `universal` (ElectronUniversalOptions) bag.
40
- export const MAC_UNIVERSAL_FIELDS = ["mergeASARs", "singleArchFiles", "x64ArchFiles"];
9
+ // The v26 -> v27 key mapping is owned by app-builder-lib so the runtime guard
10
+ // (`checkLegacyConfiguration`) and this migrator cannot drift apart. Re-exported here because these
11
+ // names are part of this module's existing public surface.
12
+ export { AZURE_KNOWN_FIELDS, ELECTRON_DOWNLOAD_DROPPED, MAC_SIGN_FIELDS, MAC_SIGN_REMOVED_FIELDS, MAC_UNIVERSAL_FIELDS } from "app-builder-lib/internal";
41
13
  // Advisory surfaced (informational only — never rewrites the config) when a project builds an nsis-web target. As of v27,
42
14
  // AppUpdater.disableWebInstaller defaults to true, so the auto-updater no longer downloads web-installer packages unless the app opts in at runtime.
43
15
  export const NSIS_WEB_ADVISORY = "nsis-web target detected. In v27, autoUpdater.disableWebInstaller defaults to true, so NSIS web-installer packages are not downloaded by default. " +
44
16
  "If your app relies on nsis-web installers for auto-updates, set autoUpdater.disableWebInstaller = false in your main process (this is an electron-updater runtime setting, not a build-config key).";
17
+ // Advisory surfaced when a mac/mas/masDev config does not name its own entitlements file. v27 stopped granting
18
+ // allow-unsigned-executable-memory and disable-library-validation in the bundled default, which only fails at runtime.
19
+ export const MAC_ENTITLEMENTS_ADVISORY = "In v27 the bundled default macOS entitlements grant only com.apple.security.cs.allow-jit (allow-unsigned-executable-memory and disable-library-validation are no longer granted). " +
20
+ "If you rely on the default (no mac.sign.entitlements and no build/entitlements.mac.plist) and your app loads native modules, frameworks, or plugins signed by another Team ID (or unsigned), " +
21
+ "add those entitlements back in build/entitlements.mac.plist. See https://www.electron.build/docs/migration/v27-breaking-changes#macos-default-entitlements-tightened";
22
+ /**
23
+ * macOS signing/universal fields that v26 accepted as `null` (meaning "unset") but whose v27 type has no `null`
24
+ * branch, so a moved `null` would fail validation. They are dropped instead of moved. `identity: null` is not in
25
+ * this list — it means "skip signing" and `sign.identity` still accepts it.
26
+ */
27
+ export const MAC_NULL_MEANS_UNSET_FIELDS = new Set(["type", "provisioningProfile", "binaries", "signIgnore", "singleArchFiles", "x64ArchFiles"]);
28
+ /** Platform keys that accepted a platform-level `asarUnpack` in v26. */
29
+ export const ASAR_PLATFORM_KEYS = ["mac", "mas", "masDev", "win", "linux"];
30
+ /** v26 `snap` options that the v27 `snapcraft.core24` shape does not accept (core24 uses the snapcraft CLI directly). */
31
+ export const SNAP_CORE24_UNSUPPORTED = ["allowNativeWayland", "useTemplateApp"];
45
32
  /** True when `target` (a string, a `{ target }` object, or an array of either) selects the nsis-web target. */
46
33
  function hasNsisWebTarget(target) {
47
34
  if (target == null) {
@@ -62,6 +49,26 @@ function hasNsisWebTarget(target) {
62
49
  function detectNsisWebTarget(config) {
63
50
  return hasNsisWebTarget(config.win?.target) || hasNsisWebTarget(config.target);
64
51
  }
52
+ /**
53
+ * True when a mac/mas/masDev section signs with electron-builder's defaults without naming its own entitlements
54
+ * file. A custom signer (string/function `sign`) or `sign.identity: null` (skip signing) bypasses the defaults.
55
+ */
56
+ function detectDefaultMacEntitlements(config) {
57
+ return ["mac", "mas", "masDev"].some(p => {
58
+ const platform = config[p];
59
+ if (!isPlainObject(platform)) {
60
+ return false;
61
+ }
62
+ const sign = platform.sign;
63
+ if (typeof sign === "string" || typeof sign === "function") {
64
+ return false;
65
+ }
66
+ return !(isPlainObject(sign) && (sign.entitlements != null || sign.identity === null));
67
+ });
68
+ }
69
+ function isPlainObject(value) {
70
+ return value != null && typeof value === "object" && !Array.isArray(value);
71
+ }
65
72
  /**
66
73
  * Applies all v26→v27 config transformations to a parsed config object.
67
74
  * Pure function — no I/O, safe to call in tests.
@@ -130,11 +137,20 @@ export function migrateConfig(raw) {
130
137
  }
131
138
  }
132
139
  if ("nativeRebuilder" in c) {
133
- sub.rebuildMode = c.nativeRebuilder;
140
+ if (c.nativeRebuilder === "legacy") {
141
+ // v27 dropped the app-builder binary rebuilder; rebuildMode only selects @electron/rebuild's mode.
142
+ warnings.push('nativeRebuilder: "legacy" (the app-builder binary rebuilder) was removed in v27 — native modules are always rebuilt with @electron/rebuild. ' +
143
+ 'The value was dropped, so the default nativeModules.rebuildMode ("sequential") applies.');
144
+ }
145
+ else {
146
+ sub.rebuildMode = c.nativeRebuilder;
147
+ }
134
148
  delete c.nativeRebuilder;
135
149
  changes.push({ key: "nativeRebuilder", description: "renamed nativeRebuilder → nativeModules.rebuildMode" });
136
150
  }
137
- c.nativeModules = sub;
151
+ if (Object.keys(sub).length > 0 || c.nativeModules != null) {
152
+ c.nativeModules = sub;
153
+ }
138
154
  }
139
155
  // ── 5. Legacy asar keys → asarUnpack ─────────────────────────────────────
140
156
  // Hyphenated root-level variants
@@ -166,12 +182,12 @@ export function migrateConfig(raw) {
166
182
  }
167
183
  // ── 7. GithubOptions.vPrefixedTagName → tagNamePrefix ─────────────────────
168
184
  if (c.publish != null) {
169
- migratePublishEntries(c, "publish", changes);
185
+ migratePublishEntries(c, "publish", changes, warnings);
170
186
  }
171
- // Also handle publish nested inside mac/win/linux
172
- for (const platform of ["mac", "win", "linux"]) {
173
- if (c[platform]?.publish != null) {
174
- migratePublishEntries(c[platform], "publish", changes);
187
+ // Also handle publish nested inside any platform/target section (mac, win, nsis, dmg, snap, …)
188
+ for (const value of Object.values(c)) {
189
+ if (isPlainObject(value) && value.publish != null) {
190
+ migratePublishEntries(value, "publish", changes, warnings);
175
191
  }
176
192
  }
177
193
  // ── 8. snap → snapcraft restructuring ─────────────────────────────────────
@@ -197,10 +213,40 @@ export function migrateConfig(raw) {
197
213
  delete sq.noMsi;
198
214
  changes.push({ key: "squirrelWindows.noMsi", description: "replaced squirrelWindows.noMsi → squirrelWindows.msi (inverted boolean)" });
199
215
  }
216
+ // ── 10b. squirrelWindows.customSquirrelVendorDir removed (not auto-migrated) ──
217
+ // The v27 replacement (a toolsets.squirrel ToolsetCustom bundle) needs a different directory layout, so the
218
+ // key is left in place: the build then fails with a targeted message instead of silently switching the
219
+ // vendored Squirrel binaries back to the default bundle.
220
+ if (isPlainObject(c.squirrelWindows) && "customSquirrelVendorDir" in c.squirrelWindows) {
221
+ warnings.push("squirrelWindows.customSquirrelVendorDir was removed in v27 and cannot be migrated automatically. " +
222
+ 'Supply the custom bundle via toolsets.squirrel: { url: "file:///abs/path" } — the bundle must contain an electron-winstaller/vendor/ subtree ' +
223
+ "(customSquirrelVendorDir pointed at the vendor files directly) — or remove the key to use the default Squirrel bundle.");
224
+ }
200
225
  // ── 11. asar consolidation ───────────────────────────────────────────────
201
226
  // Move root-level asarUnpack / disableSanityCheckAsar / disableAsarIntegrity
202
- // into the asar object. Skip entirely when asar === false (all would be no-ops).
203
- if (c.asar !== false) {
227
+ // into the asar object. In v26 these applied to every platform independently of `asar`; in v27 they live
228
+ // inside it, and a platform-level `asar` replaces the root one wholesale — so capture them first for step 11b.
229
+ const rootAsarValue = c.asar;
230
+ const rootFlatAsar = {};
231
+ if ("asarUnpack" in c) {
232
+ rootFlatAsar.unpack = c.asarUnpack;
233
+ }
234
+ if ("disableSanityCheckAsar" in c) {
235
+ rootFlatAsar.disableSanityCheck = c.disableSanityCheckAsar;
236
+ }
237
+ if ("disableAsarIntegrity" in c) {
238
+ rootFlatAsar.disableIntegrity = c.disableAsarIntegrity;
239
+ }
240
+ if (c.asar === false) {
241
+ // No effect on the root build; step 11b still carries them into any platform that re-enables asar.
242
+ for (const key of ["asarUnpack", "disableSanityCheckAsar", "disableAsarIntegrity"]) {
243
+ if (key in c) {
244
+ delete c[key];
245
+ changes.push({ key, description: `removed ${key} (no effect with asar: false)` });
246
+ }
247
+ }
248
+ }
249
+ else {
204
250
  const asarSub = typeof c.asar === "object" && c.asar != null ? { ...c.asar } : {};
205
251
  if ("asarUnpack" in c) {
206
252
  asarSub.unpack = mergeAsarUnpack(asarSub.unpack, c.asarUnpack);
@@ -220,45 +266,61 @@ export function migrateConfig(raw) {
220
266
  if (c.asar === true) {
221
267
  if (Object.keys(asarSub).length > 0) {
222
268
  c.asar = asarSub;
269
+ changes.push({ key: "asar", description: `replaced asar: true with an asar object carrying ${Object.keys(asarSub).join(", ")}` });
223
270
  }
224
271
  else {
225
272
  delete c.asar;
273
+ changes.push({ key: "asar", description: "removed redundant asar: true (asar is enabled by default; the explicit `true` is not needed)" });
226
274
  }
227
- changes.push({ key: "asar", description: "replaced asar: true with asar object (true is no longer a valid value)" });
228
275
  }
229
276
  else if (Object.keys(asarSub).length > 0) {
230
277
  c.asar = asarSub;
231
278
  }
232
279
  }
280
+ // ── 11b. platform-level asar ──────────────────────────────────────────────
281
+ for (const platform of ASAR_PLATFORM_KEYS) {
282
+ if (isPlainObject(c[platform])) {
283
+ migratePlatformAsar(c, platform, rootAsarValue, rootFlatAsar, changes);
284
+ }
285
+ }
233
286
  // ── 12. win.sign unification ───────────────────────────────────────────────
234
287
  // win.signtoolOptions → win.sign: { type: "signtool", … }
235
288
  // win.azureSignOptions → win.sign: { type: "azure", … } (extra keys → additionalMetadata)
236
289
  // win.signAndEditExecutable / win.signExecutable removed
237
290
  if (c.win != null && typeof c.win === "object") {
238
291
  const win = c.win;
239
- // Remove defunct flags — signAndEditExecutable/signExecutable no longer exist in v27.
292
+ // Remove defunct flags — signAndEditExecutable/signExecutable no longer exist in v27. Either one set to
293
+ // false skipped ALL signing in v26, so both map to win.sign: false (signAndEditExecutable also skipped
294
+ // resource editing, which always runs in v27 — that part has no equivalent).
295
+ const signingDisabled = win.signExecutable === false || win.signAndEditExecutable === false;
240
296
  if ("signAndEditExecutable" in win) {
241
297
  const val = win.signAndEditExecutable;
242
298
  delete win.signAndEditExecutable;
243
299
  if (val === false) {
244
300
  warnings.push("win.signAndEditExecutable: false was used to skip both resource editing and signing. " +
245
- "In v27, resource editing always runs. To skip signing only, set win.sign: false. " +
301
+ "In v27, resource editing always runs; signing is still skipped via win.sign: false. " +
246
302
  "There is no v27 equivalent that also skips resource editing — apply resources manually if needed.");
247
303
  }
248
- else {
249
- changes.push({ key: "win.signAndEditExecutable", description: "removed win.signAndEditExecutable (resource editing always runs in v27; was the default)" });
250
- }
304
+ changes.push({ key: "win.signAndEditExecutable", description: "removed win.signAndEditExecutable (resource editing always runs in v27)" });
251
305
  }
252
306
  if ("signExecutable" in win) {
253
- const val = win.signExecutable;
254
307
  delete win.signExecutable;
255
- if (val === false && !("sign" in win)) {
256
- win.sign = false;
257
- changes.push({ key: "win.signExecutable", description: "replaced win.signExecutable: false with win.sign: false (disables signing; resource editing still runs)" });
258
- }
259
- else {
260
- changes.push({ key: "win.signExecutable", description: "removed win.signExecutable (signing is enabled by default when credentials are available)" });
308
+ changes.push({ key: "win.signExecutable", description: "removed win.signExecutable (signing is enabled by default when credentials are available)" });
309
+ }
310
+ if (signingDisabled && win.sign == null) {
311
+ win.sign = false;
312
+ changes.push({ key: "win.sign", description: "set win.sign: false (v26 signExecutable/signAndEditExecutable: false disabled signing; resource editing still runs)" });
313
+ }
314
+ if (win.sign === false && (win.azureSignOptions != null || win.signtoolOptions != null)) {
315
+ // Signing was switched off, so the legacy options never applied. Only their key names are reported —
316
+ // the values can hold certificate passwords.
317
+ const dropped = ["signtoolOptions", "azureSignOptions"].filter(k => win[k] != null);
318
+ for (const k of dropped) {
319
+ delete win[k];
320
+ changes.push({ key: `win.${k}`, description: `removed win.${k} (signing is disabled with win.sign: false)` });
261
321
  }
322
+ warnings.push(`win.sign is false (signing disabled), so [${dropped.map(k => `win.${k}`).join(", ")}] had no effect and ${dropped.length === 1 ? "was" : "were"} removed. ` +
323
+ 'To sign in v27, replace win.sign: false with win.sign: { type: "signtool" | "azure", … } carrying those options.');
262
324
  }
263
325
  const hasAzure = win.azureSignOptions != null;
264
326
  const hasSigntool = win.signtoolOptions != null;
@@ -316,12 +378,23 @@ export function migrateConfig(raw) {
316
378
  }
317
379
  // ── 14. electronDownload → electronGet ────────────────────────────────────
318
380
  if ("electronDownload" in c) {
319
- migrateElectronDownload(c, changes, warnings);
381
+ migrateElectronDownload(c, "electronDownload", changes, warnings);
320
382
  }
321
- // Advisory (not a config change): nsis-web builders must opt back into web installers at runtime as of v27.
383
+ else if (isPlainObject(c.electronGet) && Object.keys(c.electronGet).some(isLegacyElectronDownloadKey)) {
384
+ // A hand-renamed `electronGet` that still carries the v26 shape.
385
+ migrateElectronDownload(c, "electronGet", changes, warnings);
386
+ }
387
+ // ── 15. toolsets ──────────────────────────────────────────────────────────
388
+ if (isPlainObject(c.toolsets)) {
389
+ migrateToolsets(c.toolsets, changes);
390
+ }
391
+ // Advisories (not config changes): runtime defaults that changed in v27 and only surface after the build.
322
392
  if (detectNsisWebTarget(c)) {
323
393
  advisories.push(NSIS_WEB_ADVISORY);
324
394
  }
395
+ if (detectDefaultMacEntitlements(c)) {
396
+ advisories.push(MAC_ENTITLEMENTS_ADVISORY);
397
+ }
325
398
  return { migrated: c, changes, warnings, advisories, modified: changes.length > 0 || warnings.length > 0 };
326
399
  }
327
400
  /**
@@ -335,6 +408,14 @@ function migrateMacSigning(platform, name, changes, warnings) {
335
408
  const hasSignIgnore = "signIgnore" in platform;
336
409
  const existingSign = platform.sign;
337
410
  const signIsCustom = typeof existingSign === "string" || typeof existingSign === "function";
411
+ // Removed outright, not moved: ElectronSignOptions omits these and the schema rejects them, so
412
+ // relocating them under `sign` would emit a config that fails validation on the next build.
413
+ for (const field of MAC_SIGN_REMOVED_FIELDS) {
414
+ if (field in platform) {
415
+ delete platform[field];
416
+ changes.push({ key: `${name}.${field}`, description: `removed ${name}.${field} (@electron/osx-sign 2.x dropped the spctl --assess step; there is no ${name}.sign.${field})` });
417
+ }
418
+ }
338
419
  if (present.length === 0 && !hasSignIgnore) {
339
420
  // Only thing to fix is a semantically-flipped bare null
340
421
  if (existingSign === null) {
@@ -351,16 +432,35 @@ function migrateMacSigning(platform, name, changes, warnings) {
351
432
  // existingSign is null/undefined or already an object → build the merged options bag
352
433
  const signObj = existingSign != null && typeof existingSign === "object" ? { ...existingSign } : {};
353
434
  for (const f of present) {
435
+ if (dropNullField(platform, f, name, changes)) {
436
+ continue;
437
+ }
354
438
  signObj[f] = platform[f];
355
439
  delete platform[f];
356
440
  changes.push({ key: `${name}.${f}`, description: `moved ${name}.${f} → ${name}.sign.${f}` });
357
441
  }
358
- if (hasSignIgnore) {
442
+ if (hasSignIgnore && !dropNullField(platform, "signIgnore", name, changes)) {
359
443
  signObj.ignore = platform.signIgnore;
360
444
  delete platform.signIgnore;
361
445
  changes.push({ key: `${name}.signIgnore`, description: `renamed ${name}.signIgnore → ${name}.sign.ignore` });
362
446
  }
363
- platform.sign = signObj;
447
+ if (Object.keys(signObj).length > 0 || existingSign != null) {
448
+ platform.sign = signObj;
449
+ }
450
+ else if (existingSign === null) {
451
+ // Every moved field was a dropped null — a bare `sign: null` left behind would now mean "skip signing".
452
+ delete platform.sign;
453
+ changes.push({ key: `${name}.sign`, description: `removed ${name}.sign: null (v26 "no custom signer" = v27 default; sign: null now means skip signing)` });
454
+ }
455
+ }
456
+ /** Drops a v26 `null` ("unset") for a field whose v27 type has no null branch. Returns true when dropped. */
457
+ function dropNullField(platform, field, name, changes) {
458
+ if (platform[field] !== null || !MAC_NULL_MEANS_UNSET_FIELDS.has(field)) {
459
+ return false;
460
+ }
461
+ delete platform[field];
462
+ changes.push({ key: `${name}.${field}`, description: `removed ${name}.${field}: null (null meant "unset" in v26; the v27 option does not accept null)` });
463
+ return true;
364
464
  }
365
465
  /** Moves universal-build fields from a platform config into the `universal` bag. */
366
466
  function migrateMacUniversal(platform, name, changes) {
@@ -370,30 +470,125 @@ function migrateMacUniversal(platform, name, changes) {
370
470
  }
371
471
  const universalObj = platform.universal != null && typeof platform.universal === "object" ? { ...platform.universal } : {};
372
472
  for (const f of present) {
473
+ if (dropNullField(platform, f, name, changes)) {
474
+ continue;
475
+ }
373
476
  universalObj[f] = platform[f];
374
477
  delete platform[f];
375
478
  changes.push({ key: `${name}.${f}`, description: `moved ${name}.${f} → ${name}.universal.${f}` });
376
479
  }
377
- platform.universal = universalObj;
480
+ if (Object.keys(universalObj).length > 0 || platform.universal != null) {
481
+ platform.universal = universalObj;
482
+ }
483
+ }
484
+ /**
485
+ * Moves a platform-level `asarUnpack` into that platform's `asar` object.
486
+ *
487
+ * v26 concatenated root and platform `asarUnpack` and always applied the root `disableSanityCheckAsar` /
488
+ * `disableAsarIntegrity`, whatever the platform's `asar` was. v27 keeps all three inside `asar`, and a
489
+ * platform-level `asar` replaces the root one entirely — so a platform that overrides `asar` (or gains one
490
+ * here) gets the root values folded in to keep the v26 result.
491
+ */
492
+ function migratePlatformAsar(c, name, rootAsarValue, rootFlatAsar, changes) {
493
+ const platform = c[name];
494
+ const hasUnpack = "asarUnpack" in platform;
495
+ const own = platform.asar;
496
+ if (own == null) {
497
+ // Inherits the root asar — only needs an override of its own to carry a platform asarUnpack.
498
+ if (!hasUnpack) {
499
+ return;
500
+ }
501
+ if (rootAsarValue === false) {
502
+ delete platform.asarUnpack;
503
+ changes.push({ key: `${name}.asarUnpack`, description: `removed ${name}.asarUnpack (no effect with asar: false)` });
504
+ return;
505
+ }
506
+ const next = isPlainObject(c.asar) ? { ...c.asar } : {};
507
+ next.unpack = mergeAsarUnpack(next.unpack, platform.asarUnpack);
508
+ platform.asar = next;
509
+ delete platform.asarUnpack;
510
+ changes.push({
511
+ key: `${name}.asarUnpack`,
512
+ description: `moved ${name}.asarUnpack → ${name}.asar.unpack (merged with the root asar options, since a platform-level asar replaces the root one)`,
513
+ });
514
+ return;
515
+ }
516
+ if (own === false) {
517
+ if (hasUnpack) {
518
+ delete platform.asarUnpack;
519
+ changes.push({ key: `${name}.asarUnpack`, description: `removed ${name}.asarUnpack (no effect with ${name}.asar: false)` });
520
+ }
521
+ return;
522
+ }
523
+ // own is true or an AsarOptions object: fold the v26 root-level options (and the platform asarUnpack) into it.
524
+ const next = isPlainObject(own) ? { ...own } : {};
525
+ const folded = [];
526
+ if (rootFlatAsar.unpack != null || hasUnpack) {
527
+ // v26 order: root patterns first, then the platform's own.
528
+ let unpack = undefined;
529
+ for (const it of [rootFlatAsar.unpack, next.unpack, hasUnpack ? platform.asarUnpack : undefined]) {
530
+ if (it != null) {
531
+ unpack = mergeAsarUnpack(unpack, it);
532
+ }
533
+ }
534
+ if (unpack !== undefined) {
535
+ next.unpack = unpack;
536
+ folded.push("unpack");
537
+ }
538
+ }
539
+ for (const key of ["disableSanityCheck", "disableIntegrity"]) {
540
+ if (key in rootFlatAsar && !(key in next)) {
541
+ next[key] = rootFlatAsar[key];
542
+ folded.push(key);
543
+ }
544
+ }
545
+ if (hasUnpack) {
546
+ delete platform.asarUnpack;
547
+ }
548
+ if (folded.length === 0) {
549
+ return;
550
+ }
551
+ platform.asar = next;
552
+ changes.push({
553
+ key: hasUnpack ? `${name}.asarUnpack` : `${name}.asar`,
554
+ description: `folded ${folded.map(k => `asar.${k}`).join(", ")} into ${name}.asar (a platform-level asar replaces the root one in v27, so the v26 root-level values are copied)`,
555
+ });
556
+ }
557
+ /** Removes `null` toolset values (rejected by the v27 schema) and remaps pins to bundles that are no longer published. */
558
+ function migrateToolsets(toolsets, changes) {
559
+ for (const [key, value] of Object.entries(toolsets)) {
560
+ if (value === null) {
561
+ delete toolsets[key];
562
+ changes.push({
563
+ key: `toolsets.${key}`,
564
+ description: `removed toolsets.${key}: null (v27 rejects null; unset resolves to "latest", the newest bundle — pin "0.0.0" to keep the legacy bundle v26 used)`,
565
+ });
566
+ }
567
+ }
568
+ if (toolsets.appimage === "1.0.2") {
569
+ toolsets.appimage = "1.0.3";
570
+ changes.push({
571
+ key: "toolsets.appimage",
572
+ description: 'replaced toolsets.appimage: "1.0.2" with "1.0.3" (1.0.2 is no longer offered; 1.0.3 is the same runtime with the #9598 fix)',
573
+ });
574
+ }
378
575
  }
379
- // electronDownload fields with no equivalent in the v27 ElectronGetOptions (@electron/get v5) shape.
380
- export const ELECTRON_DOWNLOAD_DROPPED = ["cache", "customDir", "customFilename", "strictSSL", "platform", "arch", "version"];
381
576
  /**
382
577
  * Renames `electronDownload` → `electronGet` and reshapes it to ElectronGetOptions.
383
578
  * `mirror` → `mirrorOptions.mirror`, `isVerifyChecksum: false` → `unsafelyDisableChecksums: true`.
384
579
  * Fields with no v5 equivalent are dropped with a warning.
385
580
  */
386
- function migrateElectronDownload(c, changes, warnings) {
387
- const old = c.electronDownload;
388
- delete c.electronDownload;
581
+ function migrateElectronDownload(c, sourceKey, changes, warnings) {
582
+ const old = c[sourceKey];
583
+ delete c[sourceKey];
389
584
  if (old == null || typeof old !== "object") {
390
585
  changes.push({ key: "electronDownload", description: "renamed electronDownload → electronGet" });
391
- if (old != null) {
586
+ if (old != null && c.electronGet == null) {
392
587
  c.electronGet = old;
393
588
  }
394
589
  return;
395
590
  }
396
- const next = { ...(c.electronGet ?? {}) };
591
+ const next = {};
397
592
  if ("mirror" in old && old.mirror != null) {
398
593
  next.mirrorOptions = { ...(next.mirrorOptions ?? {}), mirror: old.mirror };
399
594
  }
@@ -410,8 +605,39 @@ function migrateElectronDownload(c, changes, warnings) {
410
605
  if (dropped.length > 0) {
411
606
  warnings.push(`electronGet (formerly electronDownload) dropped [${dropped.join(", ")}] — these have no equivalent in @electron/get v5. Set a mirror via electronGet.mirrorOptions if needed.`);
412
607
  }
608
+ // An electronGet that already exists is the v27 intent: fold the legacy values in underneath it, never over it.
609
+ const existing = sourceKey === "electronDownload" && isPlainObject(c.electronGet) ? c.electronGet : null;
610
+ if (existing != null) {
611
+ const conflicts = [];
612
+ let mergedMirrorOptions;
613
+ for (const [k, v] of Object.entries(next)) {
614
+ if (k === "mirrorOptions" && isPlainObject(v) && isPlainObject(existing.mirrorOptions)) {
615
+ const own = existing.mirrorOptions;
616
+ conflicts.push(...Object.keys(v)
617
+ .filter(m => m in own && JSON.stringify(own[m]) !== JSON.stringify(v[m]))
618
+ .map(m => `mirrorOptions.${m}`));
619
+ mergedMirrorOptions = { ...v, ...own };
620
+ }
621
+ else if (k in existing && JSON.stringify(existing[k]) !== JSON.stringify(v)) {
622
+ conflicts.push(k);
623
+ }
624
+ }
625
+ Object.assign(next, existing, mergedMirrorOptions != null ? { mirrorOptions: mergedMirrorOptions } : {});
626
+ if (conflicts.length > 0) {
627
+ warnings.push(`Both electronDownload and electronGet set [${conflicts.join(", ")}]; kept the existing electronGet values and dropped the electronDownload ones. Verify the merged electronGet.`);
628
+ }
629
+ }
413
630
  c.electronGet = next;
414
- changes.push({ key: "electronDownload", description: "renamed electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)" });
631
+ changes.push({
632
+ key: sourceKey,
633
+ description: sourceKey === "electronDownload"
634
+ ? "renamed electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)"
635
+ : "reshaped electronGet to the v27 options (mirror → mirrorOptions.mirror; isVerifyChecksum → unsafelyDisableChecksums)",
636
+ });
637
+ }
638
+ /** Keys of the v26 `electronDownload` shape that are not valid v27 `electronGet` keys. */
639
+ export function isLegacyElectronDownloadKey(key) {
640
+ return key === "mirror" || key === "isVerifyChecksum" || ELECTRON_DOWNLOAD_DROPPED.includes(key);
415
641
  }
416
642
  function mergeAsarUnpack(existing, incoming) {
417
643
  if (existing == null) {
@@ -420,15 +646,26 @@ function mergeAsarUnpack(existing, incoming) {
420
646
  const arr = (Array.isArray(existing) ? existing : [existing]).concat(Array.isArray(incoming) ? incoming : [incoming]);
421
647
  return arr.length === 1 ? arr[0] : arr;
422
648
  }
423
- function migratePublishEntries(parent, key, changes) {
649
+ function migratePublishEntries(parent, key, changes, warnings) {
424
650
  const entries = Array.isArray(parent[key]) ? parent[key] : [parent[key]];
425
651
  let changed = false;
426
652
  for (const entry of entries) {
427
- if (entry != null && entry.provider === "github" && "vPrefixedTagName" in entry) {
428
- entry.tagNamePrefix = entry.vPrefixedTagName === false ? "" : "v";
653
+ if (entry == null || entry.provider !== "github") {
654
+ continue;
655
+ }
656
+ if ("vPrefixedTagName" in entry) {
657
+ // v26 githubTagPrefix(): a non-empty tagNamePrefix won; otherwise vPrefixedTagName (default true) chose "v" or "".
658
+ entry.tagNamePrefix = entry.tagNamePrefix ? entry.tagNamePrefix : entry.vPrefixedTagName === false ? "" : "v";
429
659
  delete entry.vPrefixedTagName;
430
660
  changed = true;
431
661
  }
662
+ else if (entry.tagNamePrefix === "") {
663
+ // v26 ignored an empty tagNamePrefix and still tagged "v1.2.3"; v27 honors it and would tag "1.2.3".
664
+ entry.tagNamePrefix = "v";
665
+ changed = true;
666
+ warnings.push('A GitHub publish entry set tagNamePrefix: "". v26 ignored an empty prefix and tagged releases "v<version>"; v27 honors it. ' +
667
+ 'It was rewritten to tagNamePrefix: "v" to keep your existing tag names — set it back to "" if you want unprefixed tags from now on.');
668
+ }
432
669
  // GitLab keeps vPrefixedTagName in v27 — it remains in the type, scheme, and runtime
433
670
  // (gitlabPublisher honors it) and has no tagNamePrefix equivalent, so leave it untouched.
434
671
  }
@@ -471,6 +708,15 @@ function migrateSnap(c, changes, warnings) {
471
708
  base = "core20";
472
709
  assumed = true;
473
710
  }
711
+ if (base === "core24") {
712
+ const unsupported = SNAP_CORE24_UNSUPPORTED.filter(k => k in rest);
713
+ for (const k of unsupported) {
714
+ delete rest[k];
715
+ }
716
+ if (unsupported.length > 0) {
717
+ warnings.push(`snap config set [${unsupported.join(", ")}], which the v27 snapcraft.core24 options do not support (core24 builds with the snapcraft CLI directly); they were dropped.`);
718
+ }
719
+ }
474
720
  snapcraft.base = base;
475
721
  snapcraft[base] = { ...(snapcraft[base] ?? {}), ...rest };
476
722
  c.snapcraft = snapcraft;
@@ -675,7 +921,11 @@ async function migrateProgrammaticConfigFile(found, location, dryRun) {
675
921
  return;
676
922
  }
677
923
  if (result.status === "no-op") {
678
- if (result.advisories.length === 0) {
924
+ // A no-op can still carry warnings for keys that cannot be rewritten (e.g. customSquirrelVendorDir).
925
+ for (const warning of result.warnings) {
926
+ log.warn(null, warning);
927
+ }
928
+ if (result.advisories.length === 0 && result.warnings.length === 0) {
679
929
  log.info(null, "config is already up to date — no changes needed");
680
930
  }
681
931
  printAdvisories();
@@ -701,23 +951,26 @@ function printManualSteps() {
701
951
  "• Remove electronCompile",
702
952
  "• Remove framework, nodeVersion, launchUiVersion",
703
953
  "• Rename npmSkipBuildFromSource → buildDependenciesFromSource",
704
- "• Move buildDependenciesFromSource, nodeGypRebuild, npmRebuild, nativeRebuilder into nativeModules (rename nativeRebuilder → rebuildMode)",
705
- "• Rename asar-unpack / asar-unpack-dir / asar.unpack / asar.unpackDir → asarUnpack; then move asarUnpack → asar.unpack",
954
+ '• Move buildDependenciesFromSource, nodeGypRebuild, npmRebuild, nativeRebuilder into nativeModules (rename nativeRebuilder → rebuildMode; drop a "legacy" value)',
955
+ "• Rename asar-unpack / asar-unpack-dir / asar.unpackDir → asarUnpack; then move asarUnpack → asar.unpack",
956
+ "• Move mac/mas/masDev/win/linux asarUnpack → <platform>.asar.unpack (a platform-level asar replaces the root one, so copy the root asar options into it)",
706
957
  "• Move disableSanityCheckAsar → asar.disableSanityCheck",
707
958
  "• Move disableAsarIntegrity → asar.disableIntegrity",
708
959
  "• Replace asar: true with an asar object (e.g. asar: {} or omit entirely - asar is enabled by default)",
709
960
  "• Remove appImage.systemIntegration",
710
961
  "• Remove linux.syncDesktopName (the installed .desktop filename is always synced from desktopName in v27)",
711
962
  "• Rename snap → snapcraft; nest options under a base-named sub-key (default base: core20)",
712
- "• Replace vPrefixedTagName with tagNamePrefix on GitHub publish entries ('v' is the default prefix - just like before - but can now be customized with tagNamePrefix)",
963
+ "• Replace vPrefixedTagName with tagNamePrefix on GitHub publish entries ('v' is the default prefix - just like before - but can now be customized with tagNamePrefix); an empty tagNamePrefix is now honored",
713
964
  "• Move win.signtoolOptions → win.sign: { type: 'signtool', ...fields }",
714
965
  "• Move win.azureSignOptions → win.sign: { type: 'azure', ...fields }; move untyped extra keys into win.sign.additionalMetadata",
715
966
  "• Remove win.signAndEditExecutable (resource editing always runs in v27); replace win.signExecutable: false with win.sign: false",
716
967
  "• Move helper-bundle-id → mac.helperBundleId",
717
968
  "• Replace squirrelWindows.noMsi with squirrelWindows.msi (inverted)",
718
- "• Move mac/mas/masDev signing fields (identity, entitlements, hardenedRuntime, type, requirements, timestamp, binaries, gatekeeperAssess, strictVerify, preAutoEntitlements, provisioningProfile, additionalArguments) into the `sign` object; rename signIgnore → sign.ignore",
969
+ "• Replace squirrelWindows.customSquirrelVendorDir with a toolsets.squirrel custom bundle (it must contain an electron-winstaller/vendor/ subtree)",
970
+ "• Move mac/mas/masDev signing fields (identity, entitlements, entitlementsInherit, entitlementsLoginHelper, hardenedRuntime, type, requirements, timestamp, binaries, strictVerify, preAutoEntitlements, provisioningProfile, additionalArguments) into the `sign` object; rename signIgnore → sign.ignore; remove gatekeeperAssess",
719
971
  "• Move mac/mas/masDev mergeASARs / singleArchFiles / x64ArchFiles into the `universal` object",
720
- "• Rename electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum:false → unsafelyDisableChecksums:true)",
972
+ "• Rename electronDownload → electronGet (mirror → mirrorOptions.mirror; isVerifyChecksum:false → unsafelyDisableChecksums:true; drop cache/customDir/customFilename/strictSSL/platform/arch/version/force)",
973
+ '• Remove toolsets.* entries set to null (unset resolves to "latest"); replace toolsets.appimage "1.0.2" with "1.0.3"',
721
974
  "• Move root-level package.json directories → build.directories",
722
975
  "",
723
976
  "• For programmatic configs (JS/TS/MJS/CJS), apply the above changes manually to your config object",