Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

1042 rindas
29 KiB

  1. # imports - standard imports
  2. import json
  3. import logging
  4. import os
  5. import re
  6. import shutil
  7. import subprocess
  8. import sys
  9. import tarfile
  10. import typing
  11. from collections import OrderedDict
  12. from datetime import date
  13. from functools import lru_cache
  14. from pathlib import Path
  15. from typing import Optional
  16. from urllib.parse import urlparse
  17. # imports - third party imports
  18. import click
  19. import git
  20. import semantic_version as sv
  21. # imports - module imports
  22. import bench
  23. from bench.exceptions import NotInBenchDirectoryError
  24. from bench.utils import (
  25. UNSET_ARG,
  26. fetch_details_from_tag,
  27. get_app_cache_extract_filter,
  28. get_available_folder_name,
  29. get_bench_cache_path,
  30. is_bench_directory,
  31. is_git_url,
  32. is_valid_xhiveframework_branch,
  33. log,
  34. run_xhiveframework_cmd,
  35. )
  36. from bench.utils.bench import build_assets, install_python_dev_dependencies
  37. from bench.utils.render import step
  38. if typing.TYPE_CHECKING:
  39. from bench.bench import Bench
  40. logger = logging.getLogger(bench.PROJECT_NAME)
  41. class AppMeta:
  42. def __init__(self, name: str, branch: str = None, to_clone: bool = True):
  43. """
  44. name (str): This could look something like
  45. 1. https://lab.membtech.com/xhiveframework/healthcare.git
  46. 2. git@github.com:xhiveframework/healthcare.git
  47. 3. xhiveframework/healthcare@develop
  48. 4. healthcare
  49. 5. healthcare@develop, healthcare@v13.12.1
  50. References for Version Identifiers:
  51. * https://www.python.org/dev/peps/pep-0440/#version-specifiers
  52. * https://docs.npmjs.com/about-semantic-versioning
  53. class Healthcare(AppConfig):
  54. dependencies = [{"xhiveframework/xhiveerp": "~13.17.0"}]
  55. """
  56. self.name = name.rstrip("/")
  57. self.remote_server = "github.com"
  58. self.to_clone = to_clone
  59. self.on_disk = False
  60. self.use_ssh = False
  61. self.from_apps = False
  62. self.is_url = False
  63. self.branch = branch
  64. self.app_name = None
  65. self.git_repo = None
  66. self.is_repo = (
  67. is_git_repo(app_path=get_repo_dir(self.name))
  68. if os.path.exists(get_repo_dir(self.name))
  69. else True
  70. )
  71. self.mount_path = os.path.abspath(
  72. os.path.join(urlparse(self.name).netloc, urlparse(self.name).path)
  73. )
  74. self.setup_details()
  75. def setup_details(self):
  76. # support for --no-git
  77. if not self.is_repo:
  78. self.repo = self.app_name = self.name
  79. return
  80. # fetch meta from installed apps
  81. if self.bench and os.path.exists(os.path.join(self.bench.name, "apps", self.name)):
  82. self.mount_path = os.path.join(self.bench.name, "apps", self.name)
  83. self.from_apps = True
  84. self._setup_details_from_mounted_disk()
  85. # fetch meta for repo on mounted disk
  86. elif os.path.exists(self.mount_path):
  87. self.on_disk = True
  88. self._setup_details_from_mounted_disk()
  89. # fetch meta for repo from remote git server - traditional get-app url
  90. elif is_git_url(self.name):
  91. self.is_url = True
  92. self._setup_details_from_git_url()
  93. # fetch meta from new styled name tags & first party apps on github
  94. else:
  95. self._setup_details_from_name_tag()
  96. if self.git_repo:
  97. self.app_name = os.path.basename(os.path.normpath(self.git_repo.working_tree_dir))
  98. else:
  99. self.app_name = self.repo
  100. def _setup_details_from_mounted_disk(self):
  101. # If app is a git repo
  102. self.git_repo = git.Repo(self.mount_path)
  103. try:
  104. self._setup_details_from_git_url(self.git_repo.remotes[0].url)
  105. if not (self.branch or self.tag):
  106. self.tag = self.branch = self.git_repo.active_branch.name
  107. except IndexError:
  108. self.org, self.repo, self.tag = os.path.split(self.mount_path)[-2:] + (self.branch,)
  109. except TypeError:
  110. # faced a "a detached symbolic reference as it points" in case you're in the middle of
  111. # some git shenanigans
  112. self.tag = self.branch = None
  113. def _setup_details_from_name_tag(self):
  114. using_cached = bool(self.cache_key)
  115. self.org, self.repo, self.tag = fetch_details_from_tag(self.name, using_cached)
  116. self.tag = self.tag or self.branch
  117. def _setup_details_from_git_url(self, url=None):
  118. return self.__setup_details_from_git(url)
  119. def __setup_details_from_git(self, url=None):
  120. name = url if url else self.name
  121. if name.startswith("git@") or name.startswith("ssh://"):
  122. self.use_ssh = True
  123. _first_part, _second_part = name.rsplit(":", 1)
  124. self.remote_server = _first_part.split("@")[-1]
  125. self.org, _repo = _second_part.rsplit("/", 1)
  126. else:
  127. protocal = "https://" if "https://" in name else "http://"
  128. self.remote_server, self.org, _repo = name.replace(protocal, "").rsplit("/", 2)
  129. self.tag = self.branch
  130. self.repo = _repo.split(".")[0]
  131. @property
  132. def url(self):
  133. if self.is_url or self.from_apps or self.on_disk:
  134. return self.name
  135. if self.use_ssh:
  136. return self.get_ssh_url()
  137. return self.get_http_url()
  138. def get_http_url(self):
  139. return f"https://{self.remote_server}/{self.org}/{self.repo}.git"
  140. def get_ssh_url(self):
  141. return f"git@{self.remote_server}:{self.org}/{self.repo}.git"
  142. @lru_cache(maxsize=None)
  143. class App(AppMeta):
  144. def __init__(
  145. self,
  146. name: str,
  147. branch: str = None,
  148. bench: "Bench" = None,
  149. soft_link: bool = False,
  150. cache_key=None,
  151. *args,
  152. **kwargs,
  153. ):
  154. self.bench = bench
  155. self.soft_link = soft_link
  156. self.required_by = None
  157. self.local_resolution = []
  158. self.cache_key = cache_key
  159. self.pyproject = None
  160. super().__init__(name, branch, *args, **kwargs)
  161. @step(title="Fetching App {repo}", success="App {repo} Fetched")
  162. def get(self):
  163. branch = f"--branch {self.tag}" if self.tag else ""
  164. shallow = "--depth 1" if self.bench.shallow_clone else ""
  165. if not self.soft_link:
  166. cmd = "git clone"
  167. args = f"{self.url} {branch} {shallow} --origin upstream"
  168. else:
  169. cmd = "ln -s"
  170. args = f"{self.name}"
  171. fetch_txt = f"Getting {self.repo}"
  172. click.secho(fetch_txt, fg="yellow")
  173. logger.log(fetch_txt)
  174. self.bench.run(
  175. f"{cmd} {args}",
  176. cwd=os.path.join(self.bench.name, "apps"),
  177. )
  178. @step(title="Archiving App {repo}", success="App {repo} Archived")
  179. def remove(self, no_backup: bool = False):
  180. active_app_path = os.path.join("apps", self.app_name)
  181. if no_backup:
  182. if not os.path.islink(active_app_path):
  183. shutil.rmtree(active_app_path)
  184. else:
  185. os.remove(active_app_path)
  186. log(f"App deleted from {active_app_path}")
  187. else:
  188. archived_path = os.path.join("archived", "apps")
  189. archived_name = get_available_folder_name(
  190. f"{self.app_name}-{date.today()}", archived_path
  191. )
  192. archived_app_path = os.path.join(archived_path, archived_name)
  193. shutil.move(active_app_path, archived_app_path)
  194. log(f"App moved from {active_app_path} to {archived_app_path}")
  195. self.from_apps = False
  196. self.on_disk = False
  197. @step(title="Installing App {repo}", success="App {repo} Installed")
  198. def install(
  199. self,
  200. skip_assets=False,
  201. verbose=False,
  202. resolved=False,
  203. restart_bench=True,
  204. ignore_resolution=False,
  205. using_cached=False,
  206. ):
  207. import bench.cli
  208. from bench.utils.app import get_app_name
  209. self.validate_app_dependencies()
  210. verbose = bench.cli.verbose or verbose
  211. app_name = get_app_name(self.bench.name, self.app_name)
  212. if not resolved and self.app_name != "xhiveframework" and not ignore_resolution:
  213. click.secho(
  214. f"Ignoring dependencies of {self.name}. To install dependencies use --resolve-deps",
  215. fg="yellow",
  216. )
  217. install_app(
  218. app=app_name,
  219. tag=self.tag,
  220. bench_path=self.bench.name,
  221. verbose=verbose,
  222. skip_assets=skip_assets,
  223. restart_bench=restart_bench,
  224. resolution=self.local_resolution,
  225. using_cached=using_cached,
  226. )
  227. @step(title="Cloning and installing {repo}", success="App {repo} Installed")
  228. def install_resolved_apps(self, *args, **kwargs):
  229. self.get()
  230. self.install(*args, **kwargs, resolved=True)
  231. @step(title="Uninstalling App {repo}", success="App {repo} Uninstalled")
  232. def uninstall(self):
  233. self.bench.run(f"{self.bench.python} -m pip uninstall -y {self.name}")
  234. def _get_dependencies(self):
  235. from bench.utils.app import get_required_deps, required_apps_from_hooks
  236. if self.on_disk:
  237. required_deps = os.path.join(self.mount_path, self.app_name, "hooks.py")
  238. try:
  239. return required_apps_from_hooks(required_deps, local=True)
  240. except IndexError:
  241. return []
  242. try:
  243. required_deps = get_required_deps(self.org, self.repo, self.tag or self.branch)
  244. return required_apps_from_hooks(required_deps)
  245. except Exception:
  246. return []
  247. def update_app_state(self):
  248. from bench.bench import Bench
  249. bench = Bench(self.bench.name)
  250. bench.apps.sync(
  251. app_dir=self.app_name,
  252. app_name=self.name,
  253. branch=self.tag,
  254. required=self.local_resolution,
  255. )
  256. def get_pyproject(self) -> Optional[dict]:
  257. from bench.utils.app import get_pyproject
  258. if self.pyproject:
  259. return self.pyproject
  260. apps_path = os.path.join(os.path.abspath(self.bench.name), "apps")
  261. pyproject_path = os.path.join(apps_path, self.app_name, "pyproject.toml")
  262. self.pyproject = get_pyproject(pyproject_path)
  263. return self.pyproject
  264. def validate_app_dependencies(self, throw=False) -> None:
  265. pyproject = self.get_pyproject() or {}
  266. deps: Optional[dict] = (
  267. pyproject.get("tool", {}).get("bench", {}).get("xhiveframework-dependencies")
  268. )
  269. if not deps:
  270. return
  271. for dep, version in deps.items():
  272. validate_dependency(self, dep, version, throw=throw)
  273. """
  274. Get App Cache
  275. Since get-app affects only the `apps`, `env`, and `sites`
  276. bench sub directories. If we assume deterministic builds
  277. when get-app is called, the `apps/app_name` sub dir can be
  278. cached.
  279. In subsequent builds this would save time by not having to:
  280. - clone repository
  281. - install frontend dependencies
  282. - building frontend assets
  283. as all of this is contained in the `apps/app_name` sub dir.
  284. Code that updates the `env` and `sites` subdirs still need
  285. to be run.
  286. """
  287. def get_app_path(self) -> Path:
  288. return Path(self.bench.name) / "apps" / self.app_name
  289. def get_app_cache_path(self, is_compressed=False) -> Path:
  290. assert self.cache_key is not None
  291. cache_path = get_bench_cache_path("apps")
  292. tarfile_name = get_cache_filename(
  293. self.app_name,
  294. self.cache_key,
  295. is_compressed,
  296. )
  297. return cache_path / tarfile_name
  298. def get_cached(self) -> bool:
  299. if not self.cache_key:
  300. return False
  301. cache_path = self.get_app_cache_path(False)
  302. mode = "r"
  303. # Check if cache exists without gzip
  304. if not cache_path.is_file():
  305. cache_path = self.get_app_cache_path(True)
  306. mode = "r:gz"
  307. # Check if cache exists with gzip
  308. if not cache_path.is_file():
  309. return False
  310. app_path = self.get_app_path()
  311. if app_path.is_dir():
  312. shutil.rmtree(app_path)
  313. click.secho(f"Getting {self.app_name} from cache", fg="yellow")
  314. with tarfile.open(cache_path, mode) as tar:
  315. extraction_filter = get_app_cache_extract_filter(count_threshold=150_000)
  316. try:
  317. tar.extractall(app_path.parent, filter=extraction_filter)
  318. except Exception:
  319. message = f"Cache extraction failed for {self.app_name}, skipping cache"
  320. click.secho(message, fg="yellow")
  321. logger.exception(message)
  322. shutil.rmtree(app_path)
  323. return False
  324. return True
  325. def set_cache(self, compress_artifacts=False) -> bool:
  326. if not self.cache_key:
  327. return False
  328. app_path = self.get_app_path()
  329. if not app_path.is_dir():
  330. return False
  331. cwd = os.getcwd()
  332. cache_path = self.get_app_cache_path(compress_artifacts)
  333. mode = "w:gz" if compress_artifacts else "w"
  334. message = f"Caching {self.app_name} app directory"
  335. if compress_artifacts:
  336. message += " (compressed)"
  337. click.secho(message)
  338. self.prune_app_directory()
  339. success = False
  340. os.chdir(app_path.parent)
  341. try:
  342. with tarfile.open(cache_path, mode) as tar:
  343. tar.add(app_path.name)
  344. success = True
  345. except Exception:
  346. log(f"Failed to cache {app_path}", level=3)
  347. success = False
  348. finally:
  349. os.chdir(cwd)
  350. return success
  351. def prune_app_directory(self):
  352. app_path = self.get_app_path()
  353. if can_xhiveframework_use_cached(self):
  354. remove_unused_node_modules(app_path)
  355. def coerce_url_to_name_if_possible(git_url: str, cache_key: str) -> str:
  356. app_name = os.path.basename(git_url)
  357. if can_get_cached(app_name, cache_key):
  358. return app_name
  359. return git_url
  360. def can_get_cached(app_name: str, cache_key: str) -> bool:
  361. """
  362. Used before App is initialized if passed `git_url` is a
  363. file URL as opposed to the app name.
  364. If True then `git_url` can be coerced into the `app_name` and
  365. checking local remote and fetching can be skipped while keeping
  366. get-app command params the same.
  367. """
  368. cache_path = get_bench_cache_path("apps")
  369. tarfile_path = cache_path / get_cache_filename(
  370. app_name,
  371. cache_key,
  372. True,
  373. )
  374. if tarfile_path.is_file():
  375. return True
  376. tarfile_path = cache_path / get_cache_filename(
  377. app_name,
  378. cache_key,
  379. False,
  380. )
  381. return tarfile_path.is_file()
  382. def get_cache_filename(app_name: str, cache_key: str, is_compressed=False):
  383. ext = "tgz" if is_compressed else "tar"
  384. return f"{app_name}-{cache_key[:10]}.{ext}"
  385. def can_xhiveframework_use_cached(app: App) -> bool:
  386. min_xhiveframework = get_required_xhiveframework_version(app)
  387. if not min_xhiveframework:
  388. return False
  389. try:
  390. return sv.Version(min_xhiveframework) in sv.SimpleSpec(">=15.12.0")
  391. except ValueError:
  392. # Passed value is not a version string, it's an expression
  393. pass
  394. try:
  395. """
  396. 15.12.0 is the first version to support USING_CACHED,
  397. but there is no way to check the last version without
  398. support. So it's not possible to have a ">" filter.
  399. Hence this excludes the first supported version.
  400. """
  401. return sv.Version("15.12.0") not in sv.SimpleSpec(min_xhiveframework)
  402. except ValueError:
  403. click.secho(f"Invalid value found for xhiveframework version '{min_xhiveframework}'", fg="yellow")
  404. # Invalid expression
  405. return False
  406. def validate_dependency(app: App, dep: str, req_version: str, throw=False) -> None:
  407. dep_path = Path(app.bench.name) / "apps" / dep
  408. if not dep_path.is_dir():
  409. click.secho(f"Required xhiveframework-dependency '{dep}' not found.", fg="yellow")
  410. if throw:
  411. sys.exit(1)
  412. return
  413. dep_version = get_dep_version(dep, dep_path)
  414. if not dep_version:
  415. return
  416. if sv.Version(dep_version) not in sv.SimpleSpec(req_version):
  417. click.secho(
  418. f"Installed xhiveframework-dependency '{dep}' version '{dep_version}' "
  419. f"does not satisfy required version '{req_version}'. "
  420. f"App '{app.name}' might not work as expected.",
  421. fg="yellow",
  422. )
  423. if throw:
  424. click.secho(f"Please install '{dep}{req_version}' first and retry", fg="red")
  425. sys.exit(1)
  426. def get_dep_version(dep: str, dep_path: Path) -> Optional[str]:
  427. from bench.utils.app import get_pyproject
  428. dep_pp = get_pyproject(str(dep_path / "pyproject.toml"))
  429. version = dep_pp.get("project", {}).get("version")
  430. if version:
  431. return version
  432. dinit_path = dep_path / dep / "__init__.py"
  433. if not dinit_path.is_file():
  434. return None
  435. with dinit_path.open("r", encoding="utf-8") as dinit:
  436. for line in dinit:
  437. if not line.startswith("__version__ =") and not line.startswith("VERSION ="):
  438. continue
  439. version = line.split("=")[1].strip().strip("\"'")
  440. if version:
  441. return version
  442. else:
  443. break
  444. return None
  445. def get_required_xhiveframework_version(app: App) -> Optional[str]:
  446. pyproject = app.get_pyproject() or {}
  447. # Reference: https://lab.membtech.com/xhiveframework/bench_new/issues/1524
  448. req_xhiveframework = (
  449. pyproject.get("tool", {})
  450. .get("bench", {})
  451. .get("xhiveframework-dependencies", {})
  452. .get("xhiveframework")
  453. )
  454. if not req_xhiveframework:
  455. click.secho(
  456. "Required xhiveframework version not set in pyproject.toml, "
  457. "please refer: https://lab.membtech.com/xhiveframework/bench_new/issues/1524",
  458. fg="yellow",
  459. )
  460. return req_xhiveframework
  461. def remove_unused_node_modules(app_path: Path) -> None:
  462. """
  463. Erring a bit the side of caution; since there is no explicit way
  464. to check if node_modules are utilized, this function checks if Vite
  465. is being used to build the frontend code.
  466. Since most popular Xhiveframework apps use Vite to build their frontends,
  467. this method should suffice.
  468. Note: root package.json is ignored cause those usually belong to
  469. apps that do not have a build step and so their node_modules are
  470. utilized during runtime.
  471. """
  472. for p in app_path.iterdir():
  473. if not p.is_dir():
  474. continue
  475. package_json = p / "package.json"
  476. if not package_json.is_file():
  477. continue
  478. node_modules = p / "node_modules"
  479. if not node_modules.is_dir():
  480. continue
  481. can_delete = False
  482. with package_json.open("r", encoding="utf-8") as f:
  483. package_json = json.loads(f.read())
  484. build_script = package_json.get("scripts", {}).get("build", "")
  485. can_delete = "vite build" in build_script
  486. if can_delete:
  487. shutil.rmtree(node_modules)
  488. def make_resolution_plan(app: App, bench: "Bench"):
  489. """
  490. decide what apps and versions to install and in what order
  491. """
  492. resolution = OrderedDict()
  493. resolution[app.app_name] = app
  494. for app_name in app._get_dependencies():
  495. dep_app = App(app_name, bench=bench)
  496. is_valid_xhiveframework_branch(dep_app.url, dep_app.branch)
  497. dep_app.required_by = app.name
  498. if dep_app.app_name in resolution:
  499. click.secho(f"{dep_app.app_name} is already resolved skipping", fg="yellow")
  500. continue
  501. resolution[dep_app.app_name] = dep_app
  502. resolution.update(make_resolution_plan(dep_app, bench))
  503. app.local_resolution = [repo_name for repo_name, _ in reversed(resolution.items())]
  504. return resolution
  505. def get_excluded_apps(bench_path="."):
  506. try:
  507. with open(os.path.join(bench_path, "sites", "excluded_apps.txt")) as f:
  508. return f.read().strip().split("\n")
  509. except OSError:
  510. return []
  511. def add_to_excluded_apps_txt(app, bench_path="."):
  512. if app == "xhiveframework":
  513. raise ValueError("Xhiveframework app cannot be excluded from update")
  514. if app not in os.listdir("apps"):
  515. raise ValueError(f"The app {app} does not exist")
  516. apps = get_excluded_apps(bench_path=bench_path)
  517. if app not in apps:
  518. apps.append(app)
  519. return write_excluded_apps_txt(apps, bench_path=bench_path)
  520. def write_excluded_apps_txt(apps, bench_path="."):
  521. with open(os.path.join(bench_path, "sites", "excluded_apps.txt"), "w") as f:
  522. return f.write("\n".join(apps))
  523. def remove_from_excluded_apps_txt(app, bench_path="."):
  524. apps = get_excluded_apps(bench_path=bench_path)
  525. if app in apps:
  526. apps.remove(app)
  527. return write_excluded_apps_txt(apps, bench_path=bench_path)
  528. def get_app(
  529. git_url,
  530. branch=None,
  531. bench_path=".",
  532. skip_assets=False,
  533. verbose=False,
  534. overwrite=False,
  535. soft_link=False,
  536. init_bench=False,
  537. resolve_deps=False,
  538. cache_key=None,
  539. compress_artifacts=False,
  540. ):
  541. """bench get-app clones a Xhiveframework App from remote (GitHub or any other git server),
  542. and installs it on the current bench. This also resolves dependencies based on the
  543. apps' required_apps defined in the hooks.py file.
  544. If the bench_path is not a bench directory, a new bench is created named using the
  545. git_url parameter.
  546. """
  547. import bench as _bench
  548. import bench.cli as bench_cli
  549. from bench.bench import Bench
  550. from bench.utils.app import check_existing_dir
  551. if urlparse(git_url).scheme == "file" and cache_key:
  552. git_url = coerce_url_to_name_if_possible(git_url, cache_key)
  553. bench = Bench(bench_path)
  554. app = App(
  555. git_url, branch=branch, bench=bench, soft_link=soft_link, cache_key=cache_key
  556. )
  557. git_url = app.url
  558. repo_name = app.repo
  559. branch = app.tag
  560. bench_setup = False
  561. restart_bench = not init_bench
  562. xhiveframework_path, xhiveframework_branch = None, None
  563. if resolve_deps:
  564. resolution = make_resolution_plan(app, bench)
  565. click.secho("Following apps will be installed", fg="bright_blue")
  566. for idx, app in enumerate(reversed(resolution.values()), start=1):
  567. print(
  568. f"{idx}. {app.name} {f'(required by {app.required_by})' if app.required_by else ''}"
  569. )
  570. if "xhiveframework" in resolution:
  571. # Todo: Make xhiveframework a terminal dependency for all xhiveframework apps.
  572. xhiveframework_path, xhiveframework_branch = resolution["xhiveframework"].url, resolution["xhiveframework"].tag
  573. if not is_bench_directory(bench_path):
  574. if not init_bench:
  575. raise NotInBenchDirectoryError(
  576. f"{os.path.realpath(bench_path)} is not a valid bench directory. "
  577. "Run with --init-bench if you'd like to create a Bench too."
  578. )
  579. from bench.utils.system import init
  580. bench_path = get_available_folder_name(f"{app.repo}-bench", bench_path)
  581. init(
  582. path=bench_path,
  583. xhiveframework_path=xhiveframework_path,
  584. xhiveframework_branch=xhiveframework_branch or branch,
  585. )
  586. os.chdir(bench_path)
  587. bench_setup = True
  588. if bench_setup and bench_cli.from_command_line and bench_cli.dynamic_feed:
  589. _bench.LOG_BUFFER.append(
  590. {
  591. "message": f"Fetching App {repo_name}",
  592. "prefix": click.style("⏼", fg="bright_yellow"),
  593. "is_parent": True,
  594. "color": None,
  595. }
  596. )
  597. if resolve_deps:
  598. install_resolved_deps(
  599. bench,
  600. resolution,
  601. bench_path=bench_path,
  602. skip_assets=skip_assets,
  603. verbose=verbose,
  604. )
  605. return
  606. if app.get_cached():
  607. app.install(
  608. verbose=verbose,
  609. skip_assets=skip_assets,
  610. restart_bench=restart_bench,
  611. using_cached=True,
  612. )
  613. return
  614. dir_already_exists, cloned_path = check_existing_dir(bench_path, repo_name)
  615. to_clone = not dir_already_exists
  616. # application directory already exists
  617. # prompt user to overwrite it
  618. if dir_already_exists and (
  619. overwrite
  620. or click.confirm(
  621. f"A directory for the application '{repo_name}' already exists. "
  622. "Do you want to continue and overwrite it?"
  623. )
  624. ):
  625. app.remove()
  626. to_clone = True
  627. if to_clone:
  628. app.get()
  629. if (
  630. to_clone
  631. or overwrite
  632. or click.confirm("Do you want to reinstall the existing application?")
  633. ):
  634. app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench)
  635. app.set_cache(compress_artifacts)
  636. def install_resolved_deps(
  637. bench,
  638. resolution,
  639. bench_path=".",
  640. skip_assets=False,
  641. verbose=False,
  642. ):
  643. from bench.utils.app import check_existing_dir
  644. if "xhiveframework" in resolution:
  645. # Terminal dependency
  646. del resolution["xhiveframework"]
  647. for repo_name, app in reversed(resolution.items()):
  648. existing_dir, path_to_app = check_existing_dir(bench_path, repo_name)
  649. if existing_dir:
  650. is_compatible = False
  651. try:
  652. installed_branch = bench.apps.states[repo_name]["resolution"]["branch"].strip()
  653. except Exception:
  654. installed_branch = (
  655. subprocess.check_output(
  656. "git rev-parse --abbrev-ref HEAD", shell=True, cwd=path_to_app
  657. )
  658. .decode("utf-8")
  659. .rstrip()
  660. )
  661. try:
  662. if app.tag is None:
  663. current_remote = (
  664. subprocess.check_output(
  665. f"git config branch.{installed_branch}.remote", shell=True, cwd=path_to_app
  666. )
  667. .decode("utf-8")
  668. .rstrip()
  669. )
  670. default_branch = (
  671. subprocess.check_output(
  672. f"git symbolic-ref refs/remotes/{current_remote}/HEAD",
  673. shell=True,
  674. cwd=path_to_app,
  675. )
  676. .decode("utf-8")
  677. .rsplit("/")[-1]
  678. .strip()
  679. )
  680. is_compatible = default_branch == installed_branch
  681. else:
  682. is_compatible = installed_branch == app.tag
  683. except Exception:
  684. is_compatible = False
  685. prefix = "C" if is_compatible else "Inc"
  686. click.secho(
  687. f"{prefix}ompatible version of {repo_name} is already installed",
  688. fg="green" if is_compatible else "red",
  689. )
  690. app.update_app_state()
  691. if click.confirm(
  692. f"Do you wish to clone and install the already installed {prefix}ompatible app"
  693. ):
  694. click.secho(f"Removing installed app {app.name}", fg="yellow")
  695. shutil.rmtree(path_to_app)
  696. else:
  697. continue
  698. app.install_resolved_apps(skip_assets=skip_assets, verbose=verbose)
  699. def new_app(app, no_git=None, bench_path="."):
  700. if bench.XHIVEFRAMEWORK_VERSION in (0, None):
  701. raise NotInBenchDirectoryError(
  702. f"{os.path.realpath(bench_path)} is not a valid bench directory."
  703. )
  704. # For backwards compatibility
  705. app = app.lower().replace(" ", "_").replace("-", "_")
  706. if app[0].isdigit() or "." in app:
  707. click.secho(
  708. "App names cannot start with numbers(digits) or have dot(.) in them", fg="red"
  709. )
  710. return
  711. apps = os.path.abspath(os.path.join(bench_path, "apps"))
  712. args = ["make-app", apps, app]
  713. if no_git:
  714. if bench.XHIVEFRAMEWORK_VERSION < 14:
  715. click.secho("Xhiveframework v14 or greater is needed for '--no-git' flag", fg="red")
  716. return
  717. args.append(no_git)
  718. logger.log(f"creating new app {app}")
  719. run_xhiveframework_cmd(*args, bench_path=bench_path)
  720. install_app(app, bench_path=bench_path)
  721. def install_app(
  722. app,
  723. tag=None,
  724. bench_path=".",
  725. verbose=False,
  726. no_cache=False,
  727. restart_bench=True,
  728. skip_assets=False,
  729. resolution=UNSET_ARG,
  730. using_cached=False,
  731. ):
  732. import bench.cli as bench_cli
  733. from bench.bench import Bench
  734. install_text = f"Installing {app}"
  735. click.secho(install_text, fg="yellow")
  736. logger.log(install_text)
  737. if resolution == UNSET_ARG:
  738. resolution = []
  739. bench = Bench(bench_path)
  740. conf = bench.conf
  741. verbose = bench_cli.verbose or verbose
  742. quiet_flag = "" if verbose else "--quiet"
  743. cache_flag = "--no-cache-dir" if no_cache else ""
  744. app_path = os.path.realpath(os.path.join(bench_path, "apps", app))
  745. bench.run(
  746. f"{bench.python} -m pip install {quiet_flag} --upgrade -e {app_path} {cache_flag}"
  747. )
  748. if conf.get("developer_mode"):
  749. install_python_dev_dependencies(apps=app, bench_path=bench_path, verbose=verbose)
  750. if not using_cached and os.path.exists(os.path.join(app_path, "package.json")):
  751. yarn_install = "yarn install --check-files"
  752. if verbose:
  753. yarn_install += " --verbose"
  754. bench.run(yarn_install, cwd=app_path)
  755. bench.apps.sync(app_name=app, required=resolution, branch=tag, app_dir=app_path)
  756. if not skip_assets:
  757. build_assets(bench_path=bench_path, app=app, using_cached=using_cached)
  758. if restart_bench:
  759. # Avoiding exceptions here as production might not be set-up
  760. # OR we might just be generating docker images.
  761. bench.reload(_raise=False)
  762. def pull_apps(apps=None, bench_path=".", reset=False):
  763. """Check all apps if there no local changes, pull"""
  764. from bench.bench import Bench
  765. from bench.utils.app import get_current_branch, get_remote
  766. bench = Bench(bench_path)
  767. rebase = "--rebase" if bench.conf.get("rebase_on_pull") else ""
  768. apps = apps or bench.apps
  769. excluded_apps = bench.excluded_apps
  770. # check for local changes
  771. if not reset:
  772. for app in apps:
  773. if app in excluded_apps:
  774. print(f"Skipping reset for app {app}")
  775. continue
  776. app_dir = get_repo_dir(app, bench_path=bench_path)
  777. if os.path.exists(os.path.join(app_dir, ".git")):
  778. out = subprocess.check_output("git status", shell=True, cwd=app_dir)
  779. out = out.decode("utf-8")
  780. if not re.search(r"nothing to commit, working (directory|tree) clean", out):
  781. print(
  782. f"""
  783. Cannot proceed with update: You have local changes in app "{app}" that are not committed.
  784. Here are your choices:
  785. 1. Merge the {app} app manually with "git pull" / "git pull --rebase" and fix conflicts.
  786. 2. Temporarily remove your changes with "git stash" or discard them completely
  787. with "bench update --reset" or for individual repositries "git reset --hard"
  788. 3. If your changes are helpful for others, send in a pull request via GitHub and
  789. wait for them to be merged in the core."""
  790. )
  791. sys.exit(1)
  792. for app in apps:
  793. if app in excluded_apps:
  794. print(f"Skipping pull for app {app}")
  795. continue
  796. app_dir = get_repo_dir(app, bench_path=bench_path)
  797. if os.path.exists(os.path.join(app_dir, ".git")):
  798. remote = get_remote(app)
  799. if not remote:
  800. # remote is False, i.e. remote doesn't exist, add the app to excluded_apps.txt
  801. add_to_excluded_apps_txt(app, bench_path=bench_path)
  802. print(
  803. f"Skipping pull for app {app}, since remote doesn't exist, and"
  804. " adding it to excluded apps"
  805. )
  806. continue
  807. if not bench.conf.get("shallow_clone") or not reset:
  808. is_shallow = os.path.exists(os.path.join(app_dir, ".git", "shallow"))
  809. if is_shallow:
  810. s = " to safely pull remote changes." if not reset else ""
  811. print(f"Unshallowing {app}{s}")
  812. bench.run(f"git fetch {remote} --unshallow", cwd=app_dir)
  813. branch = get_current_branch(app, bench_path=bench_path)
  814. logger.log(f"pulling {app}")
  815. if reset:
  816. reset_cmd = f"git reset --hard {remote}/{branch}"
  817. if bench.conf.get("shallow_clone"):
  818. bench.run(f"git fetch --depth=1 --no-tags {remote} {branch}", cwd=app_dir)
  819. bench.run(reset_cmd, cwd=app_dir)
  820. bench.run("git reflog expire --all", cwd=app_dir)
  821. bench.run("git gc --prune=all", cwd=app_dir)
  822. else:
  823. bench.run("git fetch --all", cwd=app_dir)
  824. bench.run(reset_cmd, cwd=app_dir)
  825. else:
  826. bench.run(f"git pull {rebase} {remote} {branch}", cwd=app_dir)
  827. bench.run('find . -name "*.pyc" -delete', cwd=app_dir)
  828. def use_rq(bench_path):
  829. bench_path = os.path.abspath(bench_path)
  830. celery_app = os.path.join(bench_path, "apps", "xhiveframework", "xhiveframework", "celery_app.py")
  831. return not os.path.exists(celery_app)
  832. def get_repo_dir(app, bench_path="."):
  833. return os.path.join(bench_path, "apps", app)
  834. def is_git_repo(app_path):
  835. try:
  836. git.Repo(app_path, search_parent_directories=False)
  837. return True
  838. except git.exc.InvalidGitRepositoryError:
  839. return False
  840. def install_apps_from_path(path, bench_path="."):
  841. apps = get_apps_json(path)
  842. for app in apps:
  843. get_app(
  844. app["url"],
  845. branch=app.get("branch"),
  846. bench_path=bench_path,
  847. skip_assets=True,
  848. )
  849. def get_apps_json(path):
  850. import requests
  851. if path.startswith("http"):
  852. r = requests.get(path)
  853. return r.json()
  854. with open(path) as f:
  855. return json.load(f)