openSUSE:Packaging Electron

Jump to: navigation, search

THIS IS AN UNFINISHED DRAFT


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
  • PDF
  • 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

ELECTRON_RUN_AS_NODE