[ANN] AppBundler.jl - Bundle Your Julia GUI Application

See: KiteSimulators.jl/bin/autopilot at main · aenarete/KiteSimulators.jl · GitHub
This script starts the app autopilot.

Regarding this message:

ERROR: LoadError: ArgumentError: No file exists at given path: /Users/jerdmanis/BtSync/Projects/KiteSimulators.jl/data/kite.obj

A folder “data” must exist with well-defined content. The function init_project() creates the data folder and copies the required data files. See: KiteSimulators.jl/docs/PackageInstallation.md at main · aenarete/KiteSimulators.jl · GitHub

Not sure which changes would be needed to achieve compatibility with your installer.

ImageColorThresholderApp.jl author here. I’m planning to update ImageColorThresholderApp.jl which hasn’t been touched in years, and I’m very much looking forward to integrating it with AppBundler.jl.

Thank you so much for creating and maintaining AppBundler.jl! It’s a truly invaluable tool for making Julia applications accessible to a wider audience.

3 Likes

It appears that KiteViewers.jl picked up a joinpath(pwd(), "data") directory, which overrides the data directory inside the package. When I tried KiteViewers.jl outside the KiteSimulators.jl directory, precompilation succeeded. I then copied kite.obj from KiteViewers.jl into KiteSimulators.jl, which fixed this problem.

However, I encountered an error with conda install -y pyqt that did not work. Additionally, AppBundler.jl itself does not support bundling items produced by running deps/build.jl, so I will not proceed with KiteSimulators.jl this time.

I made a simple entry point in the fork GitHub - JanisErdmanis/ImageColorThresholderApp.jl: GUI tool for thresholding color images that I built and tested locally with AppBundler. There is a minor issue with AppBundler.jl regarding its reliance on the Manifest.toml file, which I will fix later today with a patch version. Once that is done, I will open a PR.

I am not aware of using build.jl. Where am I (or one of my dependencies) is using it?

The issue is reliance on PyPlot. It actually does not have deps/build.jl but instead instantiates the project at runtime through the __init__ block, which may actually work in this case. However, python dependencies would not be bundled :man_shrugging:

Would this issue be solved if I replace PyPlot with Makie?

1 Like

I think so! we at least have a simple PackageCompiler relocatibality test these days!

@Janis_Erdmanis Following our earlier private discussion, I would be interested in using AppBundler in conjunction with JuliaC to create Windows/Mac shared libraries that can be loaded from a C binary.

These should respect each platform’s rules w.r.t. bundling, RPATHs and so on. I can provide a Mac signature to test codesigning.

Yes. ImageColorThresholderApp.jl that depends on Makie compiles fine. If you can define KiteSimulators.main function so that the application can be started with:

julia --project=. --eval "using KiteSimulators"

then bundling the application will be quite trivial by following the instructions in the anouncement.

Not clear to me yet: I need a data directory that contains a number of input/ configuration files that the user should be able to modify. Should the main() function create that relative to the current directory and populate it?

Furthermore, how can a main() function automatically be executed just by doing using MyPackage?

The way to work on input is using a file dialog. NativeFileDialog.jl is perfect for that.

A simple approach is to open the file dialogue in the main function, where the user chooses a file or folder, and then launch the application window. Later, you can add the ability to change the file directories within the UI itself.

This is the new feature of Julia 1.11 - Command-line Interface · The Julia Language It is only executed in noninteractive mode automatically when the module is loaded in the main with using MyPackage.

I encountered an issue preventing the precompilation of GLMakie.jl on headless Windows and MacOS runners. On Linux, I have a workaround, and both precompilation and the full bundle can be created.

It seems that one needs to build bundles locally on Windows and MacOS, which I tried to avoid.

AppBundler v0.4.7

Documentation | Examples

I have been actively testing the AppBundler across other repositories to uncover as many bugs as possible before I build on this foundation. I managed to make bundles for ImageColorThresholderApp (except windows) and KomaMRI.

Currently, the following GUI frameworks have been tested with AppBundler across different platforms:

Framework Platform Support Notes Examples
QML ✓ All platforms Fully supported PeaceFounderClient
GLFW ✓ All platforms Fully supported none
Gtk/Mousetrap :warning: macOS, Linux Does not launch on Windows none
Makie :warning: All platforms GLMakie may not work on Windows ImageColorThresholderApp
Blink :warning: All platforms Requires heavy patching for relocability KomaMRI
Electron ✓ All platforms Fully supported BonitoBook

I would love to add more examples; however, very few Julia GUI applications document and make clear how to launch them. Ideally, everyone would define (@main)(ARGS), introduced in Julia 1.11, which also enables distributing them as Pkg apps.

7 Likes

AppBundler v0.5.0

Documentation | Examples

AppBundler now makes it straightforward to create complete Julia distributions with packages pre-compiled and ready to use. This enables several use cases:

  • Educational environments: Deploy Julia in computer labs without requiring students to wait through compilation of large packages like Makie or DifferentialEquations

  • Onboarding: Provide newcomers with a distribution that includes popular ecosystem packages, creating a smoother first-time experience

  • Air-gapped deployments: Create self-contained Julia distributions for offline or restricted network environments

Initial groundwork has been added for distributing command-line applications, with ongoing exploration of user-facing endpoint patterns. For a simple command-line application, DMG comes in at approximately 90 MB.

The next development phase will focus on integrating PackageCompiler and JuliaC to decrease application startup times and minimise installer sizes.

17 Likes

Very cool! How does this work with lazy artifacts?

2 Likes

Currently, it seems they are downloaded at runtime and placed in the user’s DEPOT_PATH. I overlooked them. There appears to be an easy fix by using include_lazy=true when retrieving them with Artefacts.select_downloadable_artifacts, which I plan to implement.

AppBundler v0.6.0

Documentation | Examples

This release completes AppBundler’s core feature set by integrating system image generation and native compilation through JuliaC. The focus now shifts toward API refinement and documentation for the v1.0 release—feedback on naming and API design is welcome.

System image generation

AppBundler now includes SysImgTools, a module that handles system image creation in ~100 lines by building on Base.Linking. This eliminates the need for external compiler installation. To create a system image, specify which packages to bake in:

spec = JuliaAppBundle(project; sysimg_packages = ["QMLApp"])
dmg = DMG(project; windowed = true)
bundle(spec, dmg, joinpath(build_dir, "qmlapp.dmg"))

For project dependencies not included in sysimg_packages, precompilation cache is used instead. This enables hybrid approaches where critical packages use system images while others rely on pkgimages—useful for Julia distributions with GUI shells, significantly improving startup times.

Selective assets

Applications can now include only specified assets, reducing distribution size:

asset_spec = Dict{Symbol, Vector{String}}(
    :QMLApp => ["src/App.qml"]
)

spec = JuliaAppBundle(project; sysimg_packages = ["QMLApp"], 
                      asset_rpath = "assets", asset_spec)

This creates an assets directory with declared files accessible via pkgdir(@__MODULE__), while removing redundant source files. AppEnv launched via startup.jl handles this by reading a pkgorigins index at startup from $(dirname(Sys.BINDIR))/index, which maps packages to their asset locations, providing flexibility in where assets can be stored.

JuliaC integration and AppEnv

Bundling with JuliaC compilation is now supported. AppEnv package handles platform-specific initialization through a config file at $(dirname(Sys.BINDIR))/config:

import AppEnv

function (@main)(ARGS)
    AppEnv.init()
    println(AppEnv.USER_DATA)  # Sandbox-assigned user data directory
    println(pkgdir(@__MODULE__))  # Points to assets when compiled
end

To compile and bundle the application using JuliaC:

asset_spec = Dict{Symbol, Vector{String}}(
    :AppEnv => ["LICENSE"]
)

spec = JuliaCBundle(project; asset_spec, trim = true)
msix = MSIX(project; windowed = false)
bundle(spec, msix, joinpath(build_dir, "cmdapp.msix"))

During interactive development, AppEnv.init() is safely ignored. With trimming enabled, simple applications reach 20 MB (Snap) or 40 MB (MSIX). Compiled applications integrate with platform conventions—appearing as cmdapp.exe on Windows and cmdapp on Linux.

Future work and v1.0

The project is feature-complete. Work now focuses on API polish, comprehensive documentation, and addressing community feedback. See test/release.jl for complete examples until documentation is finalized.

12 Likes

Thank you for all your hard work and really quick development. It is much appreciated.

Here are some of my questions (technical details for v0.6).

From now Appbundler supports JuliaC compilation. As far I know JuliaC requires Julia 1.12. How does that reflect with open issue #20 for support of 1.12? Does that issue no longer apply for the new Appbundler or 1.12 is still not fully supported?

Examples (such as examples/QMLApp) specify AppEnv using Sources section of Project.toml. Will that be updated or can that be avoided? Is AppEnv registered package / package extension?

How does JuliaAppBundle implementation (without JuliaC) compare to 0.5 implementation? Does it still copy basically full Julia into installation directory after app install?

Looking forward to documentation and future patches to try everything out.

1 Like

Thank you for the kind words and for your detailed questions!

Regarding Julia 1.12 support and issue #20: The issue still applies. JuliaC does require Julia 1.12, but there’s currently an OpenSSL executable availability problem. My workflow is: install JuliaC with Julia 1.12, then run AppBundler via Julia 1.11. This is why JuliaC isn’t yet in the CI pipeline—I have submitted two potential fixes: this PR in Julia and OpenSSL_CLI. If neither resolves, I’ll investigate alternatives like vendoring Artifacts.toml and downloading OpenSSL manually via build/deps.jl.

AppEnv is a registered package that anyone can include in their projects. When using JuliaAppBundle, AppEnv is optional—it’s automatically incorporated during bundling and launched at startup.jl. However, it’s necessary for JuliaC-created bundles, which is why I registered it. It’s harmless during development: it checks for a config file and does nothing if absent, except setting AppEnv.USER_DATA to a temporactory (or respecting the USER_DATA environment variable if set).

Comparing JuliaAppBundle to v0.5: JuliaAppBundle extends the v0.5 implementation and still copies the full Julia installation directory. The key addition is that it can now override the system image. Testing shows it adds about 50MB in compressed form compared to JuliaCBundle (without trimming) but doesn’t affect launch performance. JuliaAppBundle is more suitable for creating various Julia distributions while also supporting app creation, whereas JuliaCBundle is exclusively for app deployment. JuliaAppBundle is particularly useful for debugging—you can launch the Julia REPL and investigate issues interactively, then switch to JuliaCBundle once everything works.

Looking forward to your feedback once you try it out!

1 Like