Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ use_repo(
"somepkg_with_build_files",
"whl_library_extras_direct_dep",
"whl_with_build_files",
"whl_with_data",
)

dev_rules_python_config = use_extension(
Expand Down
2 changes: 1 addition & 1 deletion python/config_settings/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ string_flag(
)

config_setting(
name = "is_venvs_site_packages",
name = "_is_venvs_site_packages_yes",
flag_values = {
":venvs_site_packages": VenvsSitePackages.YES,
},
Expand Down
11 changes: 11 additions & 0 deletions python/private/internal_dev_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ def _internal_dev_deps_impl(mctx):
enable_implicit_namespace_pkgs = False,
)

whl_from_dir_repo(
name = "whl_with_data_whl",
root = "//tests/repos/whl_with_data:BUILD.bazel",
output = "whl_with_data-1.0-any-none-any.whl",
)
whl_library(
name = "whl_with_data",
whl_file = "@whl_with_data_whl//:whl_with_data-1.0-any-none-any.whl",
requirement = "whl-with-data",
)

_whl_library_from_dir(
name = "whl_library_extras_direct_dep",
root = "//tests/pypi/whl_library/testdata/pkg:BUILD.bazel",
Expand Down
23 changes: 15 additions & 8 deletions python/private/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,10 @@ def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root
)

venv_dir_map = {
VenvSymlinkKind.BIN: venv_details.bin_dir,
VenvSymlinkKind.BIN: "{}/{}".format(venv_ctx_rel_root, venv_details.bin_dir),
VenvSymlinkKind.LIB: site_packages,
VenvSymlinkKind.INCLUDE: "{}/{}".format(venv_ctx_rel_root, venv_details.include_dir),
VenvSymlinkKind.DATA: "{}/data".format(venv_ctx_rel_root),
}
venv_app_files = create_venv_app_files(
ctx,
Expand Down Expand Up @@ -659,7 +661,7 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa

recreate_venv_at_runtime = False

bin_dir = "{}/bin".format(venv_ctx_rel_root)
venv_bin_ctx_rel_path = "{}/bin".format(venv_ctx_rel_root)
if create_full_venv:
# Some wrappers around the interpreter (e.g. pyenv) use the program
# name to decide what to do, so preserve the name.
Expand All @@ -671,15 +673,15 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
# When the venv symlinks are disabled, the $venv/bin/python3 file isn't
# needed or used at runtime. However, the zip code uses the interpreter
# File object to figure out some paths.
interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename))
interpreter = ctx.actions.declare_file("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))

elif runtime.interpreter:
# Even though ctx.actions.symlink() is used, using
# declare_symlink() is required to ensure that the resulting file
# in runfiles is always a symlink. An RBE implementation, for example,
# may choose to write what symlink() points to instead.
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
interpreter_runfiles.add(interpreter)

rel_path = relative_path(
Expand All @@ -690,7 +692,7 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
)
ctx.actions.symlink(output = interpreter, target_path = rel_path)
else:
interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename))
interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename))
interpreter_runfiles.add(interpreter)
ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path)
else:
Expand All @@ -715,7 +717,8 @@ def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_pa
interpreter = interpreter,
pyvenv_cfg = pyvenv_cfg,
site_packages = site_packages,
bin_dir = bin_dir,
bin_dir = "bin",
include_dir = "include",
recreate_venv_at_runtime = recreate_venv_at_runtime,
interpreter_runfiles = interpreter_runfiles.build(ctx),
interpreter_symlinks = depset(),
Expand Down Expand Up @@ -777,7 +780,8 @@ def _create_venv_windows(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_
interpreter = interpreter,
pyvenv_cfg = None,
site_packages = site_packages,
bin_dir = venv_bin_ctx_rel_path,
bin_dir = venv_bin_rel_path,
include_dir = "Include",
recreate_venv_at_runtime = True,
interpreter_runfiles = interpreter_runfiles.build(ctx),
interpreter_symlinks = interpreter_symlinks.build(),
Expand All @@ -789,6 +793,7 @@ def _venv_details(
pyvenv_cfg,
site_packages,
bin_dir,
include_dir,
recreate_venv_at_runtime,
interpreter_runfiles,
interpreter_symlinks):
Expand All @@ -801,8 +806,10 @@ def _venv_details(
pyvenv_cfg = pyvenv_cfg,
# str; venv-relative path to the site-packages directory
site_packages = site_packages,
# str; ctx-relative path to the venv's bin directory.
# str; venv-relative path to the venv's bin directory.
bin_dir = bin_dir,
# str; venv-relative-path to the venv's include directory.
include_dir = include_dir,
# bool; True if the venv needs to be recreated at runtime (because the
# build-time construction isn't sufficient). False if the build-time
# constructed venv is sufficient.
Expand Down
1 change: 1 addition & 0 deletions python/private/py_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ VenvSymlinkKind = struct(
BIN = "BIN",
LIB = "LIB",
INCLUDE = "INCLUDE",
DATA = "DATA",
)

def _VenvSymlinkEntry_init(**kwargs):
Expand Down
14 changes: 11 additions & 3 deletions python/private/pypi/whl_library_targets.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ _BAZEL_REPO_FILE_GLOBS = [
"WORKSPACE.bazel",
]

_IS_VENV_SITE_PACKAGES_YES = Label("//python/config_settings:_is_venvs_site_packages_yes")

def whl_library_targets_from_requires(
*,
name,
Expand Down Expand Up @@ -194,8 +196,10 @@ def whl_library_targets(
DIST_INFO_LABEL: dict(
include = ["site-packages/*.dist-info/**"],
),

## TO CHECK: should bin/ and include/ be part of the data target?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This internal comment should be removed or resolved before merging. Based on the implementation in venv_runfiles.bzl, these directories are indeed necessary to be part of the runfiles so that the venv construction logic can discover and symlink them into the appropriate platform-specific locations.

DATA_LABEL: dict(
include = ["data/**"],
include = ["data/**", "bin/**", "include/**"],
),
}

Expand Down Expand Up @@ -356,11 +360,15 @@ def whl_library_targets(
if item not in _data_exclude:
_data_exclude.append(item)

data = data + native.glob(
data = data + [":" + DATA_LABEL] + native.glob(
["site-packages/**/*"],
exclude = _data_exclude,
allow_empty = True,
)
data = data + select({
_IS_VENV_SITE_PACKAGES_YES: [DATA_LABEL],
"//conditions:default": [],
})
Comment on lines +363 to +371
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The DATA_LABEL target is being added to the data attribute twice when _IS_VENV_SITE_PACKAGES_YES is true. Additionally, adding it unconditionally at line 363 includes bin/ and include/ files in the runfiles for all users of the wheel, even when venvs are not enabled. This might lead to unexpected bloat or conflicts in non-venv environments.

It is recommended to remove the unconditional addition and rely on the select block, or better yet, split DATA_LABEL into separate filegroups for data, bin, and include so they can be managed independently.

Suggested change
data = data + [":" + DATA_LABEL] + native.glob(
["site-packages/**/*"],
exclude = _data_exclude,
allow_empty = True,
)
data = data + select({
_IS_VENV_SITE_PACKAGES_YES: [DATA_LABEL],
"//conditions:default": [],
})
data = data + native.glob(
["site-packages/**/*"],
exclude = _data_exclude,
allow_empty = True,
)
data = data + select({
_IS_VENV_SITE_PACKAGES_YES: [DATA_LABEL],
"//conditions:default": [],
})


pyi_srcs = native.glob(
["site-packages/**/*.pyi"],
Expand All @@ -369,7 +377,7 @@ def whl_library_targets(

if not enable_implicit_namespace_pkgs:
generated_namespace_package_files = select({
Label("//python/config_settings:is_venvs_site_packages"): [],
_IS_VENV_SITE_PACKAGES_YES: [],
"//conditions:default": rules.create_inits(
srcs = srcs + data + pyi_srcs,
ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so.
Expand Down
31 changes: 27 additions & 4 deletions python/private/venv_runfiles.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,16 @@ def _get_file_venv_path(ctx, f, site_packages_root):

Returns:
A tuple `(venv_path, rf_root_path)` if the file is under
`site_packages_root`, otherwise `(None, None)`.
`site_packages_root` or data/, bin/, include/ otherwise `(None, None)`.
"""
rf_root_path = runfiles_root_path(ctx, f.short_path)
_, _, repo_rel_path = rf_root_path.partition("/")

# Check for wheel data/bin/include folders first
for prefix in ["data/", "bin/", "include/"]:
if repo_rel_path.startswith(prefix):
return (repo_rel_path, rf_root_path)

head, found_sp_root, venv_path = repo_rel_path.partition(site_packages_root)
if head or not found_sp_root:
# If head is set, then the path didn't start with site_packages_root
Expand Down Expand Up @@ -452,19 +458,36 @@ def get_venv_symlinks(

# Finally, for each group, we create the VenvSymlinkEntry objects
for venv_path, files in optimized_groups.items():
if venv_path.startswith("data/"):
out_venv_path = venv_path[len("data/"):]
kind = VenvSymlinkKind.DATA
prefix = ""
elif venv_path.startswith("include/"):
out_venv_path = venv_path[len("include/"):]
kind = VenvSymlinkKind.INCLUDE
prefix = ""
elif venv_path.startswith("bin/"):
out_venv_path = venv_path[len("bin/"):]
kind = VenvSymlinkKind.BIN
prefix = ""
Comment on lines +461 to +472
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The startswith checks here will fail if the venv_path is exactly "data", "include", or "bin". This happens when the grouping logic at lines 429-457 successfully groups all files within one of these top-level directories. In such cases, the kind will incorrectly fall through to VenvSymlinkKind.LIB, and the prefix will be set to site_packages_root, resulting in broken symlinks (e.g., _venv/site-packages/bin pointing to repo/site-packages/bin instead of repo/bin).

Suggested change
if venv_path.startswith("data/"):
out_venv_path = venv_path[len("data/"):]
kind = VenvSymlinkKind.DATA
prefix = ""
elif venv_path.startswith("include/"):
out_venv_path = venv_path[len("include/"):]
kind = VenvSymlinkKind.INCLUDE
prefix = ""
elif venv_path.startswith("bin/"):
out_venv_path = venv_path[len("bin/"):]
kind = VenvSymlinkKind.BIN
prefix = ""
if venv_path == "data" or venv_path.startswith("data/"):
out_venv_path = venv_path[len("data"):].lstrip("/")
kind = VenvSymlinkKind.DATA
prefix = ""
elif venv_path == "include" or venv_path.startswith("include/"):
out_venv_path = venv_path[len("include"):].lstrip("/")
kind = VenvSymlinkKind.INCLUDE
prefix = ""
elif venv_path == "bin" or venv_path.startswith("bin/"):
out_venv_path = venv_path[len("bin"):].lstrip("/")
kind = VenvSymlinkKind.BIN
prefix = ""

else:
out_venv_path = venv_path
kind = VenvSymlinkKind.LIB
prefix = site_packages_root

link_to_path = (
_get_label_runfiles_repo(ctx, files[0].owner) +
"/" +
site_packages_root +
prefix +
venv_path
)
venv_symlinks[venv_path] = VenvSymlinkEntry(
kind = VenvSymlinkKind.LIB,
kind = kind,
link_to_path = link_to_path,
link_to_file = None,
package = package,
version = version_str,
venv_path = venv_path,
venv_path = out_venv_path,
files = depset(files),
)

Expand Down
12 changes: 6 additions & 6 deletions tests/pypi/whl_library_targets/whl_library_targets_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def _test_whl_and_library_deps_from_requires(env):
env.expect.that_dict(py_library_call).contains_exactly({
"name": "pkg",
"srcs": ["site-packages/foo/SRCS.py"] + select({
Label("//python/config_settings:is_venvs_site_packages"): [],
Label("//python/config_settings:_is_venvs_site_packages_yes"): [],
"//conditions:default": ["_create_inits_target"],
}),
"pyi_srcs": ["site-packages/foo/PYI.pyi"],
Expand All @@ -259,7 +259,7 @@ def _test_whl_and_library_deps_from_requires(env):
"visibility": ["//visibility:public"],
"experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"),
"namespace_package_files": [] + select({
Label("//python/config_settings:is_venvs_site_packages"): [],
Label("//python/config_settings:_is_venvs_site_packages_yes"): [],
"//conditions:default": ["_create_inits_target"],
}),
}) # buildifier: @unsorted-dict-items
Expand Down Expand Up @@ -361,7 +361,7 @@ def _test_whl_and_library_deps(env):
env.expect.that_dict(py_library_calls[0]).contains_exactly({
"name": "pkg",
"srcs": ["site-packages/foo/SRCS.py"] + select({
Label("//python/config_settings:is_venvs_site_packages"): [],
Label("//python/config_settings:_is_venvs_site_packages_yes"): [],
"//conditions:default": ["_create_inits_target"],
}),
"pyi_srcs": ["site-packages/foo/PYI.pyi"],
Expand All @@ -386,7 +386,7 @@ def _test_whl_and_library_deps(env):
"visibility": ["//visibility:public"],
"experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"),
"namespace_package_files": [] + select({
Label("//python/config_settings:is_venvs_site_packages"): [],
Label("//python/config_settings:_is_venvs_site_packages_yes"): [],
"//conditions:default": ["_create_inits_target"],
}),
}) # buildifier: @unsorted-dict-items
Expand Down Expand Up @@ -444,7 +444,7 @@ def _test_group(env):
).contains_exactly({
"name": "_pkg",
"srcs": ["site-packages/foo/srcs.py"] + select({
Label("//python/config_settings:is_venvs_site_packages"): [],
Label("//python/config_settings:_is_venvs_site_packages_yes"): [],
"//conditions:default": ["_create_inits_target"],
}),
"pyi_srcs": ["site-packages/foo/pyi.pyi"],
Expand All @@ -459,7 +459,7 @@ def _test_group(env):
"visibility": ["@pypi__config//_groups:__pkg__"],
"experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"),
"namespace_package_files": [] + select({
Label("//python/config_settings:is_venvs_site_packages"): [],
Label("//python/config_settings:_is_venvs_site_packages_yes"): [],
"//conditions:default": ["_create_inits_target"],
}),
}) # buildifier: @unsorted-dict-items
Expand Down
1 change: 1 addition & 0 deletions tests/repos/whl_with_data/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports_files(glob(["*"]))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .data/data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .data/headers
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .data/platlib
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/bin/sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Metadata-Version: 2.1
Name: whl-with-data
Version: 1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
whl_with_data-1.0.data/platlib/whl_with_data/platlib_file.txt,sha256=123,123
whl_with_data-1.0.data/scripts/whl_script.sh,sha256=123,123
whl_with_data-1.0.data/headers/whl_with_data/header_file.h,sha256=123,123
whl_with_data-1.0.data/purelib/whl_with_data/data_file.txt,sha256=123,123
whl_with_data-1.0.data/data/whl_with_data/data_data_file.txt,sha256=123,123
whl_with_data-1.0.data/data/whl_with_data/data_data_file.txt,sha256=123,123
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Wheel-Version: 1.0
1 change: 1 addition & 0 deletions tests/venv_site_packages_libs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ py_reconfig_test(
"@other//nspkg_gamma",
"@other//nspkg_single",
"@other//with_external_data",
"@whl_with_data//:pkg",
],
)

Expand Down
29 changes: 28 additions & 1 deletion tests/venv_site_packages_libs/bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_dirs_from_replaced_package_are_not_present(self):
module_path = Path(module.__file__)

site_packages = module_path.parent.parent
dist_info_dirs = [p.name for p in site_packages.glob("*.dist-info")]
dist_info_dirs = [p.name for p in site_packages.glob("simple*.dist-info")]
self.assertEqual(
["simple-1.0.0.dist-info"],
dist_info_dirs,
Expand All @@ -91,6 +91,33 @@ def test_data_from_another_pkg_is_included_via_copy_file(self):
files = [p.name for p in d.glob("*")]
self.assertIn("another_module_data.txt", files)

def test_whl_with_data_included(self):
module = self.assert_imported_from_venv("whl_with_data")
module_path = Path(module.__file__)
site_packages = module_path.parent.parent

# purelib
data_file = site_packages / "whl_with_data" / "data_file.txt"
self.assertTrue(data_file.exists(), f"Expected {data_file} to exist")

# platlib
platlib_file = site_packages / "whl_with_data" / "platlib_file.txt"
self.assertTrue(platlib_file.exists(), f"Expected {platlib_file} to exist")

venv_root = Path(self.venv)

# data
data_data_file = venv_root / "data" / "whl_with_data" / "data_data_file.txt"
self.assertTrue(data_data_file.exists(), f"Expected {data_data_file} to exist")

# scripts
script_file = venv_root / "bin" / "whl_script.sh"
self.assertTrue(script_file.exists(), f"Expected {script_file} to exist")

# headers
header_file = venv_root / "include" / "whl_with_data" / "header_file.h"
self.assertTrue(header_file.exists(), f"Expected {header_file} to exist")


if __name__ == "__main__":
unittest.main()
Loading