apps.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. __package__ = 'archivebox.plugins_pkg.pip'
  2. import os
  3. import sys
  4. import inspect
  5. import archivebox
  6. from pathlib import Path
  7. from typing import List, Dict, Optional, ClassVar
  8. from pydantic import InstanceOf, Field, model_validator
  9. import django
  10. from django.db.backends.sqlite3.base import Database as django_sqlite3 # type: ignore[import-type]
  11. from django.core.checks import Error, Tags
  12. from django.conf import settings
  13. from pydantic_pkgr import BinProvider, PipProvider, BinName, BinProviderName, ProviderLookupDict, SemVer
  14. from plugantic.base_plugin import BasePlugin
  15. from plugantic.base_configset import BaseConfigSet, ConfigSectionName
  16. from plugantic.base_check import BaseCheck
  17. from plugantic.base_binary import BaseBinary, BaseBinProvider, env, apt, brew
  18. from plugantic.base_hook import BaseHook
  19. from ...misc.logging import hint
  20. ###################### Config ##########################
  21. class PipDependencyConfigs(BaseConfigSet):
  22. section: ClassVar[ConfigSectionName] = "DEPENDENCY_CONFIG"
  23. USE_PIP: bool = True
  24. PIP_BINARY: str = Field(default='pip')
  25. PIP_ARGS: Optional[List[str]] = Field(default=None)
  26. PIP_EXTRA_ARGS: List[str] = []
  27. PIP_DEFAULT_ARGS: List[str] = []
  28. DEFAULT_GLOBAL_CONFIG = {
  29. }
  30. PIP_CONFIG = PipDependencyConfigs(**DEFAULT_GLOBAL_CONFIG)
  31. class SystemPipBinProvider(PipProvider, BaseBinProvider):
  32. name: BinProviderName = "sys_pip"
  33. INSTALLER_BIN: BinName = "pip"
  34. pip_venv: Optional[Path] = None # global pip scope
  35. def on_install(self, bin_name: str, **kwargs):
  36. # never modify system pip packages
  37. return 'refusing to install packages globally with system pip, use a venv instead'
  38. class SystemPipxBinProvider(PipProvider, BaseBinProvider):
  39. name: BinProviderName = "pipx"
  40. INSTALLER_BIN: BinName = "pipx"
  41. pip_venv: Optional[Path] = None # global pipx scope
  42. class VenvPipBinProvider(PipProvider, BaseBinProvider):
  43. name: BinProviderName = "venv_pip"
  44. INSTALLER_BIN: BinName = "pip"
  45. pip_venv: Optional[Path] = Path(os.environ.get("VIRTUAL_ENV", None) or '/tmp/NotInsideAVenv')
  46. class LibPipBinProvider(PipProvider, BaseBinProvider):
  47. name: BinProviderName = "lib_pip"
  48. INSTALLER_BIN: BinName = "pip"
  49. pip_venv: Optional[Path] = archivebox.CONSTANTS.LIB_PIP_DIR / 'venv'
  50. SYS_PIP_BINPROVIDER = SystemPipBinProvider()
  51. PIPX_PIP_BINPROVIDER = SystemPipxBinProvider()
  52. VENV_PIP_BINPROVIDER = VenvPipBinProvider()
  53. LIB_PIP_BINPROVIDER = LibPipBinProvider()
  54. pip = LIB_PIP_BINPROVIDER
  55. class ArchiveboxBinary(BaseBinary):
  56. name: BinName = 'archivebox'
  57. binproviders_supported: List[InstanceOf[BinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
  58. provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
  59. VENV_PIP_BINPROVIDER.name: {'packages': lambda: [], 'version': lambda: archivebox.__version__},
  60. SYS_PIP_BINPROVIDER.name: {'packages': lambda: [], 'version': lambda: archivebox.__version__},
  61. apt.name: {'packages': lambda: [], 'version': lambda: archivebox.__version__},
  62. brew.name: {'packages': lambda: [], 'version': lambda: archivebox.__version__},
  63. }
  64. ARCHIVEBOX_BINARY = ArchiveboxBinary()
  65. class PythonBinary(BaseBinary):
  66. name: BinName = 'python'
  67. binproviders_supported: List[InstanceOf[BinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
  68. provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
  69. SYS_PIP_BINPROVIDER.name: {
  70. 'abspath': lambda:
  71. sys.executable,
  72. 'version': lambda:
  73. '{}.{}.{}'.format(*sys.version_info[:3]),
  74. },
  75. }
  76. PYTHON_BINARY = PythonBinary()
  77. class SqliteBinary(BaseBinary):
  78. name: BinName = 'sqlite'
  79. binproviders_supported: List[InstanceOf[BaseBinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
  80. provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
  81. VENV_PIP_BINPROVIDER.name: {
  82. "abspath": lambda: Path(inspect.getfile(django_sqlite3)),
  83. "version": lambda: SemVer(django_sqlite3.version),
  84. },
  85. SYS_PIP_BINPROVIDER.name: {
  86. "abspath": lambda: Path(inspect.getfile(django_sqlite3)),
  87. "version": lambda: SemVer(django_sqlite3.version),
  88. },
  89. }
  90. @model_validator(mode='after')
  91. def validate_json_extension_is_available(self):
  92. # Check to make sure JSON extension is available in our Sqlite3 instance
  93. try:
  94. cursor = django_sqlite3.connect(':memory:').cursor()
  95. cursor.execute('SELECT JSON(\'{"a": "b"}\')')
  96. except django_sqlite3.OperationalError as exc:
  97. print(f'[red][X] Your SQLite3 version is missing the required JSON1 extension: {exc}[/red]')
  98. hint([
  99. 'Upgrade your Python version or install the extension manually:',
  100. 'https://code.djangoproject.com/wiki/JSON1Extension'
  101. ])
  102. return self
  103. SQLITE_BINARY = SqliteBinary()
  104. class DjangoBinary(BaseBinary):
  105. name: BinName = 'django'
  106. binproviders_supported: List[InstanceOf[BaseBinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
  107. provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
  108. VENV_PIP_BINPROVIDER.name: {
  109. "abspath": lambda: inspect.getfile(django),
  110. "version": lambda: django.VERSION[:3],
  111. },
  112. SYS_PIP_BINPROVIDER.name: {
  113. "abspath": lambda: inspect.getfile(django),
  114. "version": lambda: django.VERSION[:3],
  115. },
  116. }
  117. DJANGO_BINARY = DjangoBinary()
  118. class PipBinary(BaseBinary):
  119. name: BinName = "pip"
  120. binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
  121. PIP_BINARY = PipBinary()
  122. class CheckUserIsNotRoot(BaseCheck):
  123. label: str = 'CheckUserIsNotRoot'
  124. tag: str = Tags.database
  125. @staticmethod
  126. def check(settings, logger) -> List[Warning]:
  127. errors = []
  128. if getattr(settings, "USER", None) == 'root' or getattr(settings, "PUID", None) == 0:
  129. errors.append(
  130. Error(
  131. "Cannot run as root!",
  132. id="core.S001",
  133. hint=f'Run ArchiveBox as a non-root user with a UID greater than 500. (currently running as UID {os.getuid()}).',
  134. )
  135. )
  136. # logger.debug('[√] UID is not root')
  137. return errors
  138. class CheckPipEnvironment(BaseCheck):
  139. label: str = "CheckPipEnvironment"
  140. tag: str = Tags.database
  141. @staticmethod
  142. def check(settings, logger) -> List[Warning]:
  143. # hard errors: check python version
  144. if sys.version_info[:3] < (3, 10, 0):
  145. print('[red][X] Python version is not new enough: {sys.version} (>3.10 is required)[/red]', file=sys.stderr)
  146. print(' See https://github.com/ArchiveBox/ArchiveBox/wiki/Troubleshooting#python for help upgrading your Python installation.', file=sys.stderr)
  147. raise SystemExit(2)
  148. # hard errors: check django version
  149. if int(django.VERSION[0]) < 5:
  150. print('[red][X] Django version is not new enough: {django.VERSION[:3]} (>=5.0 is required)[/red]', file=sys.stderr)
  151. print(' Upgrade django using pip or your system package manager: pip3 install --upgrade django', file=sys.stderr)
  152. raise SystemExit(2)
  153. # soft errors: check that lib/pip virtualenv is setup properly
  154. errors = []
  155. LIB_PIP_BINPROVIDER.setup()
  156. if not LIB_PIP_BINPROVIDER.INSTALLER_BIN_ABSPATH:
  157. errors.append(
  158. Error(
  159. "Failed to setup data/lib/pip virtualenv for runtime dependencies!",
  160. id="pip.P001",
  161. hint="Make sure the data dir is writable and make sure python3-pip and python3-venv are installed & available on the host.",
  162. )
  163. )
  164. # logger.debug("[√] CheckPipEnvironment: data/lib/pip virtualenv is setup properly")
  165. return errors
  166. USER_IS_NOT_ROOT_CHECK = CheckUserIsNotRoot()
  167. PIP_ENVIRONMENT_CHECK = CheckPipEnvironment()
  168. class PipPlugin(BasePlugin):
  169. app_label: str = 'pip'
  170. verbose_name: str = 'PIP'
  171. hooks: List[InstanceOf[BaseHook]] = [
  172. PIP_CONFIG,
  173. SYS_PIP_BINPROVIDER,
  174. PIPX_PIP_BINPROVIDER,
  175. VENV_PIP_BINPROVIDER,
  176. LIB_PIP_BINPROVIDER,
  177. PIP_BINARY,
  178. ARCHIVEBOX_BINARY,
  179. PYTHON_BINARY,
  180. SQLITE_BINARY,
  181. DJANGO_BINARY,
  182. USER_IS_NOT_ROOT_CHECK,
  183. PIP_ENVIRONMENT_CHECK,
  184. ]
  185. PLUGIN = PipPlugin()
  186. PLUGIN.register(settings)
  187. DJANGO_APP = PLUGIN.AppConfig