mach: Use uv to manage the virtual environment (#41861)

Instead of attempting to manage the virtual environment ourselves, use
`uv` to manage the installation of dependencies.
Since we still have dependencies coming from upstream wpt, we use
`[tool.setuptool]` in our pyproject.toml to ensure that `uv` dynamically
installs our dependencies according to the requirements.txt files.

Additionally, this PR also reverts `--no-project` usage. `--no-project`
was added as a temporary workaround in
https://github.com/servo/servo/pull/37741.
It's not 100% clear to me what exactly the issue was, but
[apparently](https://github.com/servo/servo/pull/37741#pullrequestreview-2985666234)
the issue caused the build to break.
Removing the arg seems to work fine, except that we get a warning about
a missing `requiress-python` value in `pyproject.toml`.
Apparently it is good practice to specify the requirement as `>=` in th
pyroject, and lock the exact version via `uv pin` (which writes to
`.python_version`, where we already pin 3.11.


Testing: Should be covered by existing tests, which compile code on all
platforms.

---------

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
This commit is contained in:
Jonathan Schwender
2026-01-22 06:51:44 +01:00
committed by GitHub
parent 8c85c9d1bd
commit 82df609833
8 changed files with 1891 additions and 81 deletions

View File

@@ -87,7 +87,7 @@ impl phf_shared::PhfHash for Bytes<'_> {
/// Note: This function should be kept in sync with the version in `components/servo/build.rs`
fn find_python() -> Command {
let mut command = Command::new("uv");
command.args(["run", "--no-project", "python"]);
command.args(["run", "python"]);
if command.output().is_ok_and(|out| out.status.success()) {
return command;

View File

@@ -34,7 +34,7 @@ fn main() {
/// Note: This function should be kept in sync with the version in `components/script_bindings/build.rs`
fn find_python() -> Command {
let mut command = Command::new("uv");
command.args(["run", "--no-project", "python"]);
command.args(["run", "python"]);
if command.output().is_ok_and(|out| out.status.success()) {
return command;

2
mach
View File

@@ -30,7 +30,7 @@
fi
}
run_in_nix_if_needed uv run --no-project python ${MACH_DIR}/mach "$@"
run_in_nix_if_needed uv run python ${MACH_DIR}/mach "$@"
}
'''

View File

@@ -1,4 +1,4 @@
@echo off
set workdir=%~dp0
uv run --no-project python %workdir%mach %*
uv run python %workdir%mach %*

View File

@@ -23,4 +23,4 @@ if ($arguments.Count -gt 0) {
}
}
uv run --no-project python (Join-Path $workdir "mach") @arguments
uv run python (Join-Path $workdir "mach") @arguments

View File

@@ -2,6 +2,14 @@
# `uv` logs warnings if this file doesn't contain a `project` table.
name = "servo"
version = "0.0.1"
requires-python = ">=3.11"
dynamic = ["dependencies"]
[tool.setuptools.dynamic]
dependencies = { file = ["python/requirements.txt", "tests/wpt/tests/tools/requirements_tests.txt", "tests/wpt/tests/tools/wptrunner/requirements.txt"] }
[tool.setuptools.packages.find]
exclude = ["config*"]
[tool.ruff]
line-length = 120

View File

@@ -2,12 +2,8 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import hashlib
import os
import runpy
import subprocess
import sys
from os import PathLike
from typing import TYPE_CHECKING
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
@@ -86,73 +82,7 @@ if TYPE_CHECKING:
from mach.main import Mach
def _process_exec(args: list[str], cwd: PathLike[bytes] | PathLike[str] | bytes | str) -> None:
try:
subprocess.check_output(args, stderr=subprocess.STDOUT, cwd=cwd)
except subprocess.CalledProcessError as exception:
print(exception.output.decode(sys.stdout.encoding))
print(f"Process failed with return code: {exception.returncode}")
sys.exit(1)
def install_virtual_env_requirements(project_path: str, marker_path: str) -> None:
requirements_paths = [
os.path.join(project_path, "python", "requirements.txt"),
os.path.join(
project_path,
WPT_TOOLS_PATH,
"requirements_tests.txt",
),
os.path.join(
project_path,
WPT_RUNNER_PATH,
"requirements.txt",
),
]
requirements_hasher = hashlib.sha256()
for path in requirements_paths:
with open(path, "rb") as file:
requirements_hasher.update(file.read())
try:
with open(marker_path, "r") as marker_file:
marker_hash = marker_file.read()
except FileNotFoundError:
marker_hash = None
requirements_hash = requirements_hasher.hexdigest()
if marker_hash != requirements_hash:
print(" * Installing Python requirements...")
pip_install_command = ["uv", "pip", "install"]
for requirements in requirements_paths:
pip_install_command.extend(["-r", requirements])
_process_exec(pip_install_command, cwd=project_path)
with open(marker_path, "w") as marker_file:
marker_file.write(requirements_hash)
def _activate_virtualenv(topdir: str) -> None:
virtualenv_path = os.path.join(topdir, ".venv")
with open(".python-version", "r") as python_version_file:
required_python_version = python_version_file.read().strip()
marker_path = os.path.join(virtualenv_path, f"requirements.{required_python_version}.sha256")
if os.environ.get("VIRTUAL_ENV") != virtualenv_path:
if not os.path.exists(marker_path):
print(" * Setting up virtual environment...")
_process_exec(["uv", "venv"], cwd=topdir)
script_dir = "Scripts" if _is_windows() else "bin"
runpy.run_path(os.path.join(virtualenv_path, script_dir, "activate_this.py"))
# On NixOS, prepend nix-provided binary paths so they take precedence over .venv/bin
if "SERVO_NIX_BIN_DIR" in os.environ:
os.environ["PATH"] = os.environ["SERVO_NIX_BIN_DIR"] + os.pathsep + os.environ.get("PATH", "")
install_virtual_env_requirements(topdir, marker_path)
def filter_warnings() -> None:
# Turn off warnings about deprecated syntax in our indirect dependencies.
# TODO: Find a better approach for doing this.
import warnings
@@ -174,10 +104,6 @@ def _is_windows() -> bool:
def bootstrap_command_only(topdir: str) -> int:
# we should activate the venv before importing servo.boostrap
# because the module requires non-standard python packages
_activate_virtualenv(topdir)
# We cannot import these modules until the virtual environment
# is active because they depend on modules installed via the
# virtual environment.
@@ -211,7 +137,7 @@ def bootstrap(topdir: str) -> "Mach":
print("Current path:", topdir)
sys.exit(1)
_activate_virtualenv(topdir)
filter_warnings()
def populate_context(context: None, key: None | str = None) -> str | None:
if key is None:

1876
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff