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:
Checks to see if you have your ~/.julia/bin folder on your environment’s PATH. Adds it temporarily if not.
Checks to see if you have juliac installed. Installs JuliaC.jl as an app if not.
Generates a minimal “multiply_numbers” package if it does not exist.
Writes the “multiply_numbers.jl” program if it does not exist or does contain a @main function.
Compiles “multiply_numbers.jl” into “build/bin/multiply_numbers.exe”
Reports the size of build/bin/multiply_numbers.exe
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?
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
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.
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.
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.
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.
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.