diff --git a/README.md b/README.md index abbd879..caa60cf 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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` @@ -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 | diff --git a/e2e/smoke/BUILD.bazel b/e2e/smoke/BUILD.bazel index 711c704..b50359b 100644 --- a/e2e/smoke/BUILD.bazel +++ b/e2e/smoke/BUILD.bazel @@ -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"], ) diff --git a/e2e/smoke/hello/hello.odin b/e2e/smoke/hello/hello.odin index 89809d9..583b03c 100644 --- a/e2e/smoke/hello/hello.odin +++ b/e2e/smoke/hello/hello.odin @@ -1,7 +1,9 @@ package hello import "core:fmt" +import greetings "greetings:lib" main :: proc() { fmt.println("Hellope, Odin from Bazel!") + greetings.greet("Bazel") } diff --git a/e2e/smoke/lib/greetings.odin b/e2e/smoke/lib/greetings.odin new file mode 100644 index 0000000..fe89a2c --- /dev/null +++ b/e2e/smoke/lib/greetings.odin @@ -0,0 +1,7 @@ +package greetings + +import "core:fmt" + +greet :: proc(name: string) { + fmt.printf("Greetings from the library, %s!\n", name) +} diff --git a/odin/defs.bzl b/odin/defs.bzl index 911cdb9..bb932d1 100644 --- a/odin/defs.bzl +++ b/odin/defs.bzl @@ -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 diff --git a/odin/private/binary.bzl b/odin/private/binary.bzl index 8f4dc3b..7813931 100644 --- a/odin/private/binary.bzl +++ b/odin/private/binary.bzl @@ -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"] @@ -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() @@ -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 @@ -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 = "", diff --git a/odin/private/common.bzl b/odin/private/common.bzl new file mode 100644 index 0000000..2760881 --- /dev/null +++ b/odin/private/common.bzl @@ -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 diff --git a/odin/private/library.bzl b/odin/private/library.bzl new file mode 100644 index 0000000..c05ba14 --- /dev/null +++ b/odin/private/library.bzl @@ -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 ":"` 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:=` (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 ":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, + ), + }, +)