package management infrastructure
  • Rust 78.3%
  • Shell 21.1%
  • Makefile 0.6%
Find a file
2026-06-20 18:49:59 -05:00
misc/cmp feat: add bash completions 2026-05-21 13:03:37 -05:00
sh %fix: use shallow-build-deps in runner 2026-06-08 15:13:35 -05:00
src feat: use Arc<str> for immutable strings 2026-06-20 18:49:59 -05:00
.gitignore init 2026-03-18 21:33:19 -05:00
.rustfmt.toml chore: fmt 2026-05-04 17:52:29 -05:00
AGENTS.md chore: add AGENTS.md 2026-06-03 17:29:32 -05:00
Cargo.lock chore: update dependencies 2026-06-19 02:08:31 -05:00
Cargo.toml feat: use Arc<str> for immutable strings 2026-06-20 18:49:59 -05:00
CLAUDE.md chore: add AGENTS.md 2026-06-03 17:29:32 -05:00
LICENSE-0BSD doc: licensing 2026-05-27 05:33:37 -05:00
LICENSE-GLWTPL doc: licensing 2026-05-27 05:33:37 -05:00
LICENSE-WTFPL doc: licensing 2026-05-27 05:33:37 -05:00
Makefile chore: update container 2026-06-20 18:45:57 -05:00
README.md doc: remove obsolete footnote 2026-06-03 17:29:32 -05:00

ljh

package management infrastructure

/ˌl̪'/

Information

ljh is not a package manager. Its primary goal is to make building and maintaining packages from recipes trivial.

ljh is capable of dependency resolution and fetching sources, among a few other things. It can install packages, but can't remove them, since that's without the scope of building packages. Basic installation logic is necessary as builds occur in a container, and dependencies exist.

ljh is technically passable as a package manager if you intend to update packages by reinstalling your entire system from a specified list of packages. However, a front-end is being developed.

Builds are hermetic and unprivileged, thanks to bubblewrap.

Recipes

A reference recipe repository implementation exists at https://git.gay/tox/recipes.

The master branch defines packages used on my primary system.

A basic recipe might look something like this:

name=egl-wayland
version=1.1.21
release=0
container=ljh@20260506

about="Wayland EGL external platform library"
licenses=("MIT")
upstream="https://github.com/NVIDIA/$name"
packagers=("m: tox <tox@tox.wtf>")

sources=(
    "$upstream/archive/$version.tar.gz % YZV4NA9H_glOsvFqPzZmWg"
)

dependencies=(
    libdrm
    wayland
    libglvnd
    eglexternalplatform

    b:wayland-protocols
)

pkg() {

w meson
defmeson

}

# vi: set ts=4 sw=4 tw=0 ft=bash :

At the top exist some variables defining the base package. The required ones are as follows:

  • $name: The base package's name.
  • $version: The package's version. This may not contain a dash, and should be limited to ASCII.
  • $release: A 0-indexed u64 denoting the nth build of this version. This is generally incremented as patches are made, or when rebuilding against updated dependencies.
  • $container: The container in which this package is built.

Though unused by ljh, other arbitrary variables are collected by ljh and serialized to JSON, for use by a front-end. I like to include the following:

  • $about: A brief description of the package.
  • $licenses: SPDX-compliant license identifiers for the package.
  • $upstream: The package's upstream.
  • $packagers: The people who built the package. Maintainers are denoted with m: and contributors with c: .

The $sources and $dependencies arrays are both technically optional as well, but many packages will want them.

A source consists of up to three parts -- the URL, the destination file, and a b23 checksum. A complete example might look like so:

"$upstream/archive/$version.tar.gz -> $name-$version.tar.gz % YZV4NA9H_glOsvFqPzZmWg"

A dependency consists of up to two parts -- the dependency kind, and a package name. If no dependency kind is specified, the dependency is required. Dependency kinds are as follows:

  • b:: Build-time dependencies. For instance, b:rustc or b:wayland-protocols.
  • i:: Install-time dependencies. For instance, i:xdg-utils or i:desktop-file-utils.

The $opts array (not showcased here) is used to pass options to ljh. For instance, opts=(net !lto) will enable networking in the container and remove LTO flags from certain environment variables.

Options are defined under /sh/opts/.

Containers

Containers can be built by hand, but I generate mine with LFStage to save some time (and headache).

A container has roughly the following file hierarchy:

ljh@20260506/
├── lower/
│   ├── base/
│   │   ├── bin -> usr/bin/
│   │   ├── dev/
│   │   ├── etc/
│   │   ├── home/
│   │   ├── lib -> usr/lib/
│   │   ├── lib64/
│   │   ├── ljh/
│   │   ├── proc/
│   │   ├── run/
│   │   ├── sbin -> usr/sbin/
│   │   ├── sys/
│   │   ├── tmp/
│   │   ├── usr/
│   │   └── var/
│   └── net/
│       └── etc/
├── upper/
│   ├── D/
│   │   ├── libtree/
│   │   ├── libtree@95d8992968a18ae957fef723f103b416d0843398-0.tar.zst
│   │   ├── libtree.man/
│   │   └── libtree.man@95d8992968a18ae957fef723f103b416d0843398-0.tar.zst
│   ├── ljh/
│   │   └── .cache/
│   └── S/
│       └── libtree-95d8992968a18ae957fef723f103b416d0843398/
└── work/
    └── work/
  • lower/base/ contains a read-only version of the container's root filesystem. It's quite similar to a Gentoo stage file.
  • lower/net/ contains read-only networking information sourced from the host. This allows the container to use the same DNS settings and CA certificates as the host (which is usually desired).
  • upper/ holds all written changes. This is where dependencies are installed and packages are built.
  • upper/D/ contains the DESTDIRs for package installations.
  • upper/S/ contains the sources for package builds, and builds take place in the directory extracted from the first specified source archive. In the event no such directory is available, builds begin from upper/S.
  • work/work/ is used by the kernel and may be ignored for our purposes.

The generic ljh container is available at https://git.gay/tox/ljh-lfstage.

In the future, containers will be responsible for providing ljh.

Getting Started

Note

Before we begin, it's worth noting that something here is likely to break. But the general idea should remain the same: build a container, install ljh, add recipes, make any needed tweaks, and build packages.

We'll begin from scratch.

First, acquire a copy of LFStage:

git clone https://github.com/tox-wtf/lfstage && cd lfstage
make
sudo make install

Note

The lfstage command will likely take a long time to complete. Feel free to do something else in the meantime.

Then, you'll need a build container. Let's build the standard one:

sudo git clone https://git.gay/tox/ljh-lfstage /var/lib/lfstage/profiles/ljh
sudo lfstage build ljh

And copy it into place:

sudo cp /var/cache/lfstage/stages/lfstage-ljh-*.tar.xz ~/.ljh/containers/ljh@$(date +%Y%m%d).tar.xz

Once that's done, install ljh:

git clone https://git.gay/tox/ljh && cd ljh
make
make install
make CONTAINER=ljh@$(date +%Y%m%d) install-container

Copy some recipes into place:

git clone https://git.gay/tox/recipes ~/.ljh/recipes

And build a package:

ljh build libtree

This will probably fail since the date of the container specified in the recipe likely differs from the one you just built. To address this, update the recipe:

sed "/^container=/s/=.\+/=ljh@$(date +%Y%m%d)/" -i ~/.ljh/recipes/libtree/recipe

Note

You should increment package releases when updating their container, but since you haven't built anything yet, it doesn't really matter.

Then try building it again:

ljh build libtree

You can update the container for all recipes with the following command:

sed "/^container=/s/=.\+/=ljh@$(date +%Y%m%d)/" -i ~/.ljh/recipes/*/recipe

Once you're content with your recipes, you can build everything with the following command:

ljh build

Tip

You shouldn't actually use my recipes. You can reference them, but this whole section ultimately just exists to document getting started.

Good luck!

Licensing

ljh is not ready for use by people who aren't me yet, but plunder whatever you like. ljh is licensed under your choice of any of the following: