Skip to content

Commit e708301

Browse files
authored
feat: support raw symlinks from declare_symlink() (#930)
Using the `ctx.actions.declare_symlink()` API, rules can create explicit symlinks to arbitrary paths, which Bazel will keep as symlinks. The packaging logic treats these as regular files, so tries to read through them and store the content they point to. This is incorrect for two reasons: they are supposed to be left as symlinks and may be dangling symlinks from the perspective of packaging. To fix, introduce a new type of file entry: `raw_symlink`. These entries mean the `src` is a symlink whose stored path should be stored into the destination verbatim, also as a symlink. Such artifacts are identified using `File.is_symlink`, a Bazel 8+ API. Work towards #929
2 parents cd7e108 + 5695833 commit e708301

File tree

6 files changed

+63
-3
lines changed

6 files changed

+63
-3
lines changed

pkg/private/manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020

2121
# These must be kept in sync with the declarations in private/pkg_files.bzl
22+
ENTRY_IS_RAW_LINK = "raw_symlink" # Entry is a file: take content from <src>
2223
ENTRY_IS_FILE = "file" # Entry is a file: take content from <src>
2324
ENTRY_IS_LINK = "symlink" # Entry is a symlink: dest -> <src>
2425
ENTRY_IS_DIR = "dir" # Entry is an empty dir

pkg/private/pkg_files.bzl

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ load(
4545
"get_repo_mapping_manifest",
4646
)
4747

48+
ENTRY_IS_RAW_LINK = "raw_symlink" # Entry is a symlink kept as-is
4849
ENTRY_IS_FILE = "file" # Entry is a file: take content from <src>
4950
ENTRY_IS_LINK = "symlink" # Entry is a symlink: dest -> <src>
5051
ENTRY_IS_DIR = "dir" # Entry is an empty dir
@@ -444,9 +445,16 @@ def add_from_default_info(
444445
d_path = mapping_context.path_mapper(base_path + "/" + rf.short_path)
445446
fmode = "0755" if rf == the_executable else mapping_context.default_mode
446447
_check_dest(mapping_context.content_map, d_path, rf, src.label, mapping_context.allow_duplicates_with_different_content)
448+
if hasattr(rf, "is_symlink") and rf.is_symlink: # File.is_symlink is Bazel 8+
449+
entry_type = ENTRY_IS_RAW_LINK
450+
elif rf.is_directory:
451+
entry_type = ENTRY_IS_TREE
452+
else:
453+
entry_type = ENTRY_IS_FILE
454+
447455
mapping_context.content_map[d_path] = _DestFile(
448456
src = rf,
449-
entry_type = ENTRY_IS_TREE if rf.is_directory else ENTRY_IS_FILE,
457+
entry_type = entry_type,
450458
origin = src.label,
451459
mode = fmode,
452460
user = mapping_context.default_user,
@@ -531,9 +539,15 @@ def add_single_file(mapping_context, dest_path, src, origin, mode = None, user =
531539
"""
532540
dest = dest_path.strip("/")
533541
_check_dest(mapping_context.content_map, dest, src, origin, mapping_context.allow_duplicates_with_different_content)
542+
543+
if hasattr(src, "is_symlink") and src.is_symlink: # File.is_symlink is Bazel 8+
544+
entry_type = ENTRY_IS_RAW_LINK
545+
else:
546+
entry_type = ENTRY_IS_FILE
547+
534548
mapping_context.content_map[dest] = _DestFile(
535549
src = src,
536-
entry_type = ENTRY_IS_FILE,
550+
entry_type = entry_type,
537551
origin = origin,
538552
mode = mode,
539553
user = user or mapping_context.default_user,

pkg/private/tar/build_tar.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ def add_manifest_entry(self, entry, file_attributes):
332332
attrs['ids'] = (entry.uid, entry.uid)
333333
if entry.type == manifest.ENTRY_IS_LINK:
334334
self.add_link(entry.dest, entry.src, **attrs)
335+
elif entry.type == manifest.ENTRY_IS_RAW_LINK:
336+
self.add_link(entry.dest, os.readlink(entry.src), **attrs)
335337
elif entry.type == manifest.ENTRY_IS_DIR:
336338
self.add_empty_dir(self.normalize_path(entry.dest), **attrs)
337339
elif entry.type == manifest.ENTRY_IS_TREE:

pkg/verify_archive_test_main.py.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class VerifyArchiveTest(unittest.TestCase):
109109
def verify_links(self, verify_links):
110110
for link, target in verify_links.items():
111111
if link not in self.paths:
112-
self.fail('Required link (%s) is not in the archive' % link)
112+
self.fail('Required link (%s) is not in the archive, found %s' % (link, self.paths))
113113
if self.links[link] != target:
114114
self.fail('link (%s) points to the wrong place. Expected (%s) got (%s)' %
115115
(link, target, self.links[link]))

tests/tar/BUILD

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ load("//pkg:verify_archive.bzl", "verify_archive_test")
2424
load("//pkg/private/tar:tar.bzl", "SUPPORTED_TAR_COMPRESSIONS", "pkg_tar")
2525
load("//tests:my_package_name.bzl", "my_package_naming")
2626
load("//tests/util:defs.bzl", "directory", "fake_artifact", "link_tree")
27+
load(":defs.bzl", "raw_symlinks")
2728

2829
package(
2930
default_applicable_licenses = ["//:license"],
@@ -629,6 +630,31 @@ pkg_tar(
629630
# Test with symlinks
630631
#
631632

633+
raw_symlinks(name = "raw_symlinks")
634+
635+
pkg_tar(
636+
name = "raw_symlinks_tar",
637+
srcs = [":raw_symlinks"],
638+
include_runfiles = True,
639+
)
640+
641+
verify_archive_test(
642+
name = "raw_symlinks_test",
643+
tags = ["no_windows"],
644+
target = ":raw_symlinks_tar",
645+
# This test checks using relative symlinks, but Bazel always writes absolute
646+
# symlinks on windows.
647+
# See https://github.com/bazelbuild/bazel/issues/14224
648+
target_compatible_with = select({
649+
"@platforms//os:windows": ["@platforms//:incompatible"],
650+
"//conditions:default": [],
651+
}),
652+
verify_links = {
653+
"raw_symlinks_link1": "link1",
654+
"raw_symlinks_link1.runfiles/_main/tests/tar/raw_symlinks_runfile_link": "../runfile_link",
655+
},
656+
)
657+
632658
link_tree(
633659
name = "node_modules",
634660
links = {

tests/tar/defs.bzl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Helpers for testing tar packaging."""
2+
3+
def _raw_symlinks_impl(ctx):
4+
link1 = ctx.actions.declare_symlink(ctx.label.name + "_link1")
5+
ctx.actions.symlink(output = link1, target_path = "./link1")
6+
7+
runfile_link = ctx.actions.declare_symlink(ctx.label.name + "_runfile_link")
8+
ctx.actions.symlink(output = runfile_link, target_path = "../runfile_link")
9+
10+
return [DefaultInfo(
11+
files = depset([link1]),
12+
runfiles = ctx.runfiles([runfile_link]),
13+
)]
14+
15+
raw_symlinks = rule(
16+
implementation = _raw_symlinks_impl,
17+
)

0 commit comments

Comments
 (0)