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

Large Apps Result in Malformed NSIS Installer After Build - No Error(s)

Open Bug-Reaper opened this issue 1 year ago • 5 comments

  • Electron-Builder Version: 24.13.3
  • Node Version: 16.20.2
  • Electron Version: 29.4.5
  • Electron Type (current, beta, nightly): current
  • Electron Updater: 6.2.1
  • Target: Win32 x64 NSIS

I'm building an NSIS installer for a large electron app on an M1 MacPro. Not getting any errors or anything via command DEBUG=electron-builder:* electron-builder --windows --x64....

However the result AppName Setup 0.0.0.exe file does not work when put on a windows machine. When attempting to launch the installer it cannot find the binary to run see screenshot: Screenshot 2024-08-05 at 2 45 26 AM Seems like the only thing packaged in the exe is the uninstaller, at least when you browse that's the only thing present. Can also pretty clearly see something has gone awry as win-unpacked is ~4Gib and the installer exe is a mere 300mb.

My NSIS config is as follows:

win:{
      target:"nsis",
      icon:"build/SoundSafari.ico",
    },
    nsis:{
      oneClick:true,
      installerIcon:"build/SoundSafari.ico",
      installerHeaderIcon:"build/SoundSafari.ico",
      uninstallDisplayName:`SoundSafari-ver(${VERSION})`,
      deleteAppDataOnUninstall:true,
      runAfterFinish:true,
      createDesktopShortcut:true,
      createStartMenuShortcut:true
    }

Bug-Reaper avatar Aug 05 '24 06:08 Bug-Reaper

I may try and build on my x64 Win11 VM to see if error is specific to my M1 mac. Also want to try customNsisBinary but the correct usage is not clear.

Bug-Reaper avatar Aug 05 '24 07:08 Bug-Reaper

Essentially this is the same problem as https://github.com/electron-userland/electron-builder/issues/6738 .

However the fix used there does not fix for me :'(

Bug-Reaper avatar Aug 05 '24 19:08 Bug-Reaper

Build completes with no errors but I end up with an installer.exe that is empty except for the uninstaller.


Continued debug attempts:

  1. Tried $env /usr/bin/arch -x86_64 /bin/zsh --login before build to see if it did any magic.
  2. Tried the XL Nsis binary drop-in work-around (in case max nsis size was messing me up) https://github.com/electron-userland/electron-builder-binaries/issues/44
  3. Used system wine (got rid of some wine warnings but otherwise no change)

EDIT: have now also tried:

  1. Went through the pain of building directly on Windows 11 x64 and got rekt by the same issue.
  2. Removed all config options and went with defaults.
  3. Tried bumping electron version to @latest ^31.3.1

Bug-Reaper avatar Aug 05 '24 20:08 Bug-Reaper

After days of all-in debugging for days 10+ hrs I'm happy to report I have some progress.

Was able to get a windows-installer that is functional via the nsis-installer-large-files workaround but only when building on windows x64. The exact instructions were as follows:

  1. Download special NSISBI from https://sourceforge.net/projects/nsisbi/
  2. Extract and copy all contained filed into C:\Users\user\AppData\Local\electron-builder\Cache\electron-builder\nsis\nsis-3.0.4.1 and "replace all" when prompted.
  3. Delete NSIS.exe from above cache folder and rename newly copied makensisw.exe => NSIS.exe.
  4. re-run build

This type of NSIS build changes the output as a side-effect, you'll see two files now comprise the installer: .exe (~100kb). .nsisbin (Your actual app-size (~4Gib for me)).


What's next for this issue

First I'd like to test if the two-file installer setup (.exe && .nsbin) files breaks my ability to auto-update. Looking at the generated latest.yml that only mentions the exe file I'd wager it does brick auto-updates. If someone more intimate with the auto-updater wants to weigh in though that'd be appreciated.

Here's a bunch of other stuff I hope to PR in fixes for (any help appreciated) :

  1. Builder should throw an error for large-apps instead of building an unusable nsis installer with no warnings/errors.
  2. Document proper build instructions for NSIS builds of large-apps. Add a config option for it if possible.
  3. Figure out how to build large NSIS installers on non-windows devices. Following the above workaround on macOS for example results in an error from nsis-3.0.4.1/mac/makensis.sh Invalid command: "GetWinVer".

TLDR: Most egregious thing is:

Builder should throw an error for large-apps instead of building an unusable nsis installer with no warnings/errors." Other build types work fine so the error should happen specifically during NSIS flavored builds. We're hitting some ancient window98 restriction on max NSIS file size either 2 or 4Gib I think.

If you read through the whole post and want to leave a comment/emoji for moral support that'd be appreciated.

Bug-Reaper avatar Aug 06 '24 00:08 Bug-Reaper

Builder should throw an error for large-apps instead of building an unusable nsis installer with no warnings/errors.

I want to check the <installerName>.nsis.7z file size prior to it making the final exe. Maybe need to PR this upstream @electron-forge-nsis-maker? Does electron-builder use forge for all NSIS builds?

https://github.com/electron-userland/electron-builder/blob/12c52a81420f04ec0e205dd83798c2b0b773011d/packages/electron-forge-maker-nsis/main.js

Bug-Reaper avatar Aug 07 '24 18:08 Bug-Reaper

I have been actively perusing this again with the primary goal to get windows builds working on MacOS. Primarily because then I'll be able to do ALL of my target builds from a single machine.

The customNsisBinary escape hatch is mostly enriched-jank and sorely in need of some documentation. So far I can tell,

  • It requires adding a remote url to be cached. Very irksome you can't just point it at a binary you already have.
  • Undocumented No-Op if you don't provide a 512hash code
  • There's some type of custom internal wrapper that parses the download code. Also technically possible this just passes it to YET ANOTHER handler, I haven't checked.

Bug-Reaper avatar Jan 21 '25 06:01 Bug-Reaper

For windows binaries on MacOS, I think it's a fools errand to get increasingly old versions of nsis to build for increasingly new versions of MacOS as is currently seen in the builder. I do need a custom nsis binary anyways so a lil biased but still think this reasons out.

With the death of consistency to processor architecture it's almost certainly in our best interest to use wine around the true NSIS windows executable toolchain. Or possibly some docker flow.

Bug-Reaper avatar Jan 21 '25 06:01 Bug-Reaper

Before I deal with the whole escape hatch nonsense I'm manually replacing the mac/makensis executable binary with a bash script that pipes the parameters to wine makensisw like so.

#!/bin/bash
# Take input args and run 'wine makensisw.exe' with the same parameters
exec wine "makensisw.exe" "$@"

I'm a little unsure if this is the correct approach to passing the args into wine-flavored-nsis. I do get an nsis error: ShellExecuteEx failed: File not found.

I have yet to find good documentation for the arg passing to makensisw or really the build tool in general. Help appreciated

Bug-Reaper avatar Jan 21 '25 21:01 Bug-Reaper

I'm doing some magic to get the wine paths correctly flavored and passed through but I'm clearly still missing something. Output:

...
Command line defined: "HEADER_ICO=Z:\Users\llama\Sorcery\atlantis\v2\Sound-Safari\build\SoundSafari.ico"
Command line defined: "ESTIMATED_SIZE=4334202"
Command line defined: "COMPRESS=auto"
Command line defined: "BUILD_UNINSTALLER"
Command line defined: "UNINSTALLER_OUT_FILE=Z:\Users\llama\Sorcery\atlantis\v2\Sound-Safari\dist\__uninstaller-nsis-soundsafari.exe"
Processing config: Z:\Users\llama\Library\Caches\electron-builder\nsis\nsis-3.0.4.1\mac\nsisconf.nsh
Processing script file: "<stdin>" (UTF8)
!include: could not find: "/Users/llama/Sorcery/atlantis/v2/Sound-Safari/node_modules/app-builder-lib/templates/nsis/include/StdUtils.nsh"
Error in script "<stdin>" on line 1 -- aborting creation process

Of praticular note !include: could not find: "/Users/llama/Sorcery/atlantis/v2/Sound-Safari/node_modules/app-builder-lib/templates/nsis/include/StdUtils.nsh" I have no idea where this magic path to nsis/include/StdUtils.nsh path is coming in from.

It's definitely not the env vars, someting in nsis-3.0.4.1\mac\nsisconf.nsh, or CLI args.

The !include: could not find is from in nsis exec itself right? Not some JS abstraction after? Or piped in cmd?

Bug-Reaper avatar Jan 22 '25 02:01 Bug-Reaper

Fortunately

You 100% can get wine to run the for the first makensis step, here's my drop-in replacement bash script for unstable default binary at /Caches/electron-builder/nsis/nsis-3.0.4.1/mac/makensis. Compatible with custom nsis targets also (particularly useful since nsis forks are not known for their Linux/Mac build support).

#!/bin/bash
# Take input args and run 'wine makensis.exe' with the same parameters
#echo "\n\n$@\n\n" #| sed 's|/|\\|g')
INPUT_ARGS="$@"

echo "Start args count: $#"
PROCESSED_ARGS=()
for arg in "$@"; do
    if [[ $arg == *"="* ]]; then
        # Split into pre-equals and post-equals parts
        pre_equals="${arg%%=*}"
        post_equals="${arg#*=}"
        
        # Check if post-equals starts with a forward slash
        if [[ $post_equals == /* ]]; then
            # Replace first / with Z:\ and remaining / with \
            windows_path="Z:\\${post_equals:1}"
            windows_path="${windows_path//\//\\}"
            arg="${pre_equals}=${windows_path}"
        fi
    fi
    PROCESSED_ARGS+=("$arg")
done

NSISDIR="Z:${NSISDIR//\//\\}"
PWD="Z:${PWD//\//\\}"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )


exec wine "$SCRIPT_DIR/Bin/makensis.exe" "${PROCESSED_ARGS[@]}"

For wine-support, we intercept all of the arguments wine-ify all of the path args Z:\your\local\fs\someNsisFile.. Note we also wine-ify the some paths stored in ENV vars.

There's also a custom nsis script generator that has hardcoded paths we must address. It's easiest to just parse the final output and replaceAll paths. In node_modules/app-builder-lib/out/targets/nsis/NsisTarget.js I'm doing a similar trick to intercept the final file and wine-ify local paths:

..
const isWin = true;//process.platform === "win32";
defines.BUILD_UNINSTALLER = null;
defines.UNINSTALLER_OUT_FILE = isWin ? uninstallerPath : path.win32.join("Z:", uninstallerPath);
const finalScript = (sharedHeader + (await this.computeFinalScript(script, false, archs))).replace(/"([^"]*?)\/([^"]*?)"/g, (match, p1, p2) => {
    // Replace the first / with Z:\ and all subsequent / with \
    const windowsPath = `Z:\\${p1}/${p2.replace(/\//g, '\\')}`;
    return `"${windowsPath}"`;
});
..

TLDR

There are un-expolored wine-based solutions for NSIS build steps on MacOS. IIRC there's related issues with building for windows targets (Particularly on apple-silicon) that could be addressed via more investigation and development.

Unfortunately

The very next build step calls into prlctl a paid subscription proprietary binary from parallels that also necessitates a loaded and active window VM. From what I understand this is to allow for some 32 bit window binaries that make 64 bit macOS upset.

I'd imagine:

  • There's either newer 64 bit binary equivalents that can be wine or some more intricate wine workarounds.
  • There's a way to dockerize/qemu this prlctl build step.

Both of which seem preferable as alternatives options to the current prlctl implementation. However I have absolutely no idea what we're doing in the prlctl build step(s) and it's made even more unclear by the general nature of going through a 3rd party proprietary VM abstraction.

As always, general tips and or emojis appreciated.

Bug-Reaper avatar Jan 23 '25 00:01 Bug-Reaper

Makensis Binary Wine Wrapper Bash Script

Holy moley I did it, a bash-script that can be used as a drop in replacement for makensis binary when doing windows builds on MacOS/Linux. *untested on linux but it should work.

#!/bin/bash

# makensis
# A bash wrapper to run makensis.exe using wine when building on MacOS/Linux
#
#   Electron Builder Provides this Script:
#     -> A procedurally generated NSIS file is passed as string via STDIN
#     -> Various paths are passed in via command line args 
#     -> Two paths are passed via ENV vars
#
#   This script primarily transpiles native MacOS/Linux paths from above
#   to their "Z:\" wine equivelant. 
#
#   Electron Builder is hardcoded to call 
#    :>: <CacheNsisDir>/Mac/makensis or 
#    :>: <CacheNsisDir>/Linux/makensis respectively
#
#   We intercept this at the very last moment and re-route it to native
#   system wine and <CacheNsisDir>/Bin/makensis.exe
#
# =========================================================================


# =========================================================================
# First we grab the content of stdin and make sure it's not empty.
# -> (Electron Builder passes in a procedurally generated NSIS file)
# =========================================================================
STDIN_CONTENT=$(cat)
if [ -z "$STDIN_CONTENT" ]; then
    printf "❌ [makensis error] : -> No NSIS file passed via stdin to makensis bash script. Expects NSIS File via stdin.\n" >&2
    exit 1
fi
# Wine-ify all script paths in two steps: 
# ->  1) Add Z: to double quoted paths starting with /, 
# ->  2) Replace ALL slashes "/" between quotes with backslashes "\"
STDIN_CONTENT=$(echo "$STDIN_CONTENT" | sed -E '
  s|"(/[^"]+)"|"Z:\1"|g;
  :loop
  s|"([^"]*)/([^"]*)"|"\1\\\2"|g
  t loop
')


# =========================================================================
# Take input args and wine-ify detected native filepaths. 
#     ✅ SomeVar=/some/path ---> SomeVar=Z:\some\path
# =========================================================================
INPUT_ARGS="$@"
PROCESSED_ARGS=()
for arg in "$@"; do
    if [[ $arg == *"="* ]]; then
        # Split into pre-equals and post-equals parts
        pre_equals="${arg%%=*}"
        post_equals="${arg#*=}"
        
        # Check if post-equals starts with a forward slash
        if [[ $post_equals == /* ]]; then
            # Replace first / with Z:\ and remaining / with \
            windows_path="Z:\\${post_equals:1}"
            windows_path="${windows_path//\//\\}"
            arg="${pre_equals}=${windows_path}"
        fi
    fi
    PROCESSED_ARGS+=("$arg")
done


# =========================================================================
# Wine-ify two ENV var paths and setup cache-location-path
#     ✅ /some/path ---> Z:\some\path
# =========================================================================
NSISDIR="Z:${NSISDIR//\//\\}"
PWD="Z:${PWD//\//\\}"

# Just one level up
NSIS_CACHE_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 


# =========================================================================
# Run 🍷 native wine on ../Bin/makensis.exe with wine-ified inputs from Electron-Builder
# =========================================================================
echo "$STDIN_CONTENT" | exec wine "$NSIS_CACHE_DIR/Bin/makensis.exe" "${PROCESSED_ARGS[@]}"

# debug:
# echo "$STDIN_CONTENT" | grep "\!addplugindir"
# debug:
# echo "Start args count: $#"

I still personally get boned by prlctl pay-to-win build step however this gives me hope I can add some real utility to this build-chain. The bash wrapper means I could at least ship a version of NSIBI that's cross compatible. Also as mentioned before:

With the death of consistency to processor architecture it's almost certainly in our best interest to use wine around the true NSIS windows executable toolchain. Or possibly some docker flow.

Bug-Reaper avatar Jan 25 '25 04:01 Bug-Reaper

A good adventure

This adventure has led me down many rabbit holes, I hope to come back and make further improvements. Particularly around the NSIS build steps on all platforms.

For now though, I am content to publish an easy config option for people to use when they want to build windows installers >2Gib for their electron apps.

Workaround

I've published a version of NSISBI to enable builds of electron apps >=2GIB via just an electron-builder config change:

{
  // Your config
  // ...
  "build": {
    "nsis": {
      "customNsisBinary": {
        "url":"https://github.com/SoundSafari/NSISBI-ElectronBuilder/releases/download/1.0.0/nsisbi-electronbuilder-3.10.3.7z",
        "checksum":"WRmZUsACjIc2s7bvsFGFRofK31hfS7riPlcfI1V9uFB2Q8s7tidgI/9U16+X0I9X2ZhNxi8N7Z3gKvm6ojvLvg=="
      }
    }
  }
}

Enables creation of windows installers >2Gib with your current electron-builder projects. Also supports windows builds on mac/linux too via wine-shim to run NSISBI's makensis.exe.

Repo NSISBI-ElectronBuilder.

Comes to a close

Edited in the most relevant info to the top of this issue. This journey has raised a variety of related issues and areas of improvements for me. I think it is better to come back with fresh-targeted specific issue discussions when called for and close this for now.

Bug-Reaper avatar Feb 11 '25 05:02 Bug-Reaper