2323
2424import os
2525import shutil
26+ import stat
2627import subprocess
2728import tempfile
2829import zipfile
30+ from os .path import dirname , join
2931
3032# runfiles-root-relative path
3133_STAGE2_BOOTSTRAP = "%stage2_bootstrap%"
3537# executable to use.
3638_PYTHON_BINARY_ACTUAL = "%python_binary_actual%"
3739_WORKSPACE_NAME = "%workspace_name%"
40+ # relative path under EXTRACT_ROOT to extract to.
41+ EXTRACT_DIR = "%EXTRACT_DIR%"
42+
43+ EXTRACT_ROOT = os .environ .get ("RULES_PYTHON_EXTRACT_ROOT" )
3844
3945
4046def print_verbose (* args , mapping = None , values = None ):
@@ -118,7 +124,7 @@ def search_path(name):
118124 search_path = os .getenv ("PATH" , os .defpath ).split (os .pathsep )
119125 for directory in search_path :
120126 if directory :
121- path = os . path . join (directory , name )
127+ path = join (directory , name )
122128 if os .path .isfile (path ) and os .access (path , os .X_OK ):
123129 return path
124130 return None
@@ -139,7 +145,7 @@ def find_binary(runfiles_root, bin_name):
139145 # Use normpath() to convert slashes to os.sep on Windows.
140146 elif os .sep in os .path .normpath (bin_name ):
141147 # Case 3: Path is relative to the repo root.
142- return os . path . join (runfiles_root , bin_name )
148+ return join (runfiles_root , bin_name )
143149 else :
144150 # Case 4: Path has to be looked up in the search path.
145151 return search_path (bin_name )
@@ -161,10 +167,18 @@ def extract_zip(zip_path, dest_dir):
161167 dest_dir = get_windows_path_with_unc_prefix (dest_dir )
162168 with zipfile .ZipFile (zip_path ) as zf :
163169 for info in zf .infolist ():
170+ file_path = os .path .abspath (join (dest_dir , info .filename ))
171+ # If the file exists, it might be a symlink or read-only file from a previous extraction.
172+ # Unlink it first so zipfile.extract doesn't corrupt the symlink target or fail on read-only files.
173+ if os .path .lexists (file_path ) and not os .path .isdir (file_path ):
174+ try :
175+ os .unlink (file_path )
176+ except OSError :
177+ # On Windows, unlinking a read-only file fails.
178+ os .chmod (file_path , stat .S_IWRITE )
179+ os .unlink (file_path )
180+
164181 zf .extract (info , dest_dir )
165- # UNC-prefixed paths must be absolute/normalized. See
166- # https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation
167- file_path = os .path .abspath (os .path .join (dest_dir , info .filename ))
168182 # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16
169183 # bits of external_attr.
170184 attrs = info .external_attr >> 16
@@ -182,11 +196,14 @@ def extract_zip(zip_path, dest_dir):
182196
183197# Create the runfiles tree by extracting the zip file
184198def create_runfiles_root ():
185- temp_dir = tempfile .mkdtemp ("" , "Bazel.runfiles_" )
186- extract_zip (os .path .dirname (__file__ ), temp_dir )
199+ if EXTRACT_ROOT :
200+ extract_root = join (EXTRACT_ROOT , EXTRACT_DIR )
201+ else :
202+ extract_root = tempfile .mkdtemp ("" , "Bazel.runfiles_" )
203+ extract_zip (dirname (__file__ ), extract_root )
187204 # IMPORTANT: Later code does `rm -fr` on dirname(runfiles_root) -- it's
188205 # important that deletion code be in sync with this directory structure
189- return os . path . join (temp_dir , "runfiles" )
206+ return join (extract_root , "runfiles" )
190207
191208
192209def execute_file (
@@ -223,18 +240,24 @@ def execute_file(
223240 # - When running in a zip file, we need to clean up the
224241 # workspace after the process finishes so control must return here.
225242 try :
226- subprocess_argv = [python_program , main_filename ] + args
243+ subprocess_argv = [python_program ]
244+ if not EXTRACT_ROOT :
245+ subprocess_argv .append (f"-XRULES_PYTHON_ZIP_DIR={ dirname (runfiles_root )} " )
246+ subprocess_argv .append (main_filename )
247+ subprocess_argv += args
227248 print_verbose ("subprocess argv:" , values = subprocess_argv )
228249 print_verbose ("subprocess env:" , mapping = env )
229250 print_verbose ("subprocess cwd:" , workspace )
230251 ret_code = subprocess .call (subprocess_argv , env = env , cwd = workspace )
231252 sys .exit (ret_code )
232253 finally :
233- # NOTE: dirname() is called because create_runfiles_root() creates a
234- # sub-directory within a temporary directory, and we want to remove the
235- # whole temporary directory.
236- ##shutil.rmtree(os.path.dirname(runfiles_root), True)
237- pass
254+ if not EXTRACT_ROOT :
255+ # NOTE: dirname() is called because create_runfiles_root() creates a
256+ # sub-directory within a temporary directory, and we want to remove the
257+ # whole temporary directory.
258+ extract_root = dirname (runfiles_root )
259+ print_verbose ("cleanup: rmtree: " , extract_root )
260+ shutil .rmtree (extract_root , True )
238261
239262
240263def main ():
@@ -266,7 +289,7 @@ def main():
266289 # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH
267290 new_env ["PYTHONSAFEPATH" ] = "1"
268291
269- main_filename = os . path . join (runfiles_root , main_rel_path )
292+ main_filename = join (runfiles_root , main_rel_path )
270293 main_filename = get_windows_path_with_unc_prefix (main_filename )
271294 assert os .path .exists (main_filename ), (
272295 "Cannot exec() %r: file not found." % main_filename
@@ -276,7 +299,7 @@ def main():
276299 )
277300
278301 if _PYTHON_BINARY_VENV :
279- python_program = os . path . join (runfiles_root , _PYTHON_BINARY_VENV )
302+ python_program = join (runfiles_root , _PYTHON_BINARY_VENV )
280303 # When a venv is used, the `bin/python3` symlink may need to be created.
281304 # This case occurs when "create venv at runtime" or "resolve python at
282305 # runtime" modes are enabled.
@@ -288,7 +311,7 @@ def main():
288311 "Program's venv binary not under runfiles: {python_program}"
289312 )
290313 symlink_to = find_binary (runfiles_root , _PYTHON_BINARY_ACTUAL )
291- os .makedirs (os . path . dirname (python_program ), exist_ok = True )
314+ os .makedirs (dirname (python_program ), exist_ok = True )
292315 try :
293316 os .symlink (symlink_to , python_program )
294317 except OSError as e :
@@ -317,7 +340,7 @@ def main():
317340 # change directory to the right runfiles directory.
318341 # (So that the data files are accessible)
319342 if os .environ .get ("RUN_UNDER_RUNFILES" ) == "1" :
320- workspace = os . path . join (runfiles_root , _WORKSPACE_NAME )
343+ workspace = join (runfiles_root , _WORKSPACE_NAME )
321344
322345 sys .stdout .flush ()
323346 execute_file (
0 commit comments