openSUSE:Packaging Electron
THIS IS AN UNFINISHED DRAFT
Build Service Tutorial · Tips & Tricks · Cross Distribution Howto · Packaging checks
Desktop menu categories · RPM Macros · Scriptlets · Init scripts · How to write good changes
Before you begin
Electron is not to be confused with nw.js, which is way less popular and (as of May 2024) not packaged by any distro. It is unknown whether Electron can run unmodified nw.js apps.
Download dependencies
Most (all?) apps have a package.json in the root and use a popular package manager such as npm or yarn. Both of these can usually execute arbitrary commands during vendoring dependencies
Avoid using the “offline cache” feature to call the package manager during build. It makes patching hard (npm) or impossible (yarn).
Instead, you must call your package manager in a way that it does not run build scripts (and preferably produces a reproducible result):
#npm (package-lock.json)
npm ci --verbose --ignore-scripts
#yarn (yarn.lock)
yarn install --frozen-lockfile --ignore-engines --ignore-platform --ignore-scripts --link-duplicates
Yarn can sometimes fail if the shrinkwrap file does not match the package. In that case you can use this command which does less stringent checks:
yarn install --pure-lockfile --ignore-engines --ignore-platform --ignore-scripts --link-duplicates
Yarn has a known minor reproducibility problem in that it embeds your Node major version and computer architecture into the tarball.
Remove forbidden items
Precompiled binaries are a severe problem in tarballs from the NPM registry. You must ensure they do not get used during build or shipped. The best moment to remove them is before packing the vendor tarball.
An example script that gets rid of most of them:
#zypper in file findutils moreutils
find . -name '*.node' -print -delete
find . -name '*.jar' -print -delete
find . -name '*.dll' -print -delete
find . -name '*.exe' -print -delete
find . -name '*.dylib' -print -delete
find . -name '*.so' -print -delete
find . -name '*.o' -print -delete
find . -name '*.a' -print -delete
find . -name '*.wasm' -print -delete
#We use sponge to avoid a race condition between find and rm
find . -type f| sponge |\
xargs -P"$(nproc)" -- sh -c 'file -S "$@" | grep -v '\'': .*script'\'' | grep '\'': .*executable'\'' | tee /dev/stderr | sed '\''s/: .*//'\'' | xargs rm -fv'
You should also remove any bundled libraries at this point.
Package only the node_modules into the vendor tarball.
Some applications can include multiple shrinkwrap files, example vscode
Build
In general the beginning of %build
should look like
%build
export CFLAGS="%{optflags}"
export CXXFLAGS="%{optflags}"
export LDFLAGS="%{?build_ldflags}"
export MAKEFLAGS="%{_smp_mflags}"
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
%electron_rebuild
The %electron_rebuild
executes any build scripts in node_modules including, potentially, building native modules against Electron headers.
Native modules
If your package includes native modules which use only stable APIs (napi_* and uv_*) you must include the following in your spec to ensure they load successfully:
Requires: nodejs-electron%{_isa}
…
%check
%electron_check_native
This check will fail with an undefined symbol error if you use unstable APIs. In that case, use the following to ensure rebuilds on major Electron updates:
#in Requires: section
%electron_req
…
%check
%electron_check_native_unstable
C/C++ flags
- You are building a loadable plugin. The correct relocation model to use for loadable plugins is -fpic -fno-semantic-interposition. If you use a more relaxed model (-fpie or non-relocable) you will get a linker error. If you use a more strict model (-fpic) it will work but produce suboptimal code.
- Many upstream buildscripts for Node/Electron export symbols overzealously due to being tested only on Windows which does not have symbol visibility. Add -fvisibility=hidden to fix this. This won't break Node's required exports which correctly tag their visibility.
If all C/C++ code is built by %electron_rebuild
macro, these flags will be injected automatically. You need to add them manually otherwise.
Asar
Avoid due to both reproducibility issues and the contents of the asar being opaque to rpmlint. Prefer shipping the app unpacked. TODO: Document how to disable asar in electron-builder.
Specific technologies
yarn
The %electron_rebuild
macro executes npm. It is not possible to use yarn to execute build scripts.
If your build scripts check for yarn, you need to patch them out (example from vscode) or fool them in other ways.
Rust
In general, Rust modules use only stable APIs and do not need the electron headers to build. General Rust guidelines can apply to them (modified appropriately for loadable plug-ins)
Rust does not have the equivalent of gcc's global -fvisibility=hidden; you should check for useless exports and patch them if possible.
Mixed Rust/C++ codebases need individual attention due to needing to work around rustc's numerous FFI bugs.
esbuild
This is a javascript bundler/transpiler consisting of two parts: the main program written in Go and a JS helper library. As with every JS library, it is going to be vendored in your node_modules. There are two main things you need to know when dealing with a program using esbuild:
- version 0.17 is a complete rewrite breaking both the command-line API and the JS library. (As of 2024, we're not seeing old versions of the esbuild client library in the wild anymore)
- esbuild is a hostile upstream and the JS library contains multiple crude DRM checks designed to fail distro builds (The program has a DRM too but we've already patched it out
Just getting %electron_rebuild
to succeed
%build
#esbuild is not actually used, it is only declared as a transitive dependency of some webpack plugin
export ESBUILD_BINARY_PATH=/bin/true
…
%electron_rebuild
--- a/node_modules/esbuild/install.js
+++ b/node_modules/esbuild/install.js
@@ -85,7 +85,7 @@
}
throw err;
}
- if (stdout !== versionFromPackageJSON) {
+ if (0) {
throw new Error(`Expected ${JSON.stringify(versionFromPackageJSON)} but got ${JSON.stringify(stdout)}`);
}
}
It's enough for eg. Bitwarden, which does not use esbuild (only has it included as a transitive dependency).
Actually using esbuild
BuildRequires: esbuild >= 0.17
%build
export ESBUILD_BINARY_PATH=/usr/bin/esbuild
…
%electron_rebuild
As of version 0.23 there are three more checks:
--- a/node_modules/esbuild/install.js
+++ b/node_modules/esbuild/install.js
@@ -27,7 +27,7 @@
var os = require("os");
var path = require("path");
var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH;
-var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild";
+var isValidBinaryPath = (x) => !!x
var knownWindowsPackages = {
"win32 arm64 LE": "@esbuild/win32-arm64",
"win32 ia32 LE": "@esbuild/win32-ia32",
--- a/build/node_modules/esbuild/lib/main.js
+++ b/build/node_modules/esbuild/lib/main.js
@@ -710,7 +710,6 @@
isFirstPacket = false;
let binaryVersion = String.fromCharCode(...bytes);
if (binaryVersion !== "0.23.0") {
- throw new Error(`Cannot start service: Host version "${"0.23.0"}" does not match binary version ${quote(binaryVersion)}`);
}
return;
}
@@ -1734,7 +1733,7 @@
var os = require("os");
var path = require("path");
var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH;
-var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild";
+var isValidBinaryPath = (x) => !!x
var packageDarwin_arm64 = "@esbuild/darwin-arm64";
var packageDarwin_x64 = "@esbuild/darwin-x64";
var knownWindowsPackages = {
@electron/fuses
This library is broken by design. It tries to do binary witchcraft on the Electron executable, which is impossible, because there is only one Electron copy in the entire system. It will (intentionally) fail when presented with openSUSE's electron binary to ensure you will be reading this page instead of shipping a broken package.
Fuses that are known to be safe to ignore
- run_as_node
- node_options
- node_cli_inspect
- embedded_asar_integrity_validation
- only_load_app_from_asar
- grant_file_protocol_extra_privileges
cookie_encryption
If upstream uses this fuse, running a locally built application against user data produced by upstream binaries can lead to user data loss!
This fuse has been observed in Signal and Element (fortunately neither of these apps uses cookies for anything).
electron-builder
Removed features
These features are available in upstream Electron builds but openSUSE currently does not build them due to being unused:
- PPAPI
- Printing
- WebExtensions
- WebGPU
Known bugs and gotchas
app.isPackaged
Returns false
all the time. Patches are welcome.
process.execPath
This API is broken by design. Most Electron applications need a /usr/bin launcher script, and it's inherently not possible for the app to know its name. Every use of process.execPath
must be patched out, otherwise the “About Electron” dialog will open instead of your app. Example from vscode.
app.getPath('exe')
Alternate way of doing the above. See this bitwarden bug for an example.
app.relaunch
This API is broken by design for the same reason as the previous one (and with the same symptoms). Provide a suitable wrapper script as an additional parameter, example from vscode