diff --git a/docs/project/contribution_tools.md b/docs/project/contribution_tools.md index d0d67b5707b2..c90405b20c7e 100644 --- a/docs/project/contribution_tools.md +++ b/docs/project/contribution_tools.md @@ -305,7 +305,7 @@ bazel build -c dbg //toolchain Then debugging works with LLDB: ```shell -lldb bazel-bin/toolchain/install/prefix_root/bin/carbon +lldb bazel-bin/toolchain/install/prefix_root/lib/carbon/carbon-busybox ``` Any installed version of LLDB at least as recent as the installed Clang used for diff --git a/toolchain/install/BUILD b/toolchain/install/BUILD index d8857b11e847..87a56629ae8d 100644 --- a/toolchain/install/BUILD +++ b/toolchain/install/BUILD @@ -5,7 +5,7 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") load("//bazel/cc_toolchains:defs.bzl", "cc_env") load("//bazel/manifest:defs.bzl", "manifest") -load("install_filegroups.bzl", "install_filegroup", "install_symlink", "install_target", "make_install_filegroups") +load("install_filegroups.bzl", "install_busybox_wrapper", "install_filegroup", "install_symlink", "install_target", "make_install_filegroups") load("pkg_helpers.bzl", "pkg_naming_variables", "pkg_tar_and_test") load("run_tool.bzl", "run_tool") @@ -108,10 +108,9 @@ lld_aliases = [ # based on the FHS (Filesystem Hierarchy Standard). install_dirs = { "bin": [ - install_symlink( + install_busybox_wrapper( "carbon", "../lib/carbon/carbon-busybox", - is_driver = True, ), ], "lib/carbon": [ @@ -130,10 +129,13 @@ install_dirs = { "@llvm-project//lld:lld", executable = True, ), - install_symlink( + install_busybox_wrapper( "clang", "../../carbon-busybox", - is_driver = True, + [ + "clang", + "--", + ], ), ] + [install_symlink(name, "lld") for name in lld_aliases], } diff --git a/toolchain/install/install_filegroups.bzl b/toolchain/install/install_filegroups.bzl index eb938af81e40..95df1db34a4f 100644 --- a/toolchain/install/install_filegroups.bzl +++ b/toolchain/install/install_filegroups.bzl @@ -5,7 +5,25 @@ """Rules for constructing install information.""" load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_filegroup", "pkg_files", "pkg_mklink", "strip_prefix") -load("symlink_helpers.bzl", "symlink_file", "symlink_filegroup") +load("symlink_helpers.bzl", "busybox_wrapper", "symlink_file", "symlink_filegroup") + +def install_busybox_wrapper(name, busybox, busybox_args = []): + """Adds a busybox wrapper for install. + + Used in the `install_dirs` dict. + + Args: + name: The filename to use. + busybox: A relative path for the busybox. + busybox_args: Arguments needed to simulate busybox when a symlink isn't + actually used. + """ + return { + "busybox": busybox, + "busybox_args": busybox_args, + "is_driver": True, + "name": name, + } def install_filegroup(name, filegroup_target): """Adds a filegroup for install. @@ -22,7 +40,7 @@ def install_filegroup(name, filegroup_target): "name": name, } -def install_symlink(name, symlink_to, is_driver = False): +def install_symlink(name, symlink_to): """Adds a symlink for install. Used in the `install_dirs` dict. @@ -30,11 +48,9 @@ def install_symlink(name, symlink_to, is_driver = False): Args: name: The filename to use. symlink_to: A relative path for the symlink. - is_driver: False if it should be included in the `no_driver_name` - filegroup. """ return { - "is_driver": is_driver, + "is_driver": False, "name": name, "symlink": symlink_to, } @@ -106,6 +122,19 @@ def make_install_filegroups(name, no_driver_name, pkg_name, install_dirs, prefix attributes = pkg_attributes(mode = mode), renames = {entry["target"]: path}, ) + elif "busybox" in entry: + busybox_wrapper( + name = prefixed_path, + symlink = entry["busybox"], + busybox_args = entry["busybox_args"], + ) + + # For the distributed package, we retain relative symlinks. + pkg_mklink( + name = pkg_path, + link_name = path, + target = entry["busybox"], + ) elif "filegroup" in entry: symlink_filegroup( name = prefixed_path, diff --git a/toolchain/install/run_tool.bzl b/toolchain/install/run_tool.bzl index 1f74f60ebd9e..eaf2e51a75e3 100644 --- a/toolchain/install/run_tool.bzl +++ b/toolchain/install/run_tool.bzl @@ -4,14 +4,34 @@ """Supports running a tool from the install filegroup.""" +_RUN_TOOL_TMPL = """#!/usr/bin/env python3 + +import os +import sys + +# These will be relative locations in bazel-out. +_SCRIPT_LOCATION = "{0}" +_TOOL_LOCATION = "{1}" + +# Make sure we have the expected structure. +if not __file__.endswith(_SCRIPT_LOCATION): + exit( + "Unable to figure out path:\\n" + f" __file__: {{__file__}}\\n" + f" script: {{_SCRIPT_LOCATION}}\\n" + ) + +# Run the tool using the absolute path, forwarding arguments. +tool_path = __file__.removesuffix(_SCRIPT_LOCATION) + _TOOL_LOCATION +os.execv(tool_path, [tool_path] + sys.argv[1:]) +""" + def _run_tool_impl(ctx): - tool_files = ctx.attr.tool.files.to_list() - if len(tool_files) != 1: - fail("Expected 1 tool file, found {0}".format(len(tool_files))) - ctx.actions.symlink( + content = _RUN_TOOL_TMPL.format(ctx.outputs.executable.path, ctx.file.tool.path) + ctx.actions.write( output = ctx.outputs.executable, - target_file = tool_files[0], is_executable = True, + content = content, ) return [ DefaultInfo( @@ -30,6 +50,7 @@ run_tool = rule( allow_single_file = True, executable = True, cfg = "target", + mandatory = True, ), }, executable = True, diff --git a/toolchain/install/symlink_helpers.bzl b/toolchain/install/symlink_helpers.bzl index f3efe71d8ab1..c39fdb541cbe 100644 --- a/toolchain/install/symlink_helpers.bzl +++ b/toolchain/install/symlink_helpers.bzl @@ -4,36 +4,66 @@ """Rules for symlinking in ways that assist install_filegroups.""" -def _symlink_filegroup_impl(ctx): - prefix = ctx.attr.out_prefix +_SYMLINK_BUSYBOX_TMPL = """#!/usr/bin/env python3 - outputs = [] - for f in ctx.files.srcs: - # We normalize the path to be package-relative in order to ensure - # consistent paths across possible repositories. - relative_path = f.short_path.removeprefix(f.owner.package) +from pathlib import Path +import os +import sys - out = ctx.actions.declare_file(prefix + relative_path) - outputs.append(out) - ctx.actions.symlink(output = out, target_file = f) +_RELATIVE_PATH = "{0}" +_BUSYBOX_ARGS = {1} - if len(ctx.files.srcs) != len(outputs): - fail("Output count mismatch!") +# Run the tool using the absolute path, forwarding arguments. +tool_path = Path(__file__).parent / _RELATIVE_PATH +os.execv(tool_path, [tool_path] + _BUSYBOX_ARGS + sys.argv[1:]) +""" - return [ - DefaultInfo( - files = depset(direct = outputs), - default_runfiles = ctx.runfiles(files = outputs), - ), - ] +def _busybox_wrapper_impl(ctx): + """Symlinking busybox things needs special logic. -symlink_filegroup = rule( - doc = "Symlinks an entire filegroup, preserving its structure", - implementation = _symlink_filegroup_impl, + This is because Bazel doesn't cache the actual symlink, resulting in + essentially resolved symlinks being produced in place of the expected tool. + As a consequence, we can't rely on the symlink name when dealing with + busybox entries. + + An example repro of this using a local build cache is: + bazel build //toolchain + bazel clean + bazel build //toolchain + + We could in theory get reasonable behavior with + `ctx.actions.declare_symlink`, but that's disallowed in our `.bazelrc` for + cross-environment compatibility. + + The particular approach here uses the Python script as a launching pad so + that the busybox still receives an appropriate location in argv[0], allowing + it to find other files in the lib directory. Arguments are inserted to get + equivalent behavior as if symlink resolution had occurred. + + The underlying bug is noted at: + https://github.com/bazelbuild/bazel/issues/23620 + """ + content = _SYMLINK_BUSYBOX_TMPL.format( + ctx.attr.symlink, + ctx.attr.busybox_args, + ) + ctx.actions.write( + output = ctx.outputs.executable, + is_executable = True, + content = content, + ) + return [] + +busybox_wrapper = rule( + doc = "Helper for running a busybox with symlink-like characteristics.", + implementation = _busybox_wrapper_impl, attrs = { - "out_prefix": attr.string(mandatory = True), - "srcs": attr.label_list(mandatory = True), + "busybox_args": attr.string_list( + doc = "Optional arguments to pass for equivalent behavior to a symlink.", + ), + "symlink": attr.string(mandatory = True), }, + executable = True, ) def _symlink_file_impl(ctx): @@ -75,3 +105,35 @@ symlink_file = rule( "symlink_label": attr.label(allow_single_file = True), }, ) + +def _symlink_filegroup_impl(ctx): + prefix = ctx.attr.out_prefix + + outputs = [] + for f in ctx.files.srcs: + # We normalize the path to be package-relative in order to ensure + # consistent paths across possible repositories. + relative_path = f.short_path.removeprefix(f.owner.package) + + out = ctx.actions.declare_file(prefix + relative_path) + outputs.append(out) + ctx.actions.symlink(output = out, target_file = f) + + if len(ctx.files.srcs) != len(outputs): + fail("Output count mismatch!") + + return [ + DefaultInfo( + files = depset(direct = outputs), + default_runfiles = ctx.runfiles(files = outputs), + ), + ] + +symlink_filegroup = rule( + doc = "Symlinks an entire filegroup, preserving its structure", + implementation = _symlink_filegroup_impl, + attrs = { + "out_prefix": attr.string(mandatory = True), + "srcs": attr.label_list(mandatory = True), + }, +)