aboutsummaryrefslogtreecommitdiff
path: root/python/pip_install/tools/dependency_resolver/dependency_resolver.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/pip_install/tools/dependency_resolver/dependency_resolver.py')
-rw-r--r--python/pip_install/tools/dependency_resolver/dependency_resolver.py221
1 files changed, 221 insertions, 0 deletions
diff --git a/python/pip_install/tools/dependency_resolver/dependency_resolver.py b/python/pip_install/tools/dependency_resolver/dependency_resolver.py
new file mode 100644
index 0000000..e277cf9
--- /dev/null
+++ b/python/pip_install/tools/dependency_resolver/dependency_resolver.py
@@ -0,0 +1,221 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"Set defaults for the pip-compile command to run it under Bazel"
+
+import atexit
+import os
+import shutil
+import sys
+from pathlib import Path
+
+import piptools.writer as piptools_writer
+from piptools.scripts.compile import cli
+
+from python.runfiles import runfiles
+
+# Replace the os.replace function with shutil.copy to work around os.replace not being able to
+# replace or move files across filesystems.
+os.replace = shutil.copy
+
+# Next, we override the annotation_style_split and annotation_style_line functions to replace the
+# backslashes in the paths with forward slashes. This is so that we can have the same requirements
+# file on Windows and Unix-like.
+original_annotation_style_split = piptools_writer.annotation_style_split
+original_annotation_style_line = piptools_writer.annotation_style_line
+
+
+def annotation_style_split(required_by) -> str:
+ required_by = set([v.replace("\\", "/") for v in required_by])
+ return original_annotation_style_split(required_by)
+
+
+def annotation_style_line(required_by) -> str:
+ required_by = set([v.replace("\\", "/") for v in required_by])
+ return original_annotation_style_line(required_by)
+
+
+piptools_writer.annotation_style_split = annotation_style_split
+piptools_writer.annotation_style_line = annotation_style_line
+
+
+def _select_golden_requirements_file(
+ requirements_txt, requirements_linux, requirements_darwin, requirements_windows
+):
+ """Switch the golden requirements file, used to validate if updates are needed,
+ to a specified platform specific one. Fallback on the platform independent one.
+ """
+
+ plat = sys.platform
+ if plat == "linux" and requirements_linux is not None:
+ return requirements_linux
+ elif plat == "darwin" and requirements_darwin is not None:
+ return requirements_darwin
+ elif plat == "win32" and requirements_windows is not None:
+ return requirements_windows
+ else:
+ return requirements_txt
+
+
+def _locate(bazel_runfiles, file):
+ """Look up the file via Rlocation"""
+
+ if not file:
+ return file
+
+ return bazel_runfiles.Rlocation(file)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 4:
+ print(
+ "Expected at least two arguments: requirements_in requirements_out",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ parse_str_none = lambda s: None if s == "None" else s
+ bazel_runfiles = runfiles.Create()
+
+ requirements_in = sys.argv.pop(1)
+ requirements_txt = sys.argv.pop(1)
+ requirements_linux = parse_str_none(sys.argv.pop(1))
+ requirements_darwin = parse_str_none(sys.argv.pop(1))
+ requirements_windows = parse_str_none(sys.argv.pop(1))
+ update_target_label = sys.argv.pop(1)
+
+ requirements_file = _select_golden_requirements_file(
+ requirements_txt=requirements_txt, requirements_linux=requirements_linux,
+ requirements_darwin=requirements_darwin, requirements_windows=requirements_windows
+ )
+
+ resolved_requirements_in = _locate(bazel_runfiles, requirements_in)
+ resolved_requirements_file = _locate(bazel_runfiles, requirements_file)
+
+ # Files in the runfiles directory has the following naming schema:
+ # Main repo: __main__/<path_to_file>
+ # External repo: <workspace name>/<path_to_file>
+ # We want to strip both __main__ and <workspace name> from the absolute prefix
+ # to keep the requirements lock file agnostic.
+ repository_prefix = requirements_file[: requirements_file.index("/") + 1]
+ absolute_path_prefix = resolved_requirements_file[
+ : -(len(requirements_file) - len(repository_prefix))
+ ]
+
+ # As requirements_in might contain references to generated files we want to
+ # use the runfiles file first. Thus, we need to compute the relative path
+ # from the execution root.
+ # Note: Windows cannot reference generated files without runfiles support enabled.
+ requirements_in_relative = requirements_in[len(repository_prefix):]
+ requirements_file_relative = requirements_file[len(repository_prefix):]
+
+ # Before loading click, set the locale for its parser.
+ # If it leaks through to the system setting, it may fail:
+ # RuntimeError: Click will abort further execution because Python 3 was configured to use ASCII
+ # as encoding for the environment. Consult https://click.palletsprojects.com/python3/ for
+ # mitigation steps.
+ os.environ["LC_ALL"] = "C.UTF-8"
+ os.environ["LANG"] = "C.UTF-8"
+
+ UPDATE = True
+ # Detect if we are running under `bazel test`.
+ if "TEST_TMPDIR" in os.environ:
+ UPDATE = False
+ # pip-compile wants the cache files to be writeable, but if we point
+ # to the real user cache, Bazel sandboxing makes the file read-only
+ # and we fail.
+ # In theory this makes the test more hermetic as well.
+ sys.argv.append("--cache-dir")
+ sys.argv.append(os.environ["TEST_TMPDIR"])
+ # Make a copy for pip-compile to read and mutate.
+ requirements_out = os.path.join(
+ os.environ["TEST_TMPDIR"], os.path.basename(requirements_file) + ".out"
+ )
+ # Those two files won't necessarily be on the same filesystem, so we can't use os.replace
+ # or shutil.copyfile, as they will fail with OSError: [Errno 18] Invalid cross-device link.
+ shutil.copy(resolved_requirements_file, requirements_out)
+
+ update_command = os.getenv("CUSTOM_COMPILE_COMMAND") or "bazel run %s" % (
+ update_target_label,
+ )
+
+ os.environ["CUSTOM_COMPILE_COMMAND"] = update_command
+ os.environ["PIP_CONFIG_FILE"] = os.getenv("PIP_CONFIG_FILE") or os.devnull
+
+ sys.argv.append("--output-file")
+ sys.argv.append(requirements_file_relative if UPDATE else requirements_out)
+ sys.argv.append(
+ requirements_in_relative
+ if Path(requirements_in_relative).exists()
+ else resolved_requirements_in
+ )
+ print(sys.argv)
+
+ if UPDATE:
+ print("Updating " + requirements_file_relative)
+ if "BUILD_WORKSPACE_DIRECTORY" in os.environ:
+ workspace = os.environ["BUILD_WORKSPACE_DIRECTORY"]
+ requirements_file_tree = os.path.join(workspace, requirements_file_relative)
+ # In most cases, requirements_file will be a symlink to the real file in the source tree.
+ # If symlinks are not enabled (e.g. on Windows), then requirements_file will be a copy,
+ # and we should copy the updated requirements back to the source tree.
+ if not os.path.samefile(resolved_requirements_file, requirements_file_tree):
+ atexit.register(
+ lambda: shutil.copy(
+ resolved_requirements_file, requirements_file_tree
+ )
+ )
+ cli()
+ requirements_file_relative_path = Path(requirements_file_relative)
+ content = requirements_file_relative_path.read_text()
+ content = content.replace(absolute_path_prefix, "")
+ requirements_file_relative_path.write_text(content)
+ else:
+ # cli will exit(0) on success
+ try:
+ print("Checking " + requirements_file)
+ cli()
+ print("cli() should exit", file=sys.stderr)
+ sys.exit(1)
+ except SystemExit as e:
+ if e.code == 2:
+ print(
+ "pip-compile exited with code 2. This means that pip-compile found "
+ "incompatible requirements or could not find a version that matches "
+ f"the install requirement in {requirements_in_relative}.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ elif e.code == 0:
+ golden = open(_locate(bazel_runfiles, requirements_file)).readlines()
+ out = open(requirements_out).readlines()
+ out = [line.replace(absolute_path_prefix, "") for line in out]
+ if golden != out:
+ import difflib
+
+ print("".join(difflib.unified_diff(golden, out)), file=sys.stderr)
+ print(
+ "Lock file out of date. Run '"
+ + update_command
+ + "' to update.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ sys.exit(0)
+ else:
+ print(
+ f"pip-compile unexpectedly exited with code {e.code}.",
+ file=sys.stderr,
+ )
+ sys.exit(1)