Skip to content

Commit bb06e05

Browse files
committed
wip: provide way to not use dangling symlinks
1 parent 309ee59 commit bb06e05

File tree

9 files changed

+177
-12
lines changed

9 files changed

+177
-12
lines changed

docs/api/rules_python/python/config_settings/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,28 @@ Values:
212212
:::
213213
::::
214214

215+
::::{bzl:flag} relative_venv_symlinks
216+
217+
Determines if relative symlinks are created using `declare_symlink()` at build
218+
time.
219+
220+
This is only intended to work around
221+
[#2489](https://github.com/bazelbuild/rules_python/issues/2489), where some
222+
packaging rules don't support `declare_symlink()` artifacts.
223+
224+
Values:
225+
* `yes`: Use `declare_symlink()` and create relative symlinks at build time.
226+
* `no`: Do not use `declare_symlink()`. Instead, the venv will be created at
227+
runtime.
228+
229+
:::{seealso}
230+
{envvar}`RULES_PYTHON_VENVS_ROOT` for customizing where the runtime venv
231+
is created.
232+
:::
233+
234+
:::{versionadded} VERSION_NEXT_PATCH
235+
:::
236+
215237
::::{bzl:flag} bootstrap_impl
216238
Determine how programs implement their startup process.
217239

docs/environment-variables.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,16 @@ When `1`, debug information about coverage behavior is printed to stderr.
6868

6969
When `1`, debug information from gazelle is printed to stderr.
7070
:::
71+
72+
:::{envvar} RULES_PYTHON_VENVS_ROOT
73+
74+
Directory to use as the root for creating venvs for binaries. Only applicable
75+
when {obj}`--relative_venvs_symlinks=no` is used. A binary will attempt to
76+
find a unique, reusable, location for itself within this directory. When set,
77+
the created venv is not deleted upon program exit; it is the responsibility of
78+
the caller to manage cleanup.
79+
80+
If not set, then a temporary directory will be created and deleted upon program
81+
exit.
82+
83+
:::

examples/bzlmod/.bazelversion

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7.x
1+
8.x

examples/bzlmod/BUILD.bazel

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test")
99
load("@pip//:requirements.bzl", "all_data_requirements", "all_requirements", "all_whl_requirements", "requirement")
1010
load("@python_3_9//:defs.bzl", py_test_with_transition = "py_test")
1111
load("@python_versions//3.10:defs.bzl", compile_pip_requirements_3_10 = "compile_pip_requirements")
12+
load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "pkg_mklink")
13+
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
1214
load("@rules_python//python:py_binary.bzl", "py_binary")
1315
load("@rules_python//python:py_library.bzl", "py_library")
1416
load("@rules_python//python:py_test.bzl", "py_test")
@@ -50,6 +52,39 @@ py_binary(
5052
],
5153
)
5254

55+
pkg_tar(
56+
name = "mytar",
57+
srcs = [
58+
":myfiles",
59+
":myinter",
60+
],
61+
allow_duplicates_with_different_content = True,
62+
###
63+
##srcs = [":bzlmod"],
64+
##include_runfiles = True,
65+
##remap_paths = {
66+
## "bzlmod.runfiles/_main/_bzlmod.venv/bin/python3": "blah/whatever",
67+
##},
68+
##symlinks = {
69+
## "bzlmod.runfiles/_main/_bzlmod.venv/bin/python3": "../../interpreter",
70+
##},
71+
)
72+
73+
pkg_files(
74+
name = "myfiles",
75+
srcs = [":bzlmod"],
76+
excludes = [
77+
"asdf",
78+
],
79+
include_runfiles = True,
80+
)
81+
82+
pkg_mklink(
83+
name = "myinter",
84+
link_name = "bzlmod.runfiles/_main/_bzlmod.venv/bin/python3",
85+
target = "mytarget",
86+
)
87+
5388
# see https://bazel.build/reference/be/python#py_test
5489
py_test(
5590
name = "test",

examples/bzlmod/MODULE.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module(
77
bazel_dep(name = "bazel_skylib", version = "1.7.1")
88
bazel_dep(name = "platforms", version = "0.0.4")
99
bazel_dep(name = "rules_python", version = "0.0.0")
10+
bazel_dep(name = "rules_pkg", version = "1.0.1")
11+
1012
local_path_override(
1113
module_name = "rules_python",
1214
path = "../..",

python/config_settings/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ load(
99
"LibcFlag",
1010
"PrecompileFlag",
1111
"PrecompileSourceRetentionFlag",
12+
"RelativeVenvSymlinksFlag",
1213
)
1314
load(
1415
"//python/private/pypi:flags.bzl",
@@ -121,6 +122,13 @@ config_setting(
121122
visibility = ["//visibility:public"],
122123
)
123124

125+
string_flag(
126+
name = "relative_venv_symlinks",
127+
build_setting_default = RelativeVenvSymlinksFlag.YES,
128+
values = RelativeVenvSymlinksFlag.flag_values(),
129+
visibility = ["//visibility:public"],
130+
)
131+
124132
# pip.parse related flags
125133

126134
string_flag(

python/private/flags.bzl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ PrecompileSourceRetentionFlag = enum(
123123
get_effective_value = _precompile_source_retention_flag_get_effective_value,
124124
)
125125

126+
def _relative_venv_symlinks_flag_get_value(ctx):
127+
return ctx.attr._relative_venv_symlinks_flag[BuildSettingInfo].value
128+
129+
# Decides if the venv created by bootstrap=script uses declare_file() to
130+
# create relative symlinks. Workaround for #2489 (packaging rules not supporting
131+
# declare_link() files).
132+
# buildifier: disable=name-conventions
133+
RelativeVenvSymlinksFlag = FlagEnum(
134+
# Use declare_file() and relative symlinks in the venv
135+
YES = "yes",
136+
# Do not use declare_file() and relative symlinks in the venv
137+
NO = "no",
138+
get_value = _relative_venv_symlinks_flag_get_value,
139+
)
140+
126141
# Used for matching freethreaded toolchains and would have to be used in wheels
127142
# as well.
128143
# buildifier: disable=name-conventions

python/private/py_executable.bzl

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ load(
5151
"target_platform_has_any_constraint",
5252
"union_attrs",
5353
)
54-
load(":flags.bzl", "BootstrapImplFlag")
54+
load(":flags.bzl", "BootstrapImplFlag", "RelativeVenvSymlinksFlag")
5555
load(":precompile.bzl", "maybe_precompile")
5656
load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
5757
load(":py_executable_info.bzl", "PyExecutableInfo")
@@ -195,6 +195,10 @@ accepting arbitrary Python versions.
195195
"_python_version_flag": attr.label(
196196
default = "//python/config_settings:python_version",
197197
),
198+
"_relative_venv_symlinks_flag": attr.label(
199+
default = "//python/config_settings:relative_venv_symlinks",
200+
providers = [BuildSettingInfo],
201+
),
198202
"_windows_constraints": attr.label_list(
199203
default = [
200204
"@platforms//os:windows",
@@ -512,7 +516,25 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
512516
ctx.actions.write(pyvenv_cfg, "")
513517

514518
runtime = runtime_details.effective_runtime
515-
if runtime.interpreter:
519+
relative_venv_symlinks_enabled = (
520+
RelativeVenvSymlinksFlag.get_value(ctx) == RelativeVenvSymlinksFlag.YES
521+
)
522+
523+
if not relative_venv_symlinks_enabled:
524+
if runtime.interpreter:
525+
interpreter_actual_path = _runfiles_root_path(ctx, runtime.interpreter.short_path)
526+
else:
527+
interpreter_actual_path = runtime.interpreter_path
528+
529+
py_exe_basename = paths.basename(interpreter_actual_path)
530+
531+
# When the venv symlinks are disabled, the $venv/bin/python3 file isn't
532+
# needed or used at runtime. However, the zip code uses the interpreter
533+
# File object to figure out some paths.
534+
interpreter = ctx.actions.declare_file("{}/bin/{}".format(venv, py_exe_basename))
535+
ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path))
536+
537+
elif runtime.interpreter:
516538
py_exe_basename = paths.basename(runtime.interpreter.short_path)
517539

518540
# Even though ctx.actions.symlink() is used, using
@@ -571,6 +593,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details):
571593

572594
return struct(
573595
interpreter = interpreter,
596+
recreate_venv_at_runtime = not relative_venv_symlinks_enabled,
574597
# Runfiles root relative path or absolute path
575598
interpreter_actual_path = interpreter_actual_path,
576599
files_without_interpreter = [pyvenv_cfg, pth, site_init],
@@ -657,15 +680,13 @@ def _create_stage1_bootstrap(
657680
else:
658681
python_binary_path = runtime_details.executable_interpreter_path
659682

660-
if is_for_zip and venv:
661-
python_binary_actual = venv.interpreter_actual_path
662-
else:
663-
python_binary_actual = ""
683+
python_binary_actual = venv.interpreter_actual_path if venv else ""
664684

665685
subs = {
666686
"%is_zipfile%": "1" if is_for_zip else "0",
667687
"%python_binary%": python_binary_path,
668688
"%python_binary_actual%": python_binary_actual,
689+
"%recreate_venv_at_runtime%": str(int(venv.recreate_venv_at_runtime)) if venv else "0",
669690
"%target%": str(ctx.label),
670691
"%workspace_name%": ctx.workspace_name,
671692
}

python/private/stage1_bootstrap_template.sh

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ fi
99
# runfiles-relative path
1010
STAGE2_BOOTSTRAP="%stage2_bootstrap%"
1111

12-
# runfiles-relative path
12+
# runfiles-relative path to python interpreter to use
1313
PYTHON_BINARY='%python_binary%'
1414
# The path that PYTHON_BINARY should symlink to.
1515
# runfiles-relative path, absolute path, or single word.
16-
# Only applicable for zip files.
16+
# Only applicable for zip files or when venv is recreated at runtime.
1717
PYTHON_BINARY_ACTUAL="%python_binary_actual%"
1818

1919
# 0 or 1
2020
IS_ZIPFILE="%is_zipfile%"
21+
# 0 or 1
22+
RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%"
2123

2224
if [[ "$IS_ZIPFILE" == "1" ]]; then
2325
# NOTE: Macs have an old version of mktemp, so we must use only the
@@ -104,6 +106,7 @@ python_exe=$(find_python_interpreter $RUNFILES_DIR $PYTHON_BINARY)
104106
# Zip files have to re-create the venv bin/python3 symlink because they
105107
# don't contain it already.
106108
if [[ "$IS_ZIPFILE" == "1" ]]; then
109+
use_exec=0
107110
# It should always be under runfiles, but double check this. We don't
108111
# want to accidentally create symlinks elsewhere.
109112
if [[ "$python_exe" != $RUNFILES_DIR/* ]]; then
@@ -121,13 +124,60 @@ if [[ "$IS_ZIPFILE" == "1" ]]; then
121124
symlink_to=$(which $PYTHON_BINARY_ACTUAL)
122125
# Guard against trying to symlink to an empty value
123126
if [[ $? -ne 0 ]]; then
124-
echo >&2 "ERROR: Python to use found on PATH: $PYTHON_BINARY_ACTUAL"
127+
echo >&2 "ERROR: Python to use not found on PATH: $PYTHON_BINARY_ACTUAL"
125128
exit 1
126129
fi
127130
fi
128131
# The bin/ directory may not exist if it is empty.
129132
mkdir -p "$(dirname $python_exe)"
130133
ln -s "$symlink_to" "$python_exe"
134+
elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then
135+
runfiles_venv="$RUNFILES_DIR/$(dirname $(dirname $PYTHON_BINARY))"
136+
if [[ -n "$RULES_PYTHON_VENVS_ROOT" ]]; then
137+
use_exec=1
138+
# Use our runfiles path as a unique, reusable, location for the
139+
# binary-specific venv being created.
140+
venv="$RULES_PYTHON_VENVS_ROOT/$(dirname $(dirname $PYTHON_BINARY))"
141+
mkdir -p $RULES_PYTHON_VENVS_ROOT
142+
else
143+
# Re-exec'ing can't be used because we have to clean up the temporary
144+
# venv directory that is created.
145+
use_exec=0
146+
venv=$(mktemp -d)
147+
if [[ -n "$venv" && -z "${RULES_PYTHON_BOOTSTRAP_VERBOSE:-}" ]]; then
148+
trap 'rm -fr "$venv"' EXIT
149+
fi
150+
fi
151+
152+
if [[ "$PYTHON_BINARY_ACTUAL" == /* ]]; then
153+
# An absolute path, i.e. platform runtime, e.g. /usr/bin/python3
154+
symlink_to=$PYTHON_BINARY_ACTUAL
155+
elif [[ "$PYTHON_BINARY_ACTUAL" == */* ]]; then
156+
# A runfiles-relative path
157+
symlink_to="$RUNFILES_DIR/$PYTHON_BINARY_ACTUAL"
158+
else
159+
# A plain word, e.g. "python3". Symlink to where PATH leads
160+
symlink_to=$(which $PYTHON_BINARY_ACTUAL)
161+
# Guard against trying to symlink to an empty value
162+
if [[ $? -ne 0 ]]; then
163+
echo >&2 "ERROR: Python to use not found on PATH: $PYTHON_BINARY_ACTUAL"
164+
exit 1
165+
fi
166+
fi
167+
mkdir -p "$venv/bin"
168+
# Match the basename; some tools, e.g. pyvenv key off the executable name
169+
python_exe="$venv/bin/$(basename $PYTHON_BINARY_ACTUAL)"
170+
if [[ ! -e "$python_exe" ]]; then
171+
ln -s "$symlink_to" "$python_exe"
172+
fi
173+
if [[ ! -e "$venv/pyvenv.cfg" ]]; then
174+
ln -s "$runfiles_venv/pyvenv.cfg" "$venv/pyvenv.cfg"
175+
fi
176+
if [[ ! -e "$venv/lib" ]]; then
177+
ln -s "$runfiles_venv/lib" "$venv/lib"
178+
fi
179+
else
180+
use_exec=1
131181
fi
132182

133183
# At this point, we should have a valid reference to the interpreter.
@@ -165,7 +215,6 @@ if [[ "$IS_ZIPFILE" == "1" ]]; then
165215
interpreter_args+=("-XRULES_PYTHON_ZIP_DIR=$zip_dir")
166216
fi
167217

168-
169218
export RUNFILES_DIR
170219

171220
command=(
@@ -186,7 +235,7 @@ command=(
186235
#
187236
# However, when running a zip file, we need to clean up the workspace after the
188237
# process finishes so control must return here.
189-
if [[ "$IS_ZIPFILE" == "1" ]]; then
238+
if [[ "$use_exec" == "0" ]]; then
190239
"${command[@]}"
191240
exit $?
192241
else

0 commit comments

Comments
 (0)