blob: 013c3f4b0a07c5c1a23706ca897738e7e8772692 [file]
# Copyright 2026 The LUCI Authors.
#
# 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.
"""Wrapper script to initialize and build a UV-backed virtualenv using UV."""
import argparse
import glob
import os
import subprocess
import sys
import shutil
import urllib.request
import json
import re
def report_to_endpoint(package, version, context, report_url):
data = {
"package": package,
"version": version,
"context": f"vpython-{context}"
}
req = urllib.request.Request(
report_url,
data=json.dumps(data).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=2) as response:
pass
except Exception:
# Ignore all errors to ensure it's non-blocking
pass
def try_report_missing_uv(stderr, report_url):
stderr = " ".join(stderr.split())
# Example: "no versions of foo were found"
match = re.search(r"no versions of ([\w.-]+) were found", stderr)
if match:
package = match.group(1)
report_to_endpoint(package, "", "uv", report_url)
return
# Example: "No compatible versions were found in ...: foo==1.2.3"
match = re.search(r"No compatible versions were found in [^:]+: ([\w.-]+)==([\w.-]+)", stderr)
if match:
package = match.group(1)
version = match.group(2)
report_to_endpoint(package, version, "uv", report_url)
return
# Example: "Because foo was not found in the package registry and you require foo==1.0.0"
match = re.search(r"Because ([\w.-]+) was not found in the package registry and you require \1==([\w.-]+)", stderr)
if match:
package = match.group(1)
version = match.group(2)
report_to_endpoint(package, version, "uv", report_url)
return
# More general fallback
match = re.search(r"Because ([\w.-]+) was not found in the package registry", stderr)
if match:
package = match.group(1)
report_to_endpoint(package, "", "uv", report_url)
return
print("vpython: Failed to parse missing package from uv error", file=sys.stderr)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--target-dir", required=False, help="Target virtualenv directory destination path")
parser.add_argument("--uv-bin", required=True, help="Absolute path to the pre-packaged uv binary")
parser.add_argument("--python-bin", required=True, help="Absolute path to the host python interpreter binary")
parser.add_argument("--req-file", required=True, help="Absolute path to standard requirements.txt file")
args = parser.parse_args()
target_dir = args.target_dir or os.getenv("out")
if not target_dir:
print("Error: either --target-dir parameter or 'out' environment variable is mandatory", file=sys.stderr)
return 1
# Create virtual environment.
venv_cmd = [
args.uv_bin,
"venv",
target_dir,
"--python",
args.python_bin,
]
env = os.environ.copy()
env["UV_PYTHON_DOWNLOADS"] = "never"
env["UV_NO_BUILD"] = "true"
env["UV_KEYRING_PROVIDER"] = "disabled"
env["UV_NO_CACHE"] = "true"
# Configure package registry mirror.
ar_url = os.getenv("VPYTHON_AR_URL")
if not ar_url:
raise RuntimeError("VPYTHON_AR_URL environment variable is mandatory")
env["UV_DEFAULT_INDEX"] = ar_url
print("Creating virtualenv via UV...")
subprocess.check_call(venv_cmd, env=env)
# Install requirements.
venv_python = os.path.join(target_dir, "bin", "python3")
if sys.platform == "win32":
venv_python = os.path.join(target_dir, "Scripts", "python.exe")
sync_cmd = [
args.uv_bin,
"pip",
"install",
"--python",
venv_python,
"--no-build",
"-r",
args.req_file,
]
print("Installing spec requirements via UV...")
try:
res = subprocess.run(sync_cmd, env=env, capture_output=True, text=True, check=True)
if res.stdout:
print(res.stdout)
except subprocess.CalledProcessError as e:
if e.stdout:
print(e.stdout)
if e.stderr:
print(e.stderr, file=sys.stderr)
report_url = os.getenv("VPYTHON_REPORT_MISSING_URL")
if report_url:
try_report_missing_uv(e.stderr, report_url)
sys.exit(e.returncode)
# Replicate python.exe to python3.exe in the venv from current executable.
# Replace venv launchers with actual physical copies of host CPython binary and
# copy DLLs so that Python can be executed entirely from Venv.
if sys.platform == "win32":
scripts_dir = os.path.join(target_dir, "Scripts")
cpython_dir = os.path.dirname(sys.executable)
p1 = os.path.join(scripts_dir, "python.exe")
p3 = os.path.join(scripts_dir, "python3.exe")
for p in (p1, p3):
if os.path.exists(p):
os.remove(p)
shutil.copyfile(sys.executable, p)
for dll in glob.glob(os.path.join(cpython_dir, '*.dll')):
target_dll = os.path.join(scripts_dir, os.path.basename(dll))
if os.path.exists(target_dll):
os.remove(target_dll)
shutil.copyfile(dll, target_dll)
print("UV Virtualenv synchronization complete successfully!")
if __name__ == "__main__":
sys.exit(main())