glm_freebsd
A Gleam package that allows you to easily create FreeBSD packages for your Gleam applications, along with a service script to manage the application (e.g. start|stop).
This is based on https://github.com/patmaddox/ex_freebsd
Further documentation can be found at https://hexdocs.pm/glm_freebsd.
Quickstart
Install gleam and erlang
We need a minimum of gleam 1.14 and erlang28:
# use `latest`
echo 'FreeBSD-ports: { url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest" }' > /usr/local/etc/pkg/repos/FreeBSD.conf
pkg update
pkg install -y erlang-runtime28 gleam rebar3
./make.sh
./test_pkg.shUsage
Create a gleam app
$ gleam new APPNAMEUpdate the APPNAME/gleam.toml
Add the relevant FreeBSD package info to the gleam.toml
$ cd APPNAME
APPNAME $ vim gleam.tomlAdd these elements:
[freebsd]
pkg_user = true
pkg_description = "This is a longer description .........................................................."
pkg_maintainer = "someone@example.com"
pkg_config_dir = "/some/path/outside/of/the/application/space"
pkg_env_file = "example.env"
[freebsd.deps]
list = "pstree,tree"
[freebsd.deps.pstree]
version = "2.36"
origin = "sysutils/pstree"
[freebsd.deps.tree]
version = "2.2.1"
origin = "sysutils/tree"Create an erlang-shipment
gleam export erlang-shipment
Build the glm_freebsd tool (if not already built)
$ make.shCreate a FreeBSD package
# clear the temporary build directory, can be anything
rm -rf ./tmp
# run glm_freebsd to generate the package, the package file will be in this directory upon completion
./glm_freebsd templates --input [PATH_TO_YOUR_APPNAME_TOML_FILE] --output ./tmp
# try installing the package in FreeBSD
sudo pkg install [PATH_TO_YOUR_GENERATED_PACKAGE_FILE].pkgSee the test_pkg.sh for further details.
Environment Files and 12 Factor Apps
Applications being bundled into a FreeBSD Service will almost certainly require some sort of configuration. Per the concept of 12 factor apps, this configuration should be external to the app and be provided to the application by the runtime.
By default, at runtime, the environment file will be read from the applications configuration directory:
/usr/local/etc/[PACKAGE_NAME].d/[PACKAGE_NAME].envHowever, the location that the service management looks for the configuration file can be configured via these fields in gleam.toml:
[freebsd]
pkg_config_dir=...
pkg_env_file=...The configuration file can be placed in the correct location by your IAC (infrastructure-as-code, e.g. ansible,chef,puppet,pulumi,terraform,etc.).
When the service manager launches the service, it reads this environment file and includes these environment key/value pairs in the process environment the service instance is started with.
Toml Elements
We pull the package name and version from the toml here:
name = "example"
version = "1.0.0"The rest of the data comes from the [freebsd] described below:
| TOML FIELD | DESCRIPTION |
|---|---|
| [freebsd] | this is the root toml element that this packaging code uses |
| pkg_user [true | false] : if true then the package creates a user when installed. defaults to true. |
| pkg_username | the username to create if pkg_user is true. defaults to the value in 'name' above. |
| pkg_description | a longer description used by the packaging system. |
| pkg_maintainer | email address of the package maintainer. e.g. someone@example.com |
| pkg_scripts | comma separated k=v pairs, where k=script name, and value is a file in the output directory<br/> typically a file generated from a template.defaults to: "post-install=post-install.sh,pre-deinstall=pre-deinstall.sh" |
| TOML FIELD | DESCRIPTION |
|---|---|
| [freebsd.deps] |
the root of the list of OS package dependencies that YOUR package needs in order to function. list is the only child element. |
| list | a list of the packages (DEP_NAME) that will follow. |
| TOML FIELD | DESCRIPTION |
|---|---|
| [freebsd.deps.DEP_NAME] |
the root of a FreeBSD OS package dependency declaration. version and origin are the only child elements. |
| version | the dependency version |
| origin | the dependency origin |
There are other fields that you might want to use. See the config.gleam file for fulll details.
Example run
Start a test run
# ./test_pkg.shOutput...
t@workstation:/opt/repos/glm_freebsd (toddg/custom-templates)# ./test_pkg.sh
update path with erlang28 binary, as that's needed for gleam
and/or the gleam json library
-------------------------------------------------------------------------------
build the 'example' app's erlang-shipment.
-------------------------------------------------------------------------------
NOTE: you will want to do this for YOUR app, prior to generating
the freebsd package for YOUR app.
/opt/repos/glm_freebsd/priv/example /opt/repos/glm_freebsd
Compiling gleam_stdlib
Compiling envoy
Compiling gleam_erlang
Compiling gleeunit
Compiling logging
Compiling example
Compiled in 0.67s
Exported example
Your Erlang shipment has been generated to /opt/repos/glm_freebsd/priv/example/build/erlang-shipment.
It can be copied to a compatible server with Erlang installed and run with
one of the following scripts:
- entrypoint.ps1 (PowerShell script)
- entrypoint.sh (POSIX Shell script)
/opt/repos/glm_freebsd
-------------------------------------------------------------------------------
building the FreeBSD application service package...
-------------------------------------------------------------------------------
Compiled in 0.02s
Running glm_freebsd.main
logging level set to: info
INFO application starting...
INFO copied custom templates over base templates, custom_templates_dir: ./priv/example//priv/package/templates/, target_dir: ./tmp/templates
INFO wrote ./tmp/rc_conf
INFO wrote ./tmp/post-install.sh
INFO wrote ./tmp/pre-deinstall.sh
INFO wrote ./tmp/rc
INFO wrote ./tmp/freebsd/+MANIFEST
INFO updated entrypoint.sh permissions: ./tmp/freebsd/stage/usr/local/libexec/example/entrypoint.sh
INFO wrote ./tmp/freebsd/stage/usr/local/etc/rc.d/example
INFO wrote ./tmp/freebsd/stage/usr/local/etc/rc.conf.d/example
INFO wrote ./tmp/freebsd/pkg-plist
INFO
INFO Build completed successfully
-------------------------------------------------------------------------------
here is the generated manifest
-------------------------------------------------------------------------------
{
"name": "example",
"version": "1.0.0",
"origin": "devel/example",
"comment": "An example app that will be used to create a FreeBSD package (with service scripts).",
"www": "git@github.com:someuser/example_app.git",
"maintainer": "someone@example.com",
"prefix": "/usr/local",
"desc": "This is a longer description ..........................................................",
"scripts": {
"pre-deinstall": "CONFIG_DIR=\"/tmp\"\n\n\nPKG_USER=\"example\"\n\nif [ -n \"${PKG_ROOTDIR}\" ] && [ \"${PKG_ROOTDIR}\" != \"/\" ]; then\n PW=\"/usr/sbin/pw -R ${PKG_ROOTDIR}\"\nelse\n PW=/usr/sbin/pw\nfi\nif ${PW} usershow ${PKG_USER} >/dev/null 2>&1; then\n echo \"==> pkg user '${PKG_USER}' should be manually removed.\"\n echo \" ${PW} userdel ${PKG_USER}\"\nfi\n\n\nif [ -d \"${CONFIG_DIR}\" ]\nthen\n echo \"==> config directory '${CONFIG_DIR}' should be manually removed.\"\n echo \" rm -rf ${CONFIG_DIR}\"\nfi\n\nif [ -d \"/var/run/example\" ]\nthen\n echo \"==> run directory '/var/run/example' should be manually removed.\"\n echo \" rm -rf /var/run/example\"\nfi\n\n# --------------------------------------------------------------------------------------\n# CUSTOM STUFF HERE\n# --------------------------------------------------------------------------------------\necho \"CUSTOM DE-INSTALL SCRIPT FINISHING\"\n",
"post-install": "PKG_NAME=\"example\"\n# --------------------------------------------------------------------------------------\n# CUSTOM STUFF HERE\n# --------------------------------------------------------------------------------------\nCONFIG_DIR=\"/tmp/CUSTOM\"\nCONFIG_FILE=\"${CONFIG_DIR}/example.env.CUSTOM\"\n\n\n\nPKG_USER=\"example\"\n\nif [ -n \"${PKG_ROOTDIR}\" ] && [ \"${PKG_ROOTDIR}\" != \"/\" ]; then\n PW=\"/usr/sbin/pw -R ${PKG_ROOTDIR}\"\nelse\n PW=/usr/sbin/pw\nfi\n\necho \"===> Creating user.\"\nif ! ${PW} groupshow ${PKG_USER} >/dev/null 2>&1; then\n echo \"Group: '${PKG_USER}'.\"\n ${PW} groupadd ${PKG_USER} -g 2001\nelse\n echo \"Using existing group: '${PKG_USER}'.\"\nfi\n\nif ! ${PW} usershow ${PKG_USER} >/dev/null 2>&1; then\n echo \"User: '${PKG_USER}'.\"\n ${PW} useradd ${PKG_USER} -u 2001 -g ${PKG_USER} -c \"${PKG_NAME} user\" -d /nonexistent -s /usr/sbin/nologin\nelse\n echo \"Using existing user: '${PKG_USER}'.\"\nfi\n\n\n# --------------------------------------------------------------------------------------\n# MORE CUSTOM STUFF HERE\n# --------------------------------------------------------------------------------------\nif [ ! -f $CONFIG_FILE ]\nthen\n echo \"===> Creating CUSTOM CONFIG dir ${CONFIG_DIR}\"\n mkdir -p ${CONFIG_DIR}\n echo \"===> Creating CUSTOM CONFIG in ${CONFIG_FILE}\"\n echo \"# example CUSTOM CONFIG FILE\" > $CONFIG_FILE\n echo 'FOO=\"bar\"' >> $CONFIG_FILE\n echo 'BING=\"bing\"' >> $CONFIG_FILE\n chmod 0444 $CONFIG_FILE\nfi\n"
},
"deps": {
"tree": {
"version": "2.2.1",
"origin": "sysutils/tree"
},
"pstree": {
"version": "2.36",
"origin": "sysutils/pstree"
}
},
"users": [
"example"
]
}
-------------------------------------------------------------------------------
building the FreeBSD application service package...
-------------------------------------------------------------------------------
create the environment file
install the (local) package
Updating FreeBSD-ports repository catalogue...
FreeBSD-ports repository is up to date.
Updating FreeBSD-ports-kmods repository catalogue...
FreeBSD-ports-kmods repository is up to date.
All repositories are up to date.
Checking integrity... done (0 conflicting)
The following 1 package(s) will be affected (of 0 checked):
New packages to be INSTALLED:
example: 1.0.0 [unknown-repository]
Number of packages to be installed: 1
[workstation.jail] [1/1] Installing example-1.0.0...
[workstation.jail] Extracting example-1.0.0: 100%
===> Creating user.
Using existing group: 'example'.
Using existing user: 'example'.
clear out the example.log so we can see what this invocation logs...
-------------------------------------------------------------------------------
you should not see the example app in yet
-------------------------------------------------------------------------------
root 67295 0.0 0.0 14164 2696 1 S+J 16:54 0:00.00 grep -i example
-------------------------------------------------------------------------------
start the example service
-------------------------------------------------------------------------------
Service example started as pid 67340.
-------------------------------------------------------------------------------
the example service should be in the process list now
-------------------------------------------------------------------------------
example 67340 18.7 0.9 1413588 79048 - SJ 16:54 0:00.44 /usr/local/lib/erlang28/erts-16.2/bin/beam.smp -- -root /usr/local/lib/erlang28 -bindir /usr/local/lib/erlang28/erts-16.2/bin -progname erl -- -home /nonexistent -- -pa /usr/local/libexec/example/envoy/ebin /usr/local/libexec/example/example/ebi
root 67338 0.2 0.0 14184 2548 - SsJ 16:54 0:00.00 daemon: example[67340] (daemon)
example 67348 0.0 0.0 14076 2444 - SsJ 16:54 0:00.00 erl_child_setup 234702
root 67350 0.0 0.0 14164 2692 1 S+J 16:54 0:00.00 grep -i example
-------------------------------------------------------------------------------
the example service should show up as started
-------------------------------------------------------------------------------
example is running as pid 67340.
-------------------------------------------------------------------------------
the example service should show in the logs now
Hello from example!
environment: dict.from_list([#("BINDIR", "/usr/local/lib/erlang28/erts-16.2/bin"), #("BLOCKSIZE", "K"), #("DEBUGGING", ""), #("DEBUG_DO", ":"), #("DEBUG_SKIP", ""), #("EMU", "beam"), #("ERL_CRASH_DUMP", "/var/run/example/example_erl_crash.dump"), #("EXAMPLE_CONF_DIR", "/tmp"), #("FOO", "bar"), #("HOME", "/nonexistent"), #("LANG", "C.UTF-8"), #("MAIL", "/var/mail/example"), #("MM_CHARSET", "UTF-8"), #("PATH", "/usr/local/lib/erlang28/erts-16.2/bin:/usr/local/lib/erlang28/bin:/sbin:/bin:/usr/sbin:/usr/bin"), #("PROGNAME", "erl"), #("PWD", "/"), #("RC_PID", "67296"), #("RELEASE_TMP", "/var/run/example"), #("ROOTDIR", "/usr/local/lib/erlang28"), #("SHELL", "/usr/sbin/nologin"), #("USER", "example"), #("_TTY", "/dev/pts/1")])
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
the example service should shut down
-------------------------------------------------------------------------------
Stopping example.
Waiting for PIDS: 67340.
-------------------------------------------------------------------------------
the example service should no longer be in the process list
-------------------------------------------------------------------------------
root 67442 0.0 0.0 3788 2284 1 R+J 16:54 0:00.00 grep -i example
-------------------------------------------------------------------------------
uninstall the package
-------------------------------------------------------------------------------
Checking integrity... done (0 conflicting)
Deinstallation has been requested for the following 1 packages (of 0 packages in the universe):
Installed packages to be REMOVED:
example: 1.0.0
Number of packages to be removed: 1
[workstation.jail] [1/1] Deinstalling example-1.0.0...
==> pkg user 'example' should be manually removed.
/usr/sbin/pw userdel example
==> config directory '/tmp' should be manually removed.
rm -rf /tmp
==> run directory '/var/run/example' should be manually removed.
rm -rf /var/run/example
CUSTOM DE-INSTALL SCRIPT FINISHING
[workstation.jail] [1/1] Deleting files for example-1.0.0: 100%