A recap of some things I learned recently while packaging a Go binary for deployment on CentOS hosts.
RPM spec file
Until now I had never released any of my software as an RPM. I just dropped minimal instructions into README.md
and called it a day:
$ sudo yum install <list of dependencies>
$ wget <link_to_my_project_binary_release>
$ mv binary /usr/bin/
$ chmod +x binary
Even worse is software where I also had systemd unit files:
$ cp file.service /etc/systemd/system/
$ sudo systemctl daemon-reload
$ sudo systemctl enable my.service
$ sudo systmectl start my.service
Unfortunately, regardless of how well-intentioned my users were (including public users for my open source projects, and coworkers for my company/private projects), it’s not the best UX to require a user to execute a handful of shell commands.
I pieced together an RPM spec file for my project goat, and installation (on RPM and systemd-enabled distros) became a breeze.
The first part is simple, some naming and descriptions:
%define pkgname goat
Name: %{pkgname}
Version: %{_version}
Release: 1%{?dist}
Summary: Attach and mount EBS volumes
License: BSD 3-clause
URL: https://github.com/sevagh/goat
As we see here, pkgname
is defined as goat
and used throughout the rest of the spec file, but we never %define version
- this is because I pass the version from the outside (so I could have one source of version truth): @rpmbuild [...] --define "_version $(VERSION)"
.
Then we declare the source files:
Source0: %{pkgname}
Source1: %{pkgname}.service
This says that the files my RPM will include in it are goat
(the Go binary, built with go build
), and goat.service
, the systemd unit file.
Here we define the requirements:
Requires: systemd mdadm
Some unimportant steps (in goat’s case at least):
%description
Automatically attach and mount EBS volumes to a running EC2 instance.
#%prep
#%setup
#%build
Now for the meat of the install phase:
%install
%{__mkdir} -p %{buildroot}/%{_bindir}
%{__mkdir} -p %{buildroot}/%{_unitdir}
%{__install} -m0775 %{SOURCE0} %{buildroot}/%{_bindir}/%{pkgname}
%{__install} -m0777 %{SOURCE1} %{buildroot}/%{_unitdir}/%{pkgname}.service
%files
%{_bindir}/%{pkgname}
%{_unitdir}/%{pkgname}.service
These are all spec file macros that I learned from the excellent documentation. Read the lines and familiarize yourself with them. It’s basically saying to install the binary and systemd file into their respective locations (/usr/bin/
and /usr/lib/systemd/
).
Finally, some pre/post shell script sections to run some systemd commands.
Post install; systemctl daemon-reload
if it’s the first time goat is being installed:
%post
if [ $1 -eq 1 ]; then
/bin/systemctl daemon-reload >/dev/null 2>&1 || :
fi
#/bin/systemctl enable goat.service >/dev/null 2>&1 || :
You can choose to enable your service (and even start it) here but I chose to allow my users to have more control over what goat does, and have my RPM just install it.
The pre-uninstall phase; disable and stop goat:
%preun
if [ $1 -eq 0 ] ; then
# Package removal, not upgrade
/bin/systemctl disable goat.service >/dev/null 2>&1 || :
/bin/systemctl stop goat.service >/dev/null 2>&1 || :
fi
The post-uninstall phase; another daemon-reload:
%postun
/bin/systemctl daemon-reload >/dev/null 2>&1 || :
rpmlint and rpmbuild
This is the Make rule I wrote for goat to generate RPMs:
@rpmlint specfile.spec
@rpmbuild -ba specfile.spec --define "_sourcedir $$PWD" --define "_version $(VERSION)"
rpmlint
does what the name suggests: lints your specfile. Useful.
The rpmbuild command has two defines: one mentioned above, where the _version
is sourced from the same place in the Makefile ($(VERSION)
) so I don’t have to run around and change a string in 5 different places when making a release.
The other define is _sourcedir $$PWD
, which localizes the build phase to $PWD
. This way I can run the rpmbuild command from the goat repo.
Go version from Makefile
Tangentially, how I pass the same $(VERSION)
to the Go code itself is with:
@go build -ldflags "-X main.VERSION=$(VERSION)" .
In the Go code this is how it’s used:
# in main.go
var VERSION string
func main