The FreeBSD Ports Collection is the way almost everyone installs applications (“ports”) on FreeBSD. Like everything else about FreeBSD, it is primarily a volunteer effort. It is important to keep this in mind when reading this document. In FreeBSD, anyone may submit a new port, or volunteer to maintain an existing unmaintained port. No special commit privilege is needed.

For this guide I will use fd tool written by David Peter as example project.

Prerequisites

FreeBSD installation (VM is fine)

Local ports tree (done via svn)

portlint (located at devel/portlint )

) poudriere (located at ports-mgmt/poudriere )[optional]

Getting ports tree

When you install FreeBSD opt-out of the ports tree. Install svn:

pkg install svn svn checkout https://svn.freebsd.org/ports/head /usr/ports

Poudriere

Poudriere is highly recommended for testing / packaging and QA”. The project actively promotes QA before submission and it helps get things into the tree quicker. You should try to test it at least on one production release on `i386` and `amd64`. Mark port arch specific if that’s your case. If you choose to use poudriere, use ZFS. There are plenty of guides on the subject. FreeBSD Porter’s Handbook is the most complete source of information on porting to FreeBSD in general.

Makefile

Whole porting process in most cases is writing one Makefile . I recommend doing something like this.

Here is the one I wrote for fd:

Header

Order of items here is important, portlint -AC will help you to get it right. You can look at existing ports if you need help. The header is just one line:

# $FreeBSD$

Port metadata

Each port must have one primary category in case of fd it will be sysutils , therefore it's located in /usr/ports/systuils/fd .

PORTNAME= fd

CATEGORIES= sysutils

Since this port conflicts with other util named fd I specified package suffix as: PKGNAMESUFFIX= -find and indicate conflict: CONFLICTS_INSTALL= fd-[0-9]* . That means to install it from packages user will have to type:

pkg install fd-find

This section is different for every port, but in case of fd it's pretty straightforward:

LICENSE= MIT APACHE20

LICENSE_COMB= dual

Since fd includes the text of licenses you should do this as well:

LICENSE_FILE_MIT= ${WRKSRC}/LICENSE-MIT

LICENSE_FILE_APACHE20= ${WRKSRC}/LICENSE-APACHE

Distfiles

FreeBSD has a requirement that all ports must allow offline building. That means you have specified which files are needed to be downloaded. Luckily we now have helpers to download GitHub sources directly from GitHub:

USE_GITHUB= yes

GH_ACCOUNT= sharkdp

Since PORTNANE is fd it will try to download sources for sharkdp/fd . By default it's going to download tag: ${DISTVERSIONPREFIX}${DISTVERSION}${DISTVERSIONSUFFIX}

fd uses v as the prefix, therefore we need to specify: DISTVERSIONPREFIX= v .

It's also possible to specify GH_TAGNAME in case tag name doesn't match that pattern.

Extra packages

There are very few rust projects that are standalone and use no crates dependencies. It’s used to be PITA to make it work offline, but now cargo is a first class citizen in ports:

USES= cargo

CARGO_CRATES= aho-corasick-0.6.3 \

atty-0.2.3 \

# and so goes on

Yes, you have to specify each dependency. Luckily, there is a magic awk script that turns Cargo.lock into what you need. Execute make cargo-crates in the port root. This will fail because you're missing checksum for the original source files:

make makesum

make cargo-crates

This will give you what you need. Double check that result is correct. There is a way to ignore checksum error, but I can’t remember… Execute make makesum again.

CARGO_OUT

If. build.rs relies on that you have to change it. fd allows you to use SHELL_COMPLETIONS_DIR to specify where completions go, while ripgrep doesn't. In our case we just specify SHELL_COMPLETIONS_DIR :

SHELL_COMPLETIONS_DIR= ${WRKDIR}/shell-completions-dir CARGO_ENV= SHELL_COMPLETIONS_DIR=${SHELL_COMPLETIONS_DIR}

PLIST

FreeBSD is very strict about files it’s installing and it won’t allow you to install random files that get lost. You have to specify which files you’re installing. In this case, it’s just two:

PLIST_FILES= bin/fd \

man/man1/fd.1.gz

Note that sources for fd have uncompressed man file, while here it’s listed as compressed. If port installs a lot of files, specify them in pkg-plist like here. To actually install them:

post-install:

@${STRIP_CMD} ${STAGEDIR}${PREFIX}/bin/fd

${INSTALL_MAN}${WRKSRC}/doc/fd.1 ${STAGEDIR}${MAN1PREFIX}/man/man1

Shell completions

clap-rs can generate shell completions for you, it's usually handled by build.rs script. First, we need to define options:

OPTIONS_DEFINE= BASH FISH ZSH # list options

OPTIONS_DEFAULT= BASH FISH ZSH # select them by default

BASH_PLIST_FILES= etc/bash_completion.d/fd.bash-completion

FISH_PLIST_FILES= share/fish/completions/fd.fish

ZSH_PLIST_FILES= share/zsh/site-functions/_fd

To actually install them:

post-install-BASH-on:

@${MKDIR} ${STAGEDIR}${PREFIX}/etc/bash_completion.d

${INSTALL_DATA} ${SHELL_COMPLETIONS_DIR}/fd.bash-completion \

${STAGEDIR}${PREFIX}/etc/bash_completion.d post-install-FISH-on:

@${MKDIR} ${STAGEDIR}${PREFIX}/share/fish/completions

${INSTALL_DATA} ${SHELL_COMPLETIONS_DIR}/fd.fish \

${STAGEDIR}${PREFIX}/share/fish/completions post-install-ZSH-on:

@${MKDIR} ${STAGEDIR}${PREFIX}/share/zsh/site-functions

${INSTALL_DATA} ${SHELL_COMPLETIONS_DIR}/_fd \

${STAGEDIR}${PREFIX}/share/zsh/site-functions

Bonus round

Patching source code

Sometimes you have to patch it and send the patch upstream. Merging it upstream can take awhile, so you can patch it as part of the install process. An easy way to do it:

Go to work/ dir

dir Copy file you want to patch and add .orig suffix to it

suffix to it Edit file you want to patch

Execute make makepatch in port's root

Submitting port

First, make sure portlint -AC doesn't give you any errors or warnings. Second, make sure poudriere can build it on both amd64 and i386 . If it can't — you have to either fix it or mark port broken for that arch.