Skip to content

Windows single binary application#151

Merged
RoiArthurB merged 12 commits into
devfrom
windows-single-binary-application
May 29, 2026
Merged

Windows single binary application#151
RoiArthurB merged 12 commits into
devfrom
windows-single-binary-application

Conversation

@RoiArthurB
Copy link
Copy Markdown
Contributor

Pull Request

Checklist

  • Code is complete and ready for review
  • Tests have been added/updated (if applicable)
  • Documentation has been updated (if applicable)

Description

Fix single binary application issue on windows

Related Issue

RoiArthurB and others added 8 commits May 29, 2026 11:36
The shutdown command used Linux-only flags (-h now) which silently did
nothing on Windows. Replaced with the Windows equivalent (/s /t 0).

The ping command also used Linux-only flags (-c/-W) and passed the IP
directly into a shell string, opening a command injection vector. Fixed
by using platform-specific flags and validating the IP against a strict
IPv4 regex before building the command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous uwsCompanionPlugin copied the uWebSockets.js .node binary
alongside the pkg output. This does not work with SEA (no node_modules).

The new uwsSeaPlugin does a string replacement at bundle time on the
compiled CJS output, replacing require("uWebSockets.js") with an IIFE
that in SEA mode extracts the embedded .node asset from the SEA blob to
a temp directory and loads it via process.dlopen. In dev/pkg mode it
falls through to the regular require() call unchanged.

process.dlopen is used instead of require() for the extracted path
because embedderRequire (the SEA require) cannot load .node files from
arbitrary absolute paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added _isSea() helper that checks node:sea.isSea() to determine whether
the process is running inside a Node.js SEA binary. This is included in
the IS_PLATFORM_PACKAGED condition so that the static server and other
packaged-mode behaviour activate correctly when running as a SEA binary.

createRequire(process.execPath) is used instead of import.meta.url
because import.meta.url is undefined in Vite's CJS bundle output.
process.execPath is always a valid absolute path on all platforms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In SEA mode there is no dist/ directory on disk — frontend files are
embedded as assets in the SEA blob. StaticServer now detects SEA mode
via getSea() (using createRequire(process.execPath) for the same reason
as index.ts) and serves all responses directly from sea.getAsset().

Asset keys follow the convention "dist/<relative-path>" matching the
keys written into sea-config.json by the build script. A SPA catch-all
fallback serves dist/index.html for unknown routes.

The original filesystem-based serving path is kept as a fallback for
dev mode and pkg (Linux/macOS) builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds scripts/build-sea-win.mjs which produces a single Windows binary
using Node.js SEA (Single Executable Application) instead of pkg.

The pipeline:
  1. Builds frontend (Vite) and backend (Vite CJS)
  2. Embeds all dist/ files and uws_win32_x64_<ver>.node as SEA assets
  3. Generates the SEA blob via node --experimental-sea-config
  4. Copies the node binary from pkg cache or process.execPath
  5. Strips the Authenticode signature and clears CFG via patch-pe-no-cfg.mjs
     so postject does not leave the binary in a corrupted-signature state
  6. Injects the blob with postject
  7. Compiles and bundles the Go launcher (see next commit)

The output binary MUST be named node.exe — Windows applies an AppCompat
shim exclusively to executables with that name, which is required for
process.dlopen to load the uWebSockets.js NAPI module without crashing
with 0xC0000005. The launcher (simple-win.exe) provides the user-facing
entry point.

Removes node24-windows-x64 from pkg targets; Windows now uses SEA.
Adds postject devDependency and build:sea:win npm script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SEA worker binary must be named node.exe, but users need a
recognisable entry point. launcher-win.go is a small Go program that:

  - Reads the footer appended to itself at build time
    ([8-byte embedded size LE][4-byte magic "SWPN"]) to locate the
    embedded node.exe bytes
  - On first launch, extracts node.exe to a temp directory keyed by the
    embedded size (%TEMP%\swp-node-<hex-size>\node.exe) and reuses the
    cache on subsequent launches (validated by file size)
  - Spawns the extracted node.exe with inherited stdin/stdout/stderr,
    ignores SIGINT so node.exe handles its own graceful shutdown, and
    forwards the exit code

The build script appends the SEA node.exe bytes and footer to the
compiled launcher, producing a single distributable file (simple-win.exe)
that contains everything. Go was chosen because it produces a statically
linked native Windows PE with no runtime dependencies (~2 MB overhead).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build-ci.yaml:
  - Renamed the ubuntu job to compilation-unix (builds Linux + macOS
    via pkg with cross-compilation tools: qemu, ldid)
  - Removed the dead Windows artifact upload (pkg no longer targets
    Windows)
  - Added a parallel compilation-windows job on windows-latest that
    sets up Node 24 and Go, runs build:sea:win, and uploads
    bin/win/simple-win.exe as the windows-compiled-archive artifact

release.yaml:
  - Split the single job into three:
      build-unix (ubuntu): builds Linux + macOS, uploads each as a
        separate named artifact
      build-windows (windows-latest): builds the SEA binary, uploads
        bin/win/simple-win.exe
      release: depends on both build jobs, downloads all three
        artifacts into release-files/ and publishes them to GitHub
        in a single softprops/action-gh-release step

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sea-config.json and sea-prep.blob are auto-generated by build:sea:win
on every build and should not be committed. Added both to .gitignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@RoiArthurB RoiArthurB changed the base branch from main to dev May 29, 2026 04:50
RoiArthurB and others added 4 commits May 29, 2026 13:33
Adds scripts/build-sea-unix.mjs, a single script that builds a SEA
binary for the current Unix platform (Linux or macOS). It follows the
same pipeline as build-sea-win.mjs: builds frontend and backend, embeds
all dist/ files and the platform-specific uWebSockets.js .node file as
SEA assets, generates the blob, and injects it via postject.

Platform differences handled by the script:
  - macOS (Mach-O): postject requires --macho-segment-name NODE_SEA,
    and the binary must be re-signed with codesign --sign - after
    injection (Apple Silicon blocks modified unsigned binaries)
  - Linux: no signing or extra flags needed

Unlike Windows, neither Linux nor macOS require the binary to be named
node.exe, so no launcher or self-extraction is needed — the output is a
directly executable single file (bin/simple-linux or bin/simple-macos).

Removes @yao-pkg/pkg devDependency and its config block. The
build:executable and build:executable-compressed scripts are removed;
all three platforms now use the build:sea:* family of scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three platforms now build on their native runner using Node.js SEA,
removing the need for qemu, ldid, and cross-compilation from ubuntu.

build-ci.yaml:
  - compilation-linux  (ubuntu-latest): build:sea:linux
  - compilation-macos  (macos-latest):  build:sea:macos
  - compilation-windows (windows-latest): build:sea:win + Go

release.yaml:
  - build-linux  (ubuntu-latest)
  - build-macos  (macos-latest)
  - build-windows (windows-latest)
  - release: waits for all three, collects artifacts, publishes
- My laptop uses latest NodeJS 26 over which the compilation is failing
- Add a catch failing the compilation with explicit message
@RoiArthurB RoiArthurB force-pushed the windows-single-binary-application branch from ad226cb to 4d8f109 Compare May 29, 2026 08:09
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
7 Security Hotspots
4.2% Duplication on New Code (required ≤ 3%)
D Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@RoiArthurB RoiArthurB merged commit 013b4c5 into dev May 29, 2026
2 of 4 checks passed
@RoiArthurB RoiArthurB deleted the windows-single-binary-application branch May 29, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant