diff --git a/MODULE.bazel b/MODULE.bazel
index 9634bb6a..4f477878 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -43,6 +43,27 @@ zig_dev = use_extension(
zig_dev.toolchain(zig_version = "0.16.0")
zig_dev.toolchain(zig_version = "0.15.2")
+zls_dev = use_extension(
+ "//zig/zls:extensions.bzl",
+ "zls",
+ dev_dependency = True,
+)
+zls_dev.index(file = "//zig/zls/private:versions.json")
+zls_dev.toolchain(
+ zig_version = "0.16.0",
+ zls_version = "0.16.0",
+)
+zls_dev.toolchain(
+ zig_version = "0.15.2",
+ zls_version = "0.15.1",
+)
+use_repo(zls_dev, "zls_toolchains")
+
+register_toolchains(
+ "@zls_toolchains//:all",
+ dev_dependency = True,
+)
+
bazel_dep(name = "toolchains_buildbuddy", version = "0.0.4", dev_dependency = True)
buildbuddy = use_extension(
diff --git a/README.md b/README.md
index 98f5ee52..43487406 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,48 @@ them through Bazel by using the `--repo_env` flag.
Examples can be found among the end-to-end tests under
[`./e2e/workspace`](./e2e/workspace).
+## ZLS
+
+ZLS toolchains are provided by a separate extension and selected by the active
+Zig SDK version.
+
+```starlark
+zls = use_extension("@rules_zig//zig/zls:extensions.bzl", "zls")
+zls.index(file = "@rules_zig//zig/zls/private:versions.json")
+zls.toolchain(
+ zig_version = "0.16.0",
+ zls_version = "0.16.0",
+)
+use_repo(zls, "zls_toolchains")
+register_toolchains("@zls_toolchains//:all")
+```
+
+Use `zig_version` as the selector and `zls_version` as the artifact version.
+They do not need to match, which allows a dev ZLS build to be tied to a stable
+Zig SDK.
+
+Use the [`zls_completion`](./zig/zls/zls_completion.bzl) macro to define a ZLS
+entry point for the Zig targets you want to expose to the language server.
+For example, in `tools/BUILD.bazel`:
+
+```starlark
+load("@rules_zig//zig/zls:defs.bzl", "zls_completion")
+
+zls_completion(
+ name = "zls",
+ deps = ["//src:app"],
+)
+```
+
+Then point your editor's ZLS binary setting at a wrapper script that runs that
+target.
+
+```bash
+#!/usr/bin/env bash
+cd "$(dirname "${BASH_SOURCE[0]}")/.."
+exec bazel run -- //tools:zls "${@}"
+```
+
## Reference Documentation
Generated API documentation for the provided rules is available in
diff --git a/util/BUILD.bazel b/util/BUILD.bazel
index ecf9e564..aa98f066 100644
--- a/util/BUILD.bazel
+++ b/util/BUILD.bazel
@@ -82,3 +82,13 @@ py_binary(
"//zig/private:versions.json",
],
)
+
+py_binary(
+ name = "update_zls_versions",
+ srcs = ["update_zls_versions.py"],
+ args = [
+ "--output",
+ "$(rootpath //zig/zls/private:versions.json)",
+ ],
+ data = ["//zig/zls/private:versions.json"],
+)
diff --git a/util/update_zls_versions.py b/util/update_zls_versions.py
new file mode 100755
index 00000000..96a1b3e8
--- /dev/null
+++ b/util/update_zls_versions.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+
+import argparse
+import json
+import urllib.request
+
+
+_ZLS_INDEX_URL = "https://builds.zigtools.org/index.json"
+
+_UNSUPPORTED_VERSIONS = [
+]
+
+_SUPPORTED_PLATFORMS = [
+ "aarch64-linux",
+ "aarch64-macos",
+ "aarch64-windows",
+ "x86_64-linux",
+ "x86_64-macos",
+ "x86_64-windows"]
+
+
+def fetch_zls_versions(url):
+ request = urllib.request.Request(
+ url,
+ headers={"User-Agent": "rules_zig update_zls_versions.py"},
+ )
+ with urllib.request.urlopen(request) as response:
+ if response.status != 200:
+ raise Exception(f"HTTP error: {response.status}")
+ data = response.read()
+ return json.loads(data.decode('utf-8'))
+
+
+def _parse_semver(version_str):
+ """Split a semantic version into its components.
+
+ Raises an error if the version is malformed.
+
+ If the version contains no pre-release component, then a sentinel of
+ `0x10FFFF` is returned. The intent is that it sorts higher than any other
+ code-point, therefore making versions without pre-release component sort
+ higher than this with.
+
+ If the version is the string `master` then it returns a maximum version
+ comprising `float("inf")` components and the pre-release sentinel.
+
+ Returns:
+ (major, minor, patch, pre_release)
+ """
+ max_component = float("inf")
+ max_prerelease = chr(0x10FFFF) # Highest valid code point in Unicode
+
+ if version_str == "master":
+ return max_component, max_component, max_component, max_prerelease
+
+ pre_version, *_ = version_str.split("+", maxsplit=1)
+ main_version, *pre_release = pre_version.split("-", maxsplit=1)
+ major, minor, patch = map(int, main_version.split("."))
+
+ pre_release_segment = pre_release[0] if pre_release else max_prerelease
+
+ return major, minor, patch, pre_release_segment
+
+
+def generate_json_content(data, unsupported_versions, supported_platforms):
+ content = {}
+
+ for version, platforms in sorted(data.items(), key=lambda x: _parse_semver(x[0]), reverse=True):
+ if version in unsupported_versions or version == "master":
+ continue
+
+ for platform, info in sorted(platforms.items()):
+ if platform not in supported_platforms or not isinstance(info, dict):
+ continue
+
+ content.setdefault(version, {})[platform] = {
+ "tarball": info["tarball"],
+ "shasum": info["shasum"],
+ }
+
+ return content
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate JSON file for ZLS versions.")
+ parser.add_argument("--output", type=argparse.FileType('w'), default='-', help="Output file path or '-' for stdout.")
+ parser.add_argument("--url", default=_ZLS_INDEX_URL, help="URL to fetch ZLS versions JSON")
+ parser.add_argument("--unsupported-versions", nargs="*", default=_UNSUPPORTED_VERSIONS, help="List of unsupported ZLS versions")
+ parser.add_argument("--supported-platforms", nargs="*", default=_SUPPORTED_PLATFORMS, help="List of supported platforms")
+ args = parser.parse_args()
+
+ zls_data = fetch_zls_versions(args.url)
+ json_content = generate_json_content(zls_data, set(args.unsupported_versions), set(args.supported_platforms))
+
+ json.dump(json_content, args.output, indent=2)
+ args.output.write("\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/zig/BUILD.bazel b/zig/BUILD.bazel
index 6dae3b9f..6e77cd79 100644
--- a/zig/BUILD.bazel
+++ b/zig/BUILD.bazel
@@ -91,6 +91,7 @@ filegroup(
"//zig/settings:all_files",
"//zig/target:all_files",
"//zig/translate-c:all_files",
+ "//zig/zls:all_files",
],
visibility = ["//:__pkg__"],
)
diff --git a/zig/defs.bzl b/zig/defs.bzl
index ae2456da..cd3b9ca5 100644
--- a/zig/defs.bzl
+++ b/zig/defs.bzl
@@ -18,6 +18,7 @@ load("//zig/private:zig_shared_library.bzl", _zig_shared_library = "zig_shared_l
load("//zig/private:zig_static_library.bzl", _zig_static_library = "zig_static_library")
load("//zig/private:zig_test.bzl", _zig_test = "zig_test")
+# Keep //zig/zls:zls_write_build_config.bzl _is_zig_target in sync with this rule list.
zig_binary = _zig_binary
zig_static_library = _zig_static_library
zig_shared_library = _zig_shared_library
diff --git a/zig/private/common/BUILD.bazel b/zig/private/common/BUILD.bazel
index 6d2f3632..9437d746 100644
--- a/zig/private/common/BUILD.bazel
+++ b/zig/private/common/BUILD.bazel
@@ -42,6 +42,7 @@ bzl_library(
"@build_bazel_rules_android//:cc_common_link.bzl",
"@rules_cc//cc:find_cc_toolchain_bzl",
"@rules_cc//cc/common",
+ "@rules_cc//cc/private/rules_impl:objc_common", # keep
],
)
@@ -103,6 +104,7 @@ bzl_library(
"@apple_support//lib:apple_support",
"@rules_cc//cc:action_names.bzl",
"@rules_cc//cc/common",
+ "@rules_cc//cc/private/rules_impl:objc_common", # keep
],
)
diff --git a/zig/private/common/zig_build.bzl b/zig/private/common/zig_build.bzl
index 37cafdf6..294fb14e 100644
--- a/zig/private/common/zig_build.bzl
+++ b/zig/private/common/zig_build.bzl
@@ -4,6 +4,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths")
load("@build_bazel_rules_android//:cc_common_link.bzl", "cc_common_link")
load("@rules_cc//cc:find_cc_toolchain.bzl", "use_cc_toolchain")
load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
+load("@rules_cc//cc/common:cc_helper.bzl", "cc_helper")
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
load("//zig/private:cc_helper.bzl", "find_cc_toolchain", "need_translate_c")
load(
@@ -752,6 +753,9 @@ def zig_build_impl(ctx, *, kind):
else:
fail("Unknown rule kind '{}'.".format(kind))
+ if root_module.cc_info:
+ transitive_data.append(depset(cc_helper.get_dynamic_libraries_for_runtime(root_module.cc_info.linking_context, True)))
+
providers.extend([
DefaultInfo(
executable = bin_output if bin_output_is_executable else None,
diff --git a/zig/private/providers/BUILD.bazel b/zig/private/providers/BUILD.bazel
index 25c33b93..22076822 100644
--- a/zig/private/providers/BUILD.bazel
+++ b/zig/private/providers/BUILD.bazel
@@ -7,6 +7,7 @@ bzl_library(
deps = [
"//zig/private:cc_helper",
"@rules_cc//cc/common",
+ "@rules_cc//cc/private/rules_impl:objc_common", # keep
],
)
diff --git a/zig/tests/zls-completion/BUILD.bazel b/zig/tests/zls-completion/BUILD.bazel
new file mode 100644
index 00000000..d1d82738
--- /dev/null
+++ b/zig/tests/zls-completion/BUILD.bazel
@@ -0,0 +1,100 @@
+load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+load("@rules_python//python:defs.bzl", "py_binary")
+load("//zig:defs.bzl", "zig_binary", "zig_library")
+load("//zig/zls:defs.bzl", "zls_completion")
+load("//zig/zls:zls_write_build_config.bzl", "zls_write_build_config")
+
+zig_library(
+ name = "lib",
+ main = "main.zig",
+)
+
+zls_completion(
+ name = "completion",
+ testonly = True,
+ deps = [":lib"],
+)
+
+zig_library(
+ name = "leaf",
+ import_name = "custom/leaf",
+ main = "leaf.zig",
+)
+
+zig_library(
+ name = "named",
+ import_name = "custom/named",
+ main = "named.zig",
+)
+
+zig_library(
+ name = "mid",
+ main = "mid.zig",
+ deps = [":leaf"],
+)
+
+write_file(
+ name = "generated_src",
+ out = "generated.zig",
+ content = ["pub const generated_value = 7;"],
+)
+
+zig_library(
+ name = "generated",
+ main = ":generated_src",
+)
+
+zig_library(
+ name = "app",
+ main = "app.zig",
+ deps = [
+ ":generated",
+ ":mid",
+ ":named",
+ "//zig/tests/zls-completion/nested",
+ ],
+)
+
+zig_binary(
+ name = "binary_with_main",
+ main = "binary_with_main.zig",
+ deps = [":app"],
+)
+
+zig_binary(
+ name = "binary_from_root_module",
+ deps = [":app"],
+)
+
+zls_write_build_config(
+ name = "golden_build_config",
+ testonly = True,
+ out = "golden_build_config.json",
+ deps = [
+ ":app",
+ ":binary_from_root_module",
+ ":binary_with_main",
+ ],
+)
+
+py_binary(
+ name = "normalize_zls_build_config",
+ srcs = ["normalize_zls_build_config.py"],
+)
+
+genrule(
+ name = "golden_build_config_normalized",
+ testonly = True,
+ srcs = [":golden_build_config"],
+ outs = ["golden_build_config.normalized.json"],
+ cmd = "$(execpath :normalize_zls_build_config) $(location :golden_build_config) $@",
+ tools = [":normalize_zls_build_config"],
+)
+
+diff_test(
+ name = "golden_build_config_test",
+ size = "small",
+ file1 = "golden_build_config.expected.json",
+ file2 = ":golden_build_config_normalized",
+)
diff --git a/zig/tests/zls-completion/app.zig b/zig/tests/zls-completion/app.zig
new file mode 100644
index 00000000..835a42dc
--- /dev/null
+++ b/zig/tests/zls-completion/app.zig
@@ -0,0 +1,12 @@
+const generated = @import("generated");
+const mid = @import("mid");
+const named = @import("custom/named");
+const nested = @import("nested/module");
+
+pub fn value() i32 {
+ return generated.generated_value + mid.value() + @as(i32, @intFromBool(named.enabled)) + nested.value;
+}
+
+pub fn main() void {
+ _ = value();
+}
diff --git a/zig/tests/zls-completion/binary_with_main.zig b/zig/tests/zls-completion/binary_with_main.zig
new file mode 100644
index 00000000..164e7231
--- /dev/null
+++ b/zig/tests/zls-completion/binary_with_main.zig
@@ -0,0 +1,5 @@
+const app = @import("app");
+
+pub fn main() void {
+ _ = app.value();
+}
diff --git a/zig/tests/zls-completion/golden_build_config.expected.json b/zig/tests/zls-completion/golden_build_config.expected.json
new file mode 100644
index 00000000..1f5643c6
--- /dev/null
+++ b/zig/tests/zls-completion/golden_build_config.expected.json
@@ -0,0 +1,92 @@
+{
+ "available_options": {},
+ "compilations": [],
+ "dependencies": {},
+ "modules": {
+ "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Capp.zig": {
+ "c_macros": [],
+ "import_table": {},
+ "include_dirs": []
+ },
+ "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cgenerated.zig": {
+ "c_macros": [],
+ "import_table": {},
+ "include_dirs": []
+ },
+ "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cleaf.zig": {
+ "c_macros": [],
+ "import_table": {},
+ "include_dirs": []
+ },
+ "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cmid.zig": {
+ "c_macros": [],
+ "import_table": {},
+ "include_dirs": []
+ },
+ "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cnamed.zig": {
+ "c_macros": [],
+ "import_table": {},
+ "include_dirs": []
+ },
+ "__BAZEL_BIN__/zig/tests/zls-completion/generated.zig": {
+ "c_macros": [],
+ "import_table": {
+ "bazel_builtin": "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cgenerated.zig"
+ },
+ "include_dirs": []
+ },
+ "__BAZEL_BIN__/zig/tests/zls-completion/nested/bazel_builtin_zig_Stests_Szls-completion_Snested_Cnested.zig": {
+ "c_macros": [],
+ "import_table": {},
+ "include_dirs": []
+ },
+ "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/app.zig": {
+ "c_macros": [],
+ "import_table": {
+ "bazel_builtin": "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Capp.zig",
+ "custom/named": "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/named.zig",
+ "generated": "__BAZEL_BIN__/zig/tests/zls-completion/generated.zig",
+ "mid": "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/mid.zig",
+ "nested/module": "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/nested/nested.zig"
+ },
+ "include_dirs": []
+ },
+ "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/binary_with_main.zig": {
+ "c_macros": [],
+ "import_table": {
+ "app": "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/app.zig"
+ },
+ "include_dirs": []
+ },
+ "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/leaf.zig": {
+ "c_macros": [],
+ "import_table": {
+ "bazel_builtin": "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cleaf.zig"
+ },
+ "include_dirs": []
+ },
+ "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/mid.zig": {
+ "c_macros": [],
+ "import_table": {
+ "bazel_builtin": "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cmid.zig",
+ "custom/leaf": "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/leaf.zig"
+ },
+ "include_dirs": []
+ },
+ "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/named.zig": {
+ "c_macros": [],
+ "import_table": {
+ "bazel_builtin": "__BAZEL_BIN__/zig/tests/zls-completion/bazel_builtin_zig_Stests_Szls-completion_Cnamed.zig"
+ },
+ "include_dirs": []
+ },
+ "__BUILD_WORKSPACE_DIRECTORY__/zig/tests/zls-completion/nested/nested.zig": {
+ "c_macros": [],
+ "import_table": {
+ "bazel_builtin": "__BAZEL_BIN__/zig/tests/zls-completion/nested/bazel_builtin_zig_Stests_Szls-completion_Snested_Cnested.zig"
+ },
+ "include_dirs": []
+ }
+ },
+ "top_level_steps": []
+}
diff --git a/zig/tests/zls-completion/leaf.zig b/zig/tests/zls-completion/leaf.zig
new file mode 100644
index 00000000..643df7c7
--- /dev/null
+++ b/zig/tests/zls-completion/leaf.zig
@@ -0,0 +1 @@
+pub const value = 29;
diff --git a/zig/tests/zls-completion/main.zig b/zig/tests/zls-completion/main.zig
new file mode 100644
index 00000000..1ebf7423
--- /dev/null
+++ b/zig/tests/zls-completion/main.zig
@@ -0,0 +1,4 @@
+pub fn value() i32 {
+ return 42;
+}
+
diff --git a/zig/tests/zls-completion/mid.zig b/zig/tests/zls-completion/mid.zig
new file mode 100644
index 00000000..83de07e9
--- /dev/null
+++ b/zig/tests/zls-completion/mid.zig
@@ -0,0 +1,5 @@
+const leaf = @import("custom/leaf");
+
+pub fn value() i32 {
+ return leaf.value + 5;
+}
diff --git a/zig/tests/zls-completion/named.zig b/zig/tests/zls-completion/named.zig
new file mode 100644
index 00000000..7ee662d1
--- /dev/null
+++ b/zig/tests/zls-completion/named.zig
@@ -0,0 +1 @@
+pub const enabled = true;
diff --git a/zig/tests/zls-completion/nested/BUILD.bazel b/zig/tests/zls-completion/nested/BUILD.bazel
new file mode 100644
index 00000000..9f54ef76
--- /dev/null
+++ b/zig/tests/zls-completion/nested/BUILD.bazel
@@ -0,0 +1,8 @@
+load("//zig:defs.bzl", "zig_library")
+
+zig_library(
+ name = "nested",
+ import_name = "nested/module",
+ main = "nested.zig",
+ visibility = ["//visibility:public"],
+)
diff --git a/zig/tests/zls-completion/nested/nested.zig b/zig/tests/zls-completion/nested/nested.zig
new file mode 100644
index 00000000..70e1f9d8
--- /dev/null
+++ b/zig/tests/zls-completion/nested/nested.zig
@@ -0,0 +1 @@
+pub const value = 1;
diff --git a/zig/tests/zls-completion/normalize_zls_build_config.py b/zig/tests/zls-completion/normalize_zls_build_config.py
new file mode 100644
index 00000000..01259038
--- /dev/null
+++ b/zig/tests/zls-completion/normalize_zls_build_config.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import json
+import re
+import sys
+
+
+BAZEL_BIN_RE = re.compile(r"__BAZEL_EXECUTION_ROOT__/bazel-out/[^/]+/bin/")
+
+
+def normalize(value):
+ if isinstance(value, dict):
+ return {normalize(key): normalize(item) for key, item in value.items()}
+ if isinstance(value, list):
+ return [normalize(item) for item in value]
+ if isinstance(value, str):
+ return BAZEL_BIN_RE.sub("__BAZEL_BIN__/", value)
+ return value
+
+
+def main(argv):
+ if len(argv) != 3:
+ print("usage: normalize_zls_build_config.py