on_Binary__11_pip_install.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env python3
  2. """
  3. Install a binary using pip package manager.
  4. Usage: on_Binary__install_using_pip_provider.py --binary-id=<uuid> --machine-id=<uuid> --name=<name>
  5. Output: Binary JSONL record to stdout after installation
  6. Environment variables:
  7. LIB_DIR: Library directory including machine type (e.g., data/lib/arm64-darwin) (required)
  8. """
  9. import json
  10. import os
  11. import shutil
  12. import subprocess
  13. import sys
  14. from pathlib import Path
  15. import rich_click as click
  16. from abx_pkg import Binary, PipProvider, BinProviderOverrides
  17. # Fix pydantic forward reference issue
  18. PipProvider.model_rebuild()
  19. @click.command()
  20. @click.option('--binary-id', required=True, help="Binary UUID")
  21. @click.option('--machine-id', required=True, help="Machine UUID")
  22. @click.option('--name', required=True, help="Binary name to install")
  23. @click.option('--binproviders', default='*', help="Allowed providers (comma-separated)")
  24. @click.option('--overrides', default=None, help="JSON-encoded overrides dict")
  25. def main(binary_id: str, machine_id: str, name: str, binproviders: str, overrides: str | None):
  26. """Install binary using pip."""
  27. # Check if pip provider is allowed
  28. if binproviders != '*' and 'pip' not in binproviders.split(','):
  29. click.echo(f"pip provider not allowed for {name}", err=True)
  30. sys.exit(0)
  31. # Get LIB_DIR from environment (required)
  32. # Note: LIB_DIR already includes machine type (e.g., data/lib/arm64-darwin)
  33. lib_dir = os.environ.get('LIB_DIR')
  34. if not lib_dir:
  35. click.echo("ERROR: LIB_DIR environment variable not set", err=True)
  36. sys.exit(1)
  37. # Structure: lib/arm64-darwin/pip/venv (PipProvider will create venv automatically)
  38. pip_venv_path = Path(lib_dir) / 'pip' / 'venv'
  39. pip_venv_path.parent.mkdir(parents=True, exist_ok=True)
  40. venv_python = pip_venv_path / 'bin' / 'python'
  41. # Prefer a stable system python for venv creation if provided/available
  42. preferred_python = os.environ.get('PIP_VENV_PYTHON', '').strip()
  43. if not preferred_python:
  44. for candidate in ('python3.12', 'python3.11', 'python3.10'):
  45. if shutil.which(candidate):
  46. preferred_python = candidate
  47. break
  48. if preferred_python and not venv_python.exists():
  49. try:
  50. subprocess.run(
  51. [preferred_python, '-m', 'venv', str(pip_venv_path), '--upgrade-deps'],
  52. check=True,
  53. )
  54. except Exception:
  55. # Fall back to PipProvider-managed venv creation
  56. pass
  57. # Use abx-pkg PipProvider to install binary with custom venv
  58. provider = PipProvider(pip_venv=pip_venv_path)
  59. if not provider.INSTALLER_BIN:
  60. click.echo("pip not available on this system", err=True)
  61. sys.exit(1)
  62. click.echo(f"Installing {name} via pip to venv at {pip_venv_path}...", err=True)
  63. try:
  64. # Parse overrides if provided
  65. overrides_dict = None
  66. if overrides:
  67. try:
  68. overrides_dict = json.loads(overrides)
  69. # Extract pip-specific overrides
  70. overrides_dict = overrides_dict.get('pip', {})
  71. click.echo(f"Using pip install overrides: {overrides_dict}", err=True)
  72. except json.JSONDecodeError:
  73. click.echo(f"Warning: Failed to parse overrides JSON: {overrides}", err=True)
  74. binary = Binary(name=name, binproviders=[provider], overrides={'pip': overrides_dict} if overrides_dict else {}).install()
  75. except Exception as e:
  76. click.echo(f"pip install failed: {e}", err=True)
  77. sys.exit(1)
  78. if not binary.abspath:
  79. click.echo(f"{name} not found after pip install", err=True)
  80. sys.exit(1)
  81. # Output Binary JSONL record to stdout
  82. record = {
  83. 'type': 'Binary',
  84. 'name': name,
  85. 'abspath': str(binary.abspath),
  86. 'version': str(binary.version) if binary.version else '',
  87. 'sha256': binary.sha256 or '',
  88. 'binprovider': 'pip',
  89. }
  90. print(json.dumps(record))
  91. # Emit PATH update for pip bin dir
  92. pip_bin_dir = str(pip_venv_path / 'bin')
  93. current_path = os.environ.get('PATH', '')
  94. # Check if pip_bin_dir is already in PATH
  95. path_dirs = current_path.split(':')
  96. new_path = f"{pip_bin_dir}:{current_path}" if current_path else pip_bin_dir
  97. if pip_bin_dir in path_dirs:
  98. new_path = current_path
  99. print(json.dumps({
  100. 'type': 'Machine',
  101. 'config': {
  102. 'PATH': new_path,
  103. },
  104. }))
  105. # Log human-readable info to stderr
  106. click.echo(f"Installed {name} at {binary.abspath}", err=True)
  107. click.echo(f" version: {binary.version}", err=True)
  108. sys.exit(0)
  109. if __name__ == '__main__':
  110. main()