Skip to content

Commit deb8934

Browse files
committed
basic impl
1 parent ec1df01 commit deb8934

File tree

3 files changed

+129
-8
lines changed

3 files changed

+129
-8
lines changed

python/private/py_info.bzl

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,17 @@ VenvSymlinkKind = struct(
4747
INCLUDE = "INCLUDE",
4848
)
4949

50+
def _VenvSymlinkEntry_init(**kwargs):
51+
kwargs.setdefault("link_to_file", None)
52+
return kwargs
53+
5054
# A provider is used for memory efficiency.
5155
# buildifier: disable=name-conventions
52-
VenvSymlinkEntry = provider(
56+
VenvSymlinkEntry, _ = provider(
5357
doc = """
5458
An entry in `PyInfo.venv_symlinks`
5559
""",
60+
init = _VenvSymlinkEntry_init,
5661
fields = {
5762
"files": """
5863
:type: depset[File]
@@ -67,12 +72,18 @@ if one adds files to `venv_path=a/` and another adds files to `venv_path=a/b/`.
6772
6873
One of the {obj}`VenvSymlinkKind` values. It represents which directory within
6974
the venv to create the path under.
75+
""",
76+
"link_to_file": """
77+
:type: File | None
78+
79+
A file that `venv_path` should point to. The file to link to should also be in
80+
`files`.
7081
""",
7182
"link_to_path": """
7283
:type: str | None
7384
74-
A runfiles-root relative path that `venv_path` will symlink to. If `None`,
75-
it means to not create a symlink.
85+
A runfiles-root relative path that `venv_path` will symlink to (if
86+
`link_to_file` is None). If `None`, it means to not create it in the venv.
7687
""",
7788
"package": """
7889
:type: str | None

python/private/venv_runfiles.bzl

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def build_link_map(ctx, entries):
8181
Returns:
8282
{type}`dict[str, dict[str, str|File]]` Mappings of venv paths to their
8383
backing files. The first key is a `VenvSymlinkKind` value.
84-
The inner dict keys are venv paths relative to the kind's diretory. The
84+
The inner dict keys are venv paths relative to the kind's directory. The
8585
inner dict values are strings or Files to link to.
8686
"""
8787

@@ -116,7 +116,10 @@ def build_link_map(ctx, entries):
116116
# If there's just one group, we can symlink to the directory
117117
if len(group) == 1:
118118
entry = group[0]
119-
keep_kind_link_map[entry.venv_path] = entry.link_to_path
119+
if entry.link_to_file:
120+
keep_kind_link_map[entry.venv_path] = entry.link_to_file
121+
else:
122+
keep_kind_link_map[entry.venv_path] = entry.link_to_path
120123
else:
121124
# Merge a group of overlapping prefixes
122125
_merge_venv_path_group(ctx, group, keep_kind_link_map)
@@ -170,7 +173,9 @@ def _merge_venv_path_group(ctx, group, keep_map):
170173
# TODO: Compute the minimum number of entries to create. This can't avoid
171174
# flattening the files depset, but can lower the number of materialized
172175
# files significantly. Usually overlaps are limited to a small number
173-
# of directories.
176+
# of directories. Note that, when doing so, shared libraries need to
177+
# be symlinked directory, not the directory containing them, due to
178+
# symlink resolution semantics on Linux.
174179
for entry in group:
175180
prefix = entry.venv_path
176181
for file in entry.files.to_list():
@@ -247,13 +252,27 @@ def get_venv_symlinks(ctx, files, package, version_str, site_packages_root):
247252
continue
248253
path = path.removeprefix(site_packages_root)
249254
dir_name, _, filename = path.rpartition("/")
255+
runfiles_dir_name, _, _ = runfiles_root_path(ctx, src.short_path).partition("/")
256+
257+
if _is_linker_loaded_library(filename):
258+
entry = VenvSymlinkEntry(
259+
kind = VenvSymlinkKind.LIB,
260+
# todo: omit setting link_to_path
261+
link_to_path = paths.join(runfiles_dir_name, site_packages_root, filename),
262+
link_to_file = src,
263+
package = package,
264+
version = version_str,
265+
venv_path = path,
266+
files = depset([src]),
267+
)
268+
venv_symlinks.append(entry)
269+
continue
250270

251271
if dir_name in dir_symlinks:
252272
# we already have this dir, this allows us to short-circuit since most of the
253273
# ctx.files.data might share the same directories as ctx.files.srcs
254274
continue
255275

256-
runfiles_dir_name, _, _ = runfiles_root_path(ctx, src.short_path).partition("/")
257276
if dir_name:
258277
# This can be either:
259278
# * a directory with libs (e.g. numpy.libs, created by auditwheel)
@@ -276,6 +295,8 @@ def get_venv_symlinks(ctx, files, package, version_str, site_packages_root):
276295
files = depset([src]),
277296
)
278297
venv_symlinks.append(entry)
298+
else:
299+
fail("hit", src)
279300

280301
# Sort so that we encounter `foo` before `foo/bar`. This ensures we
281302
# see the top-most explicit package first.
@@ -310,6 +331,21 @@ def get_venv_symlinks(ctx, files, package, version_str, site_packages_root):
310331

311332
return venv_symlinks
312333

334+
def _is_linker_loaded_library(filename):
335+
"""Tells if a filename is one that `dlopen()` or the runtime linker handle.
336+
337+
Notably, it should return False for Python C extension modules.
338+
"""
339+
if not filename.startswith("lib"):
340+
return False
341+
if filename.endswith((".so", ".dylib")):
342+
return True
343+
344+
# Versioned library, e.g. libfoo.so.1.2.3
345+
if ".so." in filename:
346+
return True
347+
return False
348+
313349
def _repo_relative_short_path(short_path):
314350
# Convert `../+pypi+foo/some/file.py` to `some/file.py`
315351
if short_path.startswith("../"):

tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,25 @@ load("@bazel_skylib//lib:paths.bzl", "paths")
44
load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
55
load("@rules_testing//lib:test_suite.bzl", "test_suite")
66
load("//python/private:py_info.bzl", "VenvSymlinkEntry", "VenvSymlinkKind") # buildifier: disable=bzl-visibility
7-
load("//python/private:venv_runfiles.bzl", "build_link_map") # buildifier: disable=bzl-visibility
7+
load("//python/private:venv_runfiles.bzl", "build_link_map", "get_venv_symlinks") # buildifier: disable=bzl-visibility
8+
9+
def _empty_files_impl(ctx):
10+
files = []
11+
for p in ctx.attr.paths:
12+
f = ctx.actions.declare_file(p)
13+
ctx.actions.write(output = f, content = "")
14+
files.append(f)
15+
return [DefaultInfo(files = depset(files))]
16+
17+
empty_files = rule(
18+
implementation = _empty_files_impl,
19+
attrs = {
20+
"paths": attr.string_list(
21+
doc = "A list of paths to create as files.",
22+
mandatory = True,
23+
),
24+
},
25+
)
826

927
_tests = []
1028

@@ -240,6 +258,62 @@ def _test_multiple_venv_symlink_kinds_impl(env, _):
240258
VenvSymlinkKind.INCLUDE,
241259
])
242260

261+
def _test_shared_library_symlinking(name):
262+
empty_files(
263+
name = name + "_files",
264+
# NOTE: Test relies upon order
265+
paths = [
266+
"site-packages/bar/libs/liby.so",
267+
"site-packages/bar/x.py",
268+
"site-packages/bar/y.so",
269+
"site-packages/foo.libs/libx.so",
270+
"site-packages/foo/a.py",
271+
"site-packages/foo/b.so",
272+
],
273+
)
274+
analysis_test(
275+
name = name,
276+
impl = _test_shared_library_symlinking_impl,
277+
target = name + "_files",
278+
)
279+
280+
_tests.append(_test_shared_library_symlinking)
281+
282+
def _test_shared_library_symlinking_impl(env, target):
283+
srcs = target.files.to_list()
284+
actual_entries = get_venv_symlinks(
285+
_ctx(),
286+
srcs,
287+
package = "foo",
288+
version_str = "1.0",
289+
site_packages_root = env.ctx.label.package + "/site-packages",
290+
)
291+
292+
actual = [e for e in actual_entries if e.venv_path == "foo.libs/libx.so"]
293+
if not actual:
294+
fail("Did not find VenvSymlinkEntry with venv_path equal to foo.libs/libx.so. " +
295+
"Found: {}".format(actual_entries))
296+
elif len(actual) > 1:
297+
fail("Found multiple entries with venv_path=foo.libs/libx.so. " +
298+
"Found: {}".format(actual_entries))
299+
actual = actual[0]
300+
301+
actual_files = actual.files.to_list()
302+
expected_lib_dso = [f for f in srcs if f.basename == "libx.so"]
303+
env.expect.that_collection(actual_files).contains_exactly(expected_lib_dso)
304+
305+
entries = actual_entries
306+
actual = build_link_map(_ctx(), entries)
307+
308+
expected_libs = {
309+
"bar/libs/liby.so": srcs[0],
310+
"bar/x.py": srcs[1],
311+
"bar/y.so": srcs[2],
312+
"foo": "_main/tests/venv_site_packages_libs/app_files_building/site-packages/foo",
313+
"foo.libs/libx.so": srcs[3],
314+
}
315+
env.expect.that_dict(actual[VenvSymlinkKind.LIB]).contains_exactly(expected_libs)
316+
243317
def app_files_building_test_suite(name):
244318
test_suite(
245319
name = name,

0 commit comments

Comments
 (0)