@@ -654,7 +654,9 @@ def get_build_system_from_pyproject_toml(pyproject_file):
654654 continue
655655 if bs and line .startswith ("build-backend" ) and "poetry" in line :
656656 return "poetry"
657- if line .startswith ("[tool.uv]" ):
657+ if line .startswith ("[tool.uv]" ) or (
658+ bs and line .startswith ("build-backend" ) and "uv" in line
659+ ):
658660 return "uv"
659661
660662
@@ -720,17 +722,25 @@ def uv_install_step(
720722 if os .path .isdir (path ):
721723 uv_lock_file = os .path .join (path , "uv.lock" )
722724
723- if not os .path .isfile (uv_lock_file ):
725+ uv_project_path = os .path .dirname (uv_lock_file )
726+ pyproject_file = os .path .join (uv_project_path , "pyproject.toml" )
727+
728+ has_lock = os .path .isfile (uv_lock_file )
729+ has_pyproject = os .path .isfile (pyproject_file )
730+
731+ if not has_lock and not has_pyproject :
724732 if required :
725- raise RuntimeError ("uv.lock not found: {}" .format (uv_lock_file ))
726- else :
727- step ("uv" , runtime , path , uv_export_extra_args , prefix , tmp_dir )
728- hash (uv_lock_file )
733+ raise RuntimeError (
734+ "Neither uv.lock nor pyproject.toml found in: {}" .format (path )
735+ )
736+ return
737+
738+ step ("uv" , runtime , path , uv_export_extra_args , prefix , tmp_dir )
729739
730- uv_project_path = os . path . dirname ( uv_lock_file )
731- pyproject_file = os . path . join ( uv_project_path , "pyproject.toml" )
732- if os . path . isfile ( pyproject_file ) :
733- hash (pyproject_file )
740+ if has_lock :
741+ hash ( uv_lock_file )
742+ if has_pyproject :
743+ hash (pyproject_file )
734744
735745 def poetry_install_step (
736746 path , poetry_export_extra_args = [], prefix = None , required = False , tmp_dir = None
@@ -835,7 +845,12 @@ def commands_step(path, commands):
835845 )
836846 runtime = query .runtime
837847 if runtime .startswith ("python" ):
838- if os .path .isfile (os .path .join (path , "uv.lock" )):
848+ pyproject = os .path .join (path , "pyproject.toml" )
849+ build_system = get_build_system_from_pyproject_toml (pyproject )
850+ if (
851+ os .path .isfile (os .path .join (path , "uv.lock" ))
852+ or build_system == "uv"
853+ ):
839854 uv_install_step (path )
840855 else :
841856 pip_requirements_step (os .path .join (path , "requirements.txt" ))
@@ -1430,139 +1445,157 @@ def copy_file_to_target(file, temp_dir):
14301445
14311446@contextmanager
14321447def install_uv_dependencies (query , path , uv_export_extra_args , tmp_dir ):
1433- # uv.lock is required for uv
1448+ def copy_file_to_target (file , target_dir ):
1449+ filename = os .path .basename (file )
1450+ target_file = os .path .join (target_dir , filename )
1451+ shutil .copyfile (file , target_file )
1452+ return target_file
1453+
1454+ def strip_editable_self_dependency (requirements_file ):
1455+ cleaned = []
1456+
1457+ with open (requirements_file , "r" ) as f :
1458+ for line in f :
1459+ stripped = line .strip ()
1460+ if stripped == "-e ." :
1461+ continue
1462+ if stripped .startswith ("-e file:" ) or stripped .startswith ("file://" ):
1463+ continue
1464+ cleaned .append (line .rstrip ())
1465+
1466+ with open (requirements_file , "w" ) as f :
1467+ f .write ("\n " .join (cleaned ) + "\n " )
1468+
14341469 uv_lock_file = path
14351470 if os .path .isdir (path ):
14361471 uv_lock_file = os .path .join (path , "uv.lock" )
1437- if not os .path .exists (uv_lock_file ):
1438- yield
1439- return
1440-
1441- # pyproject.toml is required by uv
1442- project_path = os .path .dirname (uv_lock_file )
1472+ project_path = (
1473+ os .path .dirname (uv_lock_file ) if os .path .isdir (path ) else os .path .dirname (path )
1474+ )
14431475 pyproject_file = os .path .join (project_path , "pyproject.toml" )
1444- if not os .path .isfile (pyproject_file ):
1445- yield
1446- return
14471476
14481477 runtime = query .runtime
1449- artifacts_dir = query .artifacts_dir
14501478 docker = query .docker
14511479 docker_image_tag_id = None
14521480
1481+ uv_exec = "uv.exe" if WINDOWS and not docker else "uv"
1482+ subproc_env = None
1483+
1484+ if not os .path .exists (uv_lock_file ) and os .path .exists (pyproject_file ):
1485+ try :
1486+ check_call ([uv_exec , "lock" ], cwd = project_path )
1487+ except FileNotFoundError as e :
1488+ raise RuntimeError (
1489+ f"uv must be installed and available in PATH for runtime ({ runtime } )"
1490+ ) from e
1491+
14531492 if docker :
14541493 docker_file = docker .docker_file
14551494 docker_image = docker .docker_image
14561495 docker_build_root = docker .docker_build_root
14571496
14581497 if docker_image :
1459- ok = False
1460- while True :
1461- output = check_output (docker_image_id_command (docker_image ))
1462- if output :
1463- docker_image_tag_id = output .decode ().strip ()
1464- log .debug (
1465- "DOCKER TAG ID: %s -> %s" , docker_image , docker_image_tag_id
1466- )
1467- ok = True
1468- if ok :
1469- break
1498+ output = (
1499+ check_output (docker_image_id_command (docker_image )).decode ().strip ()
1500+ )
1501+ if not output :
14701502 docker_cmd = docker_build_command (
14711503 build_root = docker_build_root ,
14721504 docker_file = docker_file ,
14731505 tag = docker_image ,
14741506 )
14751507 check_call (docker_cmd )
1476- ok = True
1508+ output = (
1509+ check_output (docker_image_id_command (docker_image )).decode ().strip ()
1510+ )
1511+ docker_image_tag_id = output
14771512 elif docker_file or docker_build_root :
14781513 raise ValueError (
1479- "docker_image must be specified for a custom image future references "
1514+ "docker_image must be specified when using docker_file or docker_build_root "
14801515 )
14811516
1482- working_dir = os . getcwd ( )
1517+ log . info ( "Installing python dependencies with uv (no editable installs)" )
14831518
1484- log .info ("Installing python dependencies with uv & pip: %s" , uv_lock_file )
14851519 with tempdir (tmp_dir ) as temp_dir :
1520+ pyproject_target = copy_file_to_target (pyproject_file , temp_dir )
14861521
1487- def copy_file_to_target (file , temp_dir ):
1488- filename = os .path .basename (file )
1489- target_file = os .path .join (temp_dir , filename )
1490- shutil .copyfile (file , target_file )
1491- return target_file
1492-
1493- pyproject_target_file = copy_file_to_target (pyproject_file , temp_dir )
1494- uv_lock_target_file = copy_file_to_target (uv_lock_file , temp_dir )
1495-
1496- uv_exec = "uv"
1497- python_exec = runtime
1498- subproc_env = None
1499-
1500- if not docker :
1501- if WINDOWS :
1502- uv_exec = "uv.exe"
1522+ uv_lock_target = None
1523+ if os .path .exists (uv_lock_file ):
1524+ uv_lock_target = copy_file_to_target (uv_lock_file , temp_dir )
15031525
15041526 with cd (temp_dir ):
15051527 uv_export = [
15061528 uv_exec ,
15071529 "export" ,
1508- "--frozen" ,
1530+ "--python" ,
1531+ runtime ,
15091532 "--no-dev" ,
15101533 "-o" ,
15111534 "requirements.txt" ,
1512- ] + uv_export_extra_args
1513-
1514- uv_commands = [
1515- uv_export ,
1516- [
1517- python_exec ,
1518- "-m" ,
1519- "pip" ,
1520- "install" ,
1521- "--no-compile" ,
1522- "--target=." ,
1523- "--requirement=requirements.txt" ,
1524- ],
15251535 ]
15261536
1537+ if uv_lock_target :
1538+ uv_export .append ("--frozen" )
1539+
1540+ uv_export += uv_export_extra_args
1541+
15271542 if docker :
1528- with_ssh_agent = docker .with_ssh_agent
1529- chown_mask = "{}:{}" .format (os .getuid (), os .getgid ())
1530- uv_commands += [["chown" , "-R" , chown_mask , "." ]]
1531- shell_commands = [shlex_join (cmd ) for cmd in uv_commands ]
1532- shell_command = [" && " .join (shell_commands )]
1543+ shell_command = [
1544+ " && " .join (
1545+ [
1546+ shlex_join (uv_export ),
1547+ "sed -i.bak '/^-e \\ .\\ $/d' requirements.txt" ,
1548+ shlex_join (
1549+ [
1550+ uv_exec ,
1551+ "pip" ,
1552+ "install" ,
1553+ "--python" ,
1554+ runtime ,
1555+ "--system" ,
1556+ "--no-compile" ,
1557+ "--target=." ,
1558+ "--requirement=requirements.txt" ,
1559+ ]
1560+ ),
1561+ f"chown -R { os .getuid ()} :{ os .getgid ()} ." ,
1562+ ]
1563+ )
1564+ ]
1565+
15331566 check_call (
15341567 docker_run_command (
15351568 "." ,
15361569 shell_command ,
15371570 runtime ,
15381571 image = docker_image_tag_id ,
15391572 shell = True ,
1540- ssh_agent = with_ssh_agent ,
1573+ ssh_agent = docker . with_ssh_agent ,
15411574 docker = docker ,
15421575 )
15431576 )
15441577 else :
1545- cmd_log . info ( uv_commands )
1546- log_handler and log_handler . flush ( )
1547- for uv_command in uv_commands :
1548- try :
1549- if query . quiet :
1550- check_call (
1551- uv_command ,
1552- env = subproc_env ,
1553- stdout = subprocess . DEVNULL ,
1554- stderr = subprocess . DEVNULL ,
1555- )
1556- else :
1557- check_call ( uv_command , env = subproc_env )
1558- except FileNotFoundError as e :
1559- raise RuntimeError (
1560- "UV executable must be installed and available in PATH "
1561- "for runtime ({})" . format ( runtime )
1562- ) from e
1563-
1564- os . remove ( pyproject_target_file )
1565- os .remove (uv_lock_target_file )
1578+ check_call ( uv_export , env = subproc_env )
1579+ strip_editable_self_dependency ( "requirements.txt" )
1580+ check_call (
1581+ [
1582+ uv_exec ,
1583+ "pip" ,
1584+ "install" ,
1585+ "--python" ,
1586+ runtime ,
1587+ "--system" ,
1588+ "--no-compile" ,
1589+ "--target=." ,
1590+ "--requirement=requirements.txt" ,
1591+ ],
1592+ env = subproc_env ,
1593+ )
1594+
1595+ # Cleanup copied metadata
1596+ os . remove ( pyproject_target )
1597+ if uv_lock_target :
1598+ os .remove (uv_lock_target )
15661599
15671600 yield temp_dir
15681601
0 commit comments