--- /dev/null
+def _nfpm_package(
+ package_name,
+ config,
+ data,
+ packager,
+):
+ extension_map = {
+ "archlinux": "pkg.tar.zst",
+ }
+ distro_vars = {
+ "archlinux": {
+ "ENV_DIR": "/etc/conf.d",
+ },
+ "deb": {
+ "ENV_DIR": "/etc/default",
+ },
+ "rpm": {
+ "ENV_DIR": "/etc/default",
+ },
+ }
+ pkg_prefix = "{}_{}".format(package_name.replace("-", "_"), packager)
+ extra_args = []
+ for target in data:
+ extra_args.append('--include="$(location {})"'.format(target))
+
+ for var, val in distro_vars.get(packager, {}).items():
+ extra_args.append('--var="{}={}"'.format(var, val))
+
+ native.genrule(
+ name = "{}_nfpm_config".format(pkg_prefix),
+ tools = [
+ "//pkg/nfpmgen",
+ ],
+ srcs = [config] + data,
+ outs = [
+ "{}_nfpm_config.yaml".format(pkg_prefix),
+ ],
+ cmd_bash = """
+ $(location //pkg/nfpmgen) --in="$(location {})" --out="$@" {}
+ """.format(config, " ".join(extra_args)),
+ )
+
+ native.genrule(
+ name = "{}_nfpm_pkg".format(pkg_prefix),
+ tools = [
+ "@com_github_goreleaser_nfpm_v2//cmd/nfpm:nfpm",
+ ],
+ srcs = [
+ ":{}_nfpm_config".format(pkg_prefix),
+ ] + data,
+ outs = [
+ "{}.{}".format(package_name, extension_map.get(packager, packager)),
+ ],
+ cmd = " ".join([
+ "$(location @com_github_goreleaser_nfpm_v2//cmd/nfpm:nfpm)",
+ "package",
+ "-p {}".format(packager),
+ "-f $(location :{}_nfpm_config)".format(pkg_prefix),
+ "-t $@",
+ ]),
+ )
+
+def nfpm_package(
+ name, # type: str
+ config, # type: str
+ data, # type: list[str]
+):
+ #for packager in ["deb", "archlinux", "rpm"]:
+ for packager in ["archlinux"]:
+ _nfpm_package(name, config, data, packager)
\ No newline at end of file
--- /dev/null
+load("//bazel:pkg.bzl", "nfpm_package")
+
+nfpm_package(
+ name = "runtime-utils",
+ config = ":runtime-utils.yaml",
+ data = [
+ "//cmd/ephs_client",
+ "//cmd/metricbus_server",
+ "//cmd/metricbus_server:metric_collector_systemd_service",
+ "//cmd/mtls_exporter",
+ "//cmd/mtls_exporter:mtls_exporter_systemd_service",
+ "//cmd/mtls_supervisor",
+ "//cmd/sd_health_exporter",
+ "//cmd/sd_health_exporter:sd_health_exporter_systemd_service",
+ "//cmd/sd_publish",
+ "//cmd/sd_register",
+ "//cmd/sd_register:sd_register@_systemd_service",
+ "//cmd/sd_register:sd_register_systemd_service",
+ "//cmd/sd_watcher",
+ ],
+)
+
+nfpm_package(
+ name = "apcupsd-exporter",
+ config = ":apcupsd-exporter.yaml",
+ data = [
+ "//cmd/apcups_exporter",
+ "//cmd/apcups_exporter:apcupsd_exporter@_systemd_service",
+ "//cmd/apcups_exporter:apcupsd_exporter_systemd_service",
+ ],
+)
+
+nfpm_package(
+ name = "http-proxy",
+ config = ":http-proxy.yaml",
+ data = [
+ "//cmd/http_proxy",
+ "//cmd/http_proxy:http_proxy@_systemd_service",
+ ],
+)
+
+nfpm_package(
+ name = "ldap-health-exporter",
+ config = ":ldap-health-exporter.yaml",
+ data = [
+ "//cmd/ldap_health_exporter",
+ "//cmd/ldap_health_exporter:ldap_health_exporter@_systemd_service",
+ ],
+)
+
+nfpm_package(
+ name = "prometheus-helpers",
+ config = ":prometheus-helpers.yaml",
+ data = [
+ "//cmd/prometheus_http_discovery",
+ "//cmd/prometheus_http_discovery:prometheus_http_discovery_systemd_service",
+ ],
+)
--- /dev/null
+name: "${const:ExePrefix}apcupsd-exporter"
+arch: "amd64"
+platform: "linux"
+version: "${sub:${const:Version},+unset,}"
+section: "net"
+priority: "extra"
+maintainer: "Bazel Build Service <bazelbuild@${const:RootDomain}>"
+depends:
+ - ${const:ExePrefix}runtime-utils
+description: |
+ no description provided
+vendor: "${const:OrgName}"
+homepage: "https://go.fuhry.dev/runtime"
+license: "Proprietary"
+contents: []
\ No newline at end of file
--- /dev/null
+name: "${const:ExePrefix}http-proxy"
+arch: "amd64"
+platform: "linux"
+version: "${sub:${const:Version},+unset,}"
+section: "net"
+priority: "extra"
+maintainer: "Bazel Build Service <bazelbuild@${const:RootDomain}>"
+depends:
+ - ${const:ExePrefix}runtime-utils
+description: |
+ no description provided
+vendor: "${const:OrgName}"
+homepage: "https://go.fuhry.dev/runtime"
+license: "Proprietary"
+contents: []
\ No newline at end of file
--- /dev/null
+name: "${const:ExePrefix}ldap-health-exporter"
+arch: "amd64"
+platform: "linux"
+version: "${sub:${const:Version},+unset,}"
+section: "net"
+priority: "extra"
+maintainer: "Bazel Build Service <bazelbuild@${const:RootDomain}>"
+depends:
+ - ${const:ExePrefix}runtime-utils
+description: |
+ no description provided
+vendor: "${const:OrgName}"
+homepage: "https://go.fuhry.dev/runtime"
+license: "Proprietary"
+contents: []
\ No newline at end of file
--- /dev/null
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "nfpmgen_lib",
+ srcs = [
+ "config_generator.go",
+ "main.go",
+ ],
+ importpath = "go.fuhry.dev/runtime/pkg/nfpmgen",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//constants",
+ "//utils/subst",
+ "@com_github_goreleaser_nfpm_v2//:nfpm",
+ "@com_github_goreleaser_nfpm_v2//files",
+ "@in_gopkg_yaml_v3//:yaml_v3",
+ ],
+)
+
+go_binary(
+ name = "nfpmgen",
+ embed = [":nfpmgen_lib"],
+ visibility = ["//visibility:public"],
+)
--- /dev/null
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "path"
+ "regexp"
+ "strings"
+
+ "github.com/goreleaser/nfpm/v2"
+ "github.com/goreleaser/nfpm/v2/files"
+ "gopkg.in/yaml.v3"
+
+ "go.fuhry.dev/runtime/constants"
+ "go.fuhry.dev/runtime/utils/subst"
+)
+
+var verbose bool
+
+func debug(fmtstr string, args ...any) {
+ if !verbose {
+ return
+ }
+
+ fmt.Fprintf(os.Stderr, fmtstr+"\n", args...)
+}
+
+type NfpmConfig struct {
+ *nfpm.Config
+
+ Vars subst.KV `yaml:"vars" json:"vars" jsonschema:"title=map of context vars to inject into subst"`
+
+ files map[string]struct{} `yaml:"-" json:"-"`
+}
+
+type executable string
+
+var systemdUnitFilename = regexp.MustCompile(`\.(service|target|socket|preset)$`)
+
+func (e executable) String() string {
+ return string(e)
+}
+
+func (e executable) buildPath(bazelBinDir string) string {
+ return path.Join(
+ bazelBinDir, "cmd", e.String(), e.String()+"_", e.String())
+}
+
+func (e executable) pkgPath(vars subst.KV) string {
+ exeDir, ok := vars["EXEDIR"]
+ if !ok {
+ exeDir = "/usr/bin"
+ }
+
+ exeName := constants.ExePrefix + strings.ReplaceAll(string(e), "_", "-")
+
+ return path.Join(exeDir, exeName)
+}
+
+func LoadNfpmConfig(yamlPath string, fileAllowlist []string, varOverrides subst.KV) (*NfpmConfig, error) {
+ contents, err := os.ReadFile(yamlPath)
+ if err != nil {
+ return nil, err
+ }
+
+ // first load the "vars" key to substitute throughout the rest of the file
+ out := &NfpmConfig{
+ Config: new(nfpm.Config),
+ Vars: make(subst.KV),
+
+ files: make(map[string]struct{}),
+ }
+
+ for _, f := range fileAllowlist {
+ out.files[f] = struct{}{}
+ }
+
+ if err := yaml.Unmarshal(contents, out); err != nil {
+ return nil, err
+ }
+
+ for k, v := range varOverrides {
+ debug("var %q overridden to %q via command line", k, v)
+ out.Vars[k] = v
+ }
+
+ substContents, err := subst.Eval(out.Vars, string(contents))
+ if err != nil {
+ return nil, err
+ }
+
+ if err := yaml.Unmarshal([]byte(substContents), out.Config); err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+func (c *NfpmConfig) DiscoverBinaries(bazelBinDir string) error {
+ wd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+
+ cmdDir, err := os.ReadDir(path.Join(wd, bazelBinDir, "cmd"))
+ if err != nil {
+ return err
+ }
+
+ for _, entry := range cmdDir {
+ if entry.Name() == "." || entry.Name() == ".." || !entry.IsDir() {
+ continue
+ }
+
+ exe := executable(entry.Name())
+
+ exePath := exe.buildPath(bazelBinDir)
+
+ if _, ok := c.files[exePath]; !ok {
+ debug("exe not allowlisted in includes: %s", exePath)
+ continue
+ }
+
+ debug("evaluating exe with buildPath %s", exePath)
+
+ if derefPath, err := os.Readlink(exePath); err == nil {
+ if path.IsAbs(derefPath) {
+ exePath = derefPath
+ } else {
+ exePath = path.Join(
+ path.Dir(exePath),
+ derefPath,
+ )
+ }
+
+ debug(" -> dereferenced symlink to: %s", exePath)
+ }
+
+ st, err := os.Stat(exePath)
+ if err != nil {
+ debug(" -> executable not found")
+ continue
+ }
+
+ if st.Mode()&os.FileMode(0o500) != 0o500 || !st.Mode().IsRegular() {
+ debug(" -> not a regular file or not executable")
+ continue
+ }
+
+ c.Config.Contents = append(c.Config.Contents, &files.Content{
+ Source: exePath,
+ Destination: exe.pkgPath(c.Vars),
+ FileInfo: &files.ContentFileInfo{
+ Owner: "root",
+ Group: "root",
+ Mode: os.FileMode(0o755),
+ },
+ })
+ }
+
+ return nil
+}
+
+func recursiveDirectoryScan(baseDir string, match func(e os.DirEntry) bool) ([]string, error) {
+ var out []string
+ entries, err := os.ReadDir(baseDir)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, e := range entries {
+ abs := path.Join(baseDir, e.Name())
+ if match(e) {
+ out = append(out, abs)
+ }
+
+ if e.IsDir() {
+ children, err := recursiveDirectoryScan(abs, match)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, children...)
+ }
+ }
+
+ return out, nil
+}
+
+func (c *NfpmConfig) DiscoverSystemdUnits(bazelBinDir string) error {
+ unitFiles, err := recursiveDirectoryScan(
+ bazelBinDir,
+ func(e os.DirEntry) bool {
+ if e.IsDir() {
+ return false
+ }
+
+ return systemdUnitFilename.MatchString(e.Name())
+ },
+ )
+ if err != nil {
+ return err
+ }
+
+ for _, f := range unitFiles {
+ if _, ok := c.files[f]; !ok {
+ debug("unit file not allowlisted in includes: %s", f)
+ continue
+ }
+
+ if derefPath, err := os.Readlink(f); err == nil {
+ if path.IsAbs(derefPath) {
+ f = derefPath
+ } else {
+ f = path.Join(
+ path.Dir(f),
+ derefPath,
+ )
+ }
+ }
+
+ destName := constants.ExePrefix + path.Base(f)
+
+ c.Config.Contents = append(c.Config.Contents, &files.Content{
+ Source: f,
+ Destination: "/usr/lib/systemd/" + destName,
+ FileInfo: &files.ContentFileInfo{
+ Owner: "root",
+ Group: "root",
+ Mode: os.FileMode(0o644),
+ },
+ })
+ }
+
+ return nil
+}
+
+func (c *NfpmConfig) Generate() (string, error) {
+ out, err := yaml.Marshal(c.Config)
+ if err != nil {
+ return "", err
+ }
+
+ return string(out), nil
+}
+
+func init() {
+ flag.BoolVar(&verbose, "verbose", false, "enable debug logging")
+}
--- /dev/null
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ "go.fuhry.dev/runtime/utils/subst"
+)
+
+func main() {
+ yamlFile := flag.String("in", "", "path to yaml template to consume")
+ outFile := flag.String("out", "", "output file to write; if omitted, uses stdout")
+
+ var includes []string
+ parseInclude := func(flagVal string) error {
+ _, err := os.Lstat(flagVal)
+ if err != nil {
+ return fmt.Errorf("error processing include %q: %v", flagVal, err)
+ }
+ includes = append(includes, flagVal)
+
+ return nil
+ }
+ flag.Func("include", "file to consider for package", parseInclude)
+
+ kv := make(subst.KV)
+ addKv := func(flagVal string) error {
+ k, v, ok := strings.Cut(flagVal, "=")
+ if !ok {
+ return errors.New("-var= usage: var=value, e.g. -var=FOO=BAR")
+ }
+ kv[k] = v
+ return nil
+ }
+ flag.Func("var", "extra variable to set", addKv)
+
+ flag.Parse()
+
+ c, err := LoadNfpmConfig(*yamlFile, includes, kv)
+ if err != nil {
+ panic(err)
+ }
+
+ wd, err := os.Getwd()
+ if err != nil {
+ panic(err)
+ }
+
+ bazelBinDir := ""
+ if outFile != nil && *outFile != "" {
+ // pushd to the bazel-bin dir `bazel-bin/[arch]/bin` directory
+ parts := strings.Split(*outFile, "/")
+ if len(parts) >= 3 {
+ bazelBinDir = strings.Join(parts[:3], "/")
+ }
+ }
+
+ debug("loaded nfpm config template: %s\ncwd: %s\nout: %s\n",
+ *yamlFile, wd, *outFile)
+
+ if err := c.DiscoverBinaries(bazelBinDir); err != nil {
+ panic(err)
+ }
+
+ if err := c.DiscoverSystemdUnits(bazelBinDir); err != nil {
+ panic(err)
+ }
+
+ out, err := c.Generate()
+ if err != nil {
+ panic(err)
+ }
+
+ err = os.Chdir(wd)
+ if err != nil {
+ panic(err)
+ }
+
+ if outFile == nil || *outFile == "" {
+ fmt.Println(out)
+ } else {
+ if err := os.WriteFile(*outFile, []byte(out), os.FileMode(0o644)); err != nil {
+ panic(err)
+ }
+ }
+}
--- /dev/null
+name: "${const:ExePrefix}prometheus-helpers"
+arch: "amd64"
+platform: "linux"
+version: "${sub:${const:Version},+unset,}"
+section: "net"
+priority: "extra"
+maintainer: "Bazel Build Service <bazelbuild@${const:RootDomain}>"
+depends:
+ - ${const:ExePrefix}runtime-utils
+description: |
+ no description provided
+vendor: "${const:OrgName}"
+homepage: "https://go.fuhry.dev/runtime"
+license: "Proprietary"
+contents: []
\ No newline at end of file
--- /dev/null
+name: "${const:ExePrefix}runtime-utils"
+arch: "amd64"
+platform: "linux"
+version: "${sub:${const:Version},+unset,}"
+section: "net"
+priority: "extra"
+replaces:
+ - ${const:OrgSlug}-service-discovery
+depends:
+ - glibc
+conflicts:
+ - ${const:OrgSlug}-service-discovery
+maintainer: "Bazel Build Service <bazelbuild@${const:RootDomain}>"
+description: Tools needed by virtually all ${const:OrgName} systems for service discovery and metrics
+vendor: "${const:OrgName}"
+homepage: "https://go.fuhry.dev/runtime"
+license: "Proprietary"
+contents: []
\ No newline at end of file
--- /dev/null
+From 0edcc9ab033623894d59b9f301a45dd95b322ddd Mon Sep 17 00:00:00 2001
+From: Dan Fuhry <dan@fuhry.com>
+Date: Tue, 20 Jan 2026 21:39:00 -0500
+Subject: [PATCH] Never return nil from getBuildInfo
+
+---
+ version.go | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/version.go b/version.go
+index 5734d52..2a153e7 100644
+--- a/version.go
++++ b/version.go
+@@ -36,7 +36,7 @@ type Info struct {
+ func getBuildInfo() *debug.BuildInfo {
+ bi, ok := debug.ReadBuildInfo()
+ if !ok {
+- return nil
++ return &debug.BuildInfo{}
+ }
+ return bi
+ }
+--
+2.52.0
+