My Alternative Package Tool and Ports System for FreeBSD

This is a new package tool and ports system I’m working on. I started building it because I needed a specific set of packages that aren’t available in the FreeBSD ports tree, and one of the core libraries I rely on, skia is packaged incorrectly. I also require a different version than what’s currently provided. When I tried to package the libraries I needed using Makefiles, I realized it was taking more time and effort than simply building a new system from scratch.

Whats really nice about this system is you can have as many versions of a port installed at the same time, it also reserves all past and future ports and ports configurations.

The design is inspired by Nix. It uses TOML for port definitions and tar/zstd for packaging.

Used like this.
Code:
pkg install gnome

  Commands
  - list — list available ports
  - show <name> [ver] — show a port definition
  - deps <name> [ver] — show port dependencies
  - plan <name> [ver] — show build order
  - fetch <name> [ver] — fetch sources
  - build [--group] [--install] [--force] <name> [ver] — build a port or group
  - install [--rebuild] [--group] <name> [ver] — create profile tree
  - package all — package current profile
  - root — show ports root


  Flags
  - --group — treat <name> as a group (in build/install)
  - --install — link each built port into the profile as it finishes (build)
  - --force — override active lockfile (build)
  - --rebuild — rebuild missing deps before install (install)


This is the ports root dir.
Screenshot From 2026-02-05 09-34-37.png

This is the build/ directory, when you build a port its built in this dir.

Screenshot From 2026-02-05 09-36-44.png


build/logs/ When we build a port from src these are the logs generated, a log is
what you would see in your terminal when building src code.

Screenshot From 2026-02-05 09-39-03.png


build/distfiles/ When you build a port from src its src package is downloaded here.

group/ A group allows you to assign a group of packages to a single option. So in
this case we would do ...
Build: ./pkg build --group development
Build + install each: ./pkg build --group --install development
Install ./pkg install --group development

Screenshot From 2026-02-05 09-49-38.png


group/development.toml
Code:
ports = [
  "libepoll-shim",
  "libffi",
  "expat",
  "libglvnd",
  "hwdata",
  "libpciaccess",
  "libdrm",
  "mesa-libs",
  "pixman",
  "wayland",
  "wayland-protocols",
  "wlr-protocols",
  "wlroots",
  "yoga"
]
 
pkgs/ This is where the actual ports files are stored.
Screenshot From 2026-02-05 10-00-36.png


pkgs/expat/ Each version of a port gets its own version dir.

Screenshot From 2026-02-05 10-02-11.png


pkgs/expat/version.toml current is built by default, future is the next os version
and last is the previous os version 13 would be freebsd version 13.
Code:
future = "2.8.0"
current = "2.7.3"
last = "2.7.1"
13 = "2.6.8"
12 = "2.6.3"

pkgs/expat/2.7.3/port.toml
Code:
name = "expat"
version = "2.7.3"
summary = "XML 1.0 parser"
license = "MIT"

deps = []

[src]
type = "url"
url = "https://github.com/libexpat/libexpat/releases/download/R_2_7_3/expat-2.7.3.tar.xz"
sha256 = ""

[build]
system = "configure"
make = "make"
args = ["--prefix=/usr", "--without-docbook", "--without-examples"]

[install]
args = ["DESTDIR=$out", "install"]

Here is a more complex port.toml file pkgs/skia/m145/port.toml
Code:
name = "skia"
version = "m145"
summary = "Skia 2D graphics library"
license = "BSD-3-Clause"

deps = []

[src]
type = "path"
path = "../sdk/contrib/skia"

[build]
system = "custom"
commands = [
  "mkdir -p $WORK/build",
  '''sh -c 'cat > "$WORK/build/args.gn" << "EOF"
is_official_build=true
is_component_build=false
skia_use_vulkan=true
skia_use_gl=false
skia_use_egl=false
skia_use_x11=false
skia_use_fontconfig=false
skia_use_freetype=true
skia_use_system_freetype2=false
skia_use_harfbuzz=true
skia_use_system_harfbuzz=false
skia_use_expat=true
skia_use_system_expat=false
skia_use_icu=true
skia_use_system_icu=false
skia_use_dng_sdk=false
skia_use_system_libjpeg_turbo=false
skia_use_system_libpng=false
skia_use_system_libwebp=false
skia_enable_fontmgr_android=false
skia_enable_fontmgr_fontconfig=false
skia_enable_fontmgr_FontConfigInterface=false
skia_compile_modules=false
skia_compile_sksl_tests=false
skia_enable_skottie=true
skia_enable_svg=true
skia_enable_skshaper=true
skia_enable_skparagraph=true
EOF' ''',
  "GIT_SYNC_DEPS_SKIP_GN=1 GIT_SYNC_DEPS_SKIP_EMSDK=1 python3 \"$SRC/tools/git-sync-deps\"",
  "gn gen \"$WORK/build\" --root=\"$SRC\"",
  "ninja -C \"$WORK/build\" -v",
]

[install]
commands = [
  "rm -rf \"$out/usr\"",
  "mkdir -p \"$out/usr/lib\"",
  "mkdir -p \"$out/usr/include/skia/include\"",
  "mkdir -p \"$out/usr/include/skia/modules\"",
  "find \"$WORK/build\" -type f \\( -name 'lib*.a' -o -name 'lib*.so*' \\) -exec install -m 644 {} \"$out/usr/lib\" \\;",
  "find \"$out/usr/lib\" -type f -name '*.o' -delete",
  "cp -a \"$SRC/include/.\" \"$out/usr/include/skia/include\"",
  "cp -a \"$SRC/modules/.\" \"$out/usr/include/skia/modules\"",
]


ports.lock/ This file locks the ports build system so we can't build other packages when
one is currently build it also tells us what we are building and what failed, this file is auto generated

Code:
# Generated by pkg
state = "failed"
pid = 92377
started = 1770312618
command = "build --install wlroots"
current = "mesa-libs@24.1.7"
completed = [ "hwdata@0.399", "libpciaccess@0.18.1", "libdrm@2.4.129", "libglvnd@1.7.0", "expat@2.7.3", "spirv-tools@2026.1", "glslang@16.2.0", "py-markupsafe@3.0.2", "py-mako@1.3.5", "vulkan-headers@1.4.336", "libffi@3.5.1", "libepoll-shim@0.0.20240608", "wayland@1.24.0", "wayland-protocols@1.47" ]

[[package]]
name = "hwdata"
version = "0.399"
store = "/home/stevenstarr/Projects/ports/store/e2942dfed0c7-hwdata-0.399"
hash = "e2942dfed0c7479a459430c69a0e5ae672ce9ad2610e58f9b1af68256d57be28"
src_type = "url"
src_url = "https://github.com/vcrhonek/hwdata/archive/refs/tags/v0.399.tar.gz"

store/ This is where all ports packages are installed after they are built. Notice the hash's that's because
specific configuration should reproduce specific hashes, this is for reproducible
builds example say you have a program that needs a very specific version and configuration
if all the settings and correct patches etc are used you should get the same hash.

When packaging binary's we simply package the following directory's individually, when we install a binary port package
we would have pkg download and extract the package here, and uninstall we simply delete it.

Screenshot From 2026-02-05 10-17-49.png


store/0ff43b52f3b3-wayland-1.24.0
Code:
.
└── usr
    ├── bin
    │   └── wayland-scanner
    ├── include
    │   ├── wayland-client-core.h
    │   ├── wayland-client-protocol.h
    │   ├── wayland-client.h
    │   ├── wayland-cursor.h
    │   ├── wayland-egl-backend.h
    │   ├── wayland-egl-core.h
    │   ├── wayland-egl.h
    │   ├── wayland-server-core.h
    │   ├── wayland-server-protocol.h
    │   ├── wayland-server.h
    │   ├── wayland-util.h
    │   └── wayland-version.h
    ├── lib
    │   ├── libwayland-client.so -> libwayland-client.so.0
    │   ├── libwayland-client.so.0 -> libwayland-client.so.0.24.0
    │   ├── libwayland-client.so.0.24.0
    │   ├── libwayland-cursor.so -> libwayland-cursor.so.0
    │   ├── libwayland-cursor.so.0 -> libwayland-cursor.so.0.24.0
    │   ├── libwayland-cursor.so.0.24.0
    │   ├── libwayland-egl.so -> libwayland-egl.so.1
    │   ├── libwayland-egl.so.1 -> libwayland-egl.so.1.24.0
    │   ├── libwayland-egl.so.1.24.0
    │   ├── libwayland-server.so -> libwayland-server.so.0
    │   ├── libwayland-server.so.0 -> libwayland-server.so.0.24.0
    │   └── libwayland-server.so.0.24.0
    ├── libdata
    │   └── pkgconfig
    │       ├── wayland-client.pc
    │       ├── wayland-cursor.pc
    │       ├── wayland-egl-backend.pc
    │       ├── wayland-egl.pc
    │       ├── wayland-scanner.pc
    │       └── wayland-server.pc
    └── share
        ├── aclocal
        │   └── wayland-scanner.m4
        └── wayland
            ├── wayland-scanner.mk
            ├── wayland.dtd
            └── wayland.xml

10 directories, 35 files

profile/ This is the profile dir this is where all the installed files from store are reassembled
via symlinks. We use current by default. /usr/local would point to one of these.

Screenshot From 2026-02-05 10-26-26.png


profile/current/ This is what the inside looks like. On your file system /usr/local
points to this.
Screenshot From 2026-02-05 10-31-26.png
 
profile/current/ This is what the inside looks like. On your file system /usr/local
points to this.
You want to change the installed software based upon some profile? If you plan to do this on a per-user basis we need some explaination how this shall fly in a Multi-User system.
 
You want to change the installed software based upon some profile? If you plan to do this on a per-user basis we need some explaination how this shall fly in a Multi-User system.
Right now by default there is one shared profile for the whole system, you can run a single global profile or separate per‑user profiles. A shared
base + per‑user overlay is something I can add later if needed, but it’s not required to make multi‑user work. And for clarification I am not suggesting replacing freebsd pkg tool or ports system, I am just saying this is what I am working on and felt like sharing because I like what I built, its a sound proven design.
 
Well, this seems really interesting. The problem is that, in general, you can't come out of nowhere with something revolutionary and expect a close-knit community to do anything other than fiercely reject your ideas or simply ignore them. Maybe this will be the exception to the rule, but that is the rule of the human condition. I wonder, what has changed in your life, after 14 years (you said this in another thread) of solitary work? Also, it's obvious you are a very resourceful person. Why don't you create a repack of FreeBSD? Like the guy who controls GhostBSD did. ZestBSD.

My main concern, though, is that I see a person of great intelligence and imagination, you zester, who I suspect is having some kind of crisis, and I wouldn't like him to feel defeated or bad. I may be wrong again. It's very common to be wrong.
 
Well, this seems really interesting. The problem is that, in general, you can't come out of nowhere with something revolutionary and expect a close-knit community to do anything other than fiercely reject your ideas or simply ignore them. Maybe this will be the exception to the rule, but that is the rule of the human condition. I wonder, what has changed in your life, after 14 years (you said this in another thread) of solitary work? Also, it's obvious you are a very resourceful person. Why don't you create a repack of FreeBSD? Like the guy who controls GhostBSD did. ZestBSD.

My main concern, though, is that I see a person of great intelligence and imagination, you zester, who I suspect is having some kind of crisis, and I wouldn't like him to feel defeated or bad. I may be wrong again. It's very common to be wrong.

I don’t see a problem here, and I fully understand that this is a close-knit community I’ve been a member since 2011. What I don’t understand is the notion that someone isn’t allowed to discuss a new or unconventional idea unless they’ve been granted some kind of special permission. That’s not how open source works, and I’m fairly certain that’s also not in line with the rules of this forum.

We shouldn’t assume that simply because I share something I’m working on, it somehow affects the community, or that I’m trying to change minds or force ideas on anyone. I was very explicit in the thread title: I used the words “my” and “alternative.” I never claimed otherwise.

I also never said I was working in isolation. What I said was that it took me 14 years to go from rendering a single image to building something functional. And thank you for the compliment“it’s obvious you are a very resourceful person.”

To be absolutely clear, I have zero interest in creating a distribution.

Finally, it’s extremely inappropriate to insinuate that sharing a personal technical project on these forums implies some sort of mental health issue. That kind of assumption is both rude and unwarranted.
 
I don’t see a problem here, and I fully understand that this is a close-knit community I’ve been a member since 2011. What I don’t understand is the notion [...]
Sorry, it's just my suspicion. I might be wrong! I was only talking about my suspicion! It was ME talking! I don't represent the community. I hardly represent me.
 
Again, I was not talking about myself. I was talking about what I feared could happen. Anyway, I'm still very sorry and I'll be very sorry for a long time. I apologize again, zester.
Thanks for trying to be my protector ;) but I’ve been around a long time and I’m more than capable of handling anyone who wants to give me a hard time.
 
I'm wondering if someome familiar with synth could compare the features to zester's work.
They are completely different designs.

1. I use a toml format because I found the ports tree makefile to complex.
Mine ports.toml
Code:
name = "expat"
version = "2.7.3"
summary = "XML 1.0 parser"
license = "MIT"

deps = []

[src]
type = "url"
url = "https://github.com/libexpat/libexpat/releases/download/R_2_7_3/expat-2.7.3.tar.xz"
sha256 = ""

[build]
system = "configure"
make = "make"
args = ["--prefix=/usr", "--without-docbook", "--without-examples"]

[install]
args = ["DESTDIR=$out", "install"]

FreeBSD ports makefile
Code:
PORTNAME=    expat
DISTVERSION=    2.7.3
CATEGORIES=    textproc
MASTER_SITES=    https://github.com/libexpat/libexpat/releases/download/R_${DISTVERSION:S|.|_|g}/

MAINTAINER=    desktop@FreeBSD.org
COMMENT=    XML 1.0 parser written in C
WWW=        https://github.com/libexpat/libexpat

LICENSE=    MIT
LICENSE_FILE=    ${WRKSRC}/COPYING

TEST_DEPENDS=    bash:shells/bash

USES=        cpe libtool pathfix python:test tar:xz
CPE_VENDOR=    libexpat_project
CPE_PRODUCT=    libexpat
USE_LDCONFIG=    yes

GNU_CONFIGURE=    yes

INSTALL_TARGET=    install-strip
TEST_TARGET=    check

PLIST_SUB=    EXPAT_VERSION=${DISTVERSION}

CONFIGURE_ARGS=    --without-docbook \
        --without-examples

OPTIONS_DEFINE=    DOCS STATIC TEST
OPTIONS_SUB=    yes

STATIC_CONFIGURE_ENABLE=    static

TEST_USES=    compiler:c++11-lang shebangfix
SHEBANG_FILES=    test-driver-wrapper.sh tests/udiffer.py tests/xmltest.sh
TEST_CONFIGURE_WITH=    tests

.include <bsd.port.mk>

2. My system builds into a content‑addressed store, so every output path is a hash
of its inputs (sources, patches, and build settings). That gives an immutable,
verifiable fingerprint for what was built. For regulated environments like DoD,
you can pin the exact inputs and only accept binaries whose store hash matches
the certified signature so the build is repeatable and auditable, and any
deviation is immediately detectable.

3. Everything we build lives in a read‑only, versioned store. The ‘installed’
system is just a profile: a directory of symlinks pointing into that store.
Switching or rolling back is just swapping the profile.”

4. Synth I think builds multiple packages at a time, mine doesn't do that as that would compromise safety and reliability
in this type of design.
 
Back
Top