improve electron support

This commit is contained in:
DavHau 2021-12-19 14:39:33 +01:00
parent a08f5eb25a
commit 013dc86524
7 changed files with 497 additions and 197 deletions

View File

@ -116,12 +116,20 @@ in
build = {
nativeBuildInputs = [
pkgs.rsync
];
electronAppDir = "src";
buildScript = ''
npm run build-linux
preBuild = { outputs, ... }: ''
# link dependencies of subpackage
ln -s \
${outputs.subPackages.edex-ui-subpackage.packages.edex-ui-subpackage}/lib/node_modules/edex-ui-subpackage/node_modules \
./src/node_modules
# transform symlinked subpackage 'node-pty' to copies,
# in order to allow re-building
mv src/node_modules src/node_modules.bac
mkdir src/node_modules
cp -r src/node_modules.bac/* src/node_modules/
symlinksToCopies ./src/node_modules/node-pty
'';
};
};
@ -131,7 +139,7 @@ in
mkElectron =
pkgs.callPackage
"${pkgs.path}/pkgs/development/tools/electron/generic.nix"
./electron/generic.nix
{};
nixpkgsElectrons =
@ -139,7 +147,6 @@ in
(version: hashes:
(mkElectron version hashes).overrideAttrs (old: {
dontStrip = true;
fixupPhase = old.postFixup;
}))
hashes;
@ -227,6 +234,15 @@ in
aarch64-darwin = "0db2c021a047a4cd5b28eea16490e16bc82592e3f8a4b96fbdc72a292ce13f50";
headers = "1idam1xirxqxqg4g7n33kdx2skk0r351m00g59a8yx9z82g06ah9";
};
"13.0.0" = {
armv7l-linux = "51ddcd8c92da5dd84a6bab8304a0df6114153a884f7f185ebfc65843caa30e76";
aarch64-linux = "5b36e5bcb36cf1b90c38b346d3eae970a2aa41cb585df493bb90d86dc2e88b0a";
x86_64-linux = "ff89df221293f7253e2a29eb3028014549286179e3d91402e4911b2d086377bb";
i686-linux = "6fd7eca44302762a97c205b1a08a4178247bea89354ce84c747e09ebeb9f245b";
x86_64-darwin = "f3b9e45a442f82f06da8dd6cbdccd8031a191f3ba73e2886572f6472160d1f2d";
aarch64-darwin = "9c26405efd126d4e076fa8068e9003463be62b449182632babd5445f633712b6";
headers = "0glv92hhzg5f0fycrgv2g2b1avcw4jcrmpxxz4rpn91gd1v4n4fn";
};
"13.2.3" = {
x86_64-linux = "495b0c96427c63f6f4d08c5b58d6379f9ee3c6c81148fbbe8a7a6a872127df6d";
x86_64-darwin = "c02f116484a5e1495d9f7451638bc0d3dea8fa2fde2e4c9c88a17cff98192ddc";
@ -287,6 +303,11 @@ in
echo -n $version > ./dist/version
echo -n "electron" > ./path.txt
'';
postFixup = ''
mkdir -p $out/lib
ln -s $(realpath ./dist) $out/lib/electron
'';
};
};
};
@ -294,17 +315,50 @@ in
# TODO: fix electron-builder call or find alternative
element-desktop = {
build = {
postPatch = ''
ls tsconfig.json
cp ${./element-desktop/tsconfig.json} ./tsconfig.json
'';
buildScript = ''
# TODO: build rust extensions to enable searching encrypted messages
# TODO: create lower case symlinks for all i18n strings
buildScript = { outputs, ... }: ''
npm run build:ts
npm run i18n
npm run build:res
# electron-builder
# build rust extensions
# npm run hak
ln -s ${outputs.subPackages.element-web.packages.element-web}/lib/node_modules/element-web/webapp ./webapp
# ln -s ./lib/i18n/strings/en{-US,}.json
ln -s \
$(realpath ./lib/i18n/strings/en_US.json) \
$(realpath ./lib/i18n/strings/en-us.json)
'';
# buildInputs = old: old ++ [
# pkgs.rustc
# ];
};
};
element-web = {
build = {
installMethod = "copy";
# TODO: file upstream issue because of non-reproducible jitsi api file
buildScript = ''
# install jitsi api
mkdir webapp
cp ${./element-web/external_api.min.js} webapp/external_api.min.js
# set version variables
export DIST_VERSION=$version
export VERSION=$version
npm run reskindex
npm run build:res
npm run build:bundle
'';
nativeBuildInputs = [pkgs.breakpointHook];
b = "${pkgs.busybox}/bin/busybox";
};
};
@ -419,7 +473,7 @@ in
cp -r ./node_modules/electron/dist $TMP/dist
chmod -R +w $TMP/dist
# required if electron-buidler is used
# required if electron-builder is used
# mv $TMP/dist/electron $TMP/dist/electron-wrapper
# mv $TMP/dist/.electron-wrapped $TMP/dist/electron
@ -491,6 +545,12 @@ in
};
};
quill = {
disable-build = {
runBuild = false;
};
};
rollup = {
preserve-symlinks = {
postPatch = ''
@ -675,4 +735,13 @@ in
};
};
"@sentry/cli" = {
add-binary = {
buildScript = ''
ln -s ${pkgs.sentry-cli}/bin $out/bin
exit
'';
};
};
}

View File

@ -0,0 +1,136 @@
{
lib
, autoPatchelfHook
, alsa-lib
, stdenv
, libXScrnSaver
, makeWrapper
, fetchurl
, wrapGAppsHook
, glib
, glibc
, gtk3
, unzip
, atomEnv
, libuuid
, at-spi2-atk
, at-spi2-core
, libdrm
, mesa
, libxkbcommon
, libappindicator-gtk3
, libxshmfence
, libXdamage
, nss
}:
version: hashes:
let
name = "electron-${version}";
meta = with lib; {
description = "Cross platform desktop application shell";
homepage = "https://github.com/electron/electron";
license = licenses.mit;
maintainers = with maintainers; [ travisbhartwell manveru prusnak ];
platforms = [ "x86_64-darwin" "x86_64-linux" "i686-linux" "armv7l-linux" "aarch64-linux" ]
++ optionals (versionAtLeast version "11.0.0") [ "aarch64-darwin" ];
knownVulnerabilities = optional (versionOlder version "12.0.0") "Electron version ${version} is EOL";
};
fetcher = vers: tag: hash: fetchurl {
url = "https://github.com/electron/electron/releases/download/v${vers}/electron-v${vers}-${tag}.zip";
sha256 = hash;
};
headersFetcher = vers: hash: fetchurl {
url = "https://atom.io/download/electron/v${vers}/node-v${vers}-headers.tar.gz";
sha256 = hash;
};
tags = {
i686-linux = "linux-ia32";
x86_64-linux = "linux-x64";
armv7l-linux = "linux-armv7l";
aarch64-linux = "linux-arm64";
x86_64-darwin = "darwin-x64";
aarch64-darwin = "darwin-arm64";
};
get = as: platform: as.${platform.system} or
"Unsupported system: ${platform.system}";
common = platform: {
inherit name version meta;
src = fetcher version (get tags platform) (get hashes platform);
passthru.headers = headersFetcher version hashes.headers;
};
electronLibs = with lib;
[
alsa-lib
at-spi2-atk
at-spi2-core
glibc
libappindicator-gtk3
libuuid
libXdamage
libXScrnSaver
nss
]
++ optionals (! versionOlder version "9.0.0") [ libdrm mesa ]
++ optionals (! versionOlder version "11.0.0") [ libxkbcommon ]
++ optionals (! versionOlder version "12.0.0") [ libxshmfence ];
electronLibPath = with lib; makeLibraryPath electronLibs;
linux = {
buildInputs =
[ glib gtk3 ]
++ (builtins.map (p: lib.getOutput "lib" p) electronLibs);
nativeBuildInputs = [
# autoPatchelfHook
unzip
makeWrapper
wrapGAppsHook
];
dontWrapGApps = true; # electron is in lib, we need to wrap it manually
dontUnpack = true;
dontBuild = true;
installPhase = ''
mkdir -p $out/lib/electron $out/bin
unzip -d $out/lib/electron $src
ln -s $out/lib/electron/electron $out/bin
'';
postFixup = ''
patchelf \
--set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
--set-rpath "${electronLibPath}:${atomEnv.libPath}:$out/lib/electron" \
$out/lib/electron/electron
wrapProgram $out/lib/electron/electron \
"''${gappsWrapperArgs[@]}"
'';
};
darwin = {
nativeBuildInputs = [ unzip ];
buildCommand = ''
mkdir -p $out/Applications
unzip $src
mv Electron.app $out/Applications
mkdir -p $out/bin
ln -s $out/Applications/Electron.app/Contents/MacOS/Electron $out/bin/electron
'';
};
in
stdenv.mkDerivation (
(common stdenv.hostPlatform) //
(if stdenv.isDarwin then darwin else linux)
)

View File

@ -1,22 +0,0 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": false,
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"lib": [
"es2019",
"dom"
],
"preserveSymlinks": true
},
"include": [
"./src/**/*.ts"
]
}

File diff suppressed because one or more lines are too long

View File

@ -54,6 +54,10 @@ let
nodejsVersion = subsystemAttrs.nodejsVersion;
isMainPackage = name: version:
name == mainPackageName
&& version == mainPackageVersion;
nodejs =
if args ? nodejs then
args.nodejs
@ -81,12 +85,25 @@ let
inherit defaultPackage packages;
};
# only gets executed if package has electron dependency
electron-rebuild = electron: ''
# This is only executed for electron based packages.
# Electron ships its own version of node, requiring a rebuild of native
# extensions.
# Theoretically this requires headers for the exact electron version in use,
# but we use the headers from nixpkgs' electron instead which might have a
# different minor version.
# Alternatively the headers can be specified via `electronHeaders`.
# Also a custom electron version can be specified via `electronPackage`
electron-rebuild = ''
# prepare node headers for electron
ver="v${electron.version}"
if [ -n "$electronPackage" ]; then
export electronDist="$electronPackage/lib/electron"
else
export electronDist="$nodeModules/$packageName/node_modules/electron/dist"
fi
local ver
ver="v$(cat $electronDist/version | tr -d '\n')"
mkdir $TMP/$ver
cp ${electron.headers} $TMP/$ver/node-$ver-headers.tar.gz
cp $electronHeaders $TMP/$ver/node-$ver-headers.tar.gz
# calc checksums
cd $TMP/$ver
@ -97,13 +114,11 @@ let
python -m http.server 45034 --directory $TMP &
# copy electron distribution
cp -r ./node_modules/electron/dist $TMP/dist
chmod -R +w $TMP/dist
# mv $TMP/dist/electron $TMP/dist/electron-wrapper
# mv $TMP/dist/.electron-wrapped $TMP/dist/electron
cp -r $electronDist $TMP/electron
chmod -R +w $TMP/electron
# configure electron toolchain
${pkgs.jq}/bin/jq ".build.electronDist = \"$TMP/dist\"" package.json \
${pkgs.jq}/bin/jq ".build.electronDist = \"$TMP/electron\"" package.json \
| ${pkgs.moreutils}/bin/sponge package.json
${pkgs.jq}/bin/jq ".build.linux.target = \"dir\"" package.json \
@ -122,10 +137,12 @@ let
fi
'';
electron-wrap = electron: ''
# Only executed for electron based packages.
# Creates an executable script under /bin starting the electron app
electron-wrap = ''
mkdir -p $out/bin
makeWrapper \
${electron}/bin/electron \
$electronDist/electron \
$out/bin/$(basename "$packageName") \
--add-flags "$(realpath $electronAppDir)"
'';
@ -147,31 +164,35 @@ let
(dep: lib.nameValuePair dep.name dep.version)
deps));
electronPackage =
let
electronDep =
lib.findFirst
(dep: dep.name == "electron")
null
deps;
in
if electronDep == null then
electronDep =
if ! isMainPackage name version then
null
else
lib.findFirst
(dep: dep.name == "electron")
null
else
let
electronVersionMajor =
if electronDep == null then
null
else
lib.versions.major electronDep.version;
in
pkgs."electron_${electronVersionMajor}";
deps;
electronVersionMajor =
lib.versions.major electronDep.version;
electronHeaders =
if electronDep == null then
null
else
pkgs."electron_${electronVersionMajor}".headers;
pkg =
produceDerivation name (stdenv.mkDerivation rec {
inherit dependenciesJson nodeDeps nodeSources version;
inherit
dependenciesJson
electronHeaders
nodeDeps
nodeSources
version
;
packageName = name;
@ -182,9 +203,7 @@ let
electronAppDir = ".";
# only run build on the main package
runBuild =
packageName == mainPackageName
&& version == mainPackageVersion;
runBuild = isMainPackage name version;
src = getSource name version;
@ -195,7 +214,7 @@ let
# prevents running into ulimits
passAsFile = [ "dependenciesJson" "nodeDeps" ];
preConfigurePhases = [ "d2nPatchPhase" ];
preConfigurePhases = [ "d2nLoadFuncsPhase" "d2nPatchPhase" ];
# can be overridden to define alternative install command
# (defaults to 'npm run postinstall')
@ -205,9 +224,36 @@ let
# (see comments below on d2nPatchPhase)
fixPackage = "${./fix-package.py}";
# script to install (symlink or copy) dependencies.
installDeps = "${./install-deps.py}";
# costs performance and doesn't seem beneficial in most scenarios
dontStrip = true;
# declare some useful shell functions
d2nLoadFuncsPhase = ''
# function to resolve symlinks to copies
symlinksToCopies() {
local dir="$1"
echo "transforming symlinks to copies..."
for f in $(find -L "$dir" -xtype l); do
if [ -f $f ]; then
continue
fi
echo "copying $f"
chmod +wx $(dirname "$f")
mv "$f" "$f.bak"
mkdir "$f"
if [ -n "$(ls -A "$f.bak/")" ]; then
cp -r "$f.bak"/* "$f/"
chmod -R +w $f
fi
rm "$f.bak"
done
}
'';
# TODO: upstream fix to nixpkgs
# example which requires this:
# https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.7.tgz
@ -295,68 +341,27 @@ let
else
exit 1
fi
# configure typescript
if [ -f tsconfig.json ]; then
${pkgs.jq}/bin/jq ".compilerOptions.preserveSymlinks = true" tsconfig.json \
| ${pkgs.moreutils}/bin/sponge tsconfig.json
fi
'';
# - links all direct node dependencies into the node_modules directory
# - installs dependencies into the node_modules directory
# - adds executables of direct node module dependencies to PATH
# - adds the current node module to NODE_PATH
# - sets HOME=$TMPDIR, as this is required by some npm scripts
# TODO: don't install dev dependencies. Load into NODE_PATH instead
# TODO: move all linking to python script, as `ln` calls perform badly
configurePhase = ''
runHook preConfigure
# symlink dependency packages into node_modules
for dep in $(cat $nodeDepsPath); do
# add bin to PATH
if [ -d "$dep/bin" ]; then
export PATH="$PATH:$dep/bin"
fi
if [ -e $dep/lib/node_modules ]; then
for module in $(ls $dep/lib/node_modules); do
if [[ $module == @* ]]; then
for submodule in $(ls $dep/lib/node_modules/$module); do
mkdir -p $nodeModules/$packageName/node_modules/$module
echo "installing: $module/$submodule"
ln -s $(realpath $dep/lib/node_modules/$module/$submodule) $nodeModules/$packageName/node_modules/$module/$submodule
done
else
mkdir -p $nodeModules/$packageName/node_modules/
echo "installing: $module"
ln -s $(realpath $dep/lib/node_modules/$module) $nodeModules/$packageName/node_modules/$module
fi
done
fi
done
# symlink sub dependencies as well as this imitates npm better
python ${./symlink-deps.py}
python $installDeps
# resolve symlinks to copies
symlinksToCopies() {
local dir="$1"
echo "transforming symlinks to copies..."
for f in $(find -L "$dir" -xtype l); do
if [ -f $f ]; then
continue
fi
echo "copying $f"
chmod +wx $(dirname "$f")
mv "$f" "$f.bak"
mkdir "$f"
if [ -n "$(ls -A "$f.bak/")" ]; then
cp -r "$f.bak"/* "$f/"
chmod -R +w $f
fi
rm "$f.bak"
done
}
if [ "$installMethod" == "copy" ]; then
symlinksToCopies .
fi
# add bin path entries collected by python script
export PATH="$PATH:$(cat $TMP/ADD_BIN_PATH)"
# add dependencies to NODE_PATH
export NODE_PATH="$NODE_PATH:$nodeModules/$packageName/node_modules"
@ -372,8 +377,9 @@ let
runHook preBuild
# execute electron-rebuild
${lib.optionalString (electronPackage != null)
(electron-rebuild electronPackage)}
if [ -n "$electronHeaders" ]; then
${electron-rebuild}
fi
# execute install command
if [ -n "$buildScript" ]; then
@ -422,8 +428,10 @@ let
fi
# wrap electron app
${lib.optionalString (electronPackage != null)
(electron-wrap electronPackage)}
# execute electron-rebuild
if [ -n "$electronHeaders" ]; then
${electron-wrap}
fi
'';
});
in

View File

@ -0,0 +1,174 @@
import json
import os
import pathlib
import shutil
import subprocess as sp
import sys
out = os.environ.get('out')
pname = os.environ.get('packageName')
version = os.environ.get('version')
root = f"{os.path.abspath('.')}/node_modules"
package_json_cache = {}
satisfaction_cache = {}
with open(os.environ.get("nodeDepsPath")) as f:
nodeDeps = f.read().split()
def get_package_json(path):
if path not in package_json_cache:
with open(f"{path}/package.json") as f:
package_json_cache[path] = json.load(f)
return package_json_cache[path]
def install_direct_dependencies():
add_to_bin_path = []
if not os.path.isdir(root):
os.mkdir(root)
with open(os.environ.get('nodeDepsPath')) as f:
deps = f.read().split()
for dep in deps:
# check for bin directory
if os.path.isdir(f"{dep}/bin"):
add_to_bin_path.append(f"{dep}/bin")
if os.path.isdir(f"{dep}/lib/node_modules"):
for module in os.listdir(f"{dep}/lib/node_modules"):
# ignore hidden directories
if module[0] == ".":
continue
if module[0] == '@':
for submodule in os.listdir(f"{dep}/lib/node_modules/{module}"):
pathlib.Path(f"{root}/{module}").mkdir(exist_ok=True)
print(f"installing: {module}/{submodule}")
origin =\
os.path.realpath(f"{dep}/lib/node_modules/{module}/{submodule}")
os.symlink(origin, f"{root}/{module}/{submodule}")
else:
print(f"installing: {module}")
origin = os.path.realpath(f"{dep}/lib/node_modules/{module}")
os.symlink(origin, f"{root}/{module}")
return add_to_bin_path
def collect_dependencies(root, depth):
if not os.path.isdir(root):
return []
dirs = os.listdir(root)
currentDeps = []
for d in dirs:
if d.rpartition('/')[-1].startswith('@'):
subdirs = os.listdir(f"{root}/{d}")
for sd in subdirs:
cur_dir = f"{root}/{d}/{sd}"
currentDeps.append(f"{cur_dir}")
else:
cur_dir = f"{root}/{d}"
currentDeps.append(cur_dir)
if depth == 0:
return currentDeps
else:
depsOfDeps =\
map(lambda dep: collect_dependencies(f"{dep}/node_modules", depth - 1), currentDeps)
result = []
for deps in depsOfDeps:
result += deps
return result
def symlink_sub_dependencies():
for dep in collect_dependencies(root, 1):
# compute module path
d1, d2 = dep.split('/')[-2:]
if d1.startswith('@'):
path = f"{root}/{d1}/{d2}"
else:
path = f"{root}/{d2}"
# check for collision
if os.path.isdir(path):
continue
# create parent dir
pathlib.Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True)
# symlink dependency
os.symlink(os.path.realpath(dep), path)
# checks if dependency is already installed in the current or parent dir.
def dependency_satisfied(root, pname, version):
if (root, pname, version) in satisfaction_cache:
return satisfaction_cache[(root, pname, version)]
if root == "/nix/store":
return False
parent = os.path.dirname(root)
if os.path.isdir(f"{root}/{pname}"):
package_json_file = f"{root}/{pname}/package.json"
if os.path.isfile(package_json_file):
if version == get_package_json(f"{root}/{pname}")['version']:
satisfaction_cache[(root, pname, version)] = True
return True
satisfaction_cache[(root, pname, version)] =\
dependency_satisfied(parent, pname, version)
return satisfaction_cache[(root, pname, version)]
# transforms symlinked dependencies into real copies
def symlinks_to_copies(root):
sp.run(f"chmod +wx {root}".split())
for dep in collect_dependencies(root, 0):
# only handle symlinks to directories
if not os.path.islink(dep) or os.path.isfile(dep):
continue
version = get_package_json(dep)['version']
d1, d2 = dep.split('/')[-2:]
if d1[0] == '@':
pname = f"{d1}/{d2}"
sp.run(f"chmod +wx {root}/{d1}".split())
else:
pname = d2
if dependency_satisfied(os.path.dirname(root), pname, version):
os.remove(dep)
continue
print(f"copying {dep}")
os.rename(dep, f"{dep}.bac")
os.mkdir(dep)
contents = os.listdir(f"{dep}.bac")
if contents != []:
for node in contents:
if os.path.isdir(f"{dep}.bac/{node}"):
shutil.copytree(f"{dep}.bac/{node}", f"{dep}/{node}", symlinks=True)
if os.path.isdir(f"{dep}/node_modules"):
symlinks_to_copies(f"{dep}/node_modules")
else:
shutil.copy(f"{dep}.bac/{node}", f"{dep}/{node}")
os.remove(f"{dep}.bac")
# install direct deps
add_to_bin_path = install_direct_dependencies()
# dump bin paths
with open(f"{os.environ.get('TMP')}/ADD_BIN_PATH", 'w') as f:
f.write(':'.join(add_to_bin_path))
# symlink non-colliding deps
symlink_sub_dependencies()
# symlinks to copies
if os.environ.get('installMethod') == 'copy':
symlinks_to_copies(root)

View File

@ -1,69 +0,0 @@
import json
import os
import pathlib
import sys
out = os.environ.get('out')
pname = os.environ.get('packageName')
version = os.environ.get('version')
root = f"{out}/lib/node_modules/{pname}/node_modules"
if not os.path.isdir(root):
exit()
with open(os.environ.get("nodeDepsPath")) as f:
nodeDeps = f.read().split()
def getDependencies(root, depth):
if not os.path.isdir(root):
return []
dirs = os.listdir(root)
currentDeps = []
for d in dirs:
if d.rpartition('/')[-1].startswith('@'):
subdirs = os.listdir(f"{root}/{d}")
for sd in subdirs:
cur_dir = f"{root}/{d}/{sd}"
currentDeps.append(f"{cur_dir}")
else:
cur_dir = f"{root}/{d}"
currentDeps.append(cur_dir)
if depth == 0:
return currentDeps
else:
depsOfDeps =\
map(lambda dep: getDependencies(f"{dep}/node_modules", depth - 1), currentDeps)
result = []
for deps in depsOfDeps:
result += deps
return result
deps = getDependencies(root, 1)
# symlink deps non-colliding deps
for dep in deps:
# compute module path
d1, d2 = dep.split('/')[-2:]
if d1.startswith('@'):
path = f"{root}/{d1}/{d2}"
else:
path = f"{root}/{d2}"
# check for collision
if os.path.isdir(path):
continue
# create parent dir
pathlib.Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True)
# symlink dependency
os.symlink(os.path.realpath(dep), path)