Cracking the Rosetta Stone
A quest to automate some manual workflows, turned into a deep dive into container manifests, cross-arch libs, compilation, and of course SELinux. This will most likely be the first of the two or slightly more entries.
The Background
There’s always been one package or two I use that are not conveniently accessible. The usual form tends to be a GitHub repo hosting the source and binary releases, in addition to a rarely-if-ever-changed shell script to automatically acquire the latest version and install it locally. While this works fine most of the time, there are still several issues standing out.
- Access to GitHub can be shaky where I am.1
- Install scripts are not always very clear in terms of what is being installed and where. Most are kind enough to stick to
/usr/localand state that in their output, but not always. - Updating becomes an awkward process, where I have to grab the (possibly) same script again and redo the process from scratch.
Since most of my Linux devices are on Fedora or one of its derivations, the logical solution is to build RPMs in one place and install them directly with dnf. A quick search gives me a Red Hat blog article to do just this.
The Problem
RPM is… almost too intuitive as a packaging format. You provide a .spec text file that states the source file(s) required, the runtime and build dependencies if any, the commands to run to build the software, and the list of files to be packed in the final product. It can also optionally generate source RPMs to keep everything required for the process in one place. And as a last resort you can simply extract the file with any unarchiver.
For various reasons I’m not going to reveal exactly what package I’m trying to build. What I can confirm is that
- The program is written in Go.
- It’s completely self-contained, so that it only depends on Go and some other components at build time.
- It hosts the full source code in the repo, and releases binaries with signatures, alongside tagged source archives.
The Work
I started by looking for issues on the project repo about RPM builds, and actually found one. It’s just… not the most up to date.

Thankfully the package hasn’t really changed much over time, and the biggest change seems to be just the Version: tag. Fedora’s packging guidelines suggests using a macro to autofill from a repo URL, so I did.
rpmdev-setuptree (provided by rpmdevtools) created the required directory structure in my home folder, I placed the spec file under ~/rpmbuild/SPECS/, confirmed that rpmlint didn’t report anything breaking…
And was greeted with a No such file or directory on build.
Quickly found the issue thankfully - Source files aren’t directly downloaded for URL sources without an explicit parameter.
With that out of the way, rpmbuild -bs and rpmbuild -bb were mostly smooth riding2.
The Expansion
With local builds successful, I started to check what I needed to do to make builds for all my devices. Thankfully, the list is quite short.
- Fedora 44, x86_64 (the device used so far)
- Oracle Linux 10.1, arm64 (running my server)
- RHEL 10.1, arm64 (virtual machine running on my Mac mini, ideally where I run the builds locally)
Building an RPM against el10 for x86_64 and arm64 should cover them all, right? There’s even a parameter in rpmbuild to produce builds for alternative architectures.
rpmbuild happily produced a build with --target=arm64. I copied it to my RHEL instance, ran sudo dnf install on it, and…
- package [redacted]-1.fc44.arm64 from @commandline does not have a compatible architecture
Some searching revealed that the RPM arch naming for arm64 is aarch64.
Another rebuild and copy later, it installed, but…
-bash: /usr/bin/[redacted]: cannot execute binary file: Exec format error
And file says it’s still a x86-64 executable. Great.
The crossing
Go is… very cross-compile friendly. With GOOS and GOARCH configured you can pretty much compile for any supported platform from one place. GOOS remains unchanged for me here, and RPM spec conveniently offers the RPM_ARCH variable.
$ env GOARCH=%{RPM_ARCH} CGO_ENABLED=0 go build -v -trimpath -ldflags "$LDFLAGS" -o %{name} ./main
go: unsupported GOOS/GOARCH pair linux/aarch64
Oh right for golang it’s arm64. Thankfully there are %ifarch macros to do some quick mapping.
With that over it produced a valid .fc44.aarch64.rpm that I can install and run on my RHEL with no issue. Primary issue solved.
The Next Steps
Next up I need to find the way to automate this process. My Oracle Linux server runs a Forgejo instance, which paves the way for CI actions. I just need to commit the spec and set up a workflow around it… right?
Find out in the next post. :)
Footnotes
- It just so happens that one piece of software I need to overcome this fits in this scenario as well. In fact, it was the very package I went through the whole process with. ↩︎
- One brief issue when testing to make sure forge macros works as intended was that due to different naming with tags and tarballs (think
v1.2.3.tar.gzandfoo-1.2.3.tar.gz), the build process would attempt to pull a link that eventually 404s, resulting in weirdunexpected end of fileerrors. Fully switching to forge macros somehow fixed this. ↩︎