How build Electron app for every plateforms
Tags: electron, javascript, productivity, showdev
Published: 2020-05-21 22:32:03

A few days ago, I created an application with [ElectronJs](https://www.electronjs.org). The problem I had when creating it was to make my application available to everyone, regardless of OS and platform.
To do so, I had to be interested in several tools, including [Electron Forge](https://www.electronforge.io), [electron-packager](https://github.com/electron/electron-packager) and [electron-builder](https://www.electron.build).

After several tries with Electron Forge, I realized that it was not stable enough, and that it was not possible to compile for multi-platforms at the moment.

So I went to electron-packager. Even if this tool is very efficient, it's very difficult to customize it, like adding a custom icon to the application.

So I went to electron-builder. Once I understood the documentation, it became very easy to use it.

I also had another problem: to automate the build. Indeed, I code under Windows. It becomes impossible to build the application for Linux and Mac. So I had to use an alternative tool. My choice went to Github and its Github Actions.

Well, let's start the explanations in code form:

# Github Action

```yaml
name: Build <App name>

on:
  release:
    types:
      - published

jobs:

  build:
    name: Build <App name>
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]

    steps:

      - uses: actions/checkout@v2

      - name: Setup NodeJs
        uses: actions/setup-node@v1
        with:
          node-version: '12.x'
      
      - name: Install dependencies
        run: yarn

      - name: Build
        run: yarn export

      - name: Upload builds to release
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ github.ref }}
          files: out/*.*
          draft: true
        env:
          GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

      - name: Upload Nightly Build
        uses: actions/upload-artifact@v2
        if: success()
        with:
          name: <App name>-nightly
          path: out/**/*!(.zip)
```

What I'm doing above is what is called a Github Action. It allows me to automate my tasks. In this one I tell him that at each release he will have to execute certain tasks. 
Here I ask him to perform simple tasks:
 1) Clone my repository
 2) Prepare NodeJs
 3) Install the dependencies
 4) Export the application
 5) Send what was exported to the release that was released
 6) (Optional) Create a nightly build.

It is important to know one thing: electron-builder will create unpacked versions of your application. This means that these are folders containing the application available for any platform. If we want to put this version in our release, we have to compress it, which is not done automatically.

To do so, we need a script when we export it

# Export script

```js
const pngToIco = require('png-to-ico');
const fs = require('fs-extra');
const ora = require('ora');
const path = require('path');
const zip = require('bestzip');

const args = process.argv;

const plateforms = args.pop().replace(/^-/g, '').split('');

function getValidPlateforms() {
    const spinner = ora({
        text: 'Searching current platform build...',
        spinner: 'line',
        color: 'cyan'
    }).start();
    if (process.platform === 'win32') {
        if (plateforms.includes('w')) {
            spinner.succeed('Plateform found: ' + process.platform + ' (Only Windows build available)');
            return ['w'];
        } else {
            spinner.fail('Plateform not compatible');
            throw new Error('Can\'t compile to Windows: not compatible OS');
        }
    } else {
        spinner.succeed('Plateform found: ' + process.platform + ' (All builds available)');
        return plateforms;
    }
}

async function zipBuilds() {
    const spinner = ora({
        text: 'Zip builds...',
        spinner: 'line',
        color: 'cyan'
    }).start();
    return fs.readdir('out')
        .then(files => {
            const statsJobs = [];
            for (const file of files) {
                const filePath = path.join('out', file);
                statsJobs.push(fs.stat(filePath).then(stat => {
                    return { stat, filePath };
                }));
            }
            return Promise.all(statsJobs);
        })
        .then(stats => {
            const zipJobs = [];
            for (const statInfos of stats) {
                const { stat, filePath } = statInfos;
                if (stat.isDirectory()) {
                    if (!fs.existsSync(filePath + '.zip')) {
                        zipJobs.push(
                            zip({
                                source: filePath,
                                destination: filePath + '.zip'
                            })
                        )
                    }
                }
            }
            return Promise.all(zipJobs);
        })
        .then(() => spinner.succeed('All builds have been zipped with success'));
}

// TODO: Compile to ICNS file for Mac
if (!fs.existsSync('public/images/favicon.ico')) {
    pngToIco('public/images/favicon.png')
        .then(v => fs.writeFileSync('public/images/favicon.ico', v))
}

const validPlateforms = getValidPlateforms();
const build = require('child_process')
    .exec('electron-builder build -' + validPlateforms.join('') +  ' -c configs/build.yml');
const spinner = ora({
    text: 'Building app...',
    spinner: 'line',
    color: 'cyan'
}).start();

build.stderr.on('data', data => console.error(data));
build.stdout.on('data', data => {
    spinner.text = data;
});

['disconnect', 'exit'].forEach(listener => {
    build.on(listener, () => {
        spinner.succeed('Build completed');
        zipBuilds();
    });
});
```

This code is a little more complicated than the previous one. What it does is pretty straightforward. Apart from having a custom spinner with the [ora](https://github.com/sindresorhus/ora) module, I convert the icon to ico format, which is the windows image format, I check the user's platform to create either a Windows exclusive build or a Mac and Linux build, and finally I zip these builds so I can transfer them to my release.

Note that I did not find an interesting module to convert an image to mac format, it will have to be done from an online site.

Now we're almost done, we still have the configuration file to do. For this, we will create a file "build.yml" where we will put in the following configuration:

# Application build configuration

```yaml
appId: com.<your name>.<your app name in lower case, without spaces>
productName: <your app name>
directories:
  output: out

mac:
  category: <Category of your app> # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/TP40009250-SW8
  target:
    - dmg
    - mas
  icon: public/images/favicon.icns

win:
  icon: public/images/favicon.ico
  target:
    - portable
    - squirrel
squirrelWindows:
  iconUrl: "https://raw.githubusercontent.com/<your name>/<your app name>/master/favicon.ico"
  remoteReleases: true

linux:
  target:
    - snap
    - deb
    - rpm
    - pacman
  icon: favicon.png
  synopsis: <What is your app>
  category: <Category of your app> # https://specifications.freedesktop.org/menu-spec/latest/apa.html#main-category-registry
```

I assume that all your files are at the root of your project.

Don't forget to add an access token for your Github Action.

And here we are, we just created our build automation based on Github Actions and a simple export script. Simply execute the export script to build your ElectronJs app.