#!/usr/bin/env python3 import glob import io import tempfile import dataclasses import pathlib import sys import subprocess import os import logging import tomllib from typing import (Self, Optional, Any,) logger = logging.getLogger(__name__) @dataclasses.dataclass class PyProject: path: pathlib.Path dependencies: dict[str, list[str]] early_features: Optional[list[str]] = None pip_find_links: Optional[list[pathlib.Path]] = None runtime_libdirs: Optional[list[pathlib.Path]] = None runtime_preload: Optional[list[pathlib.Path]] = None requirements: dict[str, pathlib.Path] = dataclasses.field(default_factory=lambda : dict()) def pyproject_load( d: pathlib.Path, ) -> PyProject: with io.open(d, 'rb') as f: content = tomllib.load(f) assert isinstance(content, dict) dependencies : dict[str, list[str]] = dict() dependencies['default'] = content['project']['dependencies'] if ( 'optional-dependencies' in content['project'] ): assert isinstance( content['project']['optional-dependencies'], dict ) for k, v in content['project']['optional-dependencies'].items(): assert isinstance(v, list) assert isinstance(k, str) dependencies[k] = v res = PyProject( path=d, dependencies=dependencies, ) tool_name = 'online.fxreader.pr34'.replace('.', '-') if ( 'tool' in content and isinstance( content['tool'], dict ) and tool_name in content['tool'] and isinstance( content['tool'][tool_name], dict ) ): if 'early_features' in content['tool'][tool_name]: res.early_features = content['tool'][tool_name]['early_features'] if 'pip_find_links' in content['tool'][tool_name]: res.pip_find_links = [ d.parent / pathlib.Path(o) for o in content['tool'][tool_name]['pip_find_links'] ] if 'runtime_libdirs' in content['tool'][tool_name]: res.runtime_libdirs = [ d.parent / pathlib.Path(o) # pathlib.Path(o) for o in content['tool'][tool_name]['runtime_libdirs'] ] if 'runtime_preload' in content['tool'][tool_name]: res.runtime_preload = [ d.parent / pathlib.Path(o) # pathlib.Path(o) for o in content['tool'][tool_name]['runtime_preload'] ] if 'requirements' in content['tool'][tool_name]: assert isinstance(content['tool'][tool_name]['requirements'], dict) res.requirements = { k : d.parent / pathlib.Path(v) # pathlib.Path(o) for k, v in content['tool'][tool_name]['requirements'].items() } return res @dataclasses.dataclass class BootstrapSettings: env_path: pathlib.Path python_path: pathlib.Path base_dir: pathlib.Path uv_args: list[str] = dataclasses.field( default_factory=lambda : os.environ.get( 'UV_ARGS', '--offline', ).split(), ) @classmethod def get( cls, base_dir: Optional[pathlib.Path] = None, ) -> Self: if base_dir is None: base_dir = pathlib.Path.cwd() env_path = base_dir / '.venv' python_path = env_path / 'bin' / 'python3' return cls( base_dir=base_dir, env_path=env_path, python_path=python_path, ) def env_bootstrap( bootstrap_settings: BootstrapSettings, pyproject: PyProject, ) -> None: pip_find_links : list[pathlib.Path] = [] if not pyproject.pip_find_links is None: pip_find_links.extend(pyproject.pip_find_links) pip_find_links_args = sum([ ['-f', str(o),] for o in pip_find_links ], []) features : list[str] = [] if pyproject.early_features: features.extend(pyproject.early_features) requirements_name = '_'.join(sorted(features)) requirements_path : Optional[pathlib.Path] = None if requirements_name in pyproject.requirements: requirements_path = pyproject.requirements[requirements_name] else: requirements_path = pyproject.path.parent / 'requirements.txt' requirements_in : list[str] = [] requirements_in.extend([ 'uv', 'pip', 'build', 'setuptools', 'meson-python', 'pybind11' ]) if pyproject.early_features: early_dependencies = sum([ pyproject.dependencies[o] for o in pyproject.early_features ], []) logger.info(dict( early_dependencies=early_dependencies, )) requirements_in.extend(early_dependencies) # if len(early_dependencies) > 0: # subprocess.check_call([ # bootstrap_settings.python_path, # '-m', # 'uv', 'pip', 'install', # *pip_find_links_args, # # '-f', str(pathlib.Path(__file__).parent / 'deps' / 'dist'), # *bootstrap_settings.uv_args, # *early_dependencies, # ]) if not requirements_path.exists(): with tempfile.NamedTemporaryFile( mode='w', prefix='requirements', suffix='.in', ) as f: f.write( '\n'.join(requirements_in) ) f.flush() subprocess.check_call([ 'uv', 'pip', 'compile', '--generate-hashes', *pip_find_links_args, # '-p', # bootstrap_settings.python_path, *bootstrap_settings.uv_args, '-o', str(requirements_path), f.name, ]) subprocess.check_call([ 'uv', 'venv', *pip_find_links_args, # '--seed', *bootstrap_settings.uv_args, str(bootstrap_settings.env_path) ]) subprocess.check_call([ 'uv', 'pip', 'install', *pip_find_links_args, '-p', bootstrap_settings.python_path, '--require-hashes', *bootstrap_settings.uv_args, '-r', str(requirements_path), ]) def paths_equal( a: pathlib.Path | str, b: pathlib.Path | str ) -> bool: return ( os.path.abspath(str(a)) == os.path.abspath(str(b)) ) def run( d: Optional[pathlib.Path] = None, cli_path: Optional[pathlib.Path] = None, ) -> None: if cli_path is None: cli_path = pathlib.Path(__file__).parent / 'cli.py' if d is None: d = pathlib.Path(__file__).parent / 'pyproject.toml' bootstrap_settings = BootstrapSettings.get() pyproject : PyProject = pyproject_load( d ) logging.basicConfig(level=logging.INFO) if not bootstrap_settings.env_path.exists(): env_bootstrap( bootstrap_settings=bootstrap_settings, pyproject=pyproject, ) logger.info([sys.executable, sys.argv, bootstrap_settings.python_path]) if not paths_equal(sys.executable, bootstrap_settings.python_path): os.execv( str(bootstrap_settings.python_path), [ str(bootstrap_settings.python_path), *sys.argv, ] ) os.execv( str(bootstrap_settings.python_path), [ str(bootstrap_settings.python_path), str( cli_path ), *sys.argv[1:], ] ) if __name__ == '__main__': run( d=pathlib.Path(__file__).parent / 'python' / 'pyproject.toml', cli_path=pathlib.Path(__file__).parent / 'python' / 'cli.py', )