2
0
mirror of https://github.com/frappe/bench.git synced 2024-09-27 22:39:03 +00:00

fix: del node_modules if frappe supports app_cache

This commit is contained in:
18alantom 2024-01-31 17:30:03 +05:30
parent d84a05ed2a
commit 9988b8356b
2 changed files with 112 additions and 41 deletions

View File

@ -12,6 +12,7 @@ from collections import OrderedDict
from datetime import date from datetime import date
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Optional
from urllib.parse import urlparse from urllib.parse import urlparse
# imports - third party imports # imports - third party imports
@ -170,7 +171,7 @@ class App(AppMeta):
branch: str = None, branch: str = None,
bench: "Bench" = None, bench: "Bench" = None,
soft_link: bool = False, soft_link: bool = False,
cache_key = None, cache_key=None,
*args, *args,
**kwargs, **kwargs,
): ):
@ -178,7 +179,7 @@ class App(AppMeta):
self.soft_link = soft_link self.soft_link = soft_link
self.required_by = None self.required_by = None
self.local_resolution = [] self.local_resolution = []
self.cache_key = cache_key self.cache_key = cache_key
super().__init__(name, branch, *args, **kwargs) super().__init__(name, branch, *args, **kwargs)
@step(title="Fetching App {repo}", success="App {repo} Fetched") @step(title="Fetching App {repo}", success="App {repo} Fetched")
@ -233,7 +234,7 @@ class App(AppMeta):
resolved=False, resolved=False,
restart_bench=True, restart_bench=True,
ignore_resolution=False, ignore_resolution=False,
using_cached=False using_cached=False,
): ):
import bench.cli import bench.cli
from bench.utils.app import get_app_name from bench.utils.app import get_app_name
@ -291,8 +292,7 @@ class App(AppMeta):
branch=self.tag, branch=self.tag,
required=self.local_resolution, required=self.local_resolution,
) )
""" """
Get App Cache Get App Cache
@ -310,7 +310,7 @@ class App(AppMeta):
Code that updates the `env` and `sites` subdirs still need Code that updates the `env` and `sites` subdirs still need
to be run. to be run.
""" """
def get_app_path(self) -> Path: def get_app_path(self) -> Path:
return Path(self.bench.name) / "apps" / self.app_name return Path(self.bench.name) / "apps" / self.app_name
@ -321,14 +321,14 @@ class App(AppMeta):
ext = "tgz" if is_compressed else "tar" ext = "tgz" if is_compressed else "tar"
tarfile_name = f"{self.app_name}-{self.cache_key[:10]}.{ext}" tarfile_name = f"{self.app_name}-{self.cache_key[:10]}.{ext}"
return cache_path / tarfile_name return cache_path / tarfile_name
def get_cached(self) -> bool: def get_cached(self) -> bool:
if not self.cache_key: if not self.cache_key:
return False return False
cache_path = self.get_app_cache_path() cache_path = self.get_app_cache_path()
mode = "r" mode = "r"
# Check if cache exists without gzip # Check if cache exists without gzip
if not cache_path.is_file(): if not cache_path.is_file():
cache_path = self.get_app_cache_path(True) cache_path = self.get_app_cache_path(True)
@ -341,7 +341,7 @@ class App(AppMeta):
app_path = self.get_app_path() app_path = self.get_app_path()
if app_path.is_dir(): if app_path.is_dir():
shutil.rmtree(app_path) shutil.rmtree(app_path)
click.secho(f"Getting {self.app_name} from cache", fg="yellow") click.secho(f"Getting {self.app_name} from cache", fg="yellow")
with tarfile.open(cache_path, mode) as tar: with tarfile.open(cache_path, mode) as tar:
try: try:
@ -352,7 +352,7 @@ class App(AppMeta):
return False return False
return True return True
def set_cache(self, compress_artifacts=False) -> bool: def set_cache(self, compress_artifacts=False) -> bool:
if not self.cache_key: if not self.cache_key:
return False return False
@ -363,15 +363,15 @@ class App(AppMeta):
cwd = os.getcwd() cwd = os.getcwd()
cache_path = self.get_app_cache_path(compress_artifacts) cache_path = self.get_app_cache_path(compress_artifacts)
mode = "w:gz" if compress_artifacts else "w" mode = "w:gz" if compress_artifacts else "w"
message = f"Caching {self.app_name} app directory" message = f"Caching {self.app_name} app directory"
if compress_artifacts: if compress_artifacts:
message += " (compressed)" message += " (compressed)"
click.secho(message) click.secho(message)
self.prune_app_directory() self.prune_app_directory()
success = False success = False
os.chdir(app_path.parent) os.chdir(app_path.parent)
try: try:
@ -384,44 +384,100 @@ class App(AppMeta):
finally: finally:
os.chdir(cwd) os.chdir(cwd)
return success return success
def prune_app_directory(self): def prune_app_directory(self):
app_path = self.get_app_path() app_path = self.get_app_path()
remove_unused_node_modules(app_path) if can_frappe_use_cached(self):
remove_unused_node_modules(app_path)
def can_frappe_use_cached(app: App) -> bool:
min_frappe = get_required_frappe_version(app)
if not min_frappe:
return False
import semantic_version as sv
try:
return sv.Version(min_frappe) in sv.SimpleSpec(">=15.12.0")
except ValueError:
# Passed value is not a version string, it's an expression
pass
try:
"""
15.12.0 is the first version to support USING_CACHED,
but there is no way to check the last version without
support. So it's not possible to have a ">" filter.
Hence this excludes the first supported version.
"""
return sv.Version("15.12.0") not in sv.SimpleSpec(min_frappe)
except ValueError:
click.secho(
f"Invalid value found for frappe version '{min_frappe}'",
fg="yellow"
)
# Invalid expression
return False
def get_required_frappe_version(app: App) -> Optional[str]:
from bench.utils.app import get_pyproject
apps_path = os.path.join(os.path.abspath(app.bench.name), "apps")
pyproject = get_pyproject(apps_path, app.app_name)
# Reference: https://github.com/frappe/bench/issues/1524
req_frappe = (
pyproject.get("tool", {})
.get("bench", {})
.get("frappe-dependencies", {})
.get("frappe")
)
if not req_frappe:
click.secho(
"Required frappe version not set in pyproject.toml, "
"please refer: https://github.com/frappe/bench/issues/1524",
fg="yellow",
)
return req_frappe
def remove_unused_node_modules(app_path: Path) -> None: def remove_unused_node_modules(app_path: Path) -> None:
""" """
Erring a bit the side of caution; since there is no explicit way Erring a bit the side of caution; since there is no explicit way
to check if node_modules are utilized, this function checks if Vite to check if node_modules are utilized, this function checks if Vite
is being used to build the frontend code. is being used to build the frontend code.
Since most popular Frappe apps use Vite to build their frontends, Since most popular Frappe apps use Vite to build their frontends,
this method should suffice. this method should suffice.
Note: root package.json is ignored cause those usually belong to Note: root package.json is ignored cause those usually belong to
apps that do not have a build step and so their node_modules are apps that do not have a build step and so their node_modules are
utilized during runtime. utilized during runtime.
""" """
for p in app_path.iterdir(): for p in app_path.iterdir():
if not p.is_dir(): if not p.is_dir():
continue continue
package_json = p / "package.json" package_json = p / "package.json"
if not package_json.is_file(): if not package_json.is_file():
continue continue
node_modules = p / "node_modules" node_modules = p / "node_modules"
if not node_modules.is_dir(): if not node_modules.is_dir():
continue continue
can_delete = False can_delete = False
with package_json.open("r", encoding="utf-8") as f: with package_json.open("r", encoding="utf-8") as f:
package_json = json.loads(f.read()) package_json = json.loads(f.read())
build_script = package_json.get("scripts", {}).get("build", "") build_script = package_json.get("scripts", {}).get("build", "")
can_delete = "vite build" in build_script can_delete = "vite build" in build_script
if can_delete: if can_delete:
shutil.rmtree(node_modules) shutil.rmtree(node_modules)
@ -503,14 +559,16 @@ def get_app(
from bench.utils.app import check_existing_dir from bench.utils.app import check_existing_dir
bench = Bench(bench_path) bench = Bench(bench_path)
app = App(git_url, branch=branch, bench=bench, soft_link=soft_link, cache_key=cache_key) app = App(
git_url, branch=branch, bench=bench, soft_link=soft_link, cache_key=cache_key
)
git_url = app.url git_url = app.url
repo_name = app.repo repo_name = app.repo
branch = app.tag branch = app.tag
bench_setup = False bench_setup = False
restart_bench = not init_bench restart_bench = not init_bench
frappe_path, frappe_branch = None, None frappe_path, frappe_branch = None, None
if resolve_deps: if resolve_deps:
resolution = make_resolution_plan(app, bench) resolution = make_resolution_plan(app, bench)
click.secho("Following apps will be installed", fg="bright_blue") click.secho("Following apps will be installed", fg="bright_blue")
@ -560,9 +618,14 @@ def get_app(
verbose=verbose, verbose=verbose,
) )
return return
if app.get_cached(): if app.get_cached():
app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench, using_cached=True) app.install(
verbose=verbose,
skip_assets=skip_assets,
restart_bench=restart_bench,
using_cached=True,
)
return return
dir_already_exists, cloned_path = check_existing_dir(bench_path, repo_name) dir_already_exists, cloned_path = check_existing_dir(bench_path, repo_name)
@ -589,9 +652,8 @@ def get_app(
or click.confirm("Do you want to reinstall the existing application?") or click.confirm("Do you want to reinstall the existing application?")
): ):
app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench) app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench)
app.set_cache(compress_artifacts)
app.set_cache(compress_artifacts)
def install_resolved_deps( def install_resolved_deps(
@ -602,6 +664,7 @@ def install_resolved_deps(
verbose=False, verbose=False,
): ):
from bench.utils.app import check_existing_dir from bench.utils.app import check_existing_dir
if "frappe" in resolution: if "frappe" in resolution:
# Terminal dependency # Terminal dependency
del resolution["frappe"] del resolution["frappe"]

View File

@ -4,7 +4,7 @@ import pathlib
import re import re
import sys import sys
import subprocess import subprocess
from typing import List from typing import List, Optional
from functools import lru_cache from functools import lru_cache
# imports - module imports # imports - module imports
@ -230,18 +230,12 @@ def get_app_name(bench_path: str, folder_name: str) -> str:
app_name = None app_name = None
apps_path = os.path.join(os.path.abspath(bench_path), "apps") apps_path = os.path.join(os.path.abspath(bench_path), "apps")
pyproject_path = os.path.join(apps_path, folder_name, "pyproject.toml")
config_py_path = os.path.join(apps_path, folder_name, "setup.cfg") config_py_path = os.path.join(apps_path, folder_name, "setup.cfg")
setup_py_path = os.path.join(apps_path, folder_name, "setup.py") setup_py_path = os.path.join(apps_path, folder_name, "setup.py")
if os.path.exists(pyproject_path): pyproject = get_pyproject(apps_path, folder_name)
try: if pyproject:
from tomli import load app_name = pyproject.get("project", {}).get("name")
except ImportError:
from tomllib import load
with open(pyproject_path, "rb") as f:
app_name = load(f).get("project", {}).get("name")
if not app_name and os.path.exists(config_py_path): if not app_name and os.path.exists(config_py_path):
from setuptools.config import read_configuration from setuptools.config import read_configuration
@ -261,6 +255,20 @@ def get_app_name(bench_path: str, folder_name: str) -> str:
return folder_name return folder_name
def get_pyproject(apps_path:str, folder_name:str) -> Optional[dict]:
pyproject_path = os.path.join(apps_path, folder_name, "pyproject.toml")
if not os.path.exists(pyproject_path):
return None
try:
from tomli import load
except ImportError:
from tomllib import load
with open(pyproject_path, "rb") as f:
return load(f)
def check_existing_dir(bench_path, repo_name): def check_existing_dir(bench_path, repo_name):
cloned_path = os.path.join(bench_path, "apps", repo_name) cloned_path = os.path.join(bench_path, "apps", repo_name)
dir_already_exists = os.path.isdir(cloned_path) dir_already_exists = os.path.isdir(cloned_path)