electron-builder icon indicating copy to clipboard operation
electron-builder copied to clipboard

Option to Disable Combined NSIS Installer When Building for Multiple Architectures

Open PeterDaveHello opened this issue 1 year ago • 8 comments

  • Electron-Builder Version: 24.13.3
  • Node Version: v18.20.3
  • Electron Version: 31.1.0
  • Electron Type (current, beta, nightly): current
  • Target: nsis installer, arch: ia32, x64 on Windows 11

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 avatar Jul 07 '24 11:07 PeterDaveHello

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

PeterDaveHello avatar Jul 07 '24 13:07 PeterDaveHello

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
})

mmaietta avatar Jul 08 '24 06:07 mmaietta

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

PeterDaveHello avatar Jul 08 '24 16:07 PeterDaveHello

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

mmaietta avatar Jul 11 '24 18:07 mmaietta

+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.

MikesGlitch avatar Aug 15 '24 19:08 MikesGlitch

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.

github-actions[bot] avatar Oct 15 '24 00:10 github-actions[bot]

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

mmaietta avatar Oct 15 '24 16:10 mmaietta

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}

mmaietta avatar Oct 17 '24 17:10 mmaietta

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

alanhamlett avatar Oct 21 '24 12:10 alanhamlett

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() {

mmaietta avatar Oct 23 '24 17:10 mmaietta

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.

github-actions[bot] avatar Dec 23 '24 00:12 github-actions[bot]

Is there any progress?

m1rn avatar Dec 24 '24 13:12 m1rn

would it be possible to do this with the MSI wrapper builder?

mv2Cloud avatar Feb 13 '25 20:02 mv2Cloud

I'm trying to create individuals builds for each arch type as well as per machine and per user installers for both msi and exe (nsi) is this something possible?

mv2Cloud avatar Feb 13 '25 20:02 mv2Cloud