Lazily download tools (#62) Closes #59. Refactor os/cpu repos into per-tool repos and split bzlmod/non-bzlmod handling in order to add a workspace-only shim. As a result of these changes, only tools needed by a build will be downloaded/all tool downloads become lazy. **Breaking change** for workspace users.
diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh index f9fcec0..18221fd 100755 --- a/.github/workflows/release_prep.sh +++ b/.github/workflows/release_prep.sh
@@ -62,5 +62,10 @@ name = "multitool", lockfile = "//:multitool.lock.json", ) + +# required since 0.15.0 to enable only downloading tools used by this workspace +load("@multitool.workspace//:tools.bzl", "register_tools") + +register_tools() \`\`\` EOF
diff --git a/examples/workspace/WORKSPACE.bazel b/examples/workspace/WORKSPACE.bazel index 99dc0cb..b94bb8c 100644 --- a/examples/workspace/WORKSPACE.bazel +++ b/examples/workspace/WORKSPACE.bazel
@@ -8,3 +8,7 @@ name = "multitool", lockfile = "//:multitool.lock.json", ) + +load("@multitool.workspace//:tools.bzl", "register_tools") + +register_tools()
diff --git a/examples/workspace/minimal_download_test.sh b/examples/workspace/minimal_download_test.sh index 4ee0b88..26c16c4 100755 --- a/examples/workspace/minimal_download_test.sh +++ b/examples/workspace/minimal_download_test.sh
@@ -21,6 +21,7 @@ if [ "$file" != "$ALLOWED" ]; then cat >$DL_CONFIG <<EOF rewrite github.com/bazel-contrib/target-determinator/releases/download/([^/]+)/$file disallowed.build/\$1/$file +rewrite github.com/cli/cli/releases/download/(.*) disallowed.build/\$1 EOF fi done
diff --git a/examples/workspace/multitool.lock.json b/examples/workspace/multitool.lock.json index a7fee4f..80118c3 100644 --- a/examples/workspace/multitool.lock.json +++ b/examples/workspace/multitool.lock.json
@@ -1,35 +1,71 @@ { "$schema": "../../lockfile.schema.json", "target-determinator": { - "binaries": [ - { - "kind": "file", - "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.darwin.amd64", - "sha256": "8c7245603dede429b978e214ca327c3f3d686a1bc712c1298fca0396a0f25f23", - "os": "macos", - "cpu": "x86_64" - }, - { - "kind": "file", - "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.darwin.arm64", - "sha256": "8f975b471c4a51d32781b757e1ece9700221bfd4c0ea507c18fa382360d1111f", - "os": "macos", - "cpu": "arm64" - }, - { - "kind": "file", - "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.linux.amd64", - "sha256": "c8a09143e9fe6eccc4b27a6be92c5929e5a78034a8d0b4c43dbed4ee539ec903", - "os": "linux", - "cpu": "x86_64" - }, - { - "kind": "file", - "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.windows.amd64.exe", - "sha256": "e14fd75e33d193f579505cf3e641e07025904fc027686e13e154ba8e10ac0f58", - "os": "windows", - "cpu": "x86_64" - } - ] + "binaries": [ + { + "kind": "file", + "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.darwin.amd64", + "sha256": "8c7245603dede429b978e214ca327c3f3d686a1bc712c1298fca0396a0f25f23", + "os": "macos", + "cpu": "x86_64" + }, + { + "kind": "file", + "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.darwin.arm64", + "sha256": "8f975b471c4a51d32781b757e1ece9700221bfd4c0ea507c18fa382360d1111f", + "os": "macos", + "cpu": "arm64" + }, + { + "kind": "file", + "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.linux.amd64", + "sha256": "c8a09143e9fe6eccc4b27a6be92c5929e5a78034a8d0b4c43dbed4ee539ec903", + "os": "linux", + "cpu": "x86_64" + }, + { + "kind": "file", + "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.windows.amd64.exe", + "sha256": "e14fd75e33d193f579505cf3e641e07025904fc027686e13e154ba8e10ac0f58", + "os": "windows", + "cpu": "x86_64" + } + ] + }, + "gh": { + "binaries": [ + { + "kind": "archive", + "url": "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_macOS_amd64.zip", + "sha256": "1c545505b5b88feaffeba00b7284ccac3f2002b67461b1246eaec827eb07c31b", + "file": "gh_2.44.1_macOS_amd64/bin/gh", + "os": "macos", + "cpu": "x86_64" + }, + { + "kind": "archive", + "url": "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_macOS_amd64.zip", + "sha256": "1c545505b5b88feaffeba00b7284ccac3f2002b67461b1246eaec827eb07c31b", + "file": "gh_2.44.1_macOS_amd64/bin/gh", + "os": "macos", + "cpu": "arm64" + }, + { + "kind": "archive", + "url": "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz", + "sha256": "f11eefb646768e3f53e2185f6d3b01b4cb02112c2c60e65a4b5875150287ff97", + "file": "gh_2.44.1_linux_amd64/bin/gh", + "os": "linux", + "cpu": "x86_64" + }, + { + "kind": "archive", + "url": "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_windows_amd64.zip", + "sha256": "5a19a334d5032cfcb01e71e418c5e355104f7afd9129daf8939b729ffa46aa08", + "file": "bin/gh.exe", + "os": "windows", + "cpu": "x86_64" + } + ] } }
diff --git a/multitool/extension.bzl b/multitool/extension.bzl index 9360b05..8ab4090 100644 --- a/multitool/extension.bzl +++ b/multitool/extension.bzl
@@ -1,6 +1,6 @@ "multitool module extension" -load("//multitool/private:multitool.bzl", _hub = "hub") +load("//multitool/private:multitool.bzl", _hub = "bzlmod_hub") hub = tag_class( attrs = { @@ -18,6 +18,7 @@ _hub( name = "multitool", lockfiles = lockfiles, + module_ctx = module_ctx, ) multitool = module_extension(
diff --git a/multitool/multitool.bzl b/multitool/multitool.bzl index e4c0315..b99f067 100644 --- a/multitool/multitool.bzl +++ b/multitool/multitool.bzl
@@ -1,6 +1,6 @@ "multitool workspace macros" -load("//multitool/private:multitool.bzl", "hub") +load("//multitool/private:multitool.bzl", "workspace_hub") def multitool(name, lockfile = None, lockfiles = None): """(non-bzlmod) Create a multitool hub and register its toolchains. @@ -17,5 +17,5 @@ lockfiles = lockfiles if lockfiles else [lockfile] - hub(name, lockfiles) + workspace_hub(name, lockfiles) native.register_toolchains("@{name}//toolchains:all".format(name = name))
diff --git a/multitool/private/hub_repo_template/toolchain_info.bzl.template b/multitool/private/hub_repo_template/toolchain_info.bzl.template index 90fa4b7..f5dd020 100644 --- a/multitool/private/hub_repo_template/toolchain_info.bzl.template +++ b/multitool/private/hub_repo_template/toolchain_info.bzl.template
@@ -29,7 +29,7 @@ ext = _extension(os) toolchain_info( name = "{name}_{os}_{cpu}_toolchain_info".format(name=name, os=os, cpu=cpu), - executable = "@@{hub_name}.{os}_{cpu}//tools/{name}:{os}_{cpu}_executable{ext}".format(name=name, os=os, cpu=cpu, ext=ext), + executable = "@@{hub_name}.{name}.{os}_{cpu}//tools/{name}:{os}_{cpu}_executable{ext}".format(name=name, os=os, cpu=cpu, ext=ext), os = os, cpu = cpu, ext = ext,
diff --git a/multitool/private/lockfile.bzl b/multitool/private/lockfile.bzl new file mode 100644 index 0000000..2bc0425 --- /dev/null +++ b/multitool/private/lockfile.bzl
@@ -0,0 +1,60 @@ +"Utilities for interacting with the multitool lockfile." + +def _check(condition, message): + "fails iff condition is False and emits message" + if not condition: + fail(message) + +def _check_version(os, binary_os): + # require bazel 7.1 on windows. Only do this check for windows artifacts to avoid regressing anyone + # skip version check on windows if we don't have a release version. We can't tell from a hash what features we have. + if os == "windows" and binary_os == "windows" and native.bazel_version: + version = native.bazel_version.split(".") + if int(version[0]) > 7 or (int(version[0]) == 7 and int(version[1]) >= 1): + pass + else: + fail("rules_multitool: windows platform requires bazel 7.1+ to read artifacts; current bazel is " + native.bazel_version) + +def _load(ctx, lockfiles): + tools = {} + for lockfile in lockfiles: + # TODO: validate no conflicts from multiple hub declarations and/or + # fix toolchains to also declare their versions and enable consumers + # to use constraints to pick the right one. + # (this is also a very naive merge at the tool level) + tools = tools | json.decode(ctx.read(lockfile)) + + # a special key says this JSON document conforms to a schema + tools.pop("$schema", None) + + # validation + for tool_name, tool in tools.items(): + for binary in tool["binaries"]: + _check( + binary["os"] in ["linux", "macos", "windows"], + "{tool_name}: Unknown os '{os}'".format( + tool_name = tool_name, + os = binary["os"], + ), + ) + _check( + binary["cpu"] in ["x86_64", "arm64"], + "{tool_name}: Unknown cpu '{cpu}'".format( + tool_name = tool_name, + cpu = binary["cpu"], + ), + ) + _check_version(ctx.os.name, binary["os"]) + + return tools + +def _sort_fn(tup): + return tup[0] + +def _sorted(tools): + return sorted(tools.items(), key = _sort_fn) + +lockfile = struct( + load_defs = _load, + sorted_defs = _sorted, +)
diff --git a/multitool/private/multitool.bzl b/multitool/private/multitool.bzl index 3ba0328..7919f4b 100644 --- a/multitool/private/multitool.bzl +++ b/multitool/private/multitool.bzl
@@ -3,18 +3,22 @@ Multitool takes as input a JSON lockfile and emits the following repos: - - [hub].[os]_[cpu], for each [os]/[cpu] combo in _SUPPORTED_ENVS: - This repository holds os/cpu specific binaries for all tools in the provided - lockfile(s) and constructs clean symlinks to their content for inclusion in - toolchains defined in the [hub] repo. + - [hub].[tool].[os]_[cpu]: + This repository holds the per-tool os/cpu binary in the provided lockfile. - The structure of this repo is, very simply: + The structure of this repo is: tools/ [tool-name]/ BUILD.bazel (export all *_executable files) [os]_[cpu]_executable (a downloaded file or a symlink to a file in a downloaded and extracted archive) + - [hub].workspace: + Iff not running in bzlmod, a utility for registering the per-tool repos with Bazel. + + The structure of this repo is: + tools.bzl (a file containing one utility method, "register_tools") + - [hub]: This repository holds toolchain definitions for all tools in the provided lockfile(s), as well as an executable tool target that will pick the @@ -32,9 +36,10 @@ (additional BUILD.bazel and a WORKSPACE file are included as required by Bazel) To keep things orderly, we keep all the toolchain Bazel goo in the [hub] repo and only stash -the binaries in the [hub].[os]_[cpu] repos. It's a conscious decision not to place some fragments -of the toolchain definitions in the latter repos to make the dependencies run exactly one way: -[hub] -> [hub].[os]_[cpu]. +the binaries in the [hub].[tool].[os]_[cpu] repos. It's a conscious decision not to place some +fragments of the toolchain definitions in the latter repos to make the dependencies run exactly +one way: + [hub] -> [hub].[tool].[os]_[cpu]. This implementation depends on rendering a number of templates, which are defined in sibling folders and managed by the templates starlark file. @@ -45,32 +50,9 @@ load("@bazel_features//:features.bzl", "bazel_features") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "read_netrc", "read_user_netrc", "use_netrc") +load(":lockfile.bzl", "lockfile") load(":templates.bzl", "templates") -_SUPPORTED_ENVS = [ - ("linux", "arm64"), - ("linux", "x86_64"), - ("macos", "arm64"), - ("macos", "x86_64"), - ("windows", "arm64"), - ("windows", "x86_64"), -] - -def _check(condition, message): - "fails iff condition is False and emits message" - if not condition: - fail(message) - -def _check_version(os, binary_os): - # require bazel 7.1 on windows. Only do this check for windows artifacts to avoid regressing anyone - # skip version check on windows if we don't have a release version. We can't tell from a hash what features we have. - if os == "windows" and binary_os == "windows" and native.bazel_version: - version = native.bazel_version.split(".") - if int(version[0]) > 7 or (int(version[0]) == 7 and int(version[1]) >= 1): - pass - else: - fail("rules_multitool: windows platform requires bazel 7.1+ to read artifacts; current bazel is " + native.bazel_version) - def _get_auth(rctx, urls, auth_patterns): "Returns an auth dict for the provided list of URLs." if "NETRC" in rctx.os.environ: @@ -79,39 +61,6 @@ netrc = read_user_netrc(rctx) return use_netrc(netrc, urls, auth_patterns) -def _load_tools(rctx): - tools = {} - for lockfile in rctx.attr.lockfiles: - # TODO: validate no conflicts from multiple hub declarations and/or - # fix toolchains to also declare their versions and enable consumers - # to use constraints to pick the right one. - # (this is also a very naive merge at the tool level) - tools = tools | json.decode(rctx.read(lockfile)) - - # a special key says this JSON document conforms to a schema - tools.pop("$schema", None) - - # validation - for tool_name, tool in tools.items(): - for binary in tool["binaries"]: - _check( - binary["os"] in ["linux", "macos", "windows"], - "{tool_name}: Unknown os '{os}'".format( - tool_name = tool_name, - os = binary["os"], - ), - ) - _check( - binary["cpu"] in ["x86_64", "arm64"], - "{tool_name}: Unknown cpu '{cpu}'".format( - tool_name = tool_name, - cpu = binary["cpu"], - ), - ) - _check_version(rctx.os.name, binary["os"]) - - return tools - def _feature_sensitive_args(binary): args = {} if bazel_features.external_deps.download_has_headers_param: @@ -124,122 +73,128 @@ return ".exe" return "" -def _env_specific_tools_impl(rctx): - tools = _load_tools(rctx) +def _download_extract_tool(rctx, tool_name, binary): + target_filename = "{os}_{cpu}_executable{ext}".format( + cpu = binary["cpu"], + os = binary["os"], + ext = _extension(binary["os"]), + ) + target_executable = "tools/{tool_name}/{filename}".format( + tool_name = tool_name, + filename = target_filename, + ) - for tool_name, tool in tools.items(): - for binary in tool["binaries"]: - if binary["os"] != rctx.attr.os or binary["cpu"] != rctx.attr.cpu: - continue + if binary["kind"] == "file": + rctx.download( + url = binary["url"], + sha256 = binary["sha256"], + output = target_executable, + executable = True, + auth = _get_auth(rctx, [binary["url"]], binary.get("auth_patterns", {})), + **_feature_sensitive_args(binary) + ) + elif binary["kind"] == "archive": + archive_path = "tools/{tool_name}/{os}_{cpu}_archive".format( + tool_name = tool_name, + cpu = binary["cpu"], + os = binary["os"], + ) - target_filename = "{os}_{cpu}_executable{ext}".format( - cpu = binary["cpu"], - os = binary["os"], - ext = _extension(binary["os"]), - ) - target_executable = "tools/{tool_name}/{filename}".format( + rctx.download_and_extract( + url = binary["url"], + sha256 = binary["sha256"], + output = archive_path, + type = binary.get("type", ""), + auth = _get_auth(rctx, [binary["url"]], binary.get("auth_patterns", {})), + **_feature_sensitive_args(binary) + ) + + # link to the executable + archive_file = "{archive_path}/{file}".format(archive_path = archive_path, file = binary["file"]) + if not rctx.path(archive_file).exists: + fail("{tool_name} ({os}, {cpu}): Cannot find {file} in archive from {url}".format( tool_name = tool_name, - filename = target_filename, - ) + os = binary["os"], + cpu = binary["cpu"], + file = archive_file, + url = binary["url"], + )) + rctx.symlink(archive_file, target_executable) + elif binary["kind"] == "pkg": + # Check if pkgutil is on the path, and if not fail silently. + # repository rules execute irrespective of platform/OS, so this + # check is required for `pkg_archive` to not fail on Linux. + pkgutil_cmd = rctx.which("pkgutil") + if not pkgutil_cmd: + return - if binary["kind"] == "file": - rctx.download( - url = binary["url"], - sha256 = binary["sha256"], - output = target_executable, - executable = True, - auth = _get_auth(rctx, [binary["url"]], binary.get("auth_patterns", {})), - **_feature_sensitive_args(binary) - ) - elif binary["kind"] == "archive": - archive_path = "tools/{tool_name}/{os}_{cpu}_archive".format( - tool_name = tool_name, - cpu = binary["cpu"], - os = binary["os"], - ) + archive_path = "tools/{tool_name}/{os}_{cpu}_pkg".format( + tool_name = tool_name, + cpu = binary["cpu"], + os = binary["os"], + ) - rctx.download_and_extract( - url = binary["url"], - sha256 = binary["sha256"], - output = archive_path, - type = binary.get("type", ""), - auth = _get_auth(rctx, [binary["url"]], binary.get("auth_patterns", {})), - **_feature_sensitive_args(binary) - ) + rctx.download( + url = binary["url"], + sha256 = binary["sha256"], + output = archive_path + ".pkg", + auth = _get_auth(rctx, [binary["url"]], binary.get("auth_patterns", {})), + **_feature_sensitive_args(binary) + ) - # link to the executable - archive_file = "{archive_path}/{file}".format(archive_path = archive_path, file = binary["file"]) - if not rctx.path(archive_file).exists: - fail("{tool_name} ({os}, {cpu}): Cannot find {file} in archive from {url}".format( - tool_name = tool_name, - os = binary["os"], - cpu = binary["cpu"], - file = archive_file, - url = binary["url"], - )) - rctx.symlink(archive_file, target_executable) - elif binary["kind"] == "pkg": - # Check if pkgutil is on the path, and if not fail silently. - # repository rules execute irrespective of platform/OS, so this - # check is required for `pkg_archive` to not fail on Linux. - pkgutil_cmd = rctx.which("pkgutil") - if not pkgutil_cmd: - continue + rctx.execute([pkgutil_cmd, "--expand-full", archive_path + ".pkg", archive_path]) - archive_path = "tools/{tool_name}/{os}_{cpu}_pkg".format( - tool_name = tool_name, - cpu = binary["cpu"], - os = binary["os"], - ) + # link to the executable + archive_file = "{archive_path}/{file}".format(archive_path = archive_path, file = binary["file"]) + if not rctx.path(archive_file).exists: + fail("{tool_name} ({os}, {cpu}): Cannot find {file} in archive from {url}".format( + tool_name = tool_name, + os = binary["os"], + cpu = binary["cpu"], + file = archive_file, + url = binary["url"], + )) + rctx.symlink(archive_file, target_executable) + else: + fail("Unknown 'kind' {kind}".format(kind = binary["kind"])) - rctx.download( - url = binary["url"], - sha256 = binary["sha256"], - output = archive_path + ".pkg", - auth = _get_auth(rctx, [binary["url"]], binary.get("auth_patterns", {})), - **_feature_sensitive_args(binary) - ) + templates.tool_tool(rctx, tool_name, "BUILD.bazel", {"{target_filename}": target_filename}) - rctx.execute([pkgutil_cmd, "--expand-full", archive_path + ".pkg", archive_path]) +def _workspace_hub_impl(rctx): + tools = lockfile.load_defs(rctx, rctx.attr.lockfiles) + templates.workspace(rctx, "BUILD.bazel", rctx.attr.hub_name, {}) + templates.workspace(rctx, "tools.bzl", rctx.attr.hub_name, tools) - # link to the executable - archive_file = "{archive_path}/{file}".format(archive_path = archive_path, file = binary["file"]) - if not rctx.path(archive_file).exists: - fail("{tool_name} ({os}, {cpu}): Cannot find {file} in archive from {url}".format( - tool_name = tool_name, - os = binary["os"], - cpu = binary["cpu"], - file = archive_file, - url = binary["url"], - )) - rctx.symlink(archive_file, target_executable) - else: - fail("Unknown 'kind' {kind}".format(kind = binary["kind"])) - - templates.env_tool(rctx, tool_name, "BUILD.bazel", {"{target_filename}": target_filename}) - - templates.env(rctx, "tools/BUILD.bazel") - templates.env(rctx, "BUILD.bazel") - -_env_specific_tools = repository_rule( +_workspace_hub = repository_rule( attrs = { + "hub_name": attr.string(mandatory = True), "lockfiles": attr.label_list(mandatory = True, allow_files = True), + }, + implementation = _workspace_hub_impl, +) + +def _tool_repo_impl(rctx): + _download_extract_tool(rctx, rctx.attr.tool_name, json.decode(rctx.attr.binary)) + templates.tool(rctx, "tools/BUILD.bazel") + templates.tool(rctx, "BUILD.bazel") + +tool_repo = repository_rule( + attrs = { + "tool_name": attr.string(), + "binary": attr.string(), "os": attr.string(), "cpu": attr.string(), }, - implementation = _env_specific_tools_impl, + implementation = _tool_repo_impl, ) -def _sort_fn(tup): - return tup[0] - def _multitool_hub_impl(rctx): - tools = _load_tools(rctx) + tools = lockfile.load_defs(rctx, rctx.attr.lockfiles) loads = [] defines = [] - for tool_name, tool in sorted(tools.items(), key = _sort_fn): + for tool_name, tool in lockfile.sorted_defs(tools): toolchains = [] for binary in tool["binaries"]: @@ -278,13 +233,45 @@ implementation = _multitool_hub_impl, ) -def hub(name, lockfiles): - "Create a multitool hub." - for env in _SUPPORTED_ENVS: - _env_specific_tools( - name = "{name}.{os}_{cpu}".format(name = name, os = env[0], cpu = env[1]), - lockfiles = lockfiles, - os = env[0], - cpu = env[1], - ) +def bzlmod_hub(name, lockfiles, module_ctx): + """ + Creates a multitool hub for bzlmod. + + Args: + name: name of the hub + lockfiles: a list of lockfile labels containing multitool lockfiles + module_ctx: a valid module_ctx instance + """ + + tools = lockfile.load_defs(module_ctx, lockfiles) + + for tool_name, tool in lockfile.sorted_defs(tools): + for binary in tool["binaries"]: + tool_repo( + name = "{name}.{tool_name}.{os}_{cpu}".format( + name = name, + tool_name = tool_name, + os = binary["os"], + cpu = binary["cpu"], + ), + tool_name = tool_name, + binary = json.encode(binary), + ) + + _multitool_hub(name = name, lockfiles = lockfiles) + +def workspace_hub(name, lockfiles): + """ + Creates a multitool hub for non-bzlmod. + + Args: + name: name of the hub + lockfiles: a list of lockfile labels containing multitool lockfiles + """ + + _workspace_hub( + name = "{name}.workspace".format(name = name), + hub_name = name, + lockfiles = lockfiles, + ) _multitool_hub(name = name, lockfiles = lockfiles)
diff --git a/multitool/private/templates.bzl b/multitool/private/templates.bzl index 91a5db8..35319cd 100644 --- a/multitool/private/templates.bzl +++ b/multitool/private/templates.bzl
@@ -1,22 +1,24 @@ "multitool templating" -_ENV_TEMPLATE = "//multitool/private:env_repo_template/{filename}.template" -_ENV_TOOL_TEMPLATE = "//multitool/private:env_repo_tool_template/{filename}.template" - _HUB_TEMPLATE = "//multitool/private:hub_repo_template/{filename}.template" _HUB_TOOL_TEMPLATE = "//multitool/private:hub_repo_tool_template/{filename}.template" -def _render_env(rctx, filename, substitutions = None): +_TOOL_TEMPLATE = "//multitool/private:tool_repo_template/{filename}.template" +_TOOL_TOOL_TEMPLATE = "//multitool/private:tool_repo_tool_template/{filename}.template" + +_WORKSPACE_TEMPLATE = "//multitool/private:workspace_hub_repo_template/{filename}.template" + +def _render_tool(rctx, filename, substitutions = None): rctx.template( filename, - Label(_ENV_TEMPLATE.format(filename = filename)), + Label(_TOOL_TEMPLATE.format(filename = filename)), substitutions = substitutions or {}, ) -def _render_env_tool(rctx, tool_name, filename, substitutions = None): +def _render_tool_tool(rctx, tool_name, filename, substitutions = None): rctx.template( "tools/{tool_name}/{filename}".format(tool_name = tool_name, filename = filename), - Label(_ENV_TOOL_TEMPLATE.format(filename = filename)), + Label(_TOOL_TOOL_TEMPLATE.format(filename = filename)), substitutions = { "{name}": tool_name, } | (substitutions or {}), @@ -38,9 +40,47 @@ } | (substitutions or {}), ) +def _render_tool_repo(hub_name, tool_name, binary): + name = "{name}.{tool_name}.{os}_{cpu}".format( + name = hub_name, + tool_name = tool_name, + os = binary["os"], + cpu = binary["cpu"], + ) + return "\n".join([ + " tool_repo(", + " name = \"{name}\",".format(name = name), + " tool_name = \"{tool_name}\",".format(tool_name = tool_name), + " binary = '{binary}',".format(binary = json.encode(binary)), + " )", + "", + ]) + +def _render_tool_repos(hub_name, tools): + if len(tools) == 0: + # in the case the tool dict is empty, ensure the function body we're + # templating into is non-empty + return " pass\n" + + return "\n".join([ + _render_tool_repo(hub_name, tool_name, binary) + for tool_name, tool in tools.items() + for binary in tool["binaries"] + ]) + +def _render_workspace(rctx, filename, hub_name, tools): + rctx.template( + filename, + Label(_WORKSPACE_TEMPLATE.format(filename = filename)), + substitutions = { + "{tool_repos}": _render_tool_repos(hub_name, tools), + }, + ) + templates = struct( - env = _render_env, - env_tool = _render_env_tool, hub = _render_hub, hub_tool = _render_hub_tool, + tool = _render_tool, + tool_tool = _render_tool_tool, + workspace = _render_workspace, )
diff --git a/multitool/private/env_repo_template/BUILD.bazel.template b/multitool/private/tool_repo_template/BUILD.bazel.template similarity index 100% rename from multitool/private/env_repo_template/BUILD.bazel.template rename to multitool/private/tool_repo_template/BUILD.bazel.template
diff --git a/multitool/private/env_repo_template/tools/BUILD.bazel.template b/multitool/private/tool_repo_template/tools/BUILD.bazel.template similarity index 100% rename from multitool/private/env_repo_template/tools/BUILD.bazel.template rename to multitool/private/tool_repo_template/tools/BUILD.bazel.template
diff --git a/multitool/private/env_repo_tool_template/BUILD.bazel.template b/multitool/private/tool_repo_tool_template/BUILD.bazel.template similarity index 100% rename from multitool/private/env_repo_tool_template/BUILD.bazel.template rename to multitool/private/tool_repo_tool_template/BUILD.bazel.template
diff --git a/multitool/private/env_repo_template/BUILD.bazel.template b/multitool/private/workspace_hub_repo_template/BUILD.bazel.template similarity index 100% copy from multitool/private/env_repo_template/BUILD.bazel.template copy to multitool/private/workspace_hub_repo_template/BUILD.bazel.template
diff --git a/multitool/private/workspace_hub_repo_template/tools.bzl.template b/multitool/private/workspace_hub_repo_template/tools.bzl.template new file mode 100644 index 0000000..a1c54d4 --- /dev/null +++ b/multitool/private/workspace_hub_repo_template/tools.bzl.template
@@ -0,0 +1,4 @@ +load("@rules_multitool//multitool/private:multitool.bzl", "tool_repo") + +def register_tools(): +{tool_repos}