Building optional packages
Optional packages are FreeBSD ports that wrap a runtime (for example tftp-hpa or haproxy)
with the FreeSense WebUI pages, configuration hooks, privileges, and service registration that
make it a managed integration. This page explains where a package lives, how to build it, and how
an accepted change reaches appliances. For what the package does once it is installed, see
package integration and the
WebUI menu system.
Contributions target the main branches, which feed the rolling Development 1.1 line. A
package reaches Stable 1.0.x only through a new, explicitly sealed patch release.
Where package sources live
Section titled “Where package sources live”An optional package touches up to three repositories:
| Repository | What it contributes |
|---|---|
freesense-packages |
The package port (<category>/FreeSense-pkg-<name>/), shared package framework (Mk/bsd.freesense-package.mk), architecture policy, and CI audits. Also holds plain-named overrides of stock ports that a package needs, such as net/haproxy. |
freesense |
The build list tools/conf/pfPorts/poudriere_packages, port options in tools/conf/pfPorts/make.conf, and the product catalog src/etc/freesense-package-catalog.json that the Package Manager reads. |
freesense-system-ports |
System-only ports, including sysutils/FreeSense-platform-abi, which every optional package depends on. Optional package ports never go here. |
System-image ports must not be added to freesense-packages; tools/boundary_audit.py rejects
them. This boundary is what lets a System-only change ship without rebuilding Optional Packages.
Anatomy of a package port
Section titled “Anatomy of a package port”Package ports use the FreeSense-pkg-<name> naming convention. The part after the prefix is the
package’s short name, used by the catalog and by the WebUI. The TFTP server is a compact example:
ftp/FreeSense-pkg-tftpd/├── Makefile├── pkg-descr├── pkg-plist└── files/ ├── pkg-install.in ├── pkg-deinstall.in ├── etc/inc/priv/tftpd.priv.inc # WebUI privilege definitions └── usr/local/ ├── pkg/tftpd.xml # package manifest (menu, service, form, hooks) ├── pkg/tftpd.inc # PHP hook implementations ├── share/FreeSense-pkg-tftpd/info.xml # registration metadata └── www/ ├── shortcuts/pkg_tftpd.inc # title-bar shortcuts and service control └── tftp_files.php # custom WebUI pagefiles/ mirrors the target filesystem, so a file’s path under files/ is where it is installed.
Makefile
Section titled “Makefile”Wrapper ports have no distfile and no build step. They install the files above into the staging directory and substitute the package version into the manifests:
PORTNAME= FreeSense-pkg-tftpdPORTVERSION= 0.1.3PORTREVISION= 13CATEGORIES= ftpMASTER_SITES= # emptyDISTFILES= # emptyEXTRACT_ONLY= # empty
MAINTAINER= dev@freesense.orgCOMMENT= FreeSense package for tftp serverLICENSE= APACHE20
RUN_DEPENDS= ${LOCALBASE}/libexec/in.tftpd:ftp/tftp-hpa
NO_BUILD= yesNO_MTREE= yes
SUB_FILES= pkg-install pkg-deinstallSUB_LIST= PORTNAME=${PORTNAME}
do-extract: ${MKDIR} ${WRKSRC}
do-install: ${MKDIR} ${STAGEDIR}${PREFIX}/pkg ${INSTALL_DATA} -m 0644 ${FILESDIR}${PREFIX}/pkg/tftpd.xml ${STAGEDIR}${PREFIX}/pkg # ... remaining files ... @${REINPLACE_CMD} -i '' -e "s|%%PKGVERSION%%|${PKGVERSION}|" \ ${STAGEDIR}${PREFIX}/pkg/tftpd.xml \ ${STAGEDIR}${DATADIR}/info.xml
.include <bsd.port.mk>Rules that CI enforces:
MAINTAINERmust be an@freesense.orgaddress.NO_CHECKSUM=yesis not allowed.- Privilege files are listed in
pkg-plistwith their absolute path, for example/etc/inc/priv/tftpd.priv.inc, followed by@dir /etc/inc/privand@dir /etc/inc. - Do not include
Mk/bsd.freesense-package.mkyourself; the build injects it (see below). - Do not reference Netgate infrastructure URLs or commit credential material.
Variant ports can reuse a template directory with MASTERDIR, as
net-mgmt/FreeSense-pkg-zabbix-agent7 does. Template directories are listed in
policy/catalog-templates.txt and are not published on their own.
Install and deinstall scripts
Section titled “Install and deinstall scripts”pkg(8) runs files/pkg-install.in and files/pkg-deinstall.in. Both hand control to the
FreeSense package framework rather than doing work themselves:
#!/bin/sh
if [ "${2}" != "POST-INSTALL" ]; then exit 0fi
${PKG_ROOTDIR}/usr/local/bin/php -f ${PKG_ROOTDIR}/etc/rc.packages %%PORTNAME%% ${2}/etc/rc.packages registers or unregisters the package’s menus, services, and configuration. The
package integration page describes that lifecycle.
Platform compatibility binding
Section titled “Platform compatibility binding”Every optional package must declare which release train it was built for, so a 1.0 appliance
cannot install a package built for 1.1. Mk/bsd.freesense-package.mk adds:
FREESENSE_PACKAGE_TRAIN?= 0.0RUN_DEPENDS+= FreeSense-platform-abi=${FREESENSE_PACKAGE_TRAIN}.0:sysutils/FreeSense-platform-abiThe ports-overlay helper (freesense/tools/ci/freesense-ports-overlay.sh) inserts the include into
every FreeSense-pkg-* Makefile when it assembles the build tree, so a new package cannot forget
it. Official builds set the train from the release policy (for example 1.1). An ad-hoc build that
does not set FREESENSE_PACKAGE_TRAIN falls back to 0.0 and deliberately produces a package that
no real appliance accepts.
Building locally
Section titled “Building locally”Official package builds run in FreeBSD 16 Poudriere jails on the project build runner. A local build
uses the same scripts. You need a FreeBSD 16 build host with Poudriere and checkouts of
freesense, freesense-packages, and freesense-system-ports.
-
Place the overlays where the helper expects them, or export their locations:
Terminal window export REPO_KIND=packagesexport OVERLAY_DIR=/root/freesense-packagesexport FREESENSE_SYSTEM_OVERLAY_DIR=/root/freesense-system-ports -
In the
freesensecheckout, create the build configuration and set the train you are targeting:Terminal window cp build.conf.sample build.confecho 'export FREESENSE_PACKAGE_TRAIN=1.1' >> build.conf -
Create the Poudriere jail and ports tree, then assemble the overlay. The overlay merges
freesense-system-ports, thenfreesense-packages, file by file over the pinned upstream FreeBSD ports tree:Terminal window ./build.sh --setup-poudriere./build.sh --update-poudriere-ports -
Build one package with Poudriere’s test mode, which also checks the plist and staging:
Terminal window poudriere testport -j FreeSense_main_amd64 -p FreeSense_main -o ftp/FreeSense-pkg-tftpdTo build a set the way CI does, list origins in
tools/conf/pfPorts/poudriere_bulkand run./build.sh --update-pkg-repo. SettingFREESENSE_PORT_TESTS=1adds Poudriere’s-ttest flag.
Jails are named FreeSense_main_<arch> (amd64 or aarch64) and the ports tree FreeSense_main.
Package origins use %%PRODUCT_NAME%% in the build list, which expands to FreeSense.
To try the result on a lab appliance, install the package file with pkg add on a disposable
Development system and confirm that its menu entries, service, and catalog details appear. Never
side-load a test build onto a production appliance.
Adding a new package
Section titled “Adding a new package”-
Create the port in
freesense-packages/<category>/FreeSense-pkg-<name>/following the anatomy above. If the runtime needs a changed third-party port, add a plain-named override in the same repository. -
Add it to the build list in
freesense/tools/conf/pfPorts/poudriere_packagesas<category>/%%PRODUCT_NAME%%-pkg-<name>. Third-party ports are pulled in as dependencies and are never listed as roots. Port options belong intools/conf/pfPorts/make.conf. -
Add a catalog entry keyed by the short name in
freesense/src/etc/freesense-package-catalog.json:"tftpd": {"display_name": "TFTP Server","support": "supported","last_tested_release": "development","category": "Services","resource_profile": "lightweight","capabilities": ["file-service"],"configure_path": "/tftp_files.php","services": ["tftpd"]}resource_profileislightweight,moderate, orintensive.configure_pathandstatus_pathmust be absolute WebUI paths that exist in the base system or in a package’sfiles/usr/local/www. The catalog explains how operators see these fields. -
Handle architectures. If the package cannot build on ARM64 yet, add an exclusion to
freesense-packages/architecture-policy.jsonwith theorigin, a meaningfulreason, an HTTPSissuelink, and a near-termreview_date.tools/validate-architecture-policy.pyrejects vague or stale exclusions. -
Document it. Add the package to
src/data/package-coverage.jsoninfreesense-docsand cover it in the matching package guide; the documentation validator fails on an uncovered catalog entry. If the package has a dedicated guide, add a context-help mapping infreesense/src/etc/inc/freesense-docs.inc(see WebUI context help). -
Open matching pull requests. Use the same branch name in
freesenseandfreesense-packages: package CI checks out thefreesensebranch with the same name, falling back tomain, so the catalog and the port are validated together.
Validation
Section titled “Validation”Run these from the freesense-packages checkout before opening a pull request:
python3 tools/boundary_audit.pypython3 tools/catalog_audit.py --source ../freesensepython3 tools/validate-architecture-policy.pyphp -l <each new .php and .inc file>sh -n files/pkg-install.in files/pkg-deinstall.incatalog_audit.py checks that every published FreeSense-pkg-* origin exists, is in the build list,
has a complete supported catalog entry with real WebUI destinations, and is not a retired wrapper
returning by accident. On RELENG_* branches it runs with --release, which also forbids any
remaining exclusion.
In the freesense checkout, validate the catalog and the package framework:
python3 -m json.tool src/etc/freesense-package-catalog.json > /dev/nullphp tests/PackageCatalogSmokeTest.phpphp tests/PackageRegistrationSmokeTest.phpThe same checks run in GitHub Actions on every pull request (quality.yml and
architecture-policy.yml in freesense-packages, quality.yml in freesense).
Versioning
Section titled “Versioning”Raise PORTREVISION whenever you change a package’s files without changing the upstream runtime,
and reset it when PORTVERSION changes. Appliances upgrade only to a higher package version, so an
unchanged version means an installed system will not receive your fix. The version is substituted
into <version> in the manifest and info.xml at staging time; do not hard-code it there.
From merge to appliance
Section titled “From merge to appliance”Optional Packages are planned, built, and signed independently of the System repository:
- The Development multi-architecture cycle (
freesense-os-base/.github/workflows/development-multiarch.yml) computes an Optional Packages fingerprint for each architecture. It combines thefreesense-packagescommit, the package build options inmake.conf, the architecture policy, the package train, the pinned FreeBSD platform, the signing public key, and the build recipe. - If the fingerprint is new, the packages stage builds the set in parallel shards against the matching System repository, checks that every dependency resolves within the System plus Optional set, and signs the repository.
- The repository is published to an immutable, fingerprint-addressed path under
pkg.freesense.org/v1/artifacts/packages/<train>/. The signed channel manifest then points thedevelchannel at the verified System and Optional Packages pair. - Appliances on the Development channel see the new version in System → Package Manager.
What triggers a rebuild:
| Change | Optional Packages rebuild? |
|---|---|
A commit to freesense-packages |
Yes |
tools/conf/pfPorts/make.conf in freesense |
Yes |
architecture-policy.json |
Yes |
| The 14-day FreeBSD platform pin advances | Yes |
Only System code, including freesense-system-ports |
No; the existing repository is reused |
Only poudriere_packages or the catalog JSON in freesense |
No; these change the System fingerprint only |
Because the build list and catalog live in freesense, adding a package to poudriere_packages
does not by itself start an Optional Packages build. Merge the freesense change first and the
freesense-packages port second, so the port commit produces the new fingerprint that builds it.
Stable is different: a 1.0.x release lock in freesense-os-base/config/releases/ pins an exact
packages_sha, and Stable is published manually from a sealed lock. A package change reaches Stable
only through a new tagged patch release. See the release process.