electron-userland / electron-builder

A complete solution to package and build a ready for distribution Electron app with “auto update” support out of the box
https://www.electron.build
MIT License
13.72k stars 1.74k forks source link

Option to Disable Combined NSIS Installer When Building for Multiple Architectures #8298

Open PeterDaveHello opened 4 months ago

PeterDaveHello commented 4 months ago

When building for both 32-bit and 64-bit architectures, Electron Builder creates separate installers for each architecture as well as a combined installer. Is there an option to disable the creation of this combined installer?

Currently, I can't find a way on the doc(https://www.electron.build/configuration/nsis#32-bit-64-bit) to only generate the separate 32-bit and 64-bit installers without also producing the combined one. It would be helpful to have an option like nsis.disableCombinedInstaller: true or nsis.enableCombinedInstaller: false to prevent the creation of the combined installer when it's not needed.

Thank you for your assistance.

PeterDaveHello commented 4 months ago

It seems this feature doesn't just work on 32-bit(x86/ia32) + 64-bit(amd64), as the doc mentioned, but also on arm64+amd64, arm64+amd64+ia32, and maybe other combinations.

These are the screenshots from using 7-zip to open the $PLUGINSDIR\ folder in the NSIS installers in different combinations:

image

image

mmaietta commented 4 months ago

So I must admit I don't have the original insight as to why the installers are always combined. I had a project in the past where it was required to have two distinct installers, one for each bitness, though and I implemented an electron-builder script using the programmatic API. I think it looked something akin to the code below (just make sure your artifact name uses the {arch} macro so that the second build command doesn't overwrite the artifact of the first one.) https://www.electron.build/api/programmatic-usage

await builder.build({
  targets: platform.createTarget("nsis", 'x64')
  config: options
})
await builder.build({
  targets: platform.createTarget("nsis", 'ia32')
  config: options
})
PeterDaveHello commented 4 months ago

Sure we can do it by different approach like using command line to build them separately, but a simple option would be also nice 😄

mmaietta commented 4 months ago

Can you share your electron-builder config?

Try this win config for your electron-builder configuration. I was able to build separate installers for x64 and arm64 in this manner.

win: {
        target: [
            {
                target: 'nsis',
                arch: 'x64'
            },
            {
                target: 'nsis',
                arch: 'arm64'
            }
        ],  
}
Screenshot 2024-07-11 at 11 35 11 AM
MikesGlitch commented 3 months ago

+1 for having a config value for this.

I'm building better-sqlite3 and it needs to be built once per architecture. The combined installer would take the last built better-sqlite3 which would fail for the other archs.

github-actions[bot] commented 1 month ago

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.

mmaietta commented 1 month ago

Revisiting this request. I'll see what I can do but the logic in NsisTarget is extremely complex 😅

mmaietta commented 1 month ago

Update: I've added a configuration property to allow disabling universal builds, but it's only available to nsis. nsis-web requires universal in order for it to be able to determine what build to install. The difficulty in implementation is that nsis-web target extends NsisTarget, so the logic needed to be conditional and overridden for nsis-web target.

The PR introduces a config property buildUniversalInstaller (default true for backward compatibility) In order to build separate exe's, I needed to update the installer name template to include an arch. Unless a defaultArch is supplied in the electron-builder config, x64 will not have a suffix appended to the file name (backward compatibility) and the other arch builds will have suffix -${arch}.${ext}

alanhamlett commented 1 month ago

target: [ { target: 'nsis', arch: 'x64' }, { target: 'nsis', arch: 'arm64' } ],

That electron-builder.json config for me still builds the universal win32 installer. How do I disable that?

  • electron-builder  version=24.13.3 os=23.6.0
  • loaded configuration  file=./desktop-wakatime/electron-builder.json
  • writing effective config  file=release/builder-effective-config.yaml
  • packaging       platform=win32 arch=x64 electron=32.0.0 appOutDir=release/win-unpacked
  • packaging       platform=win32 arch=arm64 electron=32.0.0 appOutDir=release/win-arm64-unpacked
  • building        target=nsis file=release/wakatime-win32.exe archs=x64, arm64 oneClick=false perMachine=false
  • building block map  blockMapFile=release/wakatime-win32.exe.blockmap
  • building        target=nsis file=release/wakatime-win32-x64.exe archs=x64 oneClick=false perMachine=false
  • building block map  blockMapFile=release/wakatime-win32-x64.exe.blockmap
  • building        target=nsis file=release/wakatime-win32-arm64.exe archs=arm64 oneClick=false perMachine=false
  • building block map  blockMapFile=release/wakatime-win32-arm64.exe.blockmap
mmaietta commented 1 month ago

Would anyone be willing to test out this patch-package?

I'm unable to test for arch combinations including ia32, so I need a person to test with "arm64", "x64", "ia32", as ia32 isn't package-able on my mac (arm64 limitation IIRC)

Would also appreciate anyone trying to test out the updater logic, I'm still verifying that myself as well.

app-builder-lib+26.0.0-alpha.3.patch

diff --git a/node_modules/app-builder-lib/out/codeSign/signManager.js b/node_modules/app-builder-lib/out/codeSign/signManager.js
new file mode 100644
index 0000000..09883b3
--- /dev/null
+++ b/node_modules/app-builder-lib/out/codeSign/signManager.js
@@ -0,0 +1,10 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.SignManager = void 0;
+class SignManager {
+    constructor(packager) {
+        this.packager = packager;
+    }
+}
+exports.SignManager = SignManager;
+//# sourceMappingURL=signManager.js.map
\ No newline at end of file
diff --git a/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js b/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js
index e79c555..31aee98 100644
--- a/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js
+++ b/node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js
@@ -56,8 +56,15 @@ class NsisTarget extends core_1.Target {
         }
         nsisUtil_1.NsisTargetOptions.resolve(this.options);
     }
+    buildUniversalInstaller() {
+        const buildSeparateInstallers = this.options.buildUniversalInstaller === false;
+        return !buildSeparateInstallers;
+    }
     build(appOutDir, arch) {
         this.archs.set(arch, appOutDir);
+        if (!this.buildUniversalInstaller()) {
+            return this.buildInstaller(new Map().set(arch, appOutDir));
+        }
         return Promise.resolve();
     }
     get isBuildDifferentialAware() {
@@ -94,7 +101,10 @@ class NsisTarget extends core_1.Target {
             return await createPackageFileInfo(archiveFile);
         }
     }
-    get installerFilenamePattern() {
+    installerFilenamePattern(primaryArch, defaultArch) {
+        if (!this.buildUniversalInstaller()) {
+            return "${productName} " + (this.isPortable ? "" : "Setup ") + "${version}" + (primaryArch != null ? (0, builder_util_1.getArchSuffix)(primaryArch, defaultArch) : "") + ".${ext}";
+        }
         // tslint:disable:no-invalid-template-strings
         return "${productName} " + (this.isPortable ? "" : "Setup ") + "${version}.${ext}";
     }
@@ -102,8 +112,11 @@ class NsisTarget extends core_1.Target {
         return this.name === "portable";
     }
     async finishBuild() {
+        if (!this.buildUniversalInstaller()) {
+            return this.packageHelper.finishBuild();
+        }
         try {
-            const { pattern } = this.packager.artifactPatternConfig(this.options, this.installerFilenamePattern);
+            const { pattern } = this.packager.artifactPatternConfig(this.options, this.installerFilenamePattern());
             const builds = new Set([this.archs]);
             if (pattern.includes("${arch}") && this.archs.size > 1) {
                 ;
@@ -119,12 +132,13 @@ class NsisTarget extends core_1.Target {
         }
     }
     async buildInstaller(archs) {
-        var _a, _b;
+        var _a, _b, _c;
         const primaryArch = archs.size === 1 ? ((_a = archs.keys().next().value) !== null && _a !== void 0 ? _a : null) : null;
         const packager = this.packager;
         const appInfo = packager.appInfo;
         const options = this.options;
-        const installerFilename = packager.expandArtifactNamePattern(options, "exe", primaryArch, this.installerFilenamePattern, false, this.packager.platformSpecificBuildOptions.defaultArch);
+        const defaultArch = (_b = (0, platformPackager_1.chooseNotNull)(this.packager.platformSpecificBuildOptions.defaultArch, this.packager.config.defaultArch)) !== null && _b !== void 0 ? _b : undefined;
+        const installerFilename = packager.expandArtifactNamePattern(options, "exe", primaryArch, this.installerFilenamePattern(primaryArch, defaultArch), false, defaultArch);
         const oneClick = options.oneClick !== false;
         const installerPath = path.join(this.outDir, installerFilename);
         const logFields = {
@@ -160,7 +174,7 @@ class NsisTarget extends core_1.Target {
             BUILD_RESOURCES_DIR: packager.info.buildResourcesDir,
             APP_PACKAGE_NAME: (0, targetUtil_1.getWindowsInstallationAppPackageName)(appInfo.name),
         };
-        if ((_b = options.customNsisBinary) === null || _b === void 0 ? void 0 : _b.debugLogging) {
+        if ((_c = options.customNsisBinary) === null || _c === void 0 ? void 0 : _c.debugLogging) {
             defines.ENABLE_LOGGING_ELECTRON_BUILDER = null;
         }
         if (uninstallAppKey !== guid) {
@@ -279,7 +293,7 @@ class NsisTarget extends core_1.Target {
         defines.UNINSTALLER_OUT_FILE = definesUninstaller.UNINSTALLER_OUT_FILE;
         await this.executeMakensis(defines, commands, sharedHeader + (await this.computeFinalScript(script, true, archs)));
         await Promise.all([packager.sign(installerPath), defines.UNINSTALLER_OUT_FILE == null ? Promise.resolve() : (0, fs_extra_1.unlink)(defines.UNINSTALLER_OUT_FILE)]);
-        const safeArtifactName = (0, platformPackager_1.computeSafeArtifactNameIfNeeded)(installerFilename, () => this.generateGitHubInstallerName());
+        const safeArtifactName = (0, platformPackager_1.computeSafeArtifactNameIfNeeded)(installerFilename, () => this.generateGitHubInstallerName(primaryArch, defaultArch));
         let updateInfo;
         if (this.isWebInstaller) {
             updateInfo = (0, differentialUpdateInfoBuilder_1.createNsisWebDifferentialUpdateInfo)(installerPath, packageFiles);
@@ -300,10 +314,11 @@ class NsisTarget extends core_1.Target {
             isWriteUpdateInfo: !this.isPortable,
         });
     }
-    generateGitHubInstallerName() {
+    generateGitHubInstallerName(primaryArch, defaultArch) {
         const appInfo = this.packager.appInfo;
         const classifier = appInfo.name.toLowerCase() === appInfo.name ? "setup-" : "Setup-";
-        return `${appInfo.name}-${this.isPortable ? "" : classifier}${appInfo.version}.exe`;
+        const archSuffix = !this.buildUniversalInstaller() && primaryArch != null ? (0, builder_util_1.getArchSuffix)(primaryArch, defaultArch) : "";
+        return `${appInfo.name}-${this.isPortable ? "" : classifier}${appInfo.version}${archSuffix}.exe`;
     }
     get isUnicodeEnabled() {
         return this.options.unicode !== false;
diff --git a/node_modules/app-builder-lib/out/targets/nsis/WebInstallerTarget.js b/node_modules/app-builder-lib/out/targets/nsis/WebInstallerTarget.js
index 9b75baa..b7a707d 100644
--- a/node_modules/app-builder-lib/out/targets/nsis/WebInstallerTarget.js
+++ b/node_modules/app-builder-lib/out/targets/nsis/WebInstallerTarget.js
@@ -1,6 +1,7 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
 exports.WebInstallerTarget = void 0;
+const builder_util_1 = require("builder-util");
 const PublishManager_1 = require("../../publish/PublishManager");
 const NsisTarget_1 = require("./NsisTarget");
 /** @private */
@@ -27,8 +28,13 @@ class WebInstallerTarget extends NsisTarget_1.NsisTarget {
         defines.APP_PACKAGE_URL_IS_INCOMPLETE = null;
         defines.APP_PACKAGE_URL = appPackageUrl;
     }
-    get installerFilenamePattern() {
-        // tslint:disable:no-invalid-template-strings
+    buildUniversalInstaller() {
+        if (this.options.buildUniversalInstaller === false) {
+            builder_util_1.log.warn({ buildUniversalInstaller: true }, "only universal builds are supported for nsis-web installers, overriding setting");
+        }
+        return true;
+    }
+    installerFilenamePattern(_primaryArch, _defaultArch) {
         return "${productName} Web Setup ${version}.${ext}";
     }
     generateGitHubInstallerName() {