Working on Blazing Bookkeeper, an ORC based document scanner, I had to build and package various libraries and binaries through electron-builder. I couldn’t find any guides detailing this, so I’ll outline the process we took to make it work.
I used electron-boilerplate as the base. It includes build scripts using electron-builder
. This guide details MacOS builds, but could easily be modified to make Windows and Linux builds work.
Build directories
We’ll create two new directories. ./thirdparty
for build source, and ./build
for build output. We’ll add both to .gitignore
.
$ mkdir thirdparty build
$ echo "/thirdparty" >> .gitignore
$ echo "/build" >> .gitignore
3rd-party build scripts
Now we’ll add the build scripts for our 3rd-party dependencies to the ./scripts
folder.
There are many ways you can do this, but to save you some headache, I’ve included functionality to make build scripts take care of most use cases. The scripts are platform aware, hence the ./scripts/darwin
subdirectory.
The main build script takes care of installing build tools, cleaning up builds for unnecessary files (e.g. man files and include files), and finally sets relative dylib paths so we can package the binaries in the app, and be able to run it anywhere.
In this example, we’ll build libjpeg
and libpng
. There are two environment variables, THIRDPARTYDIR
and BUILDDIR
, that’ll be set in the electron build script in the next section.
## ./scripts/build/darwin/build.sh
#!/bin/bash
set -e
BASEDIR=$(dirname "$0")
echo [darwin-build] Building development dependencies with brew
brew install automake autoconf libtool pkg-config cmake
echo [darwin-build] Building dependencies
cd $BASEDIR && ./dependencies.sh
echo [darwin-build] Cleaning up builds
cd $THIRDPARTYDIR
rm -rf dependencies/share dependencies/include dependencies/bin
echo [darwin-build] Set relative dylib paths
for f in $(find $THIRDPARTYDIR -type f -name '*.dylib' -or -path '*/bin/*')
do
if [ -f "$f" ]
then
DIR_PREFIX="@loader_path/../.."
DYLIBS=`otool -L $f | grep "$THIRDPARTYDIR" | awk -F' ' '{ print $1 }'`
for dylib in $DYLIBS
do
install_name_tool -change $dylib "$DIR_PREFIX${dylib#$THIRDPARTYDIR}" "$f"
done
fi
done
## ./scripts/build/darwin/dependencies.sh
#!/bin/bash
set -e
mkdir -p $BUILDDIR
DEST_DEPENDENCIES_DIR=$THIRDPARTYDIR/dependencies
# Compile jpeg
cd $BUILDDIR
if [ ! -d "$BUILDDIR/jpeg-src" ]
then
if [ ! -f "$BUILDDIR/jpeg-v8d.tar.gz" ]
then
curl -o jpeg-v8d.tar.gz -L -z jpeg-v8d.tar.gz http://www.ijg.org/files/jpegsrc.v8d.tar.gz
fi
tar xzf jpeg-v8d.tar.gz
mv jpeg-8d jpeg-src
fi
if [ ! -f "$DEST_DEPENDENCIES_DIR/lib/libjpeg.dylib" ]
then
cd jpeg-src
./configure \
--prefix=$DEST_DEPENDENCIES_DIR \
--disable-dependency-tracking
make install
fi
# Compile libpng
cd $BUILDDIR
if [ ! -d "$BUILDDIR/libpng-src" ]
then
if [ ! -f "$BUILDDIR/libpng-1.6.26.tar.xz" ]
then
curl -o libpng-1.6.26.tar.xz -L -z libpng-1.6.26.tar.xz ftp://ftp.simplesystems.org/pub/libpng/png/src/libpng16/libpng-1.6.26.tar.xz
fi
tar xzf libpng-1.6.26.tar.xz
mv libpng-1.6.26 libpng-src
fi
if [ ! -f "$DEST_DEPENDENCIES_DIR/lib/libpng.dylib" ]
then
cd libpng-src
./configure \
--prefix=$DEST_DEPENDENCIES_DIR \
--disable-dependency-tracking
make
make install
fi
Run the build scripts
We want the binaries to be built (if not built already) when running npm start
and npm run release
. To achieve this we’ll add a gulp task to ./tasks/build_app.js
.
// ./tasks/build_app.js
// ...
var exec = require('child_process').exec;
var gutil = require('gulp-util');
// ...
gulp.task('dependencies', function () {
var rootDir = jetpack.cwd('./').path()
var platform = process.platform
var buildScript = jetpack.cwd('./scripts').path('build/' + platform + '/build.sh')
var buildDir = rootDir + '/build'
var buildDestDir = rootDir + '/thirdparty'
return new Promise(function (resolve, reject) {
if (!jetpack.exists(buildScript)) {
gutil.log('No build file for', `'${gutil.colors.cyan(platform)}'`)
return resolve()
}
gutil.log('Going to build for', `'${gutil.colors.cyan(platform)}'`)
var buildCommand = [
'BUILDDIR=' + buildDir,
'THIRDPARTYDIR=' + buildDestDir,
'sh ' + buildScript
].join(' ')
var child = exec(buildCommand, {
maxBuffer: 1024 * 1024 * 10
},
function (error, stdout, stderr) {
if (!error) {
gutil.log('Build succesful')
resolve()
} else {
gutil.log('Build failed')
reject()
throw error
}
})
child.stdout.pipe(process.stdout)
child.stderr.pipe(process.stderr)
return child
})
})
// ...
gulp.task('build', ['dependencies', 'bundle', 'less', 'environment'])
The maxBuffer
argument can be increased or decreased depending on how much your build scripts output.
Set the third party paths
To make it possible for our Electron app to access the binaries we’ll need to set the paths in the environment. To make it easier to manage with potentially multiple paths within the ./3rdparty
directory, the following script can be used.
It’s the same as running PATH=$PATH:$(pwd)/thirdparty/path/to/bin DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$(pwd)/thirdparty/dependencies/lib npm start
.
// ./src/utils/thirdparty_env.js
import getResourcesPath from './resources'
var thirdpartyPath = getResourcesPath('thirdparty')
var env = {}
if (process.platform === 'darwin') {
// 3rd party binaries path
env.PATH = [
'$PATH'
// Add third-party binaries paths here
].join(':')
// 3rd party libraries
env.DYLD_LIBRARY_PATH = [
'$DYLD_LIBRARY_PATH',
'/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/', // Core Graphics package
thirdpartyPath + '/dependencies/lib'
// Add more third-party lib paths here
].join(':')
// Add any extra environment paths here, e.g. PKG_CONFIG_PATH
}
export default env
The following utility script makes sure to get the right resource path since npm start
and npm build
/npm run release
has different paths.
// ./src/utils/resources.js
import path from 'path'
var getResourcesPath = function () {
var paths = Array.from(arguments)
if (/[\\/]Electron\.app[\\/]/.test(process.execPath)) {
// Development mode resources are located in project root.
paths.unshift(process.cwd())
} else {
// In builds the resources directory is located in 'Contents/Resources'
paths.unshift(process.resourcesPath)
}
return path.join.apply(null, paths)
}
export default getResourcesPath
And finally we can load the environment:
// ./src/background.js
// ...
import thirdparty_env from './utils/thirdparty_env'
//...
Object.assign(process.env, thirdparty_env);
console.log('Settings thirdparty environment variables:', thirdparty_env);
Get electron-builder to package the binaries
We can use the extraResources
key to get electron-builder
to package the binaries into the app. Modify package.json
, and add the directory to the build like so:
// package.json
"build": {
// ...
"extraResources": [
"thirdparty"
]
// ...
}
During build, electron-builder
will now copy the ./thirdparty
directory into the Resources
directory in our app.
Set up CI builds
Let’s set up Travis to generate the build automatically. You can set a GH_TOKEN
environment variable to automatically publish the app on GitHub.
// .travis.yml
// ...
cache:
directories:
// ...
- build
- thirdparty
before_install:
- npm install --only=dev
- node_modules/.bin/gulp dependencies
deploy:
provider: script
script: npm build --x64
Code signing
It’s very easy to code sign the 3rd party binaries. In package.json
you just add the following:
// package.json
{
"build": {
// ...
"osx-sign": {
"binaries": [
"thirdparty/path/to/bin"
]
},
// ...
},
// ...
}
You’ll need to use electron-builder
v7.9.0, as osx-sign
has been deprecated since that version.
// package.json
{
// ...
"devDependencies": {
// ...
"electron-builder": "7.9.0",
// ...
},
// ...
}
Caveats
With Blazing Bookkeeper we were building OpenCV. A pkg-config
path was needed, which meant that we had to set the PKG_CONFIG_PATH
environment variable. It’s easy enough, but you’ll need to set it before any npm build calls, like: PKG_CONFIG_PATH=$(pwd)/thirdparty/opencv/lib/pkgconfig npm install
Production example
Our app is running with OpenCV, Tesseract, Poppler and many dependencies just fine. You can take a look at it right here: https://blazingbookkeeper.com
If you would like to see the actual code changes, you can find the pull requests in the GitHub repo (#7, #28).