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
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ bazel_dep(name = "bazel_skylib", version = "1.6.1")
bazel_dep(name = "platforms", version = "0.0.10")

zig = use_extension("//zig:extensions.bzl", "zig")
zig.index(file = "//zig/private:versions.json")
use_repo(zig, "zig_toolchains")

register_toolchains("@rules_zig//zig/target:all")
Expand Down
21 changes: 21 additions & 0 deletions docs/extensions.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 99 additions & 17 deletions zig/private/bzlmod/zig.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,47 @@ declares one as the default.

_DEFAULT_NAME = "zig"

zig_toolchain = tag_class(attrs = {
"zig_version": attr.string(doc = "The Zig SDK version.", mandatory = True),
"default": attr.bool(
doc = "Make this the default Zig SDK version. Can only be used once, and only in the root module.",
mandatory = False,
default = False,
),
})
zig_toolchain = tag_class(
attrs = {
"zig_version": attr.string(doc = "The Zig SDK version.", mandatory = True),
"default": attr.bool(
doc = "Make this the default Zig SDK version. Can only be used once, and only in the root module.",
mandatory = False,
default = False,
),
},
doc = """\
Fetch and define toolchain targets for the given Zig SDK version.

Defaults to the latest known version.
""",
)

zig_index = tag_class(
attrs = {
"file": attr.label(doc = "The Zig version index JSON file.", mandatory = True),
},
doc = """\
Extend the set of known Zig SDK versions based on a Zig version index.

The provided index must use a schema that is compatible with the [upstream index].

[upstream index]: https://ziglang.org/download/index.json
""",
)

TAG_CLASSES = {
"toolchain": zig_toolchain,
"index": zig_index,
}

def handle_tags(module_ctx, *, known_versions):
"""Handle the zig module extension tags.
def handle_toolchain_tags(modules, *, known_versions):
"""Handle the zig module extension's toolchain tags.

Exposed as a standalone function for unit testing.

Args:
module_ctx: The module context object.
modules: sequence of module objects.
known_versions: sequence of string, The set of known Zig versions.

Returns:
Expand All @@ -47,7 +68,7 @@ def handle_tags(module_ctx, *, known_versions):
default = None
versions = sets.make()

for mod in module_ctx.modules:
for mod in modules:
for toolchain in mod.tags.toolchain:
if toolchain.default:
if not mod.is_root:
Expand All @@ -73,24 +94,85 @@ def handle_tags(module_ctx, *, known_versions):

return None, versions

def _parse_zig_versions_json(json_string):
def parse_zig_versions_json(json_string):
"""Parse a Zig SDK versions index in JSON format.

Exposed as a standalone function for unit testing.

Args:
json_string: String, The version index in JSON format.

Returns:
(err, data), maybe an error or a
`dict[version, dict[platform, struct(url, sha256)]]`.
"""
result = {}

data = json.decode(json_string)
data = json.decode(json_string, default = None)

if data == None:
return "Invalid JSON format in Zig SDK version index.", None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please consider raising a custom exception instead of returning an error string: that way you don't have to repeat the error strings in tests, and the control flow is more explicit with try-except.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there is no try catch in Starlark. This code uses something akin to go style error returns to make error cases testable.


for version, platforms in data.items():
if "version" in platforms:
version = platforms["version"]

if not semver.is_valid(version):
return "Malformed version number '{}' in Zig SDK version index.".format(version), None

for platform, info in platforms.items():
if type(info) != "dict" or not platform in PLATFORMS:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this can ever be true: type() returns a class, never a string.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Starlark, not Python. type returns a string in this case https://bazel.build/rules/lib/globals/all#type

continue

if not "tarball" in info:
return "Missing `tarball` field in Zig SDK version index.", None

if not "shasum" in info:
return "Missing `shasum` field in Zig SDK version index.", None

result.setdefault(version, {})[platform] = struct(
url = info["tarball"],
sha256 = info["shasum"],
)

return None, result

def merge_version_specs(version_specs):
"""Merge Zig SDK version indices.

Exposed as a standalone function for unit testing.

Args:
version_specs: sequence of `dict[version, dict[platform, struct(url, sha256)]]`.

Returns:
`dict[version, dict[platform, struct(url, sha256)]]`
"""
result = {}

for spec in version_specs:
for version, platforms in spec.items():
for platform, info in platforms.items():
result.setdefault(version, {})[platform] = info

return result

def _toolchain_extension(module_ctx):
zig_versions_json_path = module_ctx.path(Label("//zig/private:versions.json"))
known_versions = _parse_zig_versions_json(module_ctx.read(zig_versions_json_path))
version_specs = []
for mod in module_ctx.modules:
for index in mod.tags.index:
file_path = module_ctx.path(index.file)
file_content = module_ctx.read(file_path)
(err, parsed) = parse_zig_versions_json(file_content)

if err != None:
fail(err, index)

version_specs.append(parsed)

known_versions = merge_version_specs(version_specs)

(err, versions) = handle_tags(module_ctx, known_versions = known_versions)
(err, versions) = handle_toolchain_tags(module_ctx.modules, known_versions = known_versions)

if err != None:
fail(*err)
Expand Down
14 changes: 14 additions & 0 deletions zig/private/common/semver.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ def _grouped(versions):
patch = patch,
)

def _is_valid(version):
"""Validate the formatting of a semantic version."""
if version.count(".") < 2:
return False

v = _split(version)

return all([
v.major.isdigit(),
v.minor.isdigit(),
v.patch.isdigit(),
])

def _parse_pre_release_component(component):
"""Parse a pre-release component for sorting."""
num = float("+Infinity")
Expand Down Expand Up @@ -101,4 +114,5 @@ def _sorted(versions, *, reverse = False):
semver = struct(
grouped = _grouped,
sorted = _sorted,
is_valid = _is_valid,
)
Loading