Skip to content

Commit

Permalink
Change the bazel-out structure to avoid busybox symlinks. (#4505)
Browse files Browse the repository at this point in the history
As described in symlink_helpers.bzl, copied here for visibility:

Symlinking busybox things needs special logic.

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:
bazelbuild/bazel#23620
  • Loading branch information
jonmeow authored Nov 8, 2024
1 parent db43bb1 commit 6dce164
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/project/contribution_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions toolchain/install/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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": [
Expand All @@ -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],
}
Expand Down
39 changes: 34 additions & 5 deletions toolchain/install/install_filegroups.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,19 +40,17 @@ 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.
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,
}
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 26 additions & 5 deletions toolchain/install/run_tool.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -30,6 +50,7 @@ run_tool = rule(
allow_single_file = True,
executable = True,
cfg = "target",
mandatory = True,
),
},
executable = True,
Expand Down
108 changes: 85 additions & 23 deletions toolchain/install/symlink_helpers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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),
},
)

0 comments on commit 6dce164

Please sign in to comment.