# Creating fully self-contained and pre-compiled library

**URL:** https://discourse.julialang.org/t/creating-fully-self-contained-and-pre-compiled-library/131374
**Category:** General Usage
**Created:** [August 5, 2025, 1:40pm UTC](https://discourse.julialang.org/t/creating-fully-self-contained-and-pre-compiled-library/131374 "2025-08-05T13:40:46Z")
**Posts on this page:** 1
**Showing post:** 17

<div class="post-metadata">

### Author: ![RomeoV](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/romeov/32/37687_2.png) [@RomeoV](https://discourse.julialang.org/u/RomeoV)
#### Post date: [August 20, 2025, 1:00pm UTC](https://discourse.julialang.org/t/creating-fully-self-contained-and-pre-compiled-library/131374/17 "2025-08-20T13:00:42Z")

</div>

Yes, I have managed to make a self-contained library that can be relocated.  
You can find the reference here: [GitHub - RomeoV/RunwayLib.jl](https://github.com/RomeoV/RunwayLib.jl)

In essence, my build and bundling steps are as follows:

- Write your normal julia code. Make `Base.@entrypoint`s ([example](https://github.com/RomeoV/RunwayLib.jl/blob/master/src/c_api.jl)). Write tests that test your code with `JET.@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 it `juliac`. Put a Makefile with something like

```make
COMPILED_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

```

Notice the `--relative-rpath`, which will allow us to put all the required libraries next to each other.  
Remember to run `juliaup override set 1.12` in the directory.
- Manually write a [header file](https://github.com/RomeoV/RunwayLib.jl/blob/master/juliac/libposeest.h), and add

```make
# Create include directory with header file
$(INCLUDE_DIR)/libmylib.h: libmylib.h
	@mkdir -p $(INCLUDE_DIR)
	cp libmylib.h $(INCLUDE_DIR)/

```

@tim.holy has started automating this: [GitHub - JuliaInterop/JuliaLibWrapping.jl: Generate language-bindings for Julia-generated shared libraries](https://github.com/JuliaInterop/JuliaLibWrapping.jl)
- Make a `Project.toml` in `juliac` that contains something like

```toml
[sources]
MyLib = {path = ".."}

```

Also copy over any other custom sources from your main Project.toml file (it seems sources are [not applied transitively](https://github.com/JuliaLang/Pkg.jl/pull/3783#issuecomment-1935523553)…)
- Make a simple `loadmylib.jl` file that just runs `using MyLib`
- Run the compilation

```bash
make all

```

Now it’s time to create the “bundle” that we can ship. This includes three aspects.

1. 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
2. figure out which artifacts (`share/julia`) we have to ship
3. 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 like~~ **see EDIT below**

```make
$(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)"

```

in your Makefile.  
To then filter for only the required libraries, I have not found a reliable way. What I have done is make a small `mainc` file that you can execute and see if it runs, and then by hand start deleting libraries and see if `mainc` crashes. Through this process, I made a `required_libraries.txt` file and then filter the bundled libraries with

```make
$(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

```

I would warmly welcome a better way to do this.
- ~~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 the `share` directory that PackageCompiler comes up with. Probably it’s possible to directly use [`PackageCompiler.bundle_artifacts`](https://github.com/JuliaLang/PackageCompiler.jl/blob/57505caad062b5b1985bcbbb2fb15b9b6e9e6d99/src/PackageCompiler.jl#L1492), 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 under `ubuntu:20.04`, unfortunately. For examples, check [my juliac workflow with relocation test](https://github.com/RomeoV/RunwayLib.jl/blob/master/.github/workflows/juliac.yml) or [my python packaging (glibc) workflow](https://github.com/RomeoV/RunwayLib.jl/blob/master/.github/workflows/package-python.yml).

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](https://github.com/RomeoV/RunwayLib.jl?tab=readme-ov-file#python-usage).  
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:

```julia
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

```

---

_[View the full topic](https://discourse.julialang.org/t/creating-fully-self-contained-and-pre-compiled-library/131374)._
