1# Copyright 2024 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""site initialization logic for Bazel-built py_binary targets.""" 15import os 16import os.path 17import sys 18 19# Colon-delimited string of runfiles-relative import paths to add 20_IMPORTS_STR = "%imports%" 21# Though the import all value is the correct literal, we quote it 22# so this file is parsable by tools. 23_IMPORT_ALL = "%import_all%" == "True" 24_WORKSPACE_NAME = "%workspace_name%" 25# runfiles-relative path to this file 26_SELF_RUNFILES_RELATIVE_PATH = "%site_init_runfiles_path%" 27# Runfiles-relative path to the coverage tool entry point, if any. 28_COVERAGE_TOOL = "%coverage_tool%" 29 30 31def _is_verbose(): 32 return bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE")) 33 34 35def _print_verbose_coverage(*args): 36 if os.environ.get("VERBOSE_COVERAGE") or _is_verbose(): 37 _print_verbose(*args) 38 39 40def _print_verbose(*args, mapping=None, values=None): 41 if not _is_verbose(): 42 return 43 44 print("bazel_site_init:", *args, file=sys.stderr, flush=True) 45 46 47_print_verbose("imports_str:", _IMPORTS_STR) 48_print_verbose("import_all:", _IMPORT_ALL) 49_print_verbose("workspace_name:", _WORKSPACE_NAME) 50_print_verbose("self_runfiles_path:", _SELF_RUNFILES_RELATIVE_PATH) 51_print_verbose("coverage_tool:", _COVERAGE_TOOL) 52 53 54def _find_runfiles_root(): 55 # Give preference to the environment variables 56 runfiles_dir = os.environ.get("RUNFILES_DIR", None) 57 if not runfiles_dir: 58 runfiles_manifest_file = os.environ.get("RUNFILES_MANIFEST_FILE", "") 59 if runfiles_manifest_file.endswith( 60 ".runfiles_manifest" 61 ) or runfiles_manifest_file.endswith(".runfiles/MANIFEST"): 62 runfiles_dir = runfiles_manifest_file[:-9] 63 64 # Be defensive: the runfiles dir should contain ourselves. If it doesn't, 65 # then it must not be our runfiles directory. 66 if runfiles_dir and os.path.exists( 67 os.path.join(runfiles_dir, _SELF_RUNFILES_RELATIVE_PATH) 68 ): 69 return runfiles_dir 70 71 num_dirs_to_runfiles_root = _SELF_RUNFILES_RELATIVE_PATH.count("/") + 1 72 runfiles_root = os.path.dirname(__file__) 73 for _ in range(num_dirs_to_runfiles_root): 74 runfiles_root = os.path.dirname(runfiles_root) 75 return runfiles_root 76 77 78_RUNFILES_ROOT = _find_runfiles_root() 79 80_print_verbose("runfiles_root:", _RUNFILES_ROOT) 81 82 83def _is_windows(): 84 return os.name == "nt" 85 86 87def _get_windows_path_with_unc_prefix(path): 88 path = path.strip() 89 # No need to add prefix for non-Windows platforms. 90 if not _is_windows() or sys.version_info[0] < 3: 91 return path 92 93 # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been 94 # removed from common Win32 file and directory functions. 95 # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later 96 import platform 97 98 if platform.win32_ver()[1] >= "10.0.14393": 99 return path 100 101 # import sysconfig only now to maintain python 2.6 compatibility 102 import sysconfig 103 104 if sysconfig.get_platform() == "mingw": 105 return path 106 107 # Lets start the unicode fun 108 unicode_prefix = "\\\\?\\" 109 if path.startswith(unicode_prefix): 110 return path 111 112 # os.path.abspath returns a normalized absolute path 113 return unicode_prefix + os.path.abspath(path) 114 115 116def _search_path(name): 117 """Finds a file in a given search path.""" 118 search_path = os.getenv("PATH", os.defpath).split(os.pathsep) 119 for directory in search_path: 120 if directory: 121 path = os.path.join(directory, name) 122 if os.path.isfile(path) and os.access(path, os.X_OK): 123 return path 124 return None 125 126 127def _setup_sys_path(): 128 seen = set(sys.path) 129 python_path_entries = [] 130 131 def _maybe_add_path(path): 132 if path in seen: 133 return 134 path = _get_windows_path_with_unc_prefix(path) 135 if _is_windows(): 136 path = path.replace("/", os.sep) 137 138 _print_verbose("append sys.path:", path) 139 sys.path.append(path) 140 seen.add(path) 141 142 for rel_path in _IMPORTS_STR.split(":"): 143 abs_path = os.path.join(_RUNFILES_ROOT, rel_path) 144 _maybe_add_path(abs_path) 145 146 if _IMPORT_ALL: 147 repo_dirs = sorted( 148 os.path.join(_RUNFILES_ROOT, d) for d in os.listdir(_RUNFILES_ROOT) 149 ) 150 for d in repo_dirs: 151 if os.path.isdir(d): 152 _maybe_add_path(d) 153 else: 154 _maybe_add_path(os.path.join(_RUNFILES_ROOT, _WORKSPACE_NAME)) 155 156 # COVERAGE_DIR is set if coverage is enabled and instrumentation is configured 157 # for something, though it could be another program executing this one or 158 # one executed by this one (e.g. an extension module). 159 # NOTE: Coverage is added last to allow user dependencies to override it. 160 coverage_setup = False 161 if os.environ.get("COVERAGE_DIR"): 162 cov_tool = _COVERAGE_TOOL 163 if cov_tool: 164 _print_verbose_coverage(f"Using toolchain coverage_tool {cov_tool}") 165 elif cov_tool := os.environ.get("PYTHON_COVERAGE"): 166 _print_verbose_coverage(f"PYTHON_COVERAGE: {cov_tool}") 167 168 if cov_tool: 169 if os.path.isabs(cov_tool): 170 pass 171 elif os.sep in os.path.normpath(cov_tool): 172 cov_tool = os.path.join(_RUNFILES_ROOT, cov_tool) 173 else: 174 cov_tool = _search_path(cov_tool) 175 if cov_tool: 176 # The coverage entry point is `<dir>/coverage/coverage_main.py`, so 177 # we need to do twice the dirname so that `import coverage` works 178 coverage_dir = os.path.dirname(os.path.dirname(cov_tool)) 179 180 # coverage library expects sys.path[0] to contain the library, and replaces 181 # it with the directory of the program it starts. Our actual sys.path[0] is 182 # the runfiles directory, which must not be replaced. 183 # CoverageScript.do_execute() undoes this sys.path[0] setting. 184 _maybe_add_path(coverage_dir) 185 coverage_setup = True 186 else: 187 _print_verbose_coverage( 188 "Coverage was enabled, but python coverage tool was not configured." 189 + "To enable coverage, consult the docs at " 190 + "https://rules-python.readthedocs.io/en/latest/coverage.html" 191 ) 192 193 return coverage_setup 194 195 196COVERAGE_SETUP = _setup_sys_path() 197