From the dawn of time, software developers have been concerned about the distribution of their software. In the beginning, executables distributed over floppy disks were sufficient. However, as time progressed, we needed nicer installers with desktop integration for launching and uninstalling, and hence installers were born. As time progressed, hard lessons were learned that access to all system resources is a security nightmare, and sandboxing of applications was introduced in all major operating systems.
Every operating system is different and has made unique design choices. On macOS, applications are simply put within the Applications folder from DMG containers. Windows uses many formats, with MSIX being the most modern approach, and Linux uses Snap and Flatpak for external software distribution. To create an installer on each of these platforms, there is a list of common tasks that one needs to perform individually:
- Make icon assets in a form that the installer/operating system understands
- Specify the needed capabilities for the application
- Set the launching endpoint, whether it is a GUI or a terminal application
- Bundle all configuration files with the application into the installer
- Perform code signing of the installer and possibly the application
Maintaining all these configuration nuances is hard. AppBundler resolves these issues with defaults that enable shipping GUI applications effortlessly while also enabling developers to simply configure the installer with their own configuration overlay in places where they need it, making the process much easier to debug and communicate about.
AppBundler, in contrast to other solutions, only uses open source tools for making installers that are available across platforms and are compiled in a cross-platform way with Julia's BinaryBuilder infrastructure. This makes it easy to see what sources were used in making the installers, maintain them, and be sure they don't contain malware, as you can reproduce the binaries yourself. The most important reason to use open source tools is that they can be bundled with the AppBundler project in a reproducible way across all operating systems and installed as a simple Julia package.
AppBundler.jl
offers recipes for building Julia GUI applications in modern desktop application installer formats. It uses Snap for Linux, MSIX for Windows, and DMG for macOS as targets. It bundles the full Julia runtime within the app, which, together with artifact caching stored in scratch space, allows the bundling to be done quickly, significantly shortening the feedback loop.
To use AppBundler on a Julia application project, you need to use the standard package structure with a defined entry point MyApp.@main
, which allows launching the application with julia --project=. -e "using MyApp"
. Having this basic configuration allows setting up the bundling infrastructure with commands:
julia --project=meta
]add AppBundler
using AppBundler
AppBundler.install_github_workflow()
Optionally run AppBundler.generate_signing_certificates()
for persistent signing certificates and take note of MACOS_PFX_PASSWORD
and WINDOWS_PFX_PASSWORD
that need to be specified as environment variables. Alternatively, you can obtain certificates from a trusted CA and place them under meta/msix/certificate.pfx
and meta/dmg/certificate.pfx
accordingly.
The build setup installs a meta/build.jl
script in addition to .github/workflows/Releases.yml
. Once the setup is done, you can build your project:
julia --project=meta meta/build.jl --build-dir=@temp --target-platform=linux|windows|macos|all --target-arch=aarch64|x86_64
See --help
for all supported arguments. When arguments are skipped, the target platform and architecture are taken from the host system, and the build directory is a temporary directory.
The setup is designed such that meta/build.jl
can be further modified by a user in case of custom bundling needs without needing to touch .github/workflows/Releases.yml
. The custom build.jl
can be useful for custom bundling specifications, customizing the bundling process itself, or for bundling custom products.
Once applications are installed, define a USER_DATA
environment variable where apps can store their data. On Linux and Windows, those are designated application locations which get removed with the uninstallation of the app, whereas on macOS, apps use ~/.config/myapp
and ~/.cache/myapp
folders unless one manages to get an app running from a sandbox, in which case the $HOME/Library/Application Support/Local
folder will be used.
To see how this works, explore AppBundler.jl/examples and PeaceFounderClient where you can check out the releases page to see what to expect.
The following instructions are for end users installing your built applications.
- MSIX (Windows): If self-signed, go to MSIX bundle properties and add the certificate to the trusted certificate authorities first (see https://www.advancedinstaller.com/install-test-certificate-from-msix.html). Then double-click on the installer and install the app.
- Snap (Linux): The snap can be installed from a command line:
snap install --classic --dangerous MyApp.snap
- DMG (macOS): If self-signed, you need to click on the app first, then go to
Settings -> Privacy & Security
, whitelisting the launch request. Then drag and drop the application to theApplications
folder. Launch the application and go again toSettings -> Privacy & Security
to whitelist it.
Note that all these extra steps are avoidable if you are willing to buy Windows and macOS code signing certificates. For Snap, you can try to submit the app to a snap store so it can be installed with a GUI.
The highest level interface is:
build_app(Linux(:aarch64), source, "MyApp.snap"; precompile = true)
where the first argument specifies the platform. This command will take a Julia source project, compile modules, and then bundle them into the corresponding bundle format.
Platform-Specific Examples:
import AppBundler
import Pkg.BinaryPlatforms: Linux, Windows, MacOS
# Linux - Create a Snap package
AppBundler.build_app(Linux(:x86_64), "MyApp", "build/MyApp.snap")
# Windows - Create a MSIX installer
AppBundler.build_app(Windows(:x86_64), "MyApp", "build/MyApp.msix")
# macOS - Create a .app bundle
AppBundler.build_app(MacOS(:x86_64), "MyApp", "build/MyApp.app")
# macOS - Create a .dmg installer with automatic LZMA compression
AppBundler.build_app(MacOS(:x86_64), "MyApp", "build/MyApp.dmg")
The function automatically detects whether to create an installer based on the destination file extension. Precompilation is enabled by default and will error if it cannot be performed on the host system. For cross-platform building, you can disable precompilation with the precompile=false
option. In the future, Julia may implement cross-compilation, which would make this option redundant.
Lower-Level API: For more control, use the lower level API:
product = PkgImage(source; precompile = true, incremental = false)
snap = Snap(source)
bundle(product, snap, destination)
Currently, only PkgImage
compilation is supported. In the future, one will be able to specify the product to be SysImage
(or another better name) to compile the application with PackageCompiler
instead and perform corresponding bundling. There are also plans to experiment with JuliaC integration.
On the other end, we have the destination in which the product needs to be bundled. Here again, we have a variety to choose from if one were to add Deb, RPM, or Flatpak bundling formats. The Snap
constructor takes the role of finding configuration files from the user directory and from the default AppBundler recipes folder that one can inspect. Similarly, MSIX
and DMG
constructors can be called (see docstrings).
Custom Product Bundling: In some situations, you may want to bundle a library which is not supported by the AppBundler product interface, or perhaps bundle other programming language projects like C, Rust, or Python applications. In such situations, you can use:
dmg = DMG(source)
bundle(dmg, destination) do app_stage
# Compile project and install it in the appropriate directories of app_stage
end
In the future, AppBundler may add support for bundling Python and Rust projects directly.
AppBundler offers a staging API for staging products. Currently, only PkgImage
staging is supported:
pkg = PkgImage(app_dir; precompile = false)
stage(pkg, Linux(:x86_64), "build/linux_staging")
This performs the complete staging process for a Julia application, preparing it for distribution on the target platform. The process includes downloading the appropriate Julia runtime, copying application dependencies, retrieving artifacts, configuring startup files, and optionally precompiling the application.
Similar staging API will be made available with PackageCompiler and hopefully also with JuliaC integration.
It is also possible to stage application bundle files:
msix = MSIX(app_dir)
stage(msix, "build/msix_staging")
This is used internally in the bundle
function and can be useful for debugging purposes.
Configuration options for installer bundles vary greatly across platforms, making a single unified configuration file impractical. AppBundler addresses this through a recipe-based templating system that provides sensible defaults while allowing customization when needed.
AppBundler provides default configuration files that use simple variable substitution with {{ MY_VAR }}
syntax. Variables are automatically derived from your Project.toml
(name, version, etc.) or manually specified in a [bundle]
section. This default setup covers most common use cases without requiring platform-specific knowledge.
When your application requires more control (such as accessing hardware, networking capabilities, or custom launchers), you can override defaults by placing custom configuration files in your meta
folder: meta/snap/snap.yaml
for Linux, meta/msix/AppxManifest.xml
for Windows, or meta/dmg/Entitlements.plist
for macOS. Additional bundle resources (like custom icon sizes) can be provided by placing them in the corresponding folder hierarchy within meta
.
Common customization scenarios include sandboxing configuration (adding specific capabilities or interfaces your application needs), custom launchers (defining alternative entry points), and icon overrides (providing platform-specific icon assets in various sizes). By keeping templates simple and encouraging users to copy and modify complete configuration files rather than creating complex nested templates, AppBundler makes platform-specific customization straightforward and debuggable.
Currently, Julia does not cross-compile, except for macOS where :aarch64
can also run :x86_64
applications. Hence, one needs to have the host as a target, which nowadays can be easy to get via continuous integration infrastructure like GitHub, GitLab, etc. The bundling, however, is cross-platform compatible, where UNIX hosts can generate all compatible installers, which may be relevant for other programming language projects.
Another set of issues comes from sandboxing, where each of the installer formats enforces its own way to isolate the running application from the system resources. This requires careful configuration of the parameters and patience in debugging issues and coming up with workarounds. Sandboxing is often expected for app stores for the application to receive a positive review. Currently, only macOS sandboxing with a special trick seems to work, whereas Snap only works for GTK applications, and MSIX is completely broken for Julia due to msvcrt32.dll
runtime library use over the more modern ucrt.dll
.
- Confinement for some reason does not work for OpenGL applications (see issue JuliaGraphics/QML.jl#191)
To debug issues with snap packaging, stop at the staging directory stage and install it with snap try myapp_dir
. Further, you can also enter the shell of the application for inspecting the sandboxing behavior more closely with snap run --shell myapp
.
- For some reason,
com.apple.coreservices.launchservicesd
needs to be added as an exception when sandbox is enabled, which may deserve a closer inspection in the future. - macOS applications also need to be notarized.
rcodesign
can support that. The question is what the API should be for the user here.
To debug the DMG bundles, compression is not necessary, and one can stop at the staging directory stage and skip compression.
- GTK applications fail to find shared libraries in the UWP environment. This will probably be resolved once we get Julia with the
ucrt.dll
runtime. - Only runtime behavior
packagedClassicApp
is supported. This is because ofmsvcrt32.dll
reliance, which will be resolved once Julia transitions toucrt.dll
. - When OpenSSL is installed in the
C:\Windows\System32
folder,osslsigncode
fails as system-wide OpenSSL takes precedence over Julia's packagedOpenSSL_jll
.
To debug issues with MSIX, you can stop bundling at a folder stage and test it with Add-AppPackage -register AppxManifest.xml
.
This work is supported by the European Union in the Next Generation Internet initiative (NGI0 Entrust), via NLnet Julia-AppBundler project.