|
|
Subscribe / Log in / New account

Julia 1.12 brings progress on standalone binaries and more

[LWN subscriber-only content]

Welcome to LWN.net

The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider subscribing to LWN. Thank you for visiting LWN.net!

November 4, 2025

This article was contributed by Lee Phillips

Julia is a modern programming language that is of particular interest to scientists due to its high performance combined with language features such as Lisp-style macros, an advanced type system, and multiple dispatch. We last looked at Julia in January on the occasion of its 1.11 release. Early in October Julia 1.12 appeared, bringing a handful of quality-of-life improvements for Julia programmers, most notably support, though still experimental and limited, for the creation of binaries.

Standalone binaries

The big news in this latest release is that Julia programs can be compiled into small, standalone binaries. However, the reality is that the generated binaries are not exactly small, not exactly standalone, and severely limited in scope. Development is proceeding on all of these fronts. The current facility, while it could conceivably be useful in lucky circumstances, is really a kind of proof of concept: a demonstration of how things will eventually work.

My experiments with a "hello world" program resulted in a 1.7MB binary and a directory of library files that occupied a further 91MB. The binary needs to be placed alongside this library directory to run; if you want to give your binary to a friend to use, you need to bundle up the entire 93MB directory. However, your friend does not need to have a Julia installation, which occupies about a gigabyte. The binaries are small in comparison with earlier iterations of the standalone compiler technology, which stuffed most of the Julia runtime and the standard library into the executable. This progress reflects work on the "trimming" ability of the compiler, which attempts to slice out unused routines from the standard library, unneeded parts of the Julia runtime, metadata, and code from the user's program that it can determine is unreachable.

Generation of the binary takes on the order of a minute, but once that's done it starts up instantly whenever invoked. Compilation of these "standalone" binaries requires GCC, as well as the JuliaC package. The latter is most conveniently used when installed as an "app" (see the "Apps" section below) using the package-mode command "app add JuliaC", after which you can compile Julia apps from the command line. See the appendix to this article for working examples of Julia apps and binaries.

Veterans of compiled languages with static types, such as C and Fortran, are accustomed to a different experience. On my machine "hello world" in Fortran compiles in less than a second and produces a 16KB binary. The binary is portable to any machine with the same CPU architecture and commonly installed system libraries. One should not expect an identical outcome when compiling Julia binaries. Julia's "secret sauce", the dynamic type system and method dispatch that endows it with its powers of composability, will never be a feature of languages such as Fortran. The tradeoff is a more complex compilation process and the necessity to have part of the Julia runtime available during execution.

Currently, there are severe limitations imposed on the program to be compiled with juliac. The main limitation is the prohibition of dynamic dispatch. This is a key feature of Julia, where methods can be selected at run time based on the types of function arguments encountered. The consequence is that most public packages don't work, as they may contain at least some instances of dynamic dispatch in contexts that are not performance-critical. Some of these packages can and will be rewritten so that they can be used in standalone binaries, but, in others, the dynamic dispatch is a necessary or desirable feature, so they will never be suitable for static compilation.

The current state of the juliac compiler tool has other severe limitations. For example, programs cannot read from files or from the terminal; the only way to provide input is through command-line arguments.

Workspaces

Julia's package system is responsible for installing a consistent set of dependencies for a program; in support of this, it performs a growing set of tasks. The latest release adds three features to the set: workspaces, apps, and an enhancement to the status command.

The new concept of a workspace is a set of projects that share the same Manifest.toml file, which is an automatically generated and complete dependency graph of a project. The idea is to have a main project and (possibly) several subprojects, which inherit the dependencies of the main project, and might have their own additional dependencies that get added to the manifest. Some natural applications of the concept would be to create subprojects for testing, examples, or documentation.

The implementation of workspaces in the current release gives the impression of being a work in progress, although it is definitely useful once one figures out how to use it. The obstacle in the way of this is an almost complete lack of documentation. To create a workspace, you need to edit a project's Project.toml file manually. This file contains the direct dependencies of a project (not the entire dependency graph) and is distributed with it, allowing other users to recreate the environments needed by the project.

To define a workspace consisting, for example, of a main project called "BigProject" with two subprojects called "sub1" and "sub2", the following two lines are added to the BigProject's Project.toml file:

    [workspace]
    projects = ["sub1", "sub2"]   

There is no interactive package-mode command for this, as there is for other functions that alter the Project.toml file, such as adding or removing dependencies or pinning their versions.

The directories for the subprojects must be placed within the main project's directory, at the same level as the main Project.toml file. Therefore the file layout for this workspace will look like this:

    BigProject/
    ├── Manifest.toml
    ├── Project.toml
    ├── src/
    │   └── BigProject.jl
    ├── sub1/
    │   ├── Project.toml
    │   └── src/
    └── sub2/
        ├── Project.toml
        └── src/ 

The "generate" command in package mode generates the file layout, with a skeleton module in the src directory, of a single project. But it's up to the user to place the subprojects under the main project directory.

Now the programmer can add dependencies to BigProject, as well as to the subprojects, using the activate and add commands in the package mode from the Julia read-eval-print loop (REPL). This will modify each Project.toml file as appropriate, but a single dependency graph, resolved for all of the projects in the workspace, will be created in BigProject's Manifest.toml file; the subprojects will not get separate manifests. If the project creator wants to distribute BigProject without the subprojects, the lines in Project.toml defining the workspace should probably be removed, and, again, must be done manually.

The package-mode status command in the REPL has a new option, --workspace, that gives some information about the workspace layout. However, it prints a flat list of all direct dependencies where one might expect some information about which dependency belongs to which member of the workspace.

The workspace feature has enough utility that I envision using it myself on occasion, but it would benefit from better integration with the REPL's package mode and is in desperate need of documentation.

Apps

"Apps" is a new package option that provides a way to make a Julia project into a command that can be invoked from the terminal like any other command. This should not be confused with the creation of standalone binaries (see above). The use of an app requires the presence of a Julia installation. Support for apps is still experimental. As with workspaces, the implementation is bare-bones, requiring manual editing of the Project.toml file. Adding the lines:

    [apps]
    fact = {}  

indicates the desire to install an app named "fact" (with optional metadata included within the curly brackets). This app will be installed in the ~/.julia/bin directory with executable permissions. What the app actually does when invoked is to start, behind the scenes, the Julia runtime and execute the entry point of the module within the project whose Project.toml file contained the directive above. This entry point is indicated in the module source file using the @main macro:

    function @main(args)
        ...
    end   

The installation of the app into ~/.julia/bin uses a new variation of the package-mode add command:

    (@v1.12) pkg> app add <project name or path>  

The name used for the app, "fact" in this example, has no necessary relation to the name of the module or project.

Of course, one could accomplish the same thing by writing a shell script that invokes the Julia interpreter. I have several shell scripts that I've written for my own convenience that begin with lines like:

    #!/usr/local/bin/julia --project=<project path>  

followed by a normal Julia program. Scripts such as these became practical when Julia's startup time became reasonable, several versions ago. In fact, the files that the new "app" mechanism installs in ~/.julia/bin are just shell scripts. The advantage is that they can be installed with one package-mode command.

Redefinition of structs

Julia encourages development in the REPL (or using it through connected editors, IDEs, or notebooks). Programmers have always been able to freely redefine functions, as their programs grow, without having to restart the interpreter. This freedom had not been extended to the definitions of structs, however; an attempt to redefine them led to an error message. The restriction was a nagging inconvenience, as structs form the basis of user-defined types, and are second in importance only to functions themselves. This limitation is finally removed in Julia 1.12, which allows struct redefinition in the REPL.

This improvement will also make the third-party Revise package, a standard member of every Julia programmer's toolkit, even more useful. Revise greatly aids development in the REPL by automatically reloading function definitions as needed, precompiling behind the scenes when source files are changed. The new struct redefinition freedom in Julia 1.12 allows Revise to also load the redefined structs. Its new powers are being developed in an active branch of the Revise repository.

Multithreading enhancements

In our article describing the new features in Julia 1.9, we described the arrival of interactive threads: starting Julia with the flag -t m,n creates two "thread pools", a normal pool of m worker threads and a pool of n interactive threads that have higher priority. The interactive threads would be used to improve responsiveness in the REPL, for example. Starting Julia with just -t m created m worker threads and no interactive threads, and omitting the flag was the same as using -t 1.

The latest release changes the default. Now, omitting the thread flag is the same as using -t 1,1: one worker thread and one interactive thread. Apparently the utility of the interactive thread in improving the REPL experience was deemed so high that everyone should want to use this for interactive development or running programs. To get zero interactive threads, the previous default, we must explicitly use -t 1.

Unfortunately, here again, the documentation is incomplete and confusing. Experimentation reveals that using "auto" in place of a number for m reserves a number of worker threads equal to the number of logical cores available, and using "auto" for n always results in one interactive thread.

Initialization tasks in concurrent code can be awkward to program. If a multithreaded simulation, for example, needs to read some parameters from a file to set some physical constants, it's not ideal to have each thread (there may be thousands) open and read the file separately. Another common case is the initialization of a simulation with a random number, which may need to be the same for each thread.

A welcome addition to Julia's multithreading toolkit is the appearance of three new types that assist with initialization tasks. These are OncePerProcess, OncePerThread, and OncePerTask. They permit the definition of initialization functions that run once, with the granularity suggested by their names, returning the same value on subsequent calls. The new functions are more convenient than manually confining initialization to a single thread and broadcasting the results to the others.

In addition to the new features described in detail here, 1.12 comes with new compiler diagnostics, facilities for handing atomic variables, a new wall-time profiler, and several other enhancements.

Conclusion

Some of the new features described above are essentially undocumented. As has been the case in the past, and as is the case with far too many Julia packages in the public General Registry, I had to find out how they work by perusing GitHub issues, forum discussions, and source code, but mainly by extensive and time-consuming experimentation. This is a blind spot widely afflicting developers in general; in the case of Julia, it is an obstacle to wider adoption of the language.

Nevertheless, most of the new features in the latest release are immediately useful and are responsive to the needs of Julia programmers. The new code-trimming talent of the juliac compiler represents substantial progress, even if its limitations mean that it does not yet have wide practical application. The project is receiving a good deal of attention, however, and the Julia community can look forward to standalone binaries becoming continually more useful.


Index entries for this article
GuestArticlesPhillips, Lee



to post comments

Another thing to add to PATH

Posted Nov 4, 2025 20:14 UTC (Tue) by lobachevsky (subscriber, #121871) [Link] (8 responses)

> The installation of the app into ~/.julia/bin […]

If people would not add any new such directories, but use ~/.local/bin instead, that would be lovely.

Another thing to add to PATH

Posted Nov 4, 2025 21:38 UTC (Tue) by intelfx (subscriber, #130118) [Link] (2 responses)

> If people would not add any new such directories, but use ~/.local/bin instead, that would be lovely.

I mean, these are the same people who actively oppose the concept of their software being packaged in Linux distributions, so... kinda expected, I guess?

Another thing to add to PATH

Posted Nov 5, 2025 7:25 UTC (Wed) by Wol (subscriber, #4433) [Link] (1 responses)

Well, it should be configurable ...

Personally, I wish we could take a leaf out of Windows' book (shock, horror!) and use ~ like it does - don't stick anything that should be user-visible in it. Then we shouldn't need hidden files and directories - all the "system" stuff can live in ~/etc, ~/bin et al.

But then, that's just one person's preference ...

Cheers,
Wol

Another thing to add to PATH

Posted Nov 5, 2025 12:54 UTC (Wed) by intelfx (subscriber, #130118) [Link]

> Personally, I wish we could take a leaf out of Windows' book (shock, horror!) and use ~ like it does - don't stick anything that should be user-visible in it

Windows _does_ have AppData in ~, though :)

That would be directly equivalent to .config/.local/.cache (the only difference is that we have the split into config/data/cache in top-level, Windows has it on the second level and/or internally in each app).

Another thing to add to PATH

Posted Nov 4, 2025 22:22 UTC (Tue) by jokeyrhyme (subscriber, #136576) [Link] (3 responses)

the tricky thing with all language ecosystems sharing ~/.local/bin is that we effectively have a shared global namespace of executable names, and it's entirely possible for package "boo" in julia and package "boo" in rust to clobber each other

although, i suppose there's already a shared global namespace in e.g. the package repository for a single linux distribution, so there's already some mediation happening somewhere

what i _would_ like is for any language ecosystem that puts executables in PATH to be a proper package installer, i.e. maintains it's own (or a shared) registry/database of which packages have been installed, which versions, offers updates/uninstallation, etc

Another thing to add to PATH

Posted Nov 5, 2025 0:27 UTC (Wed) by mathstuf (subscriber, #69389) [Link] (2 responses)

What I do is install each *project* (regardless of language/packaging) into its own prefix under `~/misc/root/$project` and then symlink from `~/.local/bin` to there for those that I want to use "everywhere". Otherwise, there is a `module`-like script that adds a root to the relevant environment variables (PATH, PYTHONPATH, MANPATH, etc.) and launches a command with it. Much more flexible. Things tend to make noise about the thing they installed not being in `PATH`, but…meh, they're just warnings.

Another thing to add to PATH

Posted Nov 5, 2025 2:52 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link] (1 responses)

Try direnv. It integrates with the shell and customizes the PATH based on your current location. So you can have per-project PATH settings transparently.

Another thing to add to PATH

Posted Nov 5, 2025 3:20 UTC (Wed) by mathstuf (subscriber, #69389) [Link]

That's not really how I end up using it. Rarely do I need something based on my local current directory. Instead, it is stuff like `wm-add-root yt-dlp -- rss2ytdlp` which adds a venv with `yt-dlp` to then run the `rss2ytdlp` script which looks through my RSS feeds for videos to download. Fedora is fast, but YouTube has been upping their combativeness lately, so I end up needing bleeding-edge more often. Another pattern is `wm-add-root cmake-build gcc-build -- cmake -GNinja -Ssrc -Bbuild` to test some project with whatever CMake and GCC I have built (most useful when bisecting).

Another thing to add to PATH

Posted Nov 5, 2025 16:45 UTC (Wed) by mb (subscriber, #50428) [Link]

Nah, it's an absolute pain to uninstall programs that scattered their guts all over shared bin, lib, share, ... folders.
It makes much more sense to install programs into their own hierarchy.
I install programs to /opt/PROGNAME/{bin,lib,...} for system wide programs and ~/usr/PROGNAME/{bin,lib,...} for user programs.

That makes it much easier to keep a clean system.
Cleanly uninstalling is a matter of rm -r /opt/PROGNAME

And my shell automatically adds these to $PATH with something like this in the shell rc script:
for path in /opt/*/bin; do export PATH="$path:$PATH"; done


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds