In the last two posts I wrote walkthroughs for how to create new CLI and GUI apps in Erlang from scratch using a tool called ZX. Today I want to show how to package apps and publish them using Zomp (the package distribution system) and get them into the hands of your users with minimal fuss.
To understand how packages are distributed to users (the “why does anything do anything?” part of grokking ZX/Zomp), one must first understand a bit about how Zomp views the world.
Packages in the system are organized by “realms”. A code realm is like a package repository for a Linux distribution. The most important thing about realm/repository configuration is that you know by each package’s signature whether it really came from the it claims as its origin. Just like Linux repositories, anyone can create or host a Zomp realm, and realms can be mirrored.
(As you will see in a future tutorial, though, administration and and mirroring with Zomp is way easier and flexible than traditional Linux repositories. As you will see below, packaging with ZX is just a single command — ZX already knows everything it needs from the zomp.meta file.)
In this example I am going to put the example CLI project, Termifier GUI (a toy example app that converts JSON to Erlang terms) into the default FOSS realm, “otpr”. Because I am the sysop I have packaging and maintenance permissions for every package in the realm, as well as the sole authority to add projects and “accept” a package into the write-only indexes (packagers have “submit” authority, maintainers have “review”, “reject” and “approve” authorities).
[Note: The indexes are write only because dependencies in ZX are statically defined (no invisible updates) and the indexes are the only complete structure that must be mirrored by every mirroring node. Packages are not copied to new mirrors, they are cached the first time they are requested, with mirror nodes connected in a tree instead of a single hub pushing to all mirrors at once. This makes starting a new mirror very light weight, even for large realms, as no packages need to be copied to start (only the realm’s update history, from which the index is constructed), and packages in high demand experience “trickle down” replication, allowing mirrors to be sparse instead of complete. Only the “prime node” for a given realm must have a complete copy of everything in that particular realm. Nodes can mirror an arbitrary number of realms, and a node that is prime for one or more realms may mirror any number of others at the same time, making hosting of private project code mixed with mirrored public FOSS code a very efficient arrangement for organizations and devops.]
In the original Termifier GUI tutorial I simply created it and launched it from the command line using ZX’s zx rundir [path]
and zx runlocal
commands. The package was implicitly defined as being in the otpr realm because I never defined any other, but otpr itself was never told about this, so it merely remained a locally created project that could use packages hosted by Zomp as dependencies, but was not actually available through Zomp. Let’s change that:
ceverett@okonomiyaki:~/vcs$ zx add package otpr-termifierg
Done. That’s all there is to it. I’m the sysop, so this command told ZX to send a signed instruction (signed with my sysop key) to the prime node of otpr to create an entry for that package in the realm’s index.
Next we want to package the project. Last time we messed with it it was located at ~/vcs/termifierg/
, so that’s where I’ll point ZX:
ceverett@okonomiyaki:~/vcs$ zx package termifierg/ Packaging termifierg/ Writing app file: ebin/termifierg.app Wrote archive otpr-termifierg-0.1.0.zsp
Next I need to submit the package:
ceverett@okonomiyaki:~/vcs$ zx submit otpr-termifierg-0.1.0.zsp
The idea behind submission is that normally there are two cases:
- A realm is a one-man show.
- A realm has a lot of people involved in it and there is a formal preview/approval, review/acceptance process before publication (remember, the index is write-only!).
In the case where a single person is in charge rushing through the acceptance process only involves three commands (no problem). In the case where more than one person is involved the acceptance of a package should be a staged process where everyone has a chance to see each stage of the acceptance process.
Once a package has been submitted it can be checked by anyone with permissions on that project:
ceverett@okonomiyaki:~/vcs$ zx list pending otpr-termifierg 0.1.0 ceverett@okonomiyaki:~/vcs$ zx review otpr-termifierg-0.1.0 ceverett@okonomiyaki:~/vcs$ cd otpr-termifierg-0.1.0 ceverett@okonomiyaki:~/vcs/otpr-termifierg-0.1.0$
What the zx review [package_id]
command does is download the package, verify the signature belongs to the actual submitter, and unpacks it in a directory so you can inspect it (or more likely) run it with zx rundir [unpacked directory]
.
After a package is reviewed (or if you’re flying solo and already know about the project because you wrote it) then you can “approve” it:
ceverett@okonomiyaki:~/vcs$ zx approve otpr-termifierg-0.1.0
The if the sysop is someone different than the packager then the review command is actually necessary, because the next step is re-signing the package with the sysop’s key as a part of acceptance into the realm. That is, the sysop runs zx review [package_id]
, actually reviews the code, and then once satisfied runs zx package [unpacked_dir]
which results in a .zsp file signed by the sysop. If the sysop is the original packager, though, the .zsp file that was created in the packaging step above is already signed with the sysop’s key.
The sysop is the final word on inclusion of a package. If the green light is given, the sysop must “accept” the package:
ceverett@okonomiyaki:~/vcs$ zx accept otpr-termifierg-0.1.0.zsp
Done! So now let’s see if we can search the index for it, maybe by checking for the “json” tag since we know it is a JSON project:
ceverett@okonomiyaki:~/vcs/termifierg$ zx search json otpr-termifierg-0.1.0 otpr-zj-1.0.5 ceverett@okonomiyaki:~/vcs/termifierg$ zx describe otpr-termifierg-0.1.0 Package : otpr-termifierg-0.1.0 Name : Termifier GUI Type : gui Desc : Create, edit and convert JSON to Erlang terms. Author : Craig Everett zxq9@zxq9.com Web : Repo : https://gitlab.com/zxq9/termifierg Tags : ["json","eterms"]
Yay! So we can now already do zx run otpr-termifierg
and it will build itself and execute from anywhere, as long as the system has ZX installed.
I notice above that the “Web” URL is missing. The original blog post is as good a reference as this project is going to get, so I would like to add it. I do that by running the “update meta” command in the project directory:
ceverett@okonomiyaki:~/vcs/termifierg$ zx update meta
DESCRIPTION DATA [ 1] Project Name : Termifier GUI [ 2] Author : Craig Everett [ 3] Author's Email : zxq9@zxq9.com [ 4] Copyright Holder : Craig Everett [ 5] Copyright Holder's Email : zxq9@zxq9.com [ 6] Repo URL : https://gitlab.com/zxq9/termifierg [ 7] Website URL : [ 8] Description : Create, edit and convert JSON to Erlang terms. [ 9] Search Tags : ["json","eterms"] [10] File associations : [".json"] Press a number to select something to change, or [ENTER] to continue. (or "QUIT"): 7 ... [snip] ...
The “update meta” command is interactive so I’ll spare you the full output, but if you followed the previous two tutorials you already know how this works.
After I’ve done that I need to increase the “patch” version number (the “Z” part of the “X.Y.Z” semver scheme). I can do this with the “verup” command, also run in the project’s base directory:
ceverett@okonomiyaki:~/vcs/termifierg$ zx verup patch
Version changed from 0.1.0 to 0.1.1.
And now time to re-package and put it into the realm. Again, since I’m the sysop this is super fast for me working alone:
ceverett@okonomiyaki:~/vcs$ zx submit otpr-termifierg-0.1.1.zsp
ceverett@okonomiyaki:~/vcs$ zx approve otpr-termifierg-0.1.1
ceverett@okonomiyaki:~/vcs$ zx accept otpr-termifierg-0.1.1.zsp
And that’s that. It can immediately be run by anyone anywhere as long as they have ZX installed.
BONUS LEVEL!
“Neat, but what about the screenshot of it running?”
Up until now we’ve been launching code using ZX from the command line. Since Termifier GUI is a GUI program and usually the target audience for GUI programs is not programmers, yesterday I started on a new graphical front end for ZX intended for ordinary users (you know, people expert at things other than programming!). This tool is called “Vapor” and is still an ugly duckling in beta, but workable enough to demonstrate its usefulness. It allows people to graphically browse projects from their desktop, and launch by clicking if the project is actually launchable.
Vapor is like low-pressure Steam, but with a strong DIY element to it, as anyone can become a developer and host their own code.
I haven’t written the window manager/desktop registration bits yet, so I will start Vapor from the command line with ZX:

You’ll notice a few things here:
- Termifier GUI’s latest version is already selected for us, but if we click that button it will become a version selector and we can pick a specific version.
- Observer is listed, but only as a “virtual package” because it is part of OTP, not actually a real otpr package. For this reason it lacks a version selector. (More on this below.)
- Vapor lacks a “run” button of its own because it is already running (ZX is similarly special-cased)
When I click Termifier’s “run” button Vapor’s window goes away and we see that the termifierg-0.1.1 package is fetched from Zomp (along with deps, if they aren’t already present on the system), built and executed. If we run it a second time it will run immediately from the local cache since it and all deps are already built.

When Termifier terminates Vapor lets ZX know it is OK to shutdown the runtime.
A special note on Observer and “Virtual Packages”
[UPDATE 2020-01-12: The concept of virtual packages is going away, observer will have a different launch method soon, and a rather large interface change is coming to Vapor soon. The general principles and function the system remain the same, but the GUI will look significantly different in the future — the above is the day-2 functioning prototype.]
When other programs are run by Vapor the main Vapor window is closed. Remember, each execution environment is constructed at runtime for the specific application being run, so if we run two programs that have conflicting dependencies there will be confusion about the order to search for modules that are being called! To prevent contamination Vapor only allows a single application to be run at once from a single instance of Vapor (you can run several Vapor instances at once, though, as each invocation of ZX creates an independent Erlang runtime with its own context and environment — the various zx_daemon
s coordinate locally to pick a leader, though, so resource contention is avoided by proxying through the leader). If you want several inter-related apps to run at once within the same Erlang runtime, create a meta-package that has the sole function of launching them all together with commonly defined dependencies.
Because Observer is part of OTP it does not suffer from dependency or environmental conflict issues, so running Observer is safe and the “run” button does just that: it runs Observer. Vapor will stay open while Observer is running, allowing you to pick another application to run, and you can watch what it is up to using Observer as a monitoring tool, which can be quite handy (and interesting!).
If you want to run an Erlang network service type application using Vapor while using Observer (like a chat server, or even a Zomp node) you should start Vapor using the zxh
command (not just plain zx
), because that provides an Erlang shell on the command line so you can still interact with the program from there. You can also run anything using plain old zx run, and when the target application terminates that instance of the runtime will shut down (this is why ZX application templates define applications as “permanent“).
Cool story, bro. What Comes Next?
The next step for this little bundle of projects is to create an all-encompassing Windows installer for Erlang, ZX and Vapor (so it can all be a one-shot install for users), and add a desktop registration feature to Vapor so that Erlang applications can be “installed” on the local system, registered with desktop icons, menu entries and file associations in FreeDesktop and Windows conformant systems (I’ll have to learn how to do it on OSX, but that would be great, too!). Then users could run applications without really knowing about Vapor, because authors could write installation scripts that invoke Vapor’s registration routines directly.
If I have my way (and I always get my way eventually) Erlang will go from being the hardest and most annoying language to deploy client-side to being one of the easiest to deploy client-side across all supported platforms. BWAHAHAHA! (I admit, maybe this isn’t a world-changing goal, but for me it would be a world-changing thing…)