At times, I have just wanted a small executable to launch Julia with some particular command line options and a few initial commands.
This task could easily be done by a shell script, but I did not want to depend on a particular shell. I also wanted this to work on Windows.
Also this is much less ambitious than PackageCompiler.jl or StaticCompiler.jl in that I am not trying to compile Julia code into an executable.
I built a prototype of what that might look like using Zig. I chose to do this in zig because it has a compact toolchain that works well across platforms.
Here’s the prototype script. I’m considering designing a package around this.
using Libdl
using Pkg
Pkg.activate(; temp=true)
Pkg.add("zig_jll")
using zig_jll
function build_launcher(
launcher_name::AbstractString,
cli_options::Vector{String},
commands::Vector{String}
)
code = launcher_code(launcher_name, cli_options, commands)
temp_path = mktempdir()
code_path = joinpath(temp_path, launcher_name * ".zig")
write(code_path, code)
libjulia_path = dirname(dlpath("libjulia"))
julia_h_path = abspath(joinpath(libjulia_path, "..", "include", "julia"))
zig_jll.zig() do exe
run(`$exe build-exe $code_path -lc -ljulia -I $julia_h_path -L $libjulia_path`)
end
end
function launcher_code(
launcher_name::AbstractString,
cli_options::Vector{String},
commands::Vector{String}
)
libjulia_path = dirname(dlpath("libjulia"))
julia_h_path = abspath(joinpath(libjulia_path, "..", "include", "julia"))
options_buffer = IOBuffer()
for option in cli_options
println(options_buffer, " \"$option\",")
end
options = String(take!(options_buffer))
command_buffer = IOBuffer()
for command in commands
command = escape_string(command)
println(command_buffer, " _ = jl_eval_string(\"$command\");")
end
commands = String(take!(command_buffer))
"""
// Compile with
// zig build-exe launch.zig -lc -ljulia -I $julia_h_path -L $libjulia_path
const std = @import("std");
const jl = @cImport({
@cInclude("julia.h");
});
const jl_parse_opts = jl.jl_parse_opts;
const jl_init = jl.jl_init;
const jl_eval_string = jl.jl_eval_string;
const jl_atexit_hook = jl.jl_atexit_hook;
pub fn main() !u8 {
const allocator = std.heap.page_allocator;
// Julia command line arguments
const zig_argv = [_][*:0]const u8{
"$launcher_name",
$options
};
var argc: c_int = zig_argv.len;
// We need to allocate since jl_parse_opts will return arguments
var argv = try allocator.alloc([*:0]const u8, zig_argv.len);
defer allocator.free(argue);
for (zig_argv, 0..) |_, i| {
argv[i] = zig_argv[i];
}
// Basic embedding
jl_parse_opts(&argc, @ptrCast(&argv));
jl_init();
$commands
jl_atexit_hook(0);
return 0;
}
"""
end
Here’s an example of building and execution.
julia> build_launcher("julia8", ["--threads=8"], ["""@info "" Threads.nthreads()"""]);
julia> run(`./julia8`)
┌ Info:
└ Threads.nthreads() = 8
Process(`./julia8`, ProcessExited(0))
julia> filesize("julia8")
598024
Edit: Made the example Zig 0.11.0 compatible.