pw_build: Add support for --constraint options to pip

Using this with exact versions of all dependencies should result in a
deterministic python setup.

Unlike --requirement, this option does not install any packages. This
means we can ensure the desired versions will be installed without
installing everything immediately, and also means that dependencies of
conditionally installed packages do not get unconditionally installed.

Change-Id: I649ced04f46b92ed2d63a0fb664c8b9f0742db07
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/48522
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Michael Spang <spang@google.com>
diff --git a/pw_build/python.gni b/pw_build/python.gni
index fdd5b78..cb5c1e3 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -23,6 +23,10 @@
   # Python tasks, such as running tests and Pylint, are done in a single GN
   # toolchain to avoid unnecessary duplication in the build.
   pw_build_PYTHON_TOOLCHAIN = "$dir_pw_build/python_toolchain:python"
+
+  # Constraints file selection (arguments to pip install --constraint).
+  # See pip help install.
+  pw_build_PIP_CONSTRAINTS = []
 }
 
 # Python packages provide the following targets as $target_name.$subtarget.
@@ -394,12 +398,20 @@
 
         args = [ "install" ]
 
+        inputs = pw_build_PIP_CONSTRAINTS
+        foreach(_constraints_file, pw_build_PIP_CONSTRAINTS) {
+          args += [
+            "--constraint",
+            rebase_path(_constraints_file, root_build_dir),
+          ]
+        }
+
         # For generated packages, reinstall when any files change. For regular
         # packages, only reinstall when setup.py changes.
         if (_generate_package) {
           public_deps += [ ":${invoker.target_name}" ]
         } else {
-          inputs = invoker.setup
+          inputs += invoker.setup
 
           # Install with --editable since the complete package is in source.
           args += [ "--editable" ]
@@ -727,6 +739,14 @@
       ]
     }
 
+    inputs += pw_build_PIP_CONSTRAINTS
+    foreach(_constraints_file, pw_build_PIP_CONSTRAINTS) {
+      args += [
+        "--constraint",
+        rebase_path(_constraints_file, root_build_dir),
+      ]
+    }
+
     pool = "$dir_pw_build/pool:pip($default_toolchain)"
     stamp = true
   }
diff --git a/pw_build/python.rst b/pw_build/python.rst
index 71e0eb1..d19c7a9 100644
--- a/pw_build/python.rst
+++ b/pw_build/python.rst
@@ -138,6 +138,17 @@
 Represents a set of local and PyPI requirements, with no associated source
 files. These targets serve the role of a ``requirements.txt`` file.
 
+When packages are installed by Pigweed, additional version constraints can be
+provided using the ``pw_build_PIP_CONSTRAINTS`` GN arg. This option should
+contain a list of paths to pass to the ``--constraint`` option of ``pip
+install``. This can be used to synchronize dependency upgrades across a project
+which facilitates reproducibility of builds.
+
+Note using multiple ``pw_python_requirements`` that install different versions
+of the same package will currently cause unpredictable results, while using
+constraints should have correct results (which may be an error indicating a
+conflict).
+
 .. _module-pw_build-python-dist:
 
 ---------------------