How I Created My First FreeBSD Port

I created my first FreeBSD port recently. I found that FreeBSD didn't have a port for GoCD, which is a continuous integration and continuous deployment (CI/CD) system. This was a great opportunity to learn how to build a FreeBSD port while also contributing back to the community.

Edit: This post was mentioned in BSD Now episode 294.

Initial Setup

I created a FreeBSD build environment in a Digital Ocean droplet. Installed a few things I needed,

$ sudo pkg update
$ sudo pkg install -y bash vim-tiny tmux openjdk8 ruby ruby24-gems rubygem-rake

Since GoCD requires Java and Ruby I had to install those with pkg. Otherwise, they will be built with the ports system, which is very slow. Others are for my convenience.

I highly recommend you create a tmux session for your work, especially when the build environment is remote.

$ tmux attach -t gocd || tmux new-session -s gocd
$ sudo su  # become root to make it easier to work with ports
$ bash  # Many steps assume you're using bash; modify them as needed

Setup Ports

$ cd /usr/ports
$ portsnap fetch && portsnap extract && portsnap update

Tweaks

$ echo DEVELOPER=yes | tee -a /etc/make.conf # https://www.freebsd.org/doc/en/books/porters-handbook/quick-porting.html
$ export DISTDIR=/usr/ports/distfiles  # https://www.freebsd.org/doc/en/books/porters-handbook/quick-porting.html

Port Init

$ mkdir -p /usr/ports/devel/gocd-server/files
$ cd /usr/ports/devel/gocd-server
$ touch Makefile pkg-descr pkg-plist files/gocd-server.in

pkg-descr

Edit pkg-descr with the following contents.

GoCD is an open source Continuous Integration (CI) and Continuous Delivery (CD)
system sponsored by ThoughtWorks Inc. It is built with Java.

WWW: https://www.gocd.org/

Makefile (fetch)

Edit Makefile to have the following contents. This is the first round of edits which will allow us to fetch the source tarball. We'll continue to edit this file later in this post, adding more content as we need it for the related step.

At the time of writing, GoCD was at version 19.2.0-8641.

# $FreeBSD$

PORTNAME=gocd-server
DISTVERSION=19.2.0-8641
CATEGORIES=devel java
MASTER_SITES=https://download.gocd.org/binaries/${DISTVERSION}/generic/
DISTFILES=go-server-${DISTVERSION}.zip

MAINTAINER=me@example.com
COMMENT=An open-source Continuous Integration and Continuous Delivery system

LICENSE=APACHE20

.include <bsd.port.mk>

This Makefile can fetch the source code.

$ make fetch

distinfo

We are also able to create the distinfo file with the Makefile we have.

$ make makesum

Makefile (extract)

Edit Makefile to be able to extract files from the source distribution.

# $FreeBSD$

PORTNAME=gocd-server
DISTVERSION=19.2.0-8641
CATEGORIES=devel java
MASTER_SITES=https://download.gocd.org/binaries/${DISTVERSION}/generic/
DISTFILES=go-server-${DISTVERSION}.zip

MAINTAINER=me@example.com
COMMENT=An open-source Continuous Integration and Continuous Delivery system

LICENSE=APACHE20

DISTNAME=go-server-19.2.0
EXTRACT_ONLY=go-server-${DISTVERSION}.zip
EXTRACT_CMD=${UNZIP_NATIVE_CMD} ${DISTDIR}/${EXTRACT_ONLY}

.include <bsd.port.mk>

This Makefile can extract the source code.

$ make extract

GID and UID

Search /usr/ports/GIDs and /usr/ports/UIDs for matching free GID and UID and use them.

$ grep free /usr/ports/GIDs | head
$ grep free /usr/ports/UIDs | head

I used GID 237 since it was free. You will not necessarily use these IDs when you submit your port to the project. But it doesn't matter. For now, all we care about is using available IDs.

Edit /usr/ports/GIDs to claim the GID for group gocd.

$ sed -i.bak 's!# free: 237!gocd:*:237:!g' /usr/ports/GIDs

Edit /usr/ports/UIDs to claim the UID for user gocd.

$ sed -i.bak 's!# free: 237!gocd:*:237:237::0:0:GoCD:/usr/local/gocd-server:/bin/sh!g' /usr/ports/UIDs

pkg-plist

Edit pkg-plist with the following contents,

%%JAVAJARDIR%%/go.jar
@dir(%%GOCD_SERVER_USER%%,%%GOCD_SERVER_GROUP%%,) gocd-server

files/gocd-server.in

Edit files/gocd-server.in to create an rc script with the following contents,

#!/bin/sh
# $FreeBSD$
#
# PROVIDE: gocdserver
# REQUIRE: LOGIN
# KEYWORD: shutdown
#
# Configuration settings for gocdserver in /etc/rc.conf:
#
# gocd_server_enable (bool):
#   Set to "NO" by default.
#   Set it to "YES" to enable gocd
#
# gocd_server_config_dir (str)
#   Set to "%%GOCD_SERVER_CONFIG_DIR%%" by default.
#   Path where config files are stored.
#
# gocd_server_java_home (str):
#   Set to "%%JAVA_HOME%%" by default.
#   Set the Java virtual machine to run gocd
#
# gocd_server_java_opts (str):
#   Set to "" by default.
#   Java VM args to use.
#
# gocd_server_user (str):
#   Set to "%%GOCD_SERVER_USER%%" by default.
#   User to run gocd as.
#
# gocd_server_group (str):
#   Set to "%%GOCD_SERVER_GROUP%%" by default.
#   Group for data file ownership.
#
# gocd_server_log_file (str):
#   Set to "%%GOCD_SERVER_LOG_FILE%%" by default.
#   Log file location.
#
# gocd_server_memory (str):
#   Set to "%%GOCD_SERVER_MEM%%" by default.
#   Minimal memory to reserve.
#
# gocd_server_max_memory (str):
#   Set to "%%GOCD_SERVER_MAX_MEM%%" by default.
#   Maximum memory to use.
#
# gocd_server_max_metaspace (str):
#   Set to "%%GOCD_SERVER_MAX_METASPACE%%" by default.
#   Maximum memory for Java's Metaspace.
#
# gocd_server_port (str):
#   Set to "%%GOCD_SERVER_PORT%%" by default.
#   Port to use.
#
# gocd_server_ssl_port (str):
#   Set to "%%GOCD_SERVER_SSL_PORT%%" by default.
#   SSL/TLS port to use.
#
# gocd_server_user_lang (str):
#   Set to "%%GOCD_SERVER_USER_LANG%%" by default.
#   User language.
#
# gocd_server_user_country (str):
#   Set to "%%GOCD_SERVER_USER_COUNTRY%%" by default.
#   User country.
#
# gocd_server_listen_host (str):
#   Set to "%%GOCD_SERVER_LISTEN_HOST%%" by default.
#   Host address which GoCD server should bind to.
#
. /etc/rc.subr
name=gocdserver
desc="GoCD Continuous Integration and Continuous Delivery system"
rcvar=gocd_server_enable
load_rc_config "${name}"
: ${gocd_server_enable:=NO}
: ${gocd_server_config_dir="%%GOCD_SERVER_CONFIG_DIR%%"}
: ${gocd_server_java_home="%%JAVA_HOME%%"}
: ${gocd_server_user="%%GOCD_SERVER_USER%%"}
: ${gocd_server_group="%%GOCD_SERVER_GROUP%%"}
: ${gocd_server_log_file="%%GOCD_SERVER_LOG_FILE%%"}
: ${gocd_server_memory="%%GOCD_SERVER_MEM%%"}
: ${gocd_server_max_memory="%%GOCD_SERVER_MAX_MEM%%"}
: ${gocd_server_max_metaspace="%%GOCD_SERVER_MAX_METASPACE%%"}
: ${gocd_server_port="%%GOCD_SERVER_PORT%%"}
: ${gocd_server_ssl_port="%%GOCD_SERVER_SSL_PORT%%"}
: ${gocd_server_user_lang="%%GOCD_SERVER_USER_LANG%%"}
: ${gocd_server_user_country="%%GOCD_SERVER_USER_COUNTRY%%"}
: ${gocd_server_listen_host="%%GOCD_SERVER_LISTEN_HOST%%"}
: ${gocd_server_args="-server -Dgocd.redirect.stdout.to.file=${gocd_server_log_file} -Djava.io.tmpdir=%%TMPDIR%% -Djava.security.egd=file:/dev/./urandom -Xms${gocd_server_memory} -Xmx${gocd_server_max_memory} -XX:MaxMetaspaceSize=${gocd_server_max_metaspace} -Duser.language=${gocd_server_user_lang} -Djruby.rack.request.size.threshold.bytes=30000000 -Duser.country=${gocd_server_user_country} -Dcruise.config.dir=${gocd_server_config_dir} -Dcruise.config.file=${gocd_server_config_dir}/cruise-config.xml -Dcruise.server.port=${gocd_server_port} -Dcruise.server.ssl.port=${gocd_server_ssl_port}"}
if [ ! -z "${gocd_server_listen_host}" ]; then
    gocd_server_args="${gocd_server_args} -Dcruise.listen.host=${gocd_server_listen_host}"
fi
pidfile=/var/run/gocd-server/gocd.pid
command=/usr/sbin/daemon
java_cmd="${gocd_server_java_home}/bin/java"
procname="${java_cmd}"
command_args="-p ${pidfile} ${java_cmd} ${gocd_server_java_opts} -jar %%JAVAJARDIR%%/go.jar ${gocd_server_args} >> ${gocd_server_log_file} 2>&1"
required_files="${java_cmd}"
start_precmd=gocd_server_prestart
start_cmd=gocd_server_start
gocd_server_prestart()
{
    if [ ! -f "${gocd_server_log_file}" ]; then
        install -o "${gocd_server_user}" -g "${gocd_server_group}" -m 640 /dev/null "${gocd_server_log_file}"
    fi
    if [ ! -d "/var/run/gocd-server" ]; then
        install -d -o "${gocd_server_user}" -g "${gocd_server_group}" -m 750 "/var/run/gocd-server"
    fi
}
gocd_server_start()
{
    check_startmsgs && echo "Starting ${name}."
    GO_SERVER_LOG_DIR=$(dirname "${gocd_server_log_file}") su -l "${gocd_server_user}" -c "exec ${command} ${command_args} ${rc_arg}"
}
run_rc_command "$1"

Makefile (stage, package, install)

Make final edits to Makefile to be able to stage, package, and install.

# $FreeBSD$

PORTNAME=gocd-server
DISTVERSION=19.2.0-8641
CATEGORIES=devel java
MASTER_SITES=https://download.gocd.org/binaries/${DISTVERSION}/generic/
DISTFILES=go-server-${DISTVERSION}.zip

MAINTAINER=me@example.com
COMMENT=An open-source Continuous Integration and Continuous Delivery system

LICENSE=APACHE20

DISTNAME=go-server-19.2.0
EXTRACT_ONLY=go-server-${DISTVERSION}.zip
EXTRACT_CMD=${UNZIP_NATIVE_CMD} ${DISTDIR}/${EXTRACT_ONLY}

NO_BUILD=yes
USE_JAVA=yes
USE_RUBY=yes
JAVA_VERSION=1.8+
NO_ARCH=
TMPDIR?=/tmp

GOCD_SERVER_HOME?=${PREFIX}/gocd-server
GOCD_SERVER_CONFIG_DIR?=${LOCALBASE}/etc/gocd-server
GOCD_SERVER_LOG_FILE?=/var/log/gocd-server.log
GOCD_SERVER_MEM?=512m
GOCD_SERVER_MAX_MEM?=1g
GOCD_SERVER_MAX_METASPACE?=400m
GOCD_SERVER_PORT?=8153
GOCD_SERVER_SSL_PORT?=8154
GOCD_SERVER_USER_LANG?=en
GOCD_SERVER_USER_COUNTRY?=US
GOCD_SERVER_USER?=gocd
GOCD_SERVER_GROUP?=gocd
GOCD_SERVER_LISTEN_HOST?=

.if ${GOCD_SERVER_USER} == "gocd"
USERS=gocd
.endif
.if ${GOCD_SERVER_GROUP} == "gocd"
GROUPS=gocd
.endif

SUB_LIST+=GOCD_SERVER_CONFIG_DIR=${GOCD_SERVER_CONFIG_DIR} \
        GOCD_SERVER_LOG_FILE=${GOCD_SERVER_LOG_FILE} \
        GOCD_SERVER_MEM=${GOCD_SERVER_MEM} \
        GOCD_SERVER_MAX_MEM=${GOCD_SERVER_MAX_MEM} \
        GOCD_SERVER_MAX_METASPACE=${GOCD_SERVER_MAX_METASPACE} \
        GOCD_SERVER_PORT=${GOCD_SERVER_PORT} \
        GOCD_SERVER_SSL_PORT=${GOCD_SERVER_SSL_PORT} \
        GOCD_SERVER_USER_LANG=${GOCD_SERVER_USER_LANG} \
        GOCD_SERVER_USER_COUNTRY=${GOCD_SERVER_USER_COUNTRY} \
        GOCD_SERVER_USER=${GOCD_SERVER_USER} \
        GOCD_SERVER_GROUP=${GOCD_SERVER_GROUP} \
        GOCD_SERVER_LISTEN_HOST=${GOCD_SERVER_LISTEN_HOST} \
        JAVA_HOME=${JAVA_HOME} \
        JAVAJARDIR=${JAVAJARDIR} \
        TMPDIR=${TMPDIR}
PLIST_SUB+=GOCD_SERVER_USER=${GOCD_SERVER_USER} \
        GOCD_SERVER_GROUP=${GOCD_SERVER_GROUP} \
        JAVAJARDIR=${JAVAJARDIR}

do-install:
        ${MKDIR} ${STAGEDIR}${JAVAJARDIR} ${STAGEDIR}${GOCD_SERVER_HOME}
        ${INSTALL_DATA} ${INSTALL_WRKSRC}/go.jar ${STAGEDIR}${JAVAJARDIR}

.include <bsd.port.mk>

Test everything is staging ok with,

$ make stage

Test a package can be created with,

$ make package

Finally, install the port,

$ make install

Source Code

The complete source code is available under MIT license from my gocd-server git repo.

TODO

  • Log files are not being created in /var/log/ but instead in /usr/local/gocd-server/logs which needs to be fixed
  • Submit this port to the FreeBSD project