Skip to content

Commit c2cf4c2

Browse files
authored
[ABLD-395] Framework to create toolchains for OS provided tools (#54177)
### What does this PR do? - Creates a tiny framework for finding an OS provided tool and telling Bazel it is a toolchain. - Use it for macos pkgbuild and codesign. The important feature of the way this is implemented is that the toolchain always safely resolves on all platforms, so you can `bazel cquery` across platforms, even though you will fail if you try to `build`. Combined with `exec_compatible_with` you can make targets that only build if the required tools are available. This is important when there are different tools available as OSes evolve (or are simply different by vendor, like debian and redhat) and we need to create paths that can gracefully use different tools depending on what is available. There is also a capability to use a bazel target as the tool implementation instead of looking it up in $PATH. That is useful for building alternate implementations (let's say a windows tool on linux). That will come into use in full remote execution mode, when the target and exec hosts can be different architectures. **Examples:** - I'm building a tarball, see if I have the xz toolchain and use that if available, otherwise fall back to a slower compresser - The OS is evolving a tool, if we have version 1, it might have one name. In version 2, there might be a better tool. We want to make rules that can do the right thing regardless of what OS we have. This is a huge win for external users who may be using a different base linux to build. ### Motivation This is an important part of our supply chain security posture. We want to account for every binary we use to build the product. Ideally that would include even basic low level things, like bash and cat. In practice we can make the tradeoff to stop at declaring the things that might vary across OSes or from OS release to release. Eventually, we will expand this the framework to include package metadata (https://github.com/bazel-contrib/supply-chain/tree/main/metadata) that can be easily gathered into a workspace BOM. The more immediate need is to create toolchains for pkgbuild and codesign to create rules to make macos packages. #54115 ### Describe how you validated your changes With the BUILD file ``` genrule( name = "test", cmd = select({ "@macos_codesign//:have_codesign": "echo GOT IT", "//conditions:default": "echo wump, wump, wump", }), outs = ["test.out"], ) ``` macos: ``` $ bazel cquery --output=build //ztony:test genrule( name = "test", outs = ["//ztony:test.out"], cmd = "echo GOT IT", ) $ bazel cquery @macos_codesign//:all INFO: Analyzed 7 targets (0 packages loaded, 6 targets configured). INFO: Found 7 targets... @macos_codesign//:codesign_auto (eab6d8f) @macos_codesign//:codesign_toolchain (eab6d8f) @macos_codesign//:codesign_toolchain_type (eab6d8f) @macos_codesign//:have_codesign (eab6d8f) @macos_codesign//:is_codesign_available (eab6d8f) @macos_codesign//:no_codesign (eab6d8f) @macos_codesign//:zzz_codesign_missing_toolchain (eab6d8f) ``` linux: ``` $ bazel cquery --output=build //ztony:test genrule( name = "test", outs = ["//ztony:test.out"], cmd = "echo wump, wump, wump", ) $ bazel cquery @macos_codesign//:all @macos_codesign//:codesign_auto (a938602) @macos_codesign//:codesign_toolchain (a938602) @macos_codesign//:codesign_toolchain_type (a938602) @macos_codesign//:have_codesign (a938602) @macos_codesign//:is_codesign_available (a938602) @macos_codesign//:no_codesign (a938602) @macos_codesign//:zzz_codesign_missing_toolchain (a938602) ``` ### Additional This is an alternate version of #54154. I think I like it better. [ABLD-386]: https://datadoghq.atlassian.net/browse/ABLD-386 Co-authored-by: tony.aiuto <tony.aiuto@datadoghq.com>
1 parent 29a80d4 commit c2cf4c2

9 files changed

Lines changed: 220 additions & 0 deletions

File tree

MODULE.bazel

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ archive_override(
126126
urls = ["https://github.com/bazel-contrib/toolchains_llvm/archive/ffe407ca2423f4d171b7a0f3897c23bbb7e40310.tar.gz"],
127127
)
128128

129+
#################################
130+
## Tools that ship with the OS ##
131+
#################################
132+
133+
find_macos_codesign = use_repo_rule("//bazel/toolchains/codesign:configure.bzl", "find_macos_codesign")
134+
135+
find_macos_codesign(
136+
name = "macos_codesign",
137+
exec_compatible_with = ["@platforms//os:macos"],
138+
)
139+
140+
find_macos_pkgbuild = use_repo_rule("//bazel/toolchains/pkgbuild:configure.bzl", "find_macos_pkgbuild")
141+
142+
find_macos_pkgbuild(
143+
name = "macos_pkgbuild",
144+
exec_compatible_with = ["@platforms//os:macos"],
145+
)
146+
147+
register_toolchains(
148+
"@macos_codesign//:all",
149+
"@macos_pkgbuild//:all",
150+
dev_dependency = True,
151+
)
152+
129153
#########################
130154
## Prebuilt binaries ##
131155
#########################
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""toolchain to wrap the codesign binary.
2+
3+
Type: @codesign//:codesign_toolchain_type
4+
5+
Toolchains:
6+
- @codesign//:codesign_toolchain: provides the tool
7+
- @codesign//:codesign_missing_toolchain: provides a fallback toolchain for
8+
exec platforms where codesign might not be available.
9+
"""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Repository rule to autoconfigure a toolchain using the system codesign."""
2+
3+
load("//bazel/toolchains/common:defs.bzl", "make_toolchain_repository_rule")
4+
5+
# This must match the repository name used by register_toolchains in consuming MODULE.bazel files.
6+
NAME = "macos_codesign"
7+
8+
find_macos_codesign = make_toolchain_repository_rule(name = NAME, tool_name = "codesign")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# //bazel/toolchains/common

bazel/toolchains/common/defs.bzl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Utilitites for creating toolchains to wrap system provided tools."""
2+
3+
def _write_toolchain_repo(rctx, repo_name, tool_name, tool_path, tool_version = "<unknown>", exec_compatible_with = None):
4+
if not tool_path:
5+
tool_path = ""
6+
rctx.template(
7+
"BUILD",
8+
Label("@@//bazel/toolchains/common:toolchain_BUILD.tpl"),
9+
substitutions = {
10+
"{AVAILABLE}": "1" if tool_path else "0",
11+
"{EXEC_COMPATIBLE_WITH}": repr(exec_compatible_with),
12+
"{GENERATOR}": "//bazel/toolchains/common:defs.bzl",
13+
"{REPO_NAME}": repo_name,
14+
"{TOOL_NAME}": tool_name,
15+
"{TOOL_PATH}": str(tool_path),
16+
"{TOOL_VERSION}": tool_version,
17+
},
18+
executable = False,
19+
)
20+
rctx.template(
21+
"defs.bzl",
22+
Label("@@//bazel/toolchains/common:toolchain_defs.bzl.tpl"),
23+
substitutions = {
24+
"{AVAILABLE}": "1" if tool_path else "0",
25+
"{GENERATOR}": "//bazel/toolchains/common:defs.bzl",
26+
"{REPO_NAME}": repo_name,
27+
"{TOOL_NAME}": tool_name,
28+
},
29+
executable = False,
30+
)
31+
32+
def _default_repo_builder_impl(rctx):
33+
tool_name = rctx.attr.tool_name
34+
tool_path = rctx.which(tool_name)
35+
if rctx.attr.verbose:
36+
if tool_path:
37+
print("Found %s at '%s'" % (tool_name, tool_path)) # buildifier: disable=print
38+
else:
39+
print("No system %s found." % tool_name) # buildifier: disable=print
40+
_write_toolchain_repo(
41+
rctx = rctx,
42+
repo_name = rctx.original_name,
43+
tool_name = rctx.attr.tool_name,
44+
tool_path = tool_path,
45+
exec_compatible_with = rctx.attr.exec_compatible_with,
46+
)
47+
48+
def make_toolchain_repository_rule(name, tool_name, impl = _default_repo_builder_impl):
49+
return repository_rule(
50+
implementation = impl,
51+
doc = """Create a repository that defines a {name} toolchain based on tool in the default $PATH.""".format(name = name),
52+
local = True,
53+
environ = ["PATH"],
54+
attrs = {
55+
"tool_name": attr.string(doc = "The name of the tool to find.", default = tool_name),
56+
"exec_compatible_with": attr.string_list(
57+
doc = "exec_compatible_with list to apply to the created toolchain.",
58+
),
59+
"verbose": attr.bool(
60+
doc = "If true, print status messages.",
61+
),
62+
},
63+
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# This content is generated by {GENERATOR}
2+
load(":defs.bzl", "is_{TOOL_NAME}_available", "{TOOL_NAME}_toolchain")
3+
4+
toolchain_type(
5+
name = "{TOOL_NAME}_toolchain_type",
6+
visibility = ["//visibility:public"],
7+
)
8+
9+
{TOOL_NAME}_toolchain(
10+
name = "{TOOL_NAME}_auto",
11+
path = "{TOOL_PATH}",
12+
version = "{TOOL_VERSION}",
13+
)
14+
15+
toolchain(
16+
name = "{TOOL_NAME}_toolchain",
17+
toolchain = ":{TOOL_NAME}_auto",
18+
toolchain_type = ":{TOOL_NAME}_toolchain_type",
19+
exec_compatible_with = {EXEC_COMPATIBLE_WITH},
20+
)
21+
22+
# {TOOL_NAME}_missing_toolchain provides a fallback toolchain so that toolchain
23+
# resolution can succeed even on platforms that do not have that tool.
24+
# If this toolchain is selected, a rule can examine the valid field to determine
25+
# if the tool is really available.
26+
{TOOL_NAME}_toolchain(
27+
name = "no_{TOOL_NAME}",
28+
)
29+
30+
toolchain(
31+
name = "zzz_{TOOL_NAME}_missing_toolchain", # keep name lexicographically last
32+
toolchain = ":no_{TOOL_NAME}",
33+
toolchain_type = ":{TOOL_NAME}_toolchain_type",
34+
)
35+
36+
# Expose the availability of the actual tool as a flag, so we can
37+
# create a config_setting from it.
38+
is_{TOOL_NAME}_available(
39+
name = "is_{TOOL_NAME}_available",
40+
build_setting_default = {AVAILABLE},
41+
)
42+
43+
# Expose the availability of the toolchain as a config_setting, so we can
44+
# select() on it.
45+
config_setting(
46+
name = "have_{TOOL_NAME}",
47+
flag_values = {
48+
":is_{TOOL_NAME}_available": "1",
49+
},
50+
visibility = ["//visibility:public"],
51+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""toolchain to provide the {TOOL_NAME} binary."""
2+
3+
load("@@//bazel/toolchains:toolchain_info.bzl", "ToolInfo")
4+
5+
def _{TOOL_NAME}_toolchain_impl(ctx):
6+
if ctx.attr.label and ctx.attr.path:
7+
fail("{TOOL_NAME}_toolchain must not specify both label and path.")
8+
valid = bool(ctx.attr.label) or bool(ctx.attr.path)
9+
toolchain_info = platform_common.ToolchainInfo(
10+
{TOOL_NAME} = ToolInfo(
11+
name = str(ctx.label),
12+
valid = valid,
13+
label = ctx.attr.label,
14+
path = ctx.attr.path,
15+
version = ctx.attr.version,
16+
),
17+
)
18+
return [toolchain_info]
19+
20+
{TOOL_NAME}_toolchain = rule(
21+
implementation = _{TOOL_NAME}_toolchain_impl,
22+
attrs = {
23+
"label": attr.label(
24+
doc = "A valid label of a target to build or a prebuilt binary. Mutually exclusive with path.",
25+
cfg = "exec",
26+
executable = True,
27+
allow_files = True,
28+
),
29+
"path": attr.string(
30+
doc = "The path to the executable. Mutually exclusive with label.",
31+
),
32+
"version": attr.string(
33+
doc = "The version string of the executable. This should be manually set.",
34+
),
35+
},
36+
)
37+
38+
# Expose the presence of {TOOL_NAME} as a flag.
39+
def _is_{TOOL_NAME}_available_impl(ctx):
40+
return [config_common.FeatureFlagInfo(
41+
value = ("1" if ctx.build_setting_value else "0"),
42+
)]
43+
44+
is_{TOOL_NAME}_available = rule(
45+
implementation = _is_{TOOL_NAME}_available_impl,
46+
attrs = {},
47+
build_setting = config.bool(flag = False),
48+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Toolchain to wrap the pkgbuild binary.
2+
3+
Type: @macos_pkgbuild//:tool_toolchain_type
4+
5+
Toolchains:
6+
- @macos_pkgbuild//:macos_pkgbuild_toolchain: provides the tool
7+
- @macos_pkgbuild//:macos_pkgbuild_missing_toolchain: provides a fallback toolchain for exec platforms where pkgbuild might not be available.
8+
"""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Repository rule to autoconfigure a toolchain using the system pkgbuild."""
2+
3+
load("//bazel/toolchains/common:defs.bzl", "make_toolchain_repository_rule")
4+
5+
# This must match the name used by register_toolchains in MODULE.bazel.
6+
NAME = "macos_pkgbuild"
7+
8+
find_macos_pkgbuild = make_toolchain_repository_rule(name = NAME, tool_name = "pkgbuild")

0 commit comments

Comments
 (0)