• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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