2323
2424import os
2525import shutil
26+ import stat
2627import subprocess
2728import tempfile
2829import zipfile
30+ from os .path import dirname , join , basename
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+ APP_HASH = "%APP_HASH%"
43+
44+ EXTRACT_ROOT = os .environ .get ("RULES_PYTHON_EXTRACT_ROOT" )
45+ IS_WINDOWS = os .name == "nt"
3846
3947
4048def print_verbose (* args , mapping = None , values = None ):
@@ -61,10 +69,6 @@ def print_verbose(*args, mapping=None, values=None):
6169 print ("bootstrap: stage 1:" , * args , file = sys .stderr , flush = True )
6270
6371
64- # Return True if running on Windows
65- def is_windows ():
66- return os .name == "nt"
67-
6872
6973def get_windows_path_with_unc_prefix (path ):
7074 """Adds UNC prefix after getting a normalized absolute Windows path.
@@ -75,7 +79,7 @@ def get_windows_path_with_unc_prefix(path):
7579
7680 # No need to add prefix for non-Windows platforms.
7781 # And \\?\ doesn't work in python 2 or on mingw
78- if not is_windows () or sys .version_info [0 ] < 3 :
82+ if not IS_WINDOWS or sys .version_info [0 ] < 3 :
7983 return path
8084
8185 # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been
@@ -107,7 +111,7 @@ def has_windows_executable_extension(path):
107111
108112if (
109113 _PYTHON_BINARY_VENV
110- and is_windows ()
114+ and IS_WINDOWS
111115 and not has_windows_executable_extension (_PYTHON_BINARY_VENV )
112116):
113117 _PYTHON_BINARY_VENV = _PYTHON_BINARY_VENV + ".exe"
@@ -118,7 +122,7 @@ def search_path(name):
118122 search_path = os .getenv ("PATH" , os .defpath ).split (os .pathsep )
119123 for directory in search_path :
120124 if directory :
121- path = os . path . join (directory , name )
125+ path = join (directory , name )
122126 if os .path .isfile (path ) and os .access (path , os .X_OK ):
123127 return path
124128 return None
@@ -139,7 +143,7 @@ def find_binary(runfiles_root, bin_name):
139143 # Use normpath() to convert slashes to os.sep on Windows.
140144 elif os .sep in os .path .normpath (bin_name ):
141145 # Case 3: Path is relative to the repo root.
142- return os . path . join (runfiles_root , bin_name )
146+ return join (runfiles_root , bin_name )
143147 else :
144148 # Case 4: Path has to be looked up in the search path.
145149 return search_path (bin_name )
@@ -161,10 +165,18 @@ def extract_zip(zip_path, dest_dir):
161165 dest_dir = get_windows_path_with_unc_prefix (dest_dir )
162166 with zipfile .ZipFile (zip_path ) as zf :
163167 for info in zf .infolist ():
168+ file_path = os .path .abspath (join (dest_dir , info .filename ))
169+ # If the file exists, it might be a symlink or read-only file from a previous extraction.
170+ # Unlink it first so zipfile.extract doesn't corrupt the symlink target or fail on read-only files.
171+ if os .path .lexists (file_path ) and not os .path .isdir (file_path ):
172+ try :
173+ os .unlink (file_path )
174+ except OSError :
175+ # On Windows, unlinking a read-only file fails.
176+ os .chmod (file_path , stat .S_IWRITE )
177+ os .unlink (file_path )
178+
164179 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 ))
168180 # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16
169181 # bits of external_attr.
170182 attrs = info .external_attr >> 16
@@ -182,11 +194,21 @@ def extract_zip(zip_path, dest_dir):
182194
183195# Create the runfiles tree by extracting the zip file
184196def create_runfiles_root ():
185- temp_dir = tempfile .mkdtemp ("" , "Bazel.runfiles_" )
186- extract_zip (os .path .dirname (__file__ ), temp_dir )
197+ if EXTRACT_ROOT :
198+ # Shorten the path for Windows in case long path support is disabled
199+ if IS_WINDOWS :
200+ hash_dir = APP_HASH [0 :32 ]
201+ extract_dir = basename (EXTRACT_DIR )
202+ extract_root = join (EXTRACT_ROOT , extract_dir , hash_dir )
203+ else :
204+ extract_root = join (EXTRACT_ROOT , EXTRACT_DIR , APP_HASH )
205+ extract_root = get_windows_path_with_unc_prefix (extract_root )
206+ else :
207+ extract_root = tempfile .mkdtemp ("" , "Bazel.runfiles_" )
208+ extract_zip (dirname (__file__ ), extract_root )
187209 # IMPORTANT: Later code does `rm -fr` on dirname(runfiles_root) -- it's
188210 # important that deletion code be in sync with this directory structure
189- return os . path . join (temp_dir , "runfiles" )
211+ return join (extract_root , "runfiles" )
190212
191213
192214def execute_file (
@@ -223,18 +245,24 @@ def execute_file(
223245 # - When running in a zip file, we need to clean up the
224246 # workspace after the process finishes so control must return here.
225247 try :
226- subprocess_argv = [python_program , main_filename ] + args
227- print_verbose ("subprocess argv:" , values = subprocess_argv )
248+ subprocess_argv = [python_program ]
249+ if not EXTRACT_ROOT :
250+ subprocess_argv .append (f"-XRULES_PYTHON_ZIP_DIR={ dirname (runfiles_root )} " )
251+ subprocess_argv .append (main_filename )
252+ subprocess_argv += args
228253 print_verbose ("subprocess env:" , mapping = env )
229254 print_verbose ("subprocess cwd:" , workspace )
255+ print_verbose ("subprocess argv:" , values = subprocess_argv )
230256 ret_code = subprocess .call (subprocess_argv , env = env , cwd = workspace )
231257 sys .exit (ret_code )
232258 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
259+ if not EXTRACT_ROOT :
260+ # NOTE: dirname() is called because create_runfiles_root() creates a
261+ # sub-directory within a temporary directory, and we want to remove the
262+ # whole temporary directory.
263+ extract_root = dirname (runfiles_root )
264+ print_verbose ("cleanup: rmtree: " , extract_root )
265+ shutil .rmtree (extract_root , True )
238266
239267
240268def main ():
@@ -254,7 +282,7 @@ def main():
254282
255283 # The main Python source file.
256284 main_rel_path = _STAGE2_BOOTSTRAP
257- if is_windows () :
285+ if IS_WINDOWS :
258286 main_rel_path = main_rel_path .replace ("/" , os .sep )
259287
260288 runfiles_root = create_runfiles_root ()
@@ -266,7 +294,7 @@ def main():
266294 # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH
267295 new_env ["PYTHONSAFEPATH" ] = "1"
268296
269- main_filename = os . path . join (runfiles_root , main_rel_path )
297+ main_filename = join (runfiles_root , main_rel_path )
270298 main_filename = get_windows_path_with_unc_prefix (main_filename )
271299 assert os .path .exists (main_filename ), (
272300 "Cannot exec() %r: file not found." % main_filename
@@ -276,7 +304,7 @@ def main():
276304 )
277305
278306 if _PYTHON_BINARY_VENV :
279- python_program = os . path . join (runfiles_root , _PYTHON_BINARY_VENV )
307+ python_program = join (runfiles_root , _PYTHON_BINARY_VENV )
280308 # When a venv is used, the `bin/python3` symlink may need to be created.
281309 # This case occurs when "create venv at runtime" or "resolve python at
282310 # runtime" modes are enabled.
@@ -288,7 +316,7 @@ def main():
288316 "Program's venv binary not under runfiles: {python_program}"
289317 )
290318 symlink_to = find_binary (runfiles_root , _PYTHON_BINARY_ACTUAL )
291- os .makedirs (os . path . dirname (python_program ), exist_ok = True )
319+ os .makedirs (dirname (python_program ), exist_ok = True )
292320 try :
293321 os .symlink (symlink_to , python_program )
294322 except OSError as e :
@@ -317,7 +345,7 @@ def main():
317345 # change directory to the right runfiles directory.
318346 # (So that the data files are accessible)
319347 if os .environ .get ("RUN_UNDER_RUNFILES" ) == "1" :
320- workspace = os . path . join (runfiles_root , _WORKSPACE_NAME )
348+ workspace = join (runfiles_root , _WORKSPACE_NAME )
321349
322350 sys .stdout .flush ()
323351 execute_file (
0 commit comments