Self-contained juliac demonstration

I was looking for a completely self-contained tutorial or demonstration on how to use the juliac command line from JuliaC.jl and did not find one. I created this short demonstration below.

Steps

Step 1: Install Julia 1.12 or greater
Step 2: Save the script below to “create_multiply_numbers.jl” and then run julia create_multiply_numbers.jl
Step 3: Run build/bin/multiply_numbers.exe 1 2 3 to test your new program itself.

create_multiply_numbers.jl

#!/usr/bin/env -S julia
using Pkg

if !contains(ENV["PATH"], DEPOT_PATH[1]*"/bin")
    @warn "The environment variable PATH does not contain $(DEPOT_PATH[1]*"/bin"). Consider modifying your shell startup."
    @info "Adding $(DEPOT_PATH[1])/bin to \$PATH" ENV["PATH"]
    ENV["PATH"] = "$(DEPOT_PATH[1])/bin:$(ENV["PATH"])"
end

if isnothing(Sys.which("juliac"))
    @info "Installing JuliaC"
    Pkg.Apps.add("JuliaC")
end

if !isdir("multiply_numbers")
    @info "Generating multiply_numbers package"
    Pkg.generate("multiply_numbers")
end

code_file::String = "multiply_numbers/src/multiply_numbers.jl"
if !isfile(code_file) || !contains("@main", read(code_file, String))
    @info "Writing $code_file"
    program = """
    module multiply_numbers

    function (@main)(args::Vector{String})
        numbers = parse.(Int, args)
        println(Core.stdout, prod(numbers))
        return 0
    end

    end # module multiply_numbers
    """
    println(program)
    write("multiply_numbers/src/multiply_numbers.jl", program)
end

@info "Compiling multiply_numbers.exe with juliac multiply_numbers --output-exe multiply_numbers.exe --bundle build --trim"
run(`juliac multiply_numbers --output-exe multiply_numbers.exe --bundle build --trim`)

@info "The size of build/bin/multiply_numbers.exe is $(filesize("build/bin/multiply_numbers.exe")/1024^2) MiB"

@info "Running build/bin/multiply_numbers.exe 5 9 10"
run(`build/bin/multiply_numbers.exe 5 9 10`)

This script does the following:

  1. Checks to see if you have your ~/.julia/bin folder on your environment’s PATH. Adds it temporarily if not.
  2. Checks to see if you have juliac installed. Installs JuliaC.jl as an app if not.
  3. Generates a minimal “multiply_numbers” package if it does not exist.
  4. Writes the “multiply_numbers.jl” program if it does not exist or does contain a @main function.
  5. Compiles “multiply_numbers.jl” into “build/bin/multiply_numbers.exe”
  6. Reports the size of build/bin/multiply_numbers.exe
  7. Executes build/bin/multiply_numbers.exe 5 9 10
25 Likes

Nice! I got this to work on Windows (Julia 1.12.2), and the total bundled directory was 117MB. Looking at the content, it seemed like a lot of unnecessary pieces… fortran and BLAS and PCRE… Copied one at a time to a new directory and ended with just the .exe, libjulia and libjulia-internal, and libopenlibm for it to work (total of 17MB). Isn’t trim supposed to be doing this?

3 Likes

Hmm, I get an error:

melis@blackbox 08:20:/tmp/t$ j --project=. c.jl 
┌ Warning: The environment variable PATH does not contain /home/melis/.julia/bin. Consider modifying your shell startup.
└ @ Main /tmp/t/c.jl:5
┌ Info: Adding /home/melis/.julia/bin to $PATH
└   ENV["PATH"] = "/home/melis/projects/2do-tool:/home/melis/projects/bob:/home/melis/.local/bin:/home/melis/.juliaup/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/opt/cuda/bin:/opt/cuda/nsight_compute:/opt/cuda/nsight_systems/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl"
ERROR: LoadError: MethodError: no method matching occursin(::Vector{UInt8}, ::String)
The function `occursin` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  occursin(::Any)
   @ Base strings/search.jl:807
  occursin(::Regex, ::AbstractString; offset)
   @ Base regex.jl:306
  occursin(::Union{AbstractChar, AbstractString}, ::AbstractString)
   @ Base strings/search.jl:782
  ...

Stacktrace:
 [1] contains(haystack::String, needle::Vector{UInt8})
   @ Base ./strings/util.jl:140
 [2] top-level scope
   @ /tmp/t/c.jl:21
 [3] include(mod::Module, _path::String)
   @ Base ./Base.jl:306
 [4] exec_options(opts::Base.JLOptions)
   @ Base ./client.jl:317
 [5] _start()
   @ Base ./client.jl:550
in expression starting at /tmp/t/c.jl:21

Seems the read(code_file) file returns Vector{UInt8} while contains() is used to check for string "@main". Curious why the code works for you, but not for me?

julia> versioninfo()
Julia Version 1.12.2
Commit ca9b6662be4 (2025-11-20 16:25 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
  WORD_SIZE: 64
  LLVM: libLLVM-18.1.7 (ORCJIT, skylake)
  GC: Built with stock GC
Threads: 8 default, 1 interactive, 8 GC (on 8 virtual cores)
Environment:
  JULIA_PKG_USE_CLI_GIT = true
  JULIA_NUM_THREADS = auto
1 Like

Good catch. It should be read(code_file, String). I revised it.

1 Like

I think trim tries to reduce the size of the executable itself. These extra shared libraries are part of the --bundle portion which I think copies of all Julia’s dependencies.

1 Like

Sounds like something else that could be --trimmed

Thanks @mkitti
Do you know if JuliaC will change the requirements of embedding Julia in other languages? Certain aspects (libuv, signals, etc) appear to be hard roadblocks for the integration of Julia based shared libraries.

1 Like

There’s a simple, complete working example in the Appendix to Julia 1.12 brings progress on standalone binaries and more [LWN.net]

1 Like

Thank you for this.

Reading your script, am I correct in understanding that the whole process is:

A) Have juliac installed

B) Have a module with an @main

C) Call juliac modulename --output-exe exename.exe --bundle build --trim

Basically. You may also need to add ~/.julia/bin to your path in order to invoke juliac later. You may also want to consider the apps options in Project.toml.

The intention here is to develop a minimum working example in executable script form.

1 Like

Don’t know about signals, but libuv is still present with libraries compiled by JuliaC, which is non-ideal for my use cases. On the other hand, a problem JuliaC did solve is that the startup memory usage is now tiny, rather than ~200 MB with the normal Julia installation.

1 Like

That’s the mark of a great MWE: you took something that felt like it would take forever for me to figure out, and showed that it’s actually not that complicated. Thank you.

3 Likes