1. add -U to UV_ARGS,
    ignore it in venv;
  2. generate .whl;
  3. update m.py for pr34;
		
	
			
		
			
				
	
	
		
			599 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			599 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python3
 | 
						|
import glob
 | 
						|
import importlib
 | 
						|
import json
 | 
						|
import io
 | 
						|
import tempfile
 | 
						|
import dataclasses
 | 
						|
import pathlib
 | 
						|
import sys
 | 
						|
import subprocess
 | 
						|
import os
 | 
						|
import logging
 | 
						|
import typing
 | 
						|
 | 
						|
 | 
						|
from typing import (
 | 
						|
	Optional,
 | 
						|
	Any,
 | 
						|
	cast,
 | 
						|
	Type,
 | 
						|
	TypeVar,
 | 
						|
	Callable,
 | 
						|
)
 | 
						|
from typing_extensions import (
 | 
						|
	Self,
 | 
						|
	BinaryIO,
 | 
						|
	overload,
 | 
						|
)
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
def toml_load(f: BinaryIO) -> Any:
 | 
						|
	try:
 | 
						|
		tomllib = importlib.import_module('tomllib')
 | 
						|
 | 
						|
		return cast(
 | 
						|
			Callable[[Any], Any],
 | 
						|
			getattr(
 | 
						|
				tomllib,
 | 
						|
				'load',
 | 
						|
			),
 | 
						|
		)(f)
 | 
						|
	except ModuleNotFoundError:
 | 
						|
		pass
 | 
						|
 | 
						|
	try:
 | 
						|
		import tomli
 | 
						|
 | 
						|
		return tomli.load(f)
 | 
						|
	except ModuleNotFoundError:
 | 
						|
		pass
 | 
						|
 | 
						|
	raise NotImplementedError
 | 
						|
 | 
						|
 | 
						|
@dataclasses.dataclass
 | 
						|
class PyProject:
 | 
						|
	@dataclasses.dataclass
 | 
						|
	class Module:
 | 
						|
		name: str
 | 
						|
		meson: Optional[pathlib.Path] = None
 | 
						|
		tool: dict[str, Any] = dataclasses.field(default_factory=lambda: dict())
 | 
						|
 | 
						|
	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
 | 
						|
 | 
						|
	@dataclasses.dataclass
 | 
						|
	class ThirdPartyRoot:
 | 
						|
		package: Optional[str] = None
 | 
						|
		module_root: Optional[str] = None
 | 
						|
		path: Optional[str] = None
 | 
						|
 | 
						|
	third_party_roots: list[ThirdPartyRoot] = dataclasses.field(
 | 
						|
		default_factory=lambda: [],
 | 
						|
	)
 | 
						|
	requirements: dict[str, pathlib.Path] = dataclasses.field(default_factory=lambda: dict())
 | 
						|
 | 
						|
	modules: list[Module] = dataclasses.field(
 | 
						|
		default_factory=lambda: [],
 | 
						|
	)
 | 
						|
 | 
						|
	tool: dict[str, Any] = dataclasses.field(
 | 
						|
		default_factory=lambda: dict(),
 | 
						|
	)
 | 
						|
 | 
						|
 | 
						|
Key = TypeVar('Key')
 | 
						|
Value = TypeVar('Value')
 | 
						|
 | 
						|
 | 
						|
@overload
 | 
						|
def check_dict(
 | 
						|
	value: Any,
 | 
						|
	KT: Type[Key],
 | 
						|
	VT: Type[Value],
 | 
						|
) -> dict[Key, Value]: ...
 | 
						|
 | 
						|
 | 
						|
@overload
 | 
						|
def check_dict(
 | 
						|
	value: Any,
 | 
						|
	KT: Type[Key],
 | 
						|
) -> dict[Key, Any]: ...
 | 
						|
 | 
						|
 | 
						|
def check_dict(
 | 
						|
	value: Any,
 | 
						|
	KT: Type[Key],
 | 
						|
	VT: Optional[Type[Value]] = None,
 | 
						|
) -> dict[Key, Value]:
 | 
						|
	assert isinstance(value, dict)
 | 
						|
	value2 = cast(dict[Any, Any], value)
 | 
						|
 | 
						|
	VT_class: Optional[type[Any]] = None
 | 
						|
 | 
						|
	if not VT is None:
 | 
						|
		if not typing.get_origin(VT) is None:
 | 
						|
			VT_class = cast(type[Any], typing.get_origin(VT))
 | 
						|
		else:
 | 
						|
			VT_class = VT
 | 
						|
 | 
						|
	assert all([isinstance(k, KT) and (VT_class is None or isinstance(v, VT_class)) for k, v in value2.items()])
 | 
						|
 | 
						|
	if VT is None:
 | 
						|
		return cast(
 | 
						|
			dict[Key, Any],
 | 
						|
			value,
 | 
						|
		)
 | 
						|
	else:
 | 
						|
		return cast(
 | 
						|
			dict[Key, Value],
 | 
						|
			value,
 | 
						|
		)
 | 
						|
 | 
						|
 | 
						|
@overload
 | 
						|
def check_list(
 | 
						|
	value: Any,
 | 
						|
	VT: Type[Value],
 | 
						|
) -> list[Value]: ...
 | 
						|
 | 
						|
 | 
						|
@overload
 | 
						|
def check_list(
 | 
						|
	value: Any,
 | 
						|
) -> list[Any]: ...
 | 
						|
 | 
						|
 | 
						|
def check_list(
 | 
						|
	value: Any,
 | 
						|
	VT: Optional[Type[Value]] = None,
 | 
						|
) -> list[Value] | list[Any]:
 | 
						|
	assert isinstance(value, list)
 | 
						|
	value2 = cast(list[Any], value)
 | 
						|
 | 
						|
	assert all([(VT is None or isinstance(o, VT)) for o in value2])
 | 
						|
 | 
						|
	if VT is None:
 | 
						|
		return cast(
 | 
						|
			list[Any],
 | 
						|
			value,
 | 
						|
		)
 | 
						|
	else:
 | 
						|
		return cast(
 | 
						|
			list[Value],
 | 
						|
			value,
 | 
						|
		)
 | 
						|
 | 
						|
 | 
						|
def check_type(
 | 
						|
	value: Any,
 | 
						|
	VT: Type[Value],
 | 
						|
	attribute_name: Optional[str] = None,
 | 
						|
) -> Value:
 | 
						|
	if attribute_name:
 | 
						|
		attribute_value = getattr(value, attribute_name)
 | 
						|
		assert isinstance(attribute_value, VT)
 | 
						|
		return attribute_value
 | 
						|
	else:
 | 
						|
		assert isinstance(value, VT)
 | 
						|
 | 
						|
		return value
 | 
						|
 | 
						|
 | 
						|
def pyproject_load(
 | 
						|
	d: pathlib.Path,
 | 
						|
) -> PyProject:
 | 
						|
	with io.open(d, 'rb') as f:
 | 
						|
		content = toml_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 check_dict(
 | 
						|
			check_dict(
 | 
						|
				check_dict(
 | 
						|
					content,
 | 
						|
					str,
 | 
						|
					# Any,
 | 
						|
				)['project'],
 | 
						|
				str,
 | 
						|
				# Any,
 | 
						|
			)['optional-dependencies'],
 | 
						|
			str,
 | 
						|
			list[Any],
 | 
						|
		).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:
 | 
						|
		res.tool = check_dict(
 | 
						|
			content['tool'],
 | 
						|
			str,
 | 
						|
		)
 | 
						|
 | 
						|
	if 'tool' in content and isinstance(content['tool'], dict) and tool_name in content['tool'] and isinstance(content['tool'][tool_name], dict):
 | 
						|
		pr34_tool = check_dict(
 | 
						|
			check_dict(
 | 
						|
				content['tool'],
 | 
						|
				str,
 | 
						|
			)[tool_name],
 | 
						|
			str,
 | 
						|
		)
 | 
						|
 | 
						|
		if 'early_features' in pr34_tool:
 | 
						|
			res.early_features = pr34_tool['early_features']
 | 
						|
 | 
						|
		if 'pip_find_links' in pr34_tool:
 | 
						|
			res.pip_find_links = [d.parent / pathlib.Path(o) for o in pr34_tool['pip_find_links']]
 | 
						|
 | 
						|
		if 'runtime_libdirs' in pr34_tool:
 | 
						|
			res.runtime_libdirs = [
 | 
						|
				d.parent / pathlib.Path(o)
 | 
						|
				# pathlib.Path(o)
 | 
						|
				for o in check_list(pr34_tool['runtime_libdirs'], str)
 | 
						|
			]
 | 
						|
 | 
						|
		if 'runtime_preload' in pr34_tool:
 | 
						|
			res.runtime_preload = [
 | 
						|
				d.parent / pathlib.Path(o)
 | 
						|
				# pathlib.Path(o)
 | 
						|
				for o in check_list(pr34_tool['runtime_preload'], str)
 | 
						|
			]
 | 
						|
 | 
						|
		if 'third_party_roots' in pr34_tool:
 | 
						|
			for o in check_list(pr34_tool['third_party_roots']):
 | 
						|
				o2 = check_dict(o, str, str)
 | 
						|
				assert all([k in {'package', 'module_root', 'path'} for k in o2])
 | 
						|
 | 
						|
				res.third_party_roots.append(
 | 
						|
					PyProject.ThirdPartyRoot(
 | 
						|
						package=o2.get('package'),
 | 
						|
						module_root=o2.get('module_root'),
 | 
						|
						path=o2.get('path'),
 | 
						|
					)
 | 
						|
				)
 | 
						|
 | 
						|
		if 'requirements' in pr34_tool:
 | 
						|
			res.requirements = {
 | 
						|
				k: d.parent / pathlib.Path(v)
 | 
						|
				# pathlib.Path(o)
 | 
						|
				for k, v in check_dict(pr34_tool['requirements'], str, str).items()
 | 
						|
			}
 | 
						|
 | 
						|
		if 'modules' in pr34_tool:
 | 
						|
			modules = check_list(pr34_tool['modules'])
 | 
						|
			# res.modules = []
 | 
						|
 | 
						|
			for o in modules:
 | 
						|
				assert isinstance(o, dict)
 | 
						|
				assert 'name' in o and isinstance(o['name'], str)
 | 
						|
 | 
						|
				module = PyProject.Module(
 | 
						|
					name=o['name'],
 | 
						|
				)
 | 
						|
 | 
						|
				if 'meson' in o:
 | 
						|
					assert 'meson' in o and isinstance(o['meson'], str)
 | 
						|
 | 
						|
					module.meson = pathlib.Path(o['meson'])
 | 
						|
 | 
						|
				if 'tool' in o:
 | 
						|
					module.tool.update(
 | 
						|
						check_dict(
 | 
						|
							o['tool'],
 | 
						|
							str,
 | 
						|
						)
 | 
						|
					)
 | 
						|
 | 
						|
				res.modules.append(module)
 | 
						|
 | 
						|
	return res
 | 
						|
 | 
						|
 | 
						|
@dataclasses.dataclass
 | 
						|
class BootstrapSettings:
 | 
						|
	env_path: pathlib.Path
 | 
						|
	python_path: pathlib.Path
 | 
						|
	base_dir: pathlib.Path
 | 
						|
	python_version: Optional[str] = dataclasses.field(
 | 
						|
		default_factory=lambda: os.environ.get(
 | 
						|
			'PYTHON_VERSION',
 | 
						|
			'%d.%d'
 | 
						|
			% (
 | 
						|
				sys.version_info.major,
 | 
						|
				sys.version_info.minor,
 | 
						|
			),
 | 
						|
		).strip()
 | 
						|
	)
 | 
						|
	pip_check_conflicts: Optional[bool] = dataclasses.field(
 | 
						|
		default_factory=lambda: os.environ.get('PIP_CHECK_CONFLICTS', json.dumps(True)) in [json.dumps(True)],
 | 
						|
	)
 | 
						|
	uv_args: list[str] = dataclasses.field(
 | 
						|
		default_factory=lambda: os.environ.get(
 | 
						|
			'UV_ARGS',
 | 
						|
			'--offline -U',
 | 
						|
		).split(),
 | 
						|
	)
 | 
						|
 | 
						|
	@classmethod
 | 
						|
	def get(
 | 
						|
		cls,
 | 
						|
		base_dir: Optional[pathlib.Path] = None,
 | 
						|
	) -> Self:
 | 
						|
		if base_dir is None:
 | 
						|
			base_dir = pathlib.Path.cwd()
 | 
						|
 | 
						|
		env_path: Optional[pathlib.Path] = None
 | 
						|
		if 'ENV_PATH' in os.environ:
 | 
						|
			env_path = pathlib.Path(os.environ['ENV_PATH'])
 | 
						|
		else:
 | 
						|
			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,
 | 
						|
		)
 | 
						|
 | 
						|
 | 
						|
class requirements_name_get_t:
 | 
						|
	@dataclasses.dataclass
 | 
						|
	class res_t:
 | 
						|
		not_compiled: pathlib.Path
 | 
						|
		compiled: pathlib.Path
 | 
						|
		name: str
 | 
						|
 | 
						|
 | 
						|
def requirements_name_get(
 | 
						|
	source_dir: pathlib.Path,
 | 
						|
	python_version: Optional[str],
 | 
						|
	features: list[str],
 | 
						|
	requirements: dict[str, pathlib.Path],
 | 
						|
) -> requirements_name_get_t.res_t:
 | 
						|
	requirements_python_version: Optional[str] = None
 | 
						|
	if not python_version is None:
 | 
						|
		requirements_python_version = python_version.replace('.', '_')
 | 
						|
 | 
						|
	requirements_name = '_'.join(sorted(features))
 | 
						|
 | 
						|
	if requirements_python_version:
 | 
						|
		requirements_name += '_' + requirements_python_version
 | 
						|
 | 
						|
	requirements_path: Optional[pathlib.Path] = None
 | 
						|
 | 
						|
	if requirements_name in requirements:
 | 
						|
		requirements_path = requirements[requirements_name]
 | 
						|
	else:
 | 
						|
		requirements_path = source_dir / 'requirements.txt'
 | 
						|
 | 
						|
	requirements_path_in = requirements_path.parent / (requirements_path.stem + '.in')
 | 
						|
 | 
						|
	requirements_in: list[str] = []
 | 
						|
 | 
						|
	return requirements_name_get_t.res_t(
 | 
						|
		not_compiled=requirements_path_in,
 | 
						|
		compiled=requirements_path,
 | 
						|
		name=requirements_name,
 | 
						|
	)
 | 
						|
 | 
						|
 | 
						|
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
 | 
						|
		],
 | 
						|
		cast(list[str], []),
 | 
						|
	)
 | 
						|
 | 
						|
	features: list[str] = []
 | 
						|
 | 
						|
	if pyproject.early_features:
 | 
						|
		features.extend(pyproject.early_features)
 | 
						|
 | 
						|
	requirements_name_get_res = requirements_name_get(
 | 
						|
		python_version=bootstrap_settings.python_version,
 | 
						|
		features=features,
 | 
						|
		requirements=pyproject.requirements,
 | 
						|
		source_dir=pyproject.path.parent,
 | 
						|
	)
 | 
						|
	requirements_path = requirements_name_get_res.compiled
 | 
						|
 | 
						|
	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], cast(list[str], []))
 | 
						|
 | 
						|
		logger.info(
 | 
						|
			dict(
 | 
						|
				requirements_name_get_res=requirements_name_get_res,
 | 
						|
				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,
 | 
						|
		#     ])
 | 
						|
 | 
						|
	uv_python_version: list[str] = []
 | 
						|
	venv_python_version: list[str] = []
 | 
						|
 | 
						|
	if not bootstrap_settings.python_version is None:
 | 
						|
		uv_python_version.extend(
 | 
						|
			[
 | 
						|
				# '-p',
 | 
						|
				'--python-version',
 | 
						|
				bootstrap_settings.python_version,
 | 
						|
			]
 | 
						|
		)
 | 
						|
		venv_python_version.extend(
 | 
						|
			[
 | 
						|
				'-p',
 | 
						|
				# '--python-version',
 | 
						|
				bootstrap_settings.python_version,
 | 
						|
			]
 | 
						|
		)
 | 
						|
 | 
						|
	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',
 | 
						|
					*uv_python_version,
 | 
						|
					'--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',
 | 
						|
			*[o for o in bootstrap_settings.uv_args if not o in ['-U', '--upgrade']],
 | 
						|
			'venv',
 | 
						|
			*venv_python_version,
 | 
						|
			*pip_find_links_args,
 | 
						|
			# '--seed',
 | 
						|
			str(bootstrap_settings.env_path),
 | 
						|
		]
 | 
						|
	)
 | 
						|
 | 
						|
	subprocess.check_call(
 | 
						|
		[
 | 
						|
			'uv',
 | 
						|
			'pip',
 | 
						|
			'install',
 | 
						|
			*uv_python_version,
 | 
						|
			*pip_find_links_args,
 | 
						|
			'-p',
 | 
						|
			bootstrap_settings.python_path,
 | 
						|
			'--require-hashes',
 | 
						|
			*bootstrap_settings.uv_args,
 | 
						|
			'-r',
 | 
						|
			str(requirements_path),
 | 
						|
		]
 | 
						|
	)
 | 
						|
 | 
						|
	if bootstrap_settings.pip_check_conflicts:
 | 
						|
		subprocess.check_call(
 | 
						|
			[
 | 
						|
				bootstrap_settings.python_path,
 | 
						|
				'-m',
 | 
						|
				'online.fxreader.pr34.commands',
 | 
						|
				'pip_check_conflicts',
 | 
						|
			]
 | 
						|
		)
 | 
						|
 | 
						|
 | 
						|
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 / 'pyproject.toml',
 | 
						|
		cli_path=pathlib.Path(__file__).parent / 'cli.py',
 | 
						|
	)
 |