tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_systemd

rules_systemd

Bazel-idiomatic systemd unit provisioning: typed unit rules + providers + an aspect that emits the /etc/systemd/system layer with enable-symlinks

Latest0.0.1
Versions1
CategoryBazel rules
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_systemd/
Sourcegithub.com/tomato-bazel/rules_systemd
MODULE.bazelstarlark
bazel_dep(name = "rules_systemd", version = "0.0.1")

View source & releases on GitHub ↗

Bazel-idiomatic systemd unit provisioning: typed rules that render unit files from attrs, providers carrying unit metadata, an aspect that collects units across a deps graph, and a systemd_layer rule that packs them into an /etc/systemd/system tar — computing the *.target.wants/ enable-symlinks itself, since a build action never runs systemctl enable.

The tar drops straight into oci_image(tars = [...]), so you can build a systemd-PID-1 image entirely from Bazel without hand-writing .service text into a pkg_tar.

Status: v0.0.1

Public rules: systemd_service, systemd_oneshot, systemd_target, systemd_socket, systemd_timer, systemd_tmpfiles, systemd_dropin, systemd_layer. Rendering is pure Starlark (ctx.actions.write); the layer tar is built by a small deterministic tool (sorted entries, mtime=0, uid/gid=0) so it is reproducible and golden-testable.

Install

.bazelrc:

common --registry=https://registry.fastverk.com/
common --registry=https://bcr.bazel.build/

MODULE.bazel:

bazel_dep(name = "rules_systemd", version = "0.0.1")

Usage

load("@rules_systemd//systemd:defs.bzl",
     "systemd_service", "systemd_oneshot", "systemd_tmpfiles", "systemd_layer")

systemd_oneshot(
    name = "app-init",
    description = "First-boot init",
    exec_start = "/usr/local/bin/app-init",
    before = ["app.service"],
)

systemd_tmpfiles(
    name = "app-tmpfiles",
    unit_name = "app.conf",
    lines = ["d /var/lib/app 0750 app app -"],
)

systemd_service(
    name = "app",
    description = "App daemon",
    requires = ["app-init.service"],
    after = ["network.target", "app-init.service"],
    exec_start = "/usr/local/bin/app --config /etc/app/config.yaml",
    user = "app",
    restart = "on-failure",
    wanted_by = ["multi-user.target"],   # systemd_layer writes the enable-symlink
)

systemd_layer(
    name = "units",
    deps = [":app", ":app-init", ":app-tmpfiles"],
)

Then, in an image:

load("@rules_oci//oci:defs.bzl", "oci_image")

oci_image(
    name = "app_image",
    base = "//images/systemd-base",   # a base whose ENTRYPOINT is /sbin/init
    entrypoint = ["/sbin/init"],
    tars = [":units", "//toolkits/app:layer"],
)

bazel build //…:units emits the tar; its deterministic path/symlink listing is in output group listing (used for golden tests — see examples/smoke). Run it under podman with --systemd=always.

Layout

  • systemd/defs.bzl — the public rules + the _systemd_units aspect.
  • systemd/providers.bzlSystemdUnitInfo, SystemdTransitiveInfo, SystemdLayerInfo.
  • systemd/private/render.bzl — INI rendering helpers.
  • systemd/private/mklayer.py — the deterministic tar builder.
  • examples/smoke — a worked example with golden tests.

Usage#

Real usage, taken from the module’s examples/.

examples/smoke/BUILD.bazel

load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load(
    "//systemd:defs.bzl",
    "systemd_layer",
    "systemd_oneshot",
    "systemd_service",
    "systemd_target",
    "systemd_tmpfiles",
)

package(default_visibility = ["//visibility:private"])

# A one-shot first-boot init that the daemon orders After=.
systemd_oneshot(
    name = "demo-init",
    before = ["demo.service"],
    description = "Demo first-boot init",
    exec_start = "/usr/local/bin/demo-init",
)

# tmpfiles establishing ownership of the state dir before the daemon runs.
systemd_tmpfiles(
    name = "demo-tmpfiles",
    lines = ["d /var/lib/demo 0750 demo demo -"],
    unit_name = "demo.conf",
)

# The long-running daemon, enabled into multi-user.target.
systemd_service(
    name = "demo",
    after = [
        "network.target",
        "demo-init.service",
    ],
    description = "Demo daemon",
    environment = [
        "DEMO_ENV=local",
        "DEMO_PORT=8080",
    ],
    exec_start = "/usr/local/bin/demo --config /etc/demo/config.yaml",
    requires = ["demo-init.service"],
    restart = "on-failure",
    restart_sec = "2s",
    user = "demo",
    wanted_by = ["multi-user.target"],
)

# A synchronization target with no [Install] (not enabled itself).
systemd_target(
    name = "demo-stack",
    description = "Demo stack target",
    wants = ["demo.service"],
)

# Collect everything into one /etc/systemd/system tar layer. The
# demo.service enable-symlink under multi-user.target.wants/ is computed
# automatically from its wanted_by.
systemd_layer(
    name = "units",
    deps = [
        ":demo",
        ":demo-init",
        ":demo-stack",
        ":demo-tmpfiles",
    ],
)

filegroup(
    name = "units_listing",
    srcs = [":units"],
    output_group = "listing",
)

# Golden tests: the rendered daemon unit + the layer's path/symlink listing.
diff_test(
    name = "demo_service_golden",
    file1 = "demo.service.golden",
    file2 = ":demo",
)

diff_test(
    name = "units_listing_golden",
    file1 = "units.listing.golden",
    file2 = ":units_listing",
)

Conformance#

No gate findings. 8 contested atoms. See how gating works or the full report.

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
bazel_skylib 1.8.2 1.9.0 ×2
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
protobuf 33.4 34.0.bcr.1 ×2
rules_jvm_external 6.7 6.8 ×4
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1

Dependencies#

rules_systemd in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_shell0.6.1devstardoc0.7.2dev

Versions#

1 published version, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.0.1 latest LDLKb6d5pHOF8vxt… tag archive ↗

Changelog#

All notable changes to rules_systemd. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.

0.0.1

  • Initial scaffold via rels scaffold.
  • Typed unit rules: systemd_service, systemd_oneshot, systemd_target, systemd_socket, systemd_timer, systemd_tmpfiles, systemd_dropin — pure-Starlark INI rendering.
  • Providers: SystemdUnitInfo, SystemdTransitiveInfo, SystemdLayerInfo; a _systemd_units aspect over deps.
  • systemd_layer: packs transitive units into a reproducible /etc/systemd/system (+ /etc/tmpfiles.d, drop-ins) tar for oci_image(tars = [...]), auto-writing *.target.wants/ enable symlinks from each unit’s wanted_by.
  • examples/smoke with golden tests over a rendered unit + the layer listing.

← All modules