Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added instructions for packaging with Electron #169

Merged
merged 2 commits into from
Apr 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions doc/electron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Electron
How to run and package Threepenny apps with [Electron](https://electron.atom.io)
and
[Electron Packager](https://github.com/electron-userland/electron-packager#electron-packager).

For reference, a minimal working example is available
[here](https://github.com/barischj/threepenny-gui-electron-example).

## Justification
Normally when running a Threepenny app we execute our Haskell, with `stack exec`
or otherwise, which starts a local server and we open our browser on a certain
port to view our app. However this has a few subtle drawbacks.

One drawback is that most browsers are designed with remote servers in mind and
have security features which don't make much sense for local server
architectures like Threepenny's. Take file selection for example. When a user
selects a file in a browser, the browser only exposes the file name and contents
to the server, not allowing the server to receive the full file path. However
for local server architectures there isn't much point in sending the entire file
contents to the server since the server is on the same file system and could
read the file directly, if only it had the full file path. Electron displays our
app in a Chromium instance with many of these security features removed.

Another drawback is that the user has to run the app from the command line.
Using electron-packager we can package native apps for Linux, macOS and Windows.

## Running with Electron
To run a Threepenny app with Electron we need an Electron
[main process](https://electron.atom.io/docs/tutorial/quick-start/#main-process).
We provide this one: [electron.js](./electron/electron.js). It runs the
following on startup:
- selects a free port to run on
- executes our Threepenny app binary, passing the port to run on as an argument
- waits for Threepenny's server to start accepting connections
- opens an Electron window which loads the URL of our Threepenny app

To get started with the linked `electron.js` first add
this [package.json]('./electron/package.json') to your project root directory.
You'll need Node installed and `npm` on your PATH, confirm by running `which
npm`. Now run `npm install` from the project root directory to install the
necessary dependencies.

The linked `electron.js` executes the Threepenny app binary, passing the port to
run on as an argument. This of course means your Threepenny app needs to take
the port as an argument. We suggest also setting stdout to be line buffered, at
least while developing. Altogether it should look something like this:

```Haskell
module Main where

import System.Environment (getArgs)
import System.IO
import YourApp (start)

main :: IO ()
main = do
hSetBuffering stdout LineBuffering
[port, otherArg1, otherArg2] <- getArgs
start (read port)
```

Now copy the linked `electron.js` to your project root directory. You'll have to
edit the defined constants: `relBin`, which is the relative path from
`electron.js` to your Threepenny application binary; and `binArgs`, which
contains any additional arguments to pass to the binary. If you're not sure
about the relative path to your application binary, and you're using Stack, see
the [next section](#explicit-binary-location).

Now run your app with Electron: `./node_modules/.bin/electron electron.js`

### Explicit binary location
`relBin` is the relative path from `electron.js` to your Threepenny application
binary. This might change depending on which tool or platform you are building
with and thus can be a pain to set manually. If you are using Stack you can
easily build your application binary to an explicit location, possibly a `build`
directory:

```stack install --local-bin-path build```

Now you can simply set `relBin` to `./build/your-app-exe`.

## Packaging with electron-packager
This section assumes the app is already setup to run with Electron based on
the [above](#running-with-electron) instructions.

First install electron-packager: `npm install electron-packager`

Optionally edit the "name" field in `package.json` to set the name of the
packaged app. Then to package the app for the current platform, simply:

```
./node_modules/.bin/electron-packager .
```

This is the most basic way to package the app, it will copy the current
directory to the packaged app. However you'll likely want to avoid copying some
source files, which can be achieved with the `--ignore` flag. You might end up
using:

```
./node_modules/.bin/electron-packager . --ignore=app --ignore=src
```

If you are using Stack and building your application binary to an explicit
location, as explained [above](#explicit-binary-location), then you might want
to also ignore `.stack-work/`. An icon can be set by passing the icon path to
`--icon`, note that the icon format
[depends on the platform](https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#icon).
For more options use the `--help` flag.
81 changes: 81 additions & 0 deletions doc/electron/electron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const { app, BrowserWindow } = require('electron');
const freeport = require('freeport');
const spawn = require('child_process').spawn;
const path = require('path');
const waitOn = require('wait-on');

// Time to wait for Threepenny server, milliseconds
const timeout = 10000;
// Relative path to the Threepenny binary.
const relBin = './build/your-app-exe';
// Additional arguments to pass to the Threepenny binary.
const binArgs = ['otherArg1', 'otherArg2'];

// Assign a random port to run on.
freeport((err, port) => {
if (err) throw err;

const url = `http://localhost:${port}`;
let child = null; // Server process we spawn and kill

// Keep a global reference of the window object, if we don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: { nodeIntegration: false },
});

console.log(`Loading URL: ${url}`);
win.loadURL(url);

// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object for garbage collection.
win = null;
});
}

// Called when Electron has finished initialization and is ready to create
// browser windows. Some APIs can only be used after this event occurs. We
// start the child process and wait before loading the web page.
app.on('ready', () => {
child = spawn(path.join(__dirname, relBin), [port].concat(binArgs));
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', console.log);
child.stderr.on('data', console.log);
child.on('close', code =>
console.log(`Threepenny app exited with code ${code}`));

// Wait until the Threepenny server is ready for connections.
waitOn({ resources: [url], timeout }, (err_) => {
if (err_) throw err_;
createWindow();
});
});

// Quit when all windows are closed, unless on macOS. On macOS it is common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

// Kill the child process when quitting Electron.
app.on('will-quit', () => child.kill());

app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the dock icon
// is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
});
9 changes: 9 additions & 0 deletions doc/electron/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "YourApp",
"main": "electron.js",
"dependencies": {
"electron": "^1.6.1",
"freeport": "^1.0.5",
"wait-on": "^2.0.2"
}
}