Skip to content

Build libraries and binaries with Bazel

The public Bazel rules name the artifact being built. A loom_library produces reusable Loom bytecode; loom_kernel_binary and loom_command_binary close selected roots into deployment products. The same source graph can therefore stop at a linkable library or continue into the product required by one application.

Depend on HRX

HRX publishes Loom as the loom/ subproject of its root Bzlmod module. An independent authoring repository using a local HRX checkout declares that dependency in its root module:

MODULE.bazel
module(name = "hrx_loom_kernels")

bazel_dep(name = "hrx", version = "0.0.0")

local_path_override(
    module_name = "hrx",
    path = "../hrx-system",
)

The dependency's default apparent repository name is @hrx, so BUILD files load the public authoring API from @hrx//loom/build_tools/bazel:defs.bzl and select built-in profiles from @hrx//loom/target/.... The local override changes where Bazel obtains the HRX module without changing those labels. It also preserves the compiler co-development loop: editing Loom in the referenced checkout makes the next kernel build rebuild the affected source tools before consuming them.

The HRX module registers source-built toolchains for each Loom authoring role. A root module may register a higher-priority implementation of the same public toolchain types when it consumes released executables instead. The library and binary rules remain unchanged because they resolve tools by role rather than by an executable label.

Inside the HRX source tree, @hrx//loom/... and //loom/... reach the same packages. The checked examples use the external spelling so their BUILD declarations can move unchanged into a standalone kernel repository.

The declaration below is exercised by the documentation test suite:

BUILD.bazel
load(
    "@hrx//loom/build_tools/bazel:defs.bzl",
    "loom_command_binary",
    "loom_kernel_binary",
    "loom_library",
)
load("@rules_shell//shell:sh_test.bzl", "sh_test")

package(
    default_visibility = ["//visibility:private"],
    licenses = ["notice"],  # Apache 2.0
)

loom_library(
    name = "motif",
    srcs = ["motif.loom"],
)

loom_library(
    name = "kernel",
    srcs = ["kernel.loom"],
    deps = [":motif"],
)

loom_library(
    name = "model",
    srcs = ["model.loom"],
    deps = [":kernel"],
)

loom_kernel_binary(
    name = "elementwise_kernel",
    out = "elementwise_kernel.hsaco",
    target = "@hrx//loom/target/amdgpu:gfx11-generic",
    deps = [":kernel"],
)

loom_command_binary(
    name = "elementwise_command",
    roots = ["@elementwise_transform"],
    target = "@hrx//loom/target/amdgpu:gfx11-generic",
    deps = [":model"],
)

The three source libraries form one ordinary dependency graph:

model  ->  kernel  ->  motif

model.loom declares the kernel it launches, kernel.loom declares the template family it applies, and motif.loom contributes eligible template implementations. Bazel labels state which library artifacts are available; Loom declarations and template contracts state how symbols compose.

Libraries stay relocatable

loom_library merges its direct srcs into <name>.loombc. That bytecode is a single, independently reloadable Loom module, not target-native code and not an archive of named source files. Unresolved declarations may remain for a later link boundary.

Dependencies deliberately remain separate. The :model bytecode does not flatten :kernel or :motif; its LoomLibraryInfo carries those modules as an independent dependency closure. A final product can then index the complete library universe and materialize only the definitions reachable from its roots.

The rule also performs strict direct-dependency analysis. A source reference may be satisfied by its own module or by a library named directly in deps. Finding the symbol only through a transitive dependency is an error. In the example, model names kernel and kernel names motif; model does not need to repeat motif because it does not reference one of its symbols directly. This keeps large library graphs gardenable without flattening them.

Build the relocatable model library alone:

bazel build //loom/docs/examples/elementwise-transform:model

The default output is model.loombc. Request its schema-versioned dependency analysis when a build or dependency-gardening tool needs it:

bazel build //loom/docs/examples/elementwise-transform:model \
  --output_groups=+dependency_reports

Binary roots close one product

All three binary rules share the same composition contract:

Attribute Meaning
srcs Direct .loom or .loombc sources assembled as an implicit relocatable library.
deps Direct loom_library inputs whose exports may become roots and whose dependencies may satisfy the reachable closure.
roots Optional explicit @symbol roots. When omitted, every exported symbol from the direct srcs and deps is a root.
configs Compile-time configuration values keyed by config.decl symbol name.

At least one of srcs and deps must be present. A source-only binary is the compact spelling for a standalone program. A dependency-only binary is the normal shape for a reusable library graph. A mixed binary adds small application-owned sources to established libraries without creating a one-purpose library target in the BUILD file.

Transitive exports never become roots merely because their library is in the closure. They are candidates for satisfying reachable declarations and template applications. This distinction is what lets :model bring a large kernel catalog without compiling every exported kernel in that catalog.

Explicit roots replace the default export set. The checked elementwise_command target above demonstrates this by selecting only @elementwise_transform from :model.

Root selection happens during linking. Unreachable functions, templates, kernels, command programs, configuration, checks, and benchmarks are absent from the closed .loombc passed to artifact emission.

Kernel and command binaries also require a typed target label. That profile participates in the selective link itself: target facts are projected before template selection, so a target-constrained provider can win before unreachable alternatives are discarded. The same profile then drives device artifact emission. VM binaries have no device target attribute and keep this link boundary targetless.

Kernel binaries are loader-ready executables

loom_kernel_binary specializes the selected kernel closure for one immutable target profile and emits one artifact named by out. The compiler chooses the canonical kernel format for that target. The example therefore names elementwise_kernel.hsaco for its gfx11-generic AMDGPU profile, while a rule using //loom/target/spirv:vulkan1.3+bda can name an .spv output without changing the base rule.

out controls only the Bazel artifact path; the compiler never infers the format from its suffix. When omitted, the artifact has the extensionless rule name. This is useful for higher-level macros that fan out target products but do not own target-specific filename conventions.

bazel build //loom/docs/examples/elementwise-transform:elementwise_kernel

Reusable source remains targetless. Selecting the profile at the binary boundary allows the same library to produce a generic GFX11 executable or an exact architecture-specialized executable without copying its .loom files.

Command binaries package schedules with their kernels

loom_command_binary performs one selective link, lowers every selected command-program root to a portable artifact, and compiles the reachable kernel entries for its target profile:

bazel build //loom/docs/examples/elementwise-transform:elementwise_command

Its default outputs are:

Output Consumer
<name>.commands.json Maps command symbols to portable artifacts and lists their logical executable-entry requirements.
<name>.commands/*.loomcmd One target-neutral command artifact per selected command root.
<name>.kernels.hsaco The AMDGPU executable satisfying the manifest's reachable kernel entries.

The command schedule and device executable remain separate deployment artifacts because they have different portability and caching boundaries. The manifest joins them through logical entry symbols; it does not force an embedding to reverse-engineer either binary format.

Inspect the closed input and compiler evidence

Binary rules keep their primary runtime products in Bazel's default output group. Their linked input and compile reports are opt-in evidence products:

bazel build //loom/docs/examples/elementwise-transform:elementwise_command \
  --output_groups=+linked_modules,+compile_reports

linked_modules contains the closed .loombc used by every emitter for that binary. compile_reports contains the command and kernel reports for a command binary and the corresponding single report for kernel and VM binaries. This makes it possible to inspect reachability or compare compiler evidence without changing the product graph.

The CLI and in-memory APIs use the same boundaries

The Bazel rules orchestrate the public tools; they do not add a second linkage model. loom_library corresponds to a strict relocatable merge. A kernel or command binary first performs a root-selected loom-link --mode=link with the selected --target, then invokes the appropriate loom-compile backend on that one closed module. A VM binary performs the same selective link without a device profile. The command product invokes the command and AMDGPU emitters over the same linked input.

An embedding can construct the same explicit library universe with the loomc API, select roots, and retain or emit the resulting module entirely in memory. Bazel labels and CLI paths are frontend identities for artifacts and diagnostics; neither becomes a Loom symbol namespace or causes the compiler to search a filesystem.

Link and package modules gives the equivalent command-line composition workflow. Compile artifacts documents the kernel, command, and VM emitters directly.