Skip to content

Hello world (Electron)

Chung Leong edited this page Mar 24, 2024 · 8 revisions

In this example we're going to create a very simple app that outputs "Hello world!" to the console through Zig. It demonstrates the basics of working with node-zigar and the steps for deploying an app on multiple platforms.

Creating the app

We begin by creating a barebone Electron App. Open a terminal window and run the following command:

npm init electron-app@latest hello

Go into the newly created hello directory and add a zig sub-directory. This is where we'll keep our Zig source files:

cd hello
mkdir zig

In a text editor, create hello.zig:

const std = @import("std");

pub fn hello() void {
    std.debug.print("Hello world!", .{});
}

In order to use this code, we need to install node-zigar:

npm install --save node-zigar

Open index.js in the src sub-directory and insert the following line at the bottom:

require('node-zigar/cjs');
const { hello } = require('../zig/hello.zig');
hello();

The first line loads node-zigar (CommonJS version), while the second line imports the function we had written earlier.

Return to the terminal window and start the app:

npm run start

You should see something like the following:

> hello@1.0.0 start
> electron-forge start

✔ Checking your system
✔ Locating application
✔ Loading configuration
✔ Preparing native dependencies [0.3s]
✔ Running generateAssets hook

A window will not appear immediately. It takes some time for the Zig compiler to perform the initial compilation. After half a minute or so a browser window should open. You will also see our test message in the terminal window:

✔ Checking your system
✔ Locating application
✔ Loading configuration
✔ Preparing native dependencies [0.3s]
✔ Running generateAssets hook

Hello world!

That confirms our simple app is working.

Configuring the app for deployment

When you use require on a Zig file, node-zigar will place the resultant library file at a temporary location. At the root level of the app you will notice a zigar-cache sub-directory with the following structure:

📁 zigar-cache
  📁 zig-fa7aee6f
    📁 Debug
      📁 hello.zigar
        📑 linux.x64.so
      📁 node-zigar-addon
        📑 linux.x64.node

hello.zigar is a node-zigar module. It's a directory containing dynamic-link libraries for different platforms. node-zigar-addon is the Node.js native addon used to load node-zigar modules. It too comes in platform-specific versions.

The files in the cache directory aren't ones we want delivered to end-users. They're compiled at the Debug level and are therefore large and slow. Moreover, they only cover the platform we're using for development (Linux in this case) and not others (Windows and Mac).

To prepare our app for deployment, we first change the require statement so that it references a .zigar instead, stored at a more permanent location:

require('node-zigar/cjs');
const { hello } = require('../lib/hello.zigar');  // <-- 
hello();

We then create a configure file for node-zigar with the help of its CLI script:

npx node-zigar init

node-zigar.config.json will be populated with some default options:

{
  "optimize": "ReleaseSmall",
  "sourceFiles": {},
  "targets": [
    {
      "platform": "linux",
      "arch": "x64"
    }
  ]
}

sourceFiles maps .zigar modules to source files. Paths are relative to the config file.

optimize can be Debug, ReleaseSafe, ReleaseSmall, or ReleaseFast.

targets is a list of cross-compile targets. platform and arch can be one of the possible values returned by os.platform and os.arch.

We insert the following into our config file:

{
  "optimize": "ReleaseSmall",
  "sourceFiles": {
    "lib/hello.zigar": "zig/hello.zig"
  },
  "targets": [
    { "platform": "win32", "arch": "x64" },
    { "platform": "win32", "arch": "arm64" },
    { "platform": "win32", "arch": "ia32" },
    { "platform": "linux", "arch": "x64" },
    { "platform": "linux", "arch": "arm64" },
    { "platform": "darwin", "arch": "x64" },
    { "platform": "darwin", "arch": "arm64" }
  ]
}

Then we ask node-zigar to create the necessary library files:

npx node-zigar build
Building hello.zigar:
  win32.x64.dll
  win32.arm64.dll
  win32.ia32.dll
  linux.x64.so
  linux.arm64.so
  darwin.x64.dylib
  darwin.arm64.dylib
Building node-zigar-addon:
  win32.x64.node
  win32.arm64.node
  win32.ia32.node
  linux.x64.node
  linux.arm64.node
  darwin.x64.node
  darwin.arm64.node

When the script completes, start the app again to confirm that it's correctly configured.

Next, we edit forge.config.js, the config file used by Electron Forge. The first thing we do is replacing packagerConfig.asar's boolean value with an object:

module.exports = {
  packagerConfig: {
    asar: {
      unpack: '*.{dll,dylib,so}',
    },

ASAR is the archive format used by Electron to store an app's resources. As the OS is not capable of loading libraries directly off the archive, we need to tell Electron to unpack them into an outside directory.

We then add some Regular Expression filters, telling Forge which files should be omitted:

module.exports = {
  packagerConfig: {
    /* ... */
    ignore: [ 
      /\/(zig|zig-cache|zigar-cache)(\/|$)/, 
      /\/node-zigar\.config\.json$/,
    ],    
  },

Depending on which operation system you're using, you might want to adjust the markers option. Each package maker has its own set of requirements. The Squirrel maker, for instance, requires Windows. The RPM and DEB makers meanwhile work in Linux only and expect the presence of particular programs.

For simplicity sake (this is merely an example, after all), you might elect to use Zip for all platforms:

  makers: [
    {
      name: '@electron-forge/maker-zip',
    },
  ],

Now comes the time to build the packages. Let us start with Linux:

npm run make -- --platform linux --arch x64,arm64

The process will take some time, as Forge has to download the Electron executable for each architecture. When it finish, we follow up with Windows:

npm run make -- --platform win32 --arch x64,ia32,arm64

Then MacOS:

npm run make -- --platform darwin --arch x64,arm64

Note: If your computer is not running MacOS, the build process for arm64 will fail due to the absence of the codesign utility. To eliminate the need to resign the executable, comment out the FusesPlugin plugin in the config file.

Once everything is built, you're find the following in the out directory:

📁 hello-darwin-arm64
📁 hello-darwin-x64
📁 hello-linux-arm64
📁 hello-linux-x64
📁 hello-win32-arm64
📁 hello-win32-ia32
📁 hello-win32-x64
📁 make

The hello-[platform]-[arch] directories hold copies of the contents that went into the installation packages. In the make directory, you'll find the packages themselves:

📁 deb
  📁 arm64
    📦 hello_1.0.0_arm64.deb
  📁 x64
    📦 hello_1.0.0_amd64.deb
📁 rpm
  📁 arm64
    📦 hello-1.0.0-1.arm64.rpm
  📁 x64
    📦 hello-1.0.0-1.x86_64.rpm
📁 squirrel.windows
  📁 arm64
    📦 hello-1.0.0-full.nupkg
    📦 hello-1.0.0 Setup.exe
    📄 RELEASES
  📁 ia32
    📦 hello-1.0.0-full.nupkg
    📦 hello-1.0.0 Setup.exe
    📄 RELEASES
  📁 x64
    📦 hello-1.0.0-full.nupkg
    📦 hello-1.0.0 Setup.exe
    📄 RELEASES
📁 zip
  📁 darwin
    📁 arm64
      📦 hello-darwin-arm64-1.0.0.zip
    📁 x64
      📦 hello-darwin-x64-1.0.0.zip

Conclusion

Congratulation! You have created your first cross-platform app with the help of Electron and node-zigar. You can try testing the installation packages if you have the respected systems on hand. As console output is hidden when the app is launched from the graphical interface, you might not be entirely convinced it's working correctly. In the next tutorial we're going to create an app that performs more visible (and useful) work.