Yes, I have managed to make a self-contained library that can be relocated.
You can find the reference here: GitHub - RomeoV/RunwayLib.jl
In essence, my build and bundling steps are as follows:
- Write your normal julia code. Make
Base.@entrypoint
s (example). Write tests that test your code withJET.@test_opt
and fix all tests, possibly by forking upstream repositories and fixing things. Depending on what you’re doing, this can be easy or challenging. It was challenging for me. - Make a new dir next to
src
, I call itjuliac
. Put a Makefile with something like
Notice theCOMPILED_DIR = MyLibCompiled LIB_DIR = $(COMPILED_DIR)/lib $(LIB_DIR)/libmylib.so: loadmylib.jl Project.toml Manifest.toml $(SRC) @mkdir -p $(LIB_DIR) JULIAC=$$(julia -e 'print(normpath(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "juliac", "juliac.jl")))' 2>/dev/null || echo "") ; \ $(JULIA) --project=. --depwarn=error "$$JULIAC" --experimental --trim=unsafe-warn --compile-ccallable --output-lib $(LIB_DIR)/libmylib loadmylib.jl --relative-rpath Manifest.toml: Project.toml ../Project.toml -rm -f Manifest.toml $(JULIA) --project=. -e 'using Pkg; Pkg.instantiate()' @touch $@ # Pkg.instantiate doesn't update the mtime if there are no changes
--relative-rpath
, which will allow us to put all the required libraries next to each other.
Remember to runjuliaup override set 1.12
in the directory. - Manually write a header file, and add
@tim.holy has started automating this: GitHub - JuliaInterop/JuliaLibWrapping.jl: Generate language-bindings for Julia-generated shared libraries# Create include directory with header file $(INCLUDE_DIR)/libmylib.h: libmylib.h @mkdir -p $(INCLUDE_DIR) cp libmylib.h $(INCLUDE_DIR)/
- Make a
Project.toml
injuliac
that contains something like
Also copy over any other custom sources from your main Project.toml file (it seems sources are not applied transitively…)[sources] MyLib = {path = ".."}
- Make a simple
loadmylib.jl
file that just runsusing MyLib
- Run the compilation
make all
Now it’s time to create the “bundle” that we can ship. This includes three aspects.
- ship all the required libraries (such as libjulia.so, libjulia-internal.so, libopenblas, etc) but try to strip out the libraries that we don’t need
- figure out which artifacts (
share/julia
) we have to ship - make sure to compile on a machine where the GLIBC version is older than on your target deployment machine.
to bundle up all the libraries, you can do something likesee EDIT below
in your Makefile.$(LIB_DIR)/.installed: @echo "Creating directory structure..." @mkdir -p $(LIB_DIR) @echo "Copying Julia libraries..." JULIA_LIB_DIR=$$($(JULIA) -e "print(joinpath(Sys.BINDIR, \"..\", \"lib\"))"); \ cp -r "$$JULIA_LIB_DIR"/* $(LIB_DIR) @touch $(LIB_DIR)/.installed @echo "Julia libraries installed to $(LIB_DIR)"
To then filter for only the required libraries, I have not found a reliable way. What I have done is make a smallmainc
file that you can execute and see if it runs, and then by hand start deleting libraries and see ifmainc
crashes. Through this process, I made arequired_libraries.txt
file and then filter the bundled libraries with
I would warmly welcome a better way to do this.$(LIB_DIR)/.cleaned: $(LIB_DIR)/.installed required_libraries.txt @echo "Cleaning $(LIB_DIR)/julia directory based on required_libraries.txt" @echo "Before cleanup: $$(du -sh $(LIB_DIR)/ | cut -f1)" cd $(LIB_DIR)/julia && \ for file in *.so*; do \ [ -f "$$file" ] && \ stem=$$(echo "$$file" | sed 's/\.so.*/.so/') && \ ! grep -q "$$stem" ../../../required_libraries.txt && \ rm -f "$$file" || true; \ done @echo "After cleanup: $$(du -sh $(LIB_DIR)/julia/ | cut -f1)" @touch $(LIB_DIR)/.cleaned
to figure out which artifacts to bundle, I do something equally terrifying.see EDIT below I use PackageCompiler.jl to create a bundled version of my code, and then just copy over theshare
directory that PackageCompiler comes up with. Probably it’s possible to directly usePackageCompiler.bundle_artifacts
, but I haven’t dug into it enough yet.- finally, you can compile everything first locally, and then on a CI machine running e.g. a
ubuntu:22.04
container. I have had trouble getting everything to run underubuntu:20.04
, unfortunately. For examples, check my juliac workflow with relocation test or my python packaging (glibc) workflow.
Then, when this is all done you can even write a python interface to the c_api of your code and package it up as a python package, see e.g. here.
I am currently in the process of streamlining this.
Happy to have more discussions on this, and am looking forward to each step becoming more automated.
EDIT: It seems I have also found a way to bundle the libraries and artifacts more easily with PackageCompiler.jl. I think something like this should do the trick:
using PackageCompiler
ctx = PackageCompiler.create_pkg_context(".")
PackageCompiler.bundle_artifacts(ctx, ENV["COMPILED_DIR"]; include_lazy_artifacts=false)
stdlibs = PackageCompiler.gather_stdlibs_project(ctx)
PackageCompiler.bundle_julia_libraries(ENV["COMPILED_DIR"], stdlibs)
for (root, _, files) in walkdir(ENV["COMPILED_DIR"]), file in files
(contains(file, "libjulia-codegen") || contains(file, "libLLVM")) && rm(joinpath(root, file); force=true)
end