rules_runfile_codegen_go 0.1.2Latest published 1.1mo ago
MODULE.bazel
bazel_dep(name = "rules_runfile_codegen_go", version = "0.1.2")
Dependencies
local_path_override(
    module_name = "rules_runfile_codegen_core",
    path = "../core",
)
README

Bazel Runfile Code Generators (rules_runfile_codegen)

In Bazel, runfiles are the files (data dependencies, configuration files, or other executables) that a binary needs at runtime. Accessing these files programmatically requires resolving their paths relative to the workspace, which can be complex and error-prone because their physical locations change depending on the execution environment (e.g., running locally, inside a sandbox during bazel test, or in a production deployment).

Traditionally, developers must use Bazel's language-specific runfiles libraries to perform string-based lookups at runtime. This project, rules_runfile_codegen, simplifies this by generating type-safe code accessors for your runfiles.

By defining your runfile dependencies in your BUILD.bazel files, you can generate libraries that expose these runfiles as strongly-typed symbols. This eliminates the need to hardcode runfile paths as strings in your application code, prevents typos, and ensures that runfile resolution errors are caught at startup rather than deep in runtime execution.

For complete, runnable projects demonstrating these quickstarts, see the examples/ directory.


Key Features

  • Type-Safety: Runfiles are exposed as generated constants/properties. No more stringly-typed paths.
  • Explicit Resolution: Both Go and Kotlin use an explicit, non-eager (lazy) resolution model to avoid startup side-effects and improve testability. The generated libraries expose unresolved specifications (e.g. FileSpec) that must be explicitly resolved at runtime.
  • Subprocess Environment Propagation: Executable runfiles are wrapped in rich objects that facilitate launching them as subprocesses while automatically propagating the Bazel runfiles environment. This ensures that child processes can also resolve their own runfiles.[^1]
  • Zero Runtime Overhead: After successful startup-time resolution, accessing the runfile path is a simple member access with zero overhead.

Go Quickstart

Bzlmod Setup

To use these rules in your Go project, add the following to your MODULE.bazel file (see rules_runfile_codegen_go on BCR):

# MODULE.bazel
bazel_dep(name = "rules_runfile_codegen_go", version = "0.1.2")

1. Define the Runfile Library

In your BUILD.bazel, load the Go rules and define a go_runfile_library.

load("@rules_go//go:def.bzl", "go_binary")
load("@rules_runfile_codegen_go//:defs.bzl", "go_runfile", "go_runfile_library")

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

# A helper binary to demonstrate executable runfiles
go_binary(
    name = "helper",
    srcs = ["helper.go"],
)

# Generate the runfile accessor library
go_runfile_library(
    name = "resources",
    importpath = "github.com/example/project/examples/go/resources",
    entries = [
        go_runfile(
            name = "DataFile",
            target = "data/dummy.txt",
            doc = "A dummy data file.",
        ),
        go_runfile(
            name = "HelperTool",
            target = ":helper",
            doc = "A helper tool executable.",
        ),
        go_runfile(
            name = "ExampleSet",
            targets = ["data/dummy.txt", "data/info.txt"],
            base = "common_dir",
            doc = "A set of example data files.",
        ),
    ],
)

# Use the library in a binary
go_binary(
    name = "main",
    srcs = ["main.go"],
    deps = [
        ":resources",
    ],
)
# #

2. Use the Generated Symbols in Go

Import the generated package and access the symbols. Regular files are generated as Runfile types, and executables are generated as ExecutableRunfile types.

Here is the actual example:

package main

import (
    "fmt"
    "os"
    "sort"
    "strings"

    "github.com/example/project/examples/go/resources"
)

func main() {
    // 1. Access the resolved runfile path safely.
    dataFile, err := resources.DataFile.Resolve()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error resolving runfile: %v\n", err)
        os.Exit(1)
    }

    content, err := os.ReadFile(dataFile.Path())
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error reading runfile: %v\n", err)
        os.Exit(1)
    }
    fmt.Printf("Data: %s\n", string(content))

    // 2. Run an executable runfile with env propagation (fail-fast).
    helper := resources.HelperTool.MustResolve()
    cmd := helper.Cmd()
    output, err := cmd.Output()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error running helper: %v\n", err)
        os.Exit(1)
    }
    fmt.Printf("Helper output: %s", string(output))

    // 3. Access a fileset of runfiles (FileSet).
    exampleSet, err := resources.ExampleSet.Resolve()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error resolving fileset: %v\n", err)
        os.Exit(1)
    }
    paths := exampleSet.RelPaths()
    sort.Strings(paths)
    fmt.Printf("FileSet paths: %v\n", paths)
    f1, err := exampleSet.File("dummy.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error resolving dummy.txt: %v\n", err)
        os.Exit(1)
    }
    c1, _ := os.ReadFile(f1.Path())
    fmt.Printf("FileSet dummy content: %s\n", strings.TrimSpace(string(c1)))
}

Actual Generated Go Code

Click to view the actual generated Go code for the example above
// Code generated by rules_runfile_codegen. DO NOT EDIT.

// Package resources provides type-safe access to Bazel runfiles.
package resources

import (
    "github.com/meta-programming/rules_runfiles_codegen/go/runfile"
)

var (
    // DataFile is A dummy data file.
    // Source: @@//:data/dummy.txt
    DataFile = runfile.NewSpec("_main/data/dummy.txt")

    // ExampleSet is A set of example data files.
    // Source: @@//:data/dummy.txt, @@//:data/info.txt
    ExampleSet = runfile.NewFileSetSpec(map[string]string{"dummy.txt": "_main/data/dummy.txt", "info.txt": "_main/data/info.txt"})

    // HelperTool is A helper tool executable.
    // Source: @@//:helper
    HelperTool = runfile.NewExecutableSpec("_main/helper_/helper")

)

Kotlin Quickstart

Bzlmod Setup

To use these rules in your Kotlin project, add the following to your MODULE.bazel file (see rules_runfile_codegen_kotlin on BCR):

# MODULE.bazel
bazel_dep(name = "rules_runfile_codegen_kotlin", version = "0.1.2")

1. Define the Runfile Library

In your BUILD.bazel, load the Kotlin rules and define a kt_jvm_runfile_library. Note that dashed target names (like test-resources) are automatically sanitized to PascalCase Kotlin object names (TestResources).

load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_binary")
load("@rules_runfile_codegen_kotlin//:defs.bzl", "kt_runfile", "kt_jvm_runfile_library")

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

# A helper binary to demonstrate executable runfiles
kt_jvm_binary(
    name = "helper",
    srcs = ["Helper.kt"],
    main_class = "com.example.project.examples.HelperKt",
)

# Generate the runfile accessor library
kt_jvm_runfile_library(
    name = "resources",
    package = "com.example.project.examples.resources",
    entries = [
        kt_runfile(
            name = "configJson",
            target = "data/dummy.txt",
            doc = "A dummy data file.",
        ),
        kt_runfile(
            name = "helperTool",
            target = ":helper",
            doc = "A helper tool executable.",
        ),
        kt_runfile(
            name = "exampleSet",
            targets = ["data/dummy.txt", "data/info.txt"],
            base = "common_dir",
            doc = "A set of example data files.",
        ),
    ],
)

# Use the library in a binary
kt_jvm_binary(
    name = "main",
    srcs = ["Main.kt"],
    main_class = "com.example.project.examples.MainKt",
    deps = [
        ":resources",
    ],
)
# #

2. Use the Generated Symbols in Kotlin

Import the generated object and access the properties. Regular files are generated as Runfile types (exposing path and jvmPath), and executables are generated as ExecutableRunfile types (adding processBuilder()).

Here is the actual example:

package com.example.project.examples

import com.example.project.examples.resources.Resources
import kotlin.io.path.readText

fun main() {
    // 1. Access the resolved runfile path.
    // Resolve the spec and read its content directly using Path.readText().
    val content = Resources.configJson.path.readText().trim()
    println("Data: $content")

    // 2. Run an executable runfile with env propagation.
    val process = Resources.helperTool.processBuilder().start()
    val output = process.inputStream.reader().use { it.readText() }.trim()
    val exitCode = process.waitFor()
    if (exitCode != 0) {
        error("Helper tool failed with exit code $exitCode")
    }
    println("Helper output: $output")

    // 3. Access a fileset of runfiles (FileSet).
    val exampleSet = Resources.exampleSet.resolve()
    println("FileSet paths: ${exampleSet.relPaths.sorted()}")
    
    // Access a path inside the fileset using the shortcut:
    val dummyContent = Resources.exampleSet["dummy.txt"].path.readText().trim()
    println("FileSet dummy content: $dummyContent")
}

Coroutines & Blocking I/O

The resolve() method and subsequent file/process operations (like readText() or process.waitFor()) are blocking I/O operations. If you are using this library inside a coroutine-based application (such as Ktor, Spring WebFlux, or Android), you should offload these calls to Dispatchers.IO to avoid blocking event loops or the main thread:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.io.path.readText

suspend fun loadConfig(): String = withContext(Dispatchers.IO) {
    Resources.configJson.resolve().path.readText()
}

Actual Generated Kotlin Code

Click to view the actual generated Kotlin code for the example above
// Code generated by rules_runfile_codegen. DO NOT EDIT.
// This file provides type-safe access to Bazel runfiles.
package com.example.project.examples.resources

import com.github.metaprogramming.runfiles.FileSpec
import com.github.metaprogramming.runfiles.ExecutableSpec
import com.github.metaprogramming.runfiles.DirectorySpec
import com.github.metaprogramming.runfiles.FileSetSpec
import com.github.metaprogramming.runfiles.RlocationPath

object Resources {
    /**
     * A dummy data file.
     * Source: @@//:data/dummy.txt
     */
    val configJson = FileSpec(RlocationPath("_main/data/dummy.txt"))

    /**
     * A set of example data files.
     * Source: @@//:data/dummy.txt, @@//:data/info.txt
     */
    val exampleSet = FileSetSpec(mapOf("dummy.txt" to "_main/data/dummy.txt", "info.txt" to "_main/data/info.txt"))

    /**
     * A helper tool executable.
     * Source: @@//:helper
     */
    val helperTool = ExecutableSpec(RlocationPath("_main/helper"))
}

Design Philosophy

Explicit (Non-Eager) Resolution

Both Go and Kotlin use an explicit, non-eager (lazy) resolution model.

  • Why? Eager resolution (resolving everything during module initialization or init blocks) can cause dangerous side-effects, makes unit testing and mocking difficult, and violates best practices.
  • How it works: The generated code defines unresolved specifications (FileSpec, ExecutableSpec, DirectorySpec, FileSetSpec). Because runfile resolution can fail at runtime (e.g., if a data dependency is missing from the runfiles manifest), resolution is explicit and fallible. The developer must call .Resolve() (Go, which returns an error) or .resolve() (Kotlin, which throws a RunfileResolutionException) at runtime to obtain the resolved runfile reference. Go also provides .MustResolve() which panics on failure for cases where missing runfiles should be immediately fatal. This avoids initialization-time side-effects, handles failures gracefully, and allows injecting mock resolvers for testing.

Rich Object Wrapper

Rather than just returning raw string paths, the generators wrap runfiles in rich objects (Runfile and ExecutableRunfile).

  • This allows us to attach helper methods (like jvmPath in Kotlin to get a native java.nio.file.Path object).
  • Crucially, it allows us to distinguish executables and provide safe execution wrappers (Cmd in Go, processBuilder in Kotlin) that automatically handle the propagation of Bazel runfiles environment variables. This solves the common "nested runfiles" problem where a tool run from a test cannot find its own dependencies.

[^1]: Bazel runfiles discovery relies on environment variables like RUNFILES_DIR (path to the runfiles directory) and RUNFILES_MANIFEST_FILE (path to the manifest file mapping runfile paths to their physical locations, used when symlinks are not available, e.g., on Windows). If these variables are not propagated to child processes, those processes will fail to resolve their own runfiles. For details, see the Bazel Runfiles Guide and the Bazel Runfiles Library specification.

About

A set of Bazel modules (one per language, typically) for generating code to safely access runtime data dependencies.

@meta-programming/rules_runfiles_codegen@meta-programming

Languages

Go45.5%
Starlark39.9%
Kotlin14.4%
Shell0.2%

Maintainers

@reddaly

Versions

0.1.2 +10h2026-07-02
0.1.02026-07-01