Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Bazel rules for the [Odin programming language](https://odin-lang.org/).
## Features

- Hermetic Odin toolchain management (no system install required)
- `odin_binary` rule for compiling executables
- `odin_library` rule for sharing packages via collection imports
- bzlmod-first (no WORKSPACE file needed)
- Multi-platform support: Linux (x86-64, ARM64), macOS (x86-64, ARM64), Windows (x86-64)
- Bazel 7.x, 8.x, and 9.x compatible
Expand All @@ -33,14 +35,26 @@ register_toolchains("@odin_toolchains//:all")
Then in your `BUILD.bazel`:

```starlark
load("@rules_odin//odin:defs.bzl", "odin_binary")
load("@rules_odin//odin:defs.bzl", "odin_binary", "odin_library")

odin_library(
name = "greetings",
srcs = glob(["lib/*.odin"]),
)

odin_binary(
name = "hello",
srcs = glob(["*.odin"]),
srcs = glob(["hello/*.odin"]),
deps = [":greetings"],
)
```

Import the library in your Odin code using the collection name (the `odin_library` target name) and the package directory name:

```odin
import greetings "greetings:lib" // collection:name → lib/ directory
```

## Rules

### `odin_binary`
Expand All @@ -50,12 +64,21 @@ Compiles an Odin package (directory of `.odin` files) into an executable.
| Attribute | Type | Default | Description |
| ---------------------- | ------------- | ------------ | ---------------------------------------- |
| `srcs` | label_list | **required** | Odin source files (must share a package) |
| `deps` | label_list | `[]` | `odin_library` targets to import as collections |
| `optimization` | string | `"none"` | One of: none, minimal, speed, size, aggressive |
| `debug` | bool | `True` | Include debug symbols |
| `defines` | string_dict | `{}` | Compile-time `-define:` values |
| `extra_compiler_flags` | string_list | `[]` | Additional flags passed to `odin build` |
| `vet` | bool | `False` | Enable `-vet` checks |

### `odin_library`

Groups Odin source files into a library that can be imported by `odin_binary` targets via Odin's collection system. The library's target name becomes the collection name.

| Attribute | Type | Default | Description |
| --------- | ---------- | ------------ | ---------------------------------------- |
| `srcs` | label_list | **required** | Odin source files (must share a package) |

## Supported Odin Versions

| Version | Status |
Expand Down
8 changes: 7 additions & 1 deletion e2e/smoke/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
load("@rules_odin//odin:defs.bzl", "odin_binary")
load("@rules_odin//odin:defs.bzl", "odin_binary", "odin_library")

odin_library(
name = "greetings",
srcs = glob(["lib/*.odin"]),
)

odin_binary(
name = "hello",
srcs = glob(["hello/*.odin"]),
deps = [":greetings"],
)
2 changes: 2 additions & 0 deletions e2e/smoke/hello/hello.odin
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package hello

import "core:fmt"
import greetings "greetings:lib"

main :: proc() {
fmt.println("Hellope, Odin from Bazel!")
greetings.greet("Bazel")
}
7 changes: 7 additions & 0 deletions e2e/smoke/lib/greetings.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package greetings

import "core:fmt"

greet :: proc(name: string) {
fmt.printf("Greetings from the library, %s!\n", name)
}
4 changes: 3 additions & 1 deletion odin/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

Users should load rules from this file:

load("@rules_odin//odin:defs.bzl", "odin_binary")
load("@rules_odin//odin:defs.bzl", "odin_binary", "odin_library")
"""

load("//odin/private:binary.bzl", _odin_binary = "odin_binary")
load("//odin/private:library.bzl", _odin_library = "odin_library")

odin_binary = _odin_binary
odin_library = _odin_library
64 changes: 34 additions & 30 deletions odin/private/binary.bzl
Original file line number Diff line number Diff line change
@@ -1,29 +1,7 @@
"""Implementation of the odin_binary rule."""

def _get_package_dir(srcs):
"""Determine the package directory from source files.

All source files must reside in the same directory (Odin compiles
entire directories as a single package).

Returns:
The path to the package directory relative to the exec root.
"""
if not srcs:
fail("odin_binary requires at least one source file in 'srcs'.")

dirs = {}
for src in srcs:
dirs[src.dirname] = True

if len(dirs) > 1:
fail(
"All 'srcs' in an odin_binary must be in the same directory " +
"(Odin compiles entire directories as a package). " +
"Found files in: " + ", ".join(sorted(dirs.keys())),
)

return srcs[0].dirname
load("//odin/private:common.bzl", "get_package_dir")
load("//odin/private:library.bzl", "OdinLibraryInfo")

def _odin_binary_impl(ctx):
toolchain = ctx.toolchains["@rules_odin//odin:toolchain_type"]
Expand All @@ -38,7 +16,7 @@ def _odin_binary_impl(ctx):
out = ctx.actions.declare_file(out_name)

srcs = ctx.files.srcs
pkg_dir = _get_package_dir(srcs)
pkg_dir = get_package_dir(srcs, "odin_binary")

# Build the compiler arguments
args = ctx.actions.args()
Expand Down Expand Up @@ -67,16 +45,37 @@ def _odin_binary_impl(ctx):
for flag in ctx.attr.extra_compiler_flags:
args.add(flag)

# Collect all inputs: sources + entire SDK (for core/base/vendor imports)
# Collect library deps: add their source files to inputs and
# register each as a collection for Odin's import resolution.
dep_srcs = []
seen_collections = {}
for dep in ctx.attr.deps:
lib_info = dep[OdinLibraryInfo]
name = lib_info.collection_name
if name in seen_collections:
fail(
"odin_binary '{}' has duplicate collection name '{}' ".format(
ctx.label.name,
name,
) + "from deps '{}' and '{}'. ".format(
seen_collections[name],
dep.label,
) + "Odin collection names must be unique within a binary.",
)
seen_collections[name] = dep.label
dep_srcs.extend(lib_info.srcs.to_list())
args.add("-collection:{}={}".format(
name,
lib_info.collection_root,
))

# Collect all inputs: sources + library dep srcs + entire SDK
inputs = depset(
direct = srcs,
direct = srcs + dep_srcs,
transitive = [odin_info.all_files],
)

# Set ODIN_ROOT so the compiler finds core/, base/, vendor/.
# ODIN_ROOT must point to the directory containing the compiler binary,
# which is also where core/, base/, vendor/ are located after extraction.
# We derive it from the compiler file's dirname (works inside the sandbox).
env = {}
if odin_info.compiler:
env["ODIN_ROOT"] = odin_info.compiler.dirname
Expand Down Expand Up @@ -109,6 +108,11 @@ odin_binary = rule(
allow_files = [".odin"],
mandatory = True,
),
"deps": attr.label_list(
doc = "Odin library targets (odin_library) to import as collections.",
providers = [OdinLibraryInfo],
default = [],
),
"out": attr.string(
doc = "Output binary name. Defaults to the rule name.",
default = "",
Expand Down
30 changes: 30 additions & 0 deletions odin/private/common.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Shared helpers for rules_odin."""

def get_package_dir(srcs, rule_name):
"""Determine the package directory from source files.

All source files must reside in the same directory (Odin compiles
entire directories as a single package).

Args:
srcs: List of File objects (ctx.files.srcs).
rule_name: Name of the rule (for error messages).

Returns:
The path to the package directory relative to the exec root.
"""
if not srcs:
fail("{} requires at least one source file in 'srcs'.".format(rule_name))

dirs = {}
for src in srcs:
dirs[src.dirname] = True

if len(dirs) > 1:
fail(
"All 'srcs' in an {} must be in the same directory ".format(rule_name) +
"(Odin compiles entire directories as a package). " +
"Found files in: " + ", ".join(sorted(dirs.keys())),
)

return srcs[0].dirname
72 changes: 72 additions & 0 deletions odin/private/library.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Implementation of the odin_library rule.

An odin_library groups Odin source files in a directory and exposes them
via OdinLibraryInfo so that odin_binary targets can import them as collections.

The collection name is the label name of the odin_library target.
The collection root is the parent directory of the package directory,
so that `import "<name>:<pkg_dir_basename>"` resolves correctly.
"""

load("//odin/private:common.bzl", "get_package_dir")

OdinLibraryInfo = provider(
doc = "Information about an Odin library (source aggregation for collection imports).",
fields = {
"srcs": "depset: Odin source files for this library.",
"collection_name": "String: Name used in `-collection:<name>=<root>` (the label name).",
"collection_root": "String: Parent directory of the package directory, passed as the collection root path.",
"pkg_dir": "String: Package directory path (dirname of the source files).",
},
)

def _odin_library_impl(ctx):
srcs = ctx.files.srcs
pkg_dir = get_package_dir(srcs, "odin_library")

# Reject root-level srcs: the collection root is the parent of the
# package directory. If srcs are at the workspace root, there is no
# parent directory to serve as the collection root.
if not pkg_dir:
fail(
"odin_library '{}' srcs must be in a subdirectory ".format(ctx.label.name) +
"(the collection root is derived from the parent directory of the package). " +
"Files at the workspace root have no parent directory.",
)

# The collection root is the parent of the package directory.
# E.g., if pkg_dir is "e2e/smoke/lib", collection_root is "e2e/smoke".
# If pkg_dir is "lib" (top-level dir), collection_root is ".".
# The import in Odin would be: import "<name>:lib"
parts = pkg_dir.rsplit("/", 1)
if len(parts) < 2:
collection_root = "."
else:
collection_root = parts[0]

info = OdinLibraryInfo(
srcs = depset(srcs),
collection_name = ctx.label.name,
collection_root = collection_root,
pkg_dir = pkg_dir,
)

return [
DefaultInfo(
files = depset(srcs),
runfiles = ctx.runfiles(files = srcs),
),
info,
]

odin_library = rule(
implementation = _odin_library_impl,
doc = "Groups Odin source files into a library that can be imported by odin_binary targets via collections.",
attrs = {
"srcs": attr.label_list(
doc = "Odin source files. All files must be in the same directory (one Odin package).",
allow_files = [".odin"],
mandatory = True,
),
},
)
Loading