2
0
mirror of https://github.com/frappe/bench.git synced 2025-01-09 16:36:25 +00:00

Merge branch 'develop' into staging

This commit is contained in:
Gavin D'souza 2022-06-08 13:28:22 +05:30
commit 2005fde2cd
16 changed files with 619 additions and 196 deletions

View File

@ -48,32 +48,17 @@ matrix:
- name: "Python 3.7 Tests" - name: "Python 3.7 Tests"
python: 3.7 python: 3.7
env: TEST=bench env: TEST=bench
script: python -m unittest -v bench.tests.test_init script: python -m unittest -v bench.tests.test_utils && python -m unittest -v bench.tests.test_init
- name: "Python 3.8 Tests" - name: "Python 3.8 Tests"
python: 3.8 python: 3.8
env: TEST=bench env: TEST=bench
script: python -m unittest -v bench.tests.test_init script: python -m unittest -v bench.tests.test_utils && python -m unittest -v bench.tests.test_init
- name: "Python 3.9 Tests" - name: "Python 3.9 Tests"
python: 3.9 python: 3.9
env: TEST=bench env: TEST=bench
script: python -m unittest -v bench.tests.test_init script: python -m unittest -v bench.tests.test_utils && python -m unittest -v bench.tests.test_init
- name: "Python 3.7 Easy Install"
python: 3.7
env: TEST=easy_install
script: sudo python $TRAVIS_BUILD_DIR/install.py --user travis --run-travis --production --verbose
- name: "Python 3.8 Easy Install"
python: 3.8
env: TEST=easy_install
script: sudo python $TRAVIS_BUILD_DIR/install.py --user travis --run-travis --production --verbose
- name: "Python 3.9 Easy Install"
python: 3.9
env: TEST=easy_install
script: sudo python $TRAVIS_BUILD_DIR/install.py --user travis --run-travis --production --verbose
install: install:
- pip3 install urllib3 pyOpenSSL ndg-httpsclient pyasn1 - pip3 install urllib3 pyOpenSSL ndg-httpsclient pyasn1

View File

@ -54,7 +54,7 @@ The setup for each of these installations can be achieved in multiple ways:
We recommend using either the Docker Installation or the Easy Install Script to setup a Production Environment. For Development, you may choose either of the three methods to setup an instance. We recommend using either the Docker Installation or the Easy Install Script to setup a Production Environment. For Development, you may choose either of the three methods to setup an instance.
Otherwise, if you are looking to evaluate ERPNext, you can also download the [Virtual Machine Image](https://erpnext.com/download) or register for [a free trial on erpnext.com](https://erpnext.com/pricing). Otherwise, if you are looking to evaluate ERPNext, you can register for [a free trial on erpnext.com](https://erpnext.com/pricing).
### Containerized Installation ### Containerized Installation

View File

@ -8,11 +8,14 @@ import shutil
import subprocess import subprocess
import sys import sys
import typing import typing
from collections import OrderedDict
from datetime import date from datetime import date
from urllib.parse import urlparse from urllib.parse import urlparse
import os
# imports - third party imports # imports - third party imports
import click import click
from git import Repo
# imports - module imports # imports - module imports
import bench import bench
@ -22,6 +25,7 @@ from bench.utils import (
get_available_folder_name, get_available_folder_name,
is_bench_directory, is_bench_directory,
is_git_url, is_git_url,
is_valid_frappe_branch,
log, log,
run_frappe_cmd, run_frappe_cmd,
) )
@ -55,7 +59,7 @@ class AppMeta:
class Healthcare(AppConfig): class Healthcare(AppConfig):
dependencies = [{"frappe/erpnext": "~13.17.0"}] dependencies = [{"frappe/erpnext": "~13.17.0"}]
""" """
self.name = name.rstrip('/') self.name = name.rstrip("/")
self.remote_server = "github.com" self.remote_server = "github.com"
self.to_clone = to_clone self.to_clone = to_clone
self.on_disk = False self.on_disk = False
@ -63,6 +67,8 @@ class AppMeta:
self.from_apps = False self.from_apps = False
self.is_url = False self.is_url = False
self.branch = branch self.branch = branch
self.app_name = None
self.git_repo = None
self.mount_path = os.path.abspath( self.mount_path = os.path.abspath(
os.path.join(urlparse(self.name).netloc, urlparse(self.name).path) os.path.join(urlparse(self.name).netloc, urlparse(self.name).path)
) )
@ -70,13 +76,12 @@ class AppMeta:
def setup_details(self): def setup_details(self):
# fetch meta from installed apps # fetch meta from installed apps
if ( if self.bench and os.path.exists(
not self.to_clone os.path.join(self.bench.name, "apps", self.name)
and hasattr(self, "bench")
and os.path.exists(os.path.join(self.bench.name, "apps", self.name))
): ):
self.mount_path = os.path.join(self.bench.name, "apps", self.name)
self.from_apps = True self.from_apps = True
self._setup_details_from_installed_apps() self._setup_details_from_mounted_disk()
# fetch meta for repo on mounted disk # fetch meta for repo on mounted disk
elif os.path.exists(self.mount_path): elif os.path.exists(self.mount_path):
@ -86,50 +91,56 @@ class AppMeta:
# fetch meta for repo from remote git server - traditional get-app url # fetch meta for repo from remote git server - traditional get-app url
elif is_git_url(self.name): elif is_git_url(self.name):
self.is_url = True self.is_url = True
if self.name.startswith("git@") or self.name.startswith("ssh://"):
self.use_ssh = True
self._setup_details_from_git_url() self._setup_details_from_git_url()
# fetch meta from new styled name tags & first party apps on github # fetch meta from new styled name tags & first party apps on github
else: else:
self._setup_details_from_name_tag() self._setup_details_from_name_tag()
if self.git_repo:
self.app_name = os.path.basename(
os.path.normpath(self.git_repo.working_tree_dir)
)
else:
self.app_name = self.repo
def _setup_details_from_mounted_disk(self): def _setup_details_from_mounted_disk(self):
self.org, self.repo, self.tag = os.path.split(self.mount_path)[-2:] + ( # If app is a git repo
self.branch, self.git_repo = Repo(self.mount_path)
) try:
self._setup_details_from_git_url(self.git_repo.remotes[0].url)
if not (self.branch or self.tag):
self.tag = self.branch = self.git_repo.active_branch.name
except IndexError:
self.org, self.repo, self.tag = os.path.split(self.mount_path)[-2:] + (self.branch,)
except TypeError:
# faced a "a detached symbolic reference as it points" in case you're in the middle of
# some git shenanigans
self.tag = self.branch = None
def _setup_details_from_name_tag(self): def _setup_details_from_name_tag(self):
self.org, self.repo, self.tag = fetch_details_from_tag(self.name) self.org, self.repo, self.tag = fetch_details_from_tag(self.name)
self.tag = self.tag or self.branch self.tag = self.tag or self.branch
def _setup_details_from_installed_apps(self): def _setup_details_from_git_url(self, url=None):
self.org, self.repo, self.tag = os.path.split( return self.__setup_details_from_git(url)
os.path.join(self.bench.name, "apps", self.name)
)[-2:] + (self.branch,)
def _setup_details_from_git_url(self): def __setup_details_from_git(self, url=None):
return self.__setup_details_from_git() name = url if url else self.name
if name.startswith("git@") or name.startswith("ssh://"):
def __setup_details_from_git(self): self.use_ssh = True
if self.use_ssh: _first_part, _second_part = name.rsplit(":", 1)
_first_part, _second_part = self.name.split(":")
self.remote_server = _first_part.split("@")[-1] self.remote_server = _first_part.split("@")[-1]
self.org, _repo = _second_part.rsplit("/", 1) self.org, _repo = _second_part.rsplit("/", 1)
else: else:
self.remote_server, self.org, _repo = self.name.rsplit("/", 2) protocal = "https://" if "https://" in name else "http://"
self.remote_server, self.org, _repo = name.replace(protocal, "").rsplit("/", 2)
self.tag = self.branch self.tag = self.branch
self.repo = _repo.split(".")[0] self.repo = _repo.split(".")[0]
@property @property
def url(self): def url(self):
if self.from_apps:
return os.path.abspath(os.path.join("apps", self.name))
if self.on_disk:
return self.mount_path
if self.is_url: if self.is_url:
return self.name return self.name
@ -147,10 +158,10 @@ class AppMeta:
@functools.lru_cache(maxsize=None) @functools.lru_cache(maxsize=None)
class App(AppMeta): class App(AppMeta):
def __init__( def __init__(self, name: str, branch: str = None, bench: "Bench" = None, *args, **kwargs):
self, name: str, branch: str = None, bench: "Bench" = None, *args, **kwargs
):
self.bench = bench self.bench = bench
self.required_by = None
self.local_resolution = []
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")
@ -168,44 +179,100 @@ class App(AppMeta):
) )
@step(title="Archiving App {repo}", success="App {repo} Archived") @step(title="Archiving App {repo}", success="App {repo} Archived")
def remove(self): def remove(self, no_backup: bool = False):
active_app_path = os.path.join("apps", self.repo) active_app_path = os.path.join("apps", self.name)
archived_path = os.path.join("archived", "apps")
archived_name = get_available_folder_name( if no_backup:
f"{self.repo}-{date.today()}", archived_path shutil.rmtree(active_app_path)
) log(f"App deleted from {active_app_path}")
archived_app_path = os.path.join(archived_path, archived_name) else:
log(f"App moved from {active_app_path} to {archived_app_path}") archived_path = os.path.join("archived", "apps")
shutil.move(active_app_path, archived_app_path) archived_name = get_available_folder_name(f"{self.repo}-{date.today()}", archived_path)
archived_app_path = os.path.join(archived_path, archived_name)
shutil.move(active_app_path, archived_app_path)
log(f"App moved from {active_app_path} to {archived_app_path}")
@step(title="Installing App {repo}", success="App {repo} Installed") @step(title="Installing App {repo}", success="App {repo} Installed")
def install(self, skip_assets=False, verbose=False, restart_bench=True): def install(
self,
skip_assets=False,
verbose=False,
resolved=False,
restart_bench=True,
ignore_resolution=False,
):
import bench.cli import bench.cli
from bench.utils.app import get_app_name from bench.utils.app import get_app_name
verbose = bench.cli.verbose or verbose verbose = bench.cli.verbose or verbose
app_name = get_app_name(self.bench.name, self.repo) app_name = get_app_name(self.bench.name, self.app_name)
if not resolved and self.repo != "frappe" and not ignore_resolution:
# TODO: this should go inside install_app only tho - issue: default/resolved branch click.secho(
setup_app_dependencies( f"Ignoring dependencies of {self.name}. To install dependencies use --resolve-deps",
repo_name=self.repo, fg="yellow",
bench_path=self.bench.name, )
branch=self.tag,
verbose=verbose,
skip_assets=skip_assets,
)
install_app( install_app(
app=app_name, app=app_name,
tag=self.tag,
bench_path=self.bench.name, bench_path=self.bench.name,
verbose=verbose, verbose=verbose,
skip_assets=skip_assets, skip_assets=skip_assets,
restart_bench=restart_bench restart_bench=restart_bench,
resolution=self.local_resolution
) )
@step(title="Cloning and installing {repo}", success="App {repo} Installed")
def install_resolved_apps(self, *args, **kwargs):
self.get()
self.install(*args, **kwargs, resolved=True)
@step(title="Uninstalling App {repo}", success="App {repo} Uninstalled") @step(title="Uninstalling App {repo}", success="App {repo} Uninstalled")
def uninstall(self): def uninstall(self):
self.bench.run(f"{self.bench.python} -m pip uninstall -y {self.repo}") self.bench.run(f"{self.bench.python} -m pip uninstall -y {self.name}")
def _get_dependencies(self):
from bench.utils.app import get_required_deps, required_apps_from_hooks
if self.on_disk:
required_deps = os.path.join(self.mount_path, self.repo,'hooks.py')
try:
return required_apps_from_hooks(required_deps, local=True)
except IndexError:
return []
try:
required_deps = get_required_deps(self.org, self.repo, self.tag or self.branch)
return required_apps_from_hooks(required_deps)
except Exception:
return []
def update_app_state(self):
from bench.bench import Bench
bench = Bench(self.bench.name)
bench.apps.sync(app_dir=self.app_name, app_name=self.name,
branch=self.tag, required_list=self.local_resolution)
def make_resolution_plan(app: App, bench: "Bench"):
"""
decide what apps and versions to install and in what order
"""
resolution = OrderedDict()
resolution[app.repo] = app
for app_name in app._get_dependencies():
dep_app = App(app_name, bench=bench)
is_valid_frappe_branch(dep_app.url, dep_app.branch)
dep_app.required_by = app.name
if dep_app.repo in resolution:
click.secho(f"{dep_app.repo} is already resolved skipping", fg="yellow")
continue
resolution[dep_app.repo] = dep_app
resolution.update(make_resolution_plan(dep_app, bench))
app.local_resolution = [repo_name for repo_name, _ in reversed(resolution.items())]
return resolution
def add_to_appstxt(app, bench_path="."): def add_to_appstxt(app, bench_path="."):
@ -264,36 +331,6 @@ def remove_from_excluded_apps_txt(app, bench_path="."):
return write_excluded_apps_txt(apps, bench_path=bench_path) return write_excluded_apps_txt(apps, bench_path=bench_path)
def setup_app_dependencies(
repo_name, bench_path=".", branch=None, skip_assets=False, verbose=False
):
# branch kwarg is somewhat of a hack here; since we're assuming the same branches for all apps
# for eg: if you're installing erpnext@develop, you'll want frappe@develop and healthcare@develop too
import glob
import bench.cli
from bench.bench import Bench
verbose = bench.cli.verbose or verbose
apps_path = os.path.join(os.path.abspath(bench_path), "apps")
files = glob.glob(os.path.join(apps_path, repo_name, "**", "hooks.py"))
if files:
with open(files[0]) as f:
lines = [x for x in f.read().split("\n") if x.strip().startswith("required_apps")]
if lines:
required_apps = eval(lines[0].strip("required_apps").strip().lstrip("=").strip())
# TODO: when the time comes, add version check here
for app in required_apps:
if app not in Bench(bench_path).apps:
get_app(
app,
bench_path=bench_path,
branch=branch,
skip_assets=skip_assets,
verbose=verbose,
)
def get_app( def get_app(
git_url, git_url,
branch=None, branch=None,
@ -302,6 +339,7 @@ def get_app(
verbose=False, verbose=False,
overwrite=False, overwrite=False,
init_bench=False, init_bench=False,
resolve_deps=False,
): ):
"""bench get-app clones a Frappe App from remote (GitHub or any other git server), """bench get-app clones a Frappe App from remote (GitHub or any other git server),
and installs it on the current bench. This also resolves dependencies based on the and installs it on the current bench. This also resolves dependencies based on the
@ -310,9 +348,10 @@ def get_app(
If the bench_path is not a bench directory, a new bench is created named using the If the bench_path is not a bench directory, a new bench is created named using the
git_url parameter. git_url parameter.
""" """
from bench.bench import Bench
import bench as _bench import bench as _bench
import bench.cli as bench_cli import bench.cli as bench_cli
from bench.bench import Bench
from bench.utils.app import check_existing_dir
bench = Bench(bench_path) bench = Bench(bench_path)
app = App(git_url, branch=branch, bench=bench) app = App(git_url, branch=branch, bench=bench)
@ -321,6 +360,17 @@ def get_app(
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
if resolve_deps:
resolution = make_resolution_plan(app, bench)
click.secho("Following apps will be installed", fg="bright_blue")
for idx, app in enumerate(reversed(resolution.values()), start=1):
print(f"{idx}. {app.name} {f'(required by {app.required_by})' if app.required_by else ''}")
if "frappe" in resolution:
# Todo: Make frappe a terminal dependency for all frappe apps.
frappe_path, frappe_branch = resolution["frappe"].url, resolution["frappe"].tag
if not is_bench_directory(bench_path): if not is_bench_directory(bench_path):
if not init_bench: if not init_bench:
@ -332,20 +382,35 @@ def get_app(
from bench.utils.system import init from bench.utils.system import init
bench_path = get_available_folder_name(f"{app.repo}-bench", bench_path) bench_path = get_available_folder_name(f"{app.repo}-bench", bench_path)
init(path=bench_path, frappe_branch=branch) init(
path=bench_path,
frappe_path=frappe_path,
frappe_branch=frappe_branch if frappe_branch else branch,
)
os.chdir(bench_path) os.chdir(bench_path)
bench_setup = True bench_setup = True
if bench_setup and bench_cli.from_command_line and bench_cli.dynamic_feed: if bench_setup and bench_cli.from_command_line and bench_cli.dynamic_feed:
_bench.LOG_BUFFER.append({ _bench.LOG_BUFFER.append(
"message": f"Fetching App {repo_name}", {
"prefix": click.style('', fg='bright_yellow'), "message": f"Fetching App {repo_name}",
"is_parent": True, "prefix": click.style("", fg="bright_yellow"),
"color": None, "is_parent": True,
}) "color": None,
}
)
cloned_path = os.path.join(bench_path, "apps", repo_name) if resolve_deps:
dir_already_exists = os.path.isdir(cloned_path) install_resolved_deps(
bench,
resolution,
bench_path=bench_path,
skip_assets=skip_assets,
verbose=verbose,
)
return
dir_already_exists, cloned_path = check_existing_dir(bench_path, repo_name)
to_clone = not dir_already_exists to_clone = not dir_already_exists
# application directory already exists # application directory already exists
@ -371,22 +436,89 @@ def get_app(
app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench) app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench)
def install_resolved_deps(
bench,
resolution,
bench_path=".",
skip_assets=False,
verbose=False,
):
from bench.utils.app import check_existing_dir
if "frappe" in resolution:
# Terminal dependency
del resolution["frappe"]
for repo_name, app in reversed(resolution.items()):
existing_dir, path_to_app = check_existing_dir(bench_path, repo_name)
if existing_dir:
is_compatible = False
try:
installed_branch = bench.apps.states[repo_name]["resolution"]["branch"].strip()
except Exception:
installed_branch = (
subprocess.
check_output("git rev-parse --abbrev-ref HEAD", shell=True, cwd=path_to_app)
.decode("utf-8")
.rstrip()
)
try:
if app.tag is None:
current_remote = (
subprocess.check_output(f"git config branch.{installed_branch}.remote", shell=True, cwd=path_to_app)
.decode("utf-8")
.rstrip()
)
default_branch = (
subprocess.check_output(
f"git symbolic-ref refs/remotes/{current_remote}/HEAD", shell=True, cwd=path_to_app
)
.decode("utf-8")
.rsplit("/")[-1]
.strip()
)
is_compatible = default_branch == installed_branch
else:
is_compatible = installed_branch == app.tag
except Exception:
is_compatible = False
prefix = 'C' if is_compatible else 'Inc'
click.secho(
f"{prefix}ompatible version of {repo_name} is already installed",
fg="green" if is_compatible else "red",
)
app.update_app_state()
if click.confirm(
f"Do you wish to clone and install the already installed {prefix}ompatible app"
):
click.secho(f"Removing installed app {app.name}", fg="yellow")
shutil.rmtree(path_to_app)
else:
continue
app.install_resolved_apps(skip_assets=skip_assets, verbose=verbose)
def new_app(app, no_git=None, bench_path="."): def new_app(app, no_git=None, bench_path="."):
if bench.FRAPPE_VERSION in (0, None): if bench.FRAPPE_VERSION in (0, None):
raise NotInBenchDirectoryError( raise NotInBenchDirectoryError(f"{os.path.realpath(bench_path)} is not a valid bench directory.")
f"{os.path.realpath(bench_path)} is not a valid bench directory."
)
# For backwards compatibility # For backwards compatibility
app = app.lower().replace(" ", "_").replace("-", "_") app = app.lower().replace(" ", "_").replace("-", "_")
if app[0].isdigit() or "." in app:
click.secho(
"App names cannot start with numbers(digits) or have dot(.) in them",
fg="red"
)
return
apps = os.path.abspath(os.path.join(bench_path, "apps")) apps = os.path.abspath(os.path.join(bench_path, "apps"))
args = ["make-app", apps, app] args = ["make-app", apps, app]
if no_git: if no_git:
if bench.FRAPPE_VERSION < 14: if bench.FRAPPE_VERSION < 14:
click.secho( click.secho("Frappe v14 or greater is needed for '--no-git' flag", fg="red")
"Frappe v14 or greater is needed for '--no-git' flag",
fg="red"
)
return return
args.append(no_git) args.append(no_git)
@ -397,11 +529,13 @@ def new_app(app, no_git=None, bench_path="."):
def install_app( def install_app(
app, app,
tag=None,
bench_path=".", bench_path=".",
verbose=False, verbose=False,
no_cache=False, no_cache=False,
restart_bench=True, restart_bench=True,
skip_assets=False, skip_assets=False,
resolution=[]
): ):
import bench.cli as bench_cli import bench.cli as bench_cli
from bench.bench import Bench from bench.bench import Bench
@ -427,7 +561,7 @@ def install_app(
if os.path.exists(os.path.join(app_path, "package.json")): if os.path.exists(os.path.join(app_path, "package.json")):
bench.run("yarn install", cwd=app_path) bench.run("yarn install", cwd=app_path)
bench.apps.sync() bench.apps.sync(app_name=app, required=resolution, branch=tag, app_dir=app_path)
if not skip_assets: if not skip_assets:
build_assets(bench_path=bench_path, app=app) build_assets(bench_path=bench_path, app=app)
@ -526,7 +660,10 @@ def install_apps_from_path(path, bench_path="."):
apps = get_apps_json(path) apps = get_apps_json(path)
for app in apps: for app in apps:
get_app( get_app(
app["url"], branch=app.get("branch"), bench_path=bench_path, skip_assets=True, app["url"],
branch=app.get("branch"),
bench_path=bench_path,
skip_assets=True,
) )

View File

@ -1,14 +1,16 @@
# imports - standard imports # imports - standard imports
import subprocess
import functools import functools
import os import os
import shutil import shutil
import json
import sys import sys
import logging import logging
from typing import List, MutableSequence, TYPE_CHECKING from typing import List, MutableSequence, TYPE_CHECKING, Union
# imports - module imports # imports - module imports
import bench import bench
from bench.exceptions import ValidationError from bench.exceptions import AppNotInstalledError, InvalidRemoteException
from bench.config.common_site_config import setup_config from bench.config.common_site_config import setup_config
from bench.utils import ( from bench.utils import (
paths_in_bench, paths_in_bench,
@ -27,9 +29,11 @@ from bench.utils.bench import (
restart_process_manager, restart_process_manager,
remove_backups_crontab, remove_backups_crontab,
get_venv_path, get_venv_path,
get_virtualenv_path,
get_env_cmd, get_env_cmd,
) )
from bench.utils.render import job, step from bench.utils.render import job, step
from bench.utils.app import get_current_version
if TYPE_CHECKING: if TYPE_CHECKING:
@ -46,7 +50,7 @@ class Base:
class Validator: class Validator:
def validate_app_uninstall(self, app): def validate_app_uninstall(self, app):
if app not in self.apps: if app not in self.apps:
raise ValidationError(f"No app named {app}") raise AppNotInstalledError(f"No app named {app}")
validate_app_installed_on_sites(app, bench_path=self.name) validate_app_installed_on_sites(app, bench_path=self.name)
@ -116,11 +120,16 @@ class Bench(Base, Validator):
self.apps.append(app) self.apps.append(app)
self.apps.sync() self.apps.sync()
def uninstall(self, app): def uninstall(self, app, no_backup=False, force=False):
from bench.app import App from bench.app import App
self.validate_app_uninstall(app) if not force:
self.apps.remove(App(app, bench=self, to_clone=False)) self.validate_app_uninstall(app)
try:
self.apps.remove(App(app, bench=self, to_clone=False), no_backup=no_backup)
except InvalidRemoteException:
if not force:
raise
self.apps.sync() self.apps.sync()
# self.build() - removed because it seems unnecessary # self.build() - removed because it seems unnecessary
self.reload() self.reload()
@ -155,12 +164,102 @@ class Bench(Base, Validator):
class BenchApps(MutableSequence): class BenchApps(MutableSequence):
def __init__(self, bench: Bench): def __init__(self, bench: Bench):
self.bench = bench self.bench = bench
self.states_path = os.path.join(self.bench.name, "sites", "apps.json")
self.apps_path = os.path.join(self.bench.name, "apps")
self.initialize_apps()
self.set_states()
def set_states(self):
try:
with open(self.states_path, "r") as f:
self.states = json.loads(f.read() or "{}")
except FileNotFoundError:
self.states = {}
def update_apps_states(
self,
app_dir: str = None,
app_name: Union[str, None] = None,
branch: Union[str, None] = None,
required: List = [],
):
if self.apps and not os.path.exists(self.states_path):
# idx according to apps listed in apps.txt (backwards compatibility)
# Keeping frappe as the first app.
if "frappe" in self.apps:
self.apps.remove("frappe")
self.apps.insert(0, "frappe")
with open(self.bench.apps_txt, "w") as f:
f.write("\n".join(self.apps))
print("Found existing apps updating states...")
for idx, app in enumerate(self.apps, start=1):
self.states[app] = {
"resolution": {
"commit_hash": None,
"branch": None
},
"required": required,
"idx": idx,
"version": get_current_version(app, self.bench.name),
}
apps_to_remove = []
for app in self.states:
if app not in self.apps:
apps_to_remove.append(app)
for app in apps_to_remove:
del self.states[app]
if app_name and not app_dir:
app_dir = app_name
if app_name and app_name not in self.states:
version = get_current_version(app_name, self.bench.name)
app_dir = os.path.join(self.apps_path, app_dir)
if not branch:
branch = (
subprocess
.check_output("git rev-parse --abbrev-ref HEAD", shell=True, cwd=app_dir)
.decode("utf-8")
.rstrip()
)
commit_hash = subprocess.check_output(f"git rev-parse {branch}", shell=True, cwd=app_dir).decode("utf-8").rstrip()
self.states[app_name] = {
"resolution": {
"commit_hash":commit_hash,
"branch": branch
},
"required":required,
"idx":len(self.states) + 1,
"version": version,
}
with open(self.states_path, "w") as f:
f.write(json.dumps(self.states, indent=4))
def sync(
self,
app_name: Union[str, None] = None,
app_dir: Union[str, None] = None,
branch: Union[str, None] = None,
required: List = []
):
self.initialize_apps() self.initialize_apps()
def sync(self):
self.initialize_apps()
with open(self.bench.apps_txt, "w") as f: with open(self.bench.apps_txt, "w") as f:
return f.write("\n".join(self.apps)) f.write("\n".join(self.apps))
self.update_apps_states(
app_name=app_name,
app_dir=app_dir,
branch=branch,
required=required
)
def initialize_apps(self): def initialize_apps(self):
is_installed = lambda app: app in installed_packages is_installed = lambda app: app in installed_packages
@ -180,7 +279,6 @@ class BenchApps(MutableSequence):
and is_installed(x) and is_installed(x)
) )
] ]
self.apps.sort()
except FileNotFoundError: except FileNotFoundError:
self.apps = [] self.apps = []
@ -213,9 +311,9 @@ class BenchApps(MutableSequence):
super().append(app.repo) super().append(app.repo)
self.apps.sort() self.apps.sort()
def remove(self, app: "App"): def remove(self, app: "App", no_backup: bool = False):
app.uninstall() app.uninstall()
app.remove() app.remove(no_backup=no_backup)
super().remove(app.repo) super().remove(app.repo)
def append(self, app: "App"): def append(self, app: "App"):
@ -248,13 +346,22 @@ class BenchSetup(Base):
- install frappe python dependencies - install frappe python dependencies
""" """
import bench.cli import bench.cli
import click
verbose = bench.cli.verbose
click.secho("Setting Up Environment", fg="yellow")
frappe = os.path.join(self.bench.name, "apps", "frappe") frappe = os.path.join(self.bench.name, "apps", "frappe")
virtualenv = get_venv_path() virtualenv = get_virtualenv_path(verbose=verbose)
quiet_flag = "" if bench.cli.verbose else "--quiet" quiet_flag = "" if verbose else "--quiet"
if not os.path.exists(self.bench.python): if not os.path.exists(self.bench.python):
self.run(f"{virtualenv} {quiet_flag} env -p {python}") if virtualenv:
self.run(f"{virtualenv} {quiet_flag} env -p {python}")
else:
venv = get_venv_path(verbose=verbose)
self.run(f"{venv} env")
self.pip() self.pip()
@ -339,7 +446,10 @@ class BenchSetup(Base):
print(f"Installing {len(apps)} applications...") print(f"Installing {len(apps)} applications...")
for app in apps: for app in apps:
App(app, bench=self.bench, to_clone=False).install( skip_assets=True, restart_bench=False) path_to_app = os.path.join(self.bench.name, "apps", app)
app = App(path_to_app, bench=self.bench, to_clone=False).install(
skip_assets=True, restart_bench=False, ignore_resolution=True
)
def python(self, apps=None): def python(self, apps=None):
"""Install and upgrade Python dependencies for specified / all installed apps on given Bench """Install and upgrade Python dependencies for specified / all installed apps on given Bench

View File

@ -133,8 +133,20 @@ def drop(path):
@click.option( @click.option(
"--init-bench", is_flag=True, default=False, help="Initialize Bench if not in one" "--init-bench", is_flag=True, default=False, help="Initialize Bench if not in one"
) )
@click.option(
"--resolve-deps",
is_flag=True,
default=False,
help="Resolve dependencies before installing app",
)
def get_app( def get_app(
git_url, branch, name=None, overwrite=False, skip_assets=False, init_bench=False git_url,
branch,
name=None,
overwrite=False,
skip_assets=False,
init_bench=False,
resolve_deps=False,
): ):
"clone an app from the internet and set it up in your bench" "clone an app from the internet and set it up in your bench"
from bench.app import get_app from bench.app import get_app
@ -145,9 +157,9 @@ def get_app(
skip_assets=skip_assets, skip_assets=skip_assets,
overwrite=overwrite, overwrite=overwrite,
init_bench=init_bench, init_bench=init_bench,
resolve_deps=resolve_deps,
) )
@click.command("new-app", help="Create a new Frappe application under apps folder") @click.command("new-app", help="Create a new Frappe application under apps folder")
@click.option( @click.option(
"--no-git", "--no-git",
@ -168,12 +180,14 @@ def new_app(app_name, no_git=None):
"Completely remove app from bench and re-build assets if not installed on any site" "Completely remove app from bench and re-build assets if not installed on any site"
), ),
) )
@click.option("--no-backup", is_flag=True, help="Do not backup app before removing")
@click.option("--force", is_flag=True, help="Force remove app")
@click.argument("app-name") @click.argument("app-name")
def remove_app(app_name): def remove_app(app_name, no_backup=False, force=False):
from bench.bench import Bench from bench.bench import Bench
bench = Bench(".") bench = Bench(".")
bench.uninstall(app_name) bench.uninstall(app_name, no_backup=no_backup, force=force)
@click.command("exclude-app", help="Exclude app from updating") @click.command("exclude-app", help="Exclude app from updating")

View File

@ -10,11 +10,10 @@ from bench.config.nginx import make_nginx_conf
from bench.config.production_setup import service from bench.config.production_setup import service
from bench.config.site_config import get_domains, remove_domain, update_site_config from bench.config.site_config import get_domains, remove_domain, update_site_config
from bench.bench import Bench from bench.bench import Bench
from bench.utils import exec_cmd from bench.utils import exec_cmd, which
from bench.utils.bench import update_common_site_config from bench.utils.bench import update_common_site_config
from bench.exceptions import CommandFailedError from bench.exceptions import CommandFailedError
def setup_letsencrypt(site, custom_domain, bench_path, interactive): def setup_letsencrypt(site, custom_domain, bench_path, interactive):
site_path = os.path.join(bench_path, "sites", site, "site_config.json") site_path = os.path.join(bench_path, "sites", site, "site_config.json")
@ -58,7 +57,6 @@ def create_config(site, custom_domain):
def run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive=True): def run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive=True):
service('nginx', 'stop') service('nginx', 'stop')
get_certbot()
try: try:
interactive = '' if interactive else '-n' interactive = '' if interactive else '-n'
@ -88,7 +86,7 @@ def run_certbot_and_setup_ssl(site, custom_domain, bench_path, interactive=True)
def setup_crontab(): def setup_crontab():
from crontab import CronTab from crontab import CronTab
job_command = '/opt/certbot-auto renew -a nginx --post-hook "systemctl reload nginx"' job_command = f'{get_certbot_path()} renew -a nginx --post-hook "systemctl reload nginx"'
job_comment = 'Renew lets-encrypt every month' job_comment = 'Renew lets-encrypt every month'
print(f"Setting Up cron job to {job_comment}") print(f"Setting Up cron job to {job_comment}")
@ -107,20 +105,11 @@ def create_dir_if_missing(path):
os.makedirs(os.path.dirname(path)) os.makedirs(os.path.dirname(path))
def get_certbot():
from urllib.request import urlretrieve
certbot_path = get_certbot_path()
create_dir_if_missing(certbot_path)
if not os.path.isfile(certbot_path):
urlretrieve("https://dl.eff.org/certbot-auto", certbot_path)
os.chmod(certbot_path, 0o744)
def get_certbot_path(): def get_certbot_path():
return "/opt/certbot-auto" try:
return which("certbot", raise_err=True)
except FileNotFoundError:
raise CommandFailedError("Certbot is not installed on your system. Please visit https://certbot.eff.org/instructions for installation instructions, then try again.")
def renew_certs(): def renew_certs():
# Needs to be run with sudo # Needs to be run with sudo
@ -156,7 +145,6 @@ def setup_wildcard_ssl(domain, email, bench_path, exclude_base_domain):
print("You cannot setup SSL without DNS Multitenancy") print("You cannot setup SSL without DNS Multitenancy")
return return
get_certbot()
domain_list = _get_domains(domain.strip()) domain_list = _get_domains(domain.strip())
email_param = '' email_param = ''

View File

@ -15,6 +15,5 @@
{{ user }} ALL = (root) NOPASSWD: {{ nginx }} {{ user }} ALL = (root) NOPASSWD: {{ nginx }}
{% endif %} {% endif %}
{{ user }} ALL = (root) NOPASSWD: /opt/certbot-auto {{ user }} ALL = (root) NOPASSWD: {{ certbot }}
Defaults:{{ user }} !requiretty Defaults:{{ user }} !requiretty

View File

@ -14,8 +14,10 @@ map {{ from_variable }} {{ to_variable }} {
server { server {
{% if ssl_certificate and ssl_certificate_key %} {% if ssl_certificate and ssl_certificate_key %}
listen {{ port }} ssl; listen {{ port }} ssl;
listen [::]:{{ port }} ssl;
{% else %} {% else %}
listen {{ port }}; listen {{ port }};
listen [::]:{{ port }};
{% endif %} {% endif %}
server_name server_name
@ -80,7 +82,7 @@ server {
rewrite ^(.+)/index\.html$ $1 permanent; rewrite ^(.+)/index\.html$ $1 permanent;
rewrite ^(.+)\.html$ $1 permanent; rewrite ^(.+)\.html$ $1 permanent;
location ~ ^/files/.*.(htm|html|svg|xml) { location ~* ^/files/.*.(htm|html|svg|xml) {
add_header Content-disposition "attachment"; add_header Content-disposition "attachment";
try_files /{{ site_name }}/public/$uri @webserver; try_files /{{ site_name }}/public/$uri @webserver;
} }

View File

@ -21,12 +21,22 @@ class BenchNotFoundError(Exception):
class ValidationError(Exception): class ValidationError(Exception):
pass pass
class AppNotInstalledError(ValidationError):
pass
class CannotUpdateReleaseBench(ValidationError): class CannotUpdateReleaseBench(ValidationError):
pass pass
class FeatureDoesNotExistError(CommandFailedError): class FeatureDoesNotExistError(CommandFailedError):
pass pass
class NotInBenchDirectoryError(Exception): class NotInBenchDirectoryError(Exception):
pass pass
class VersionNotFound(Exception):
pass

View File

@ -12,6 +12,7 @@ from bench.utils import exec_cmd
from bench.release import get_bumped_version from bench.release import get_bumped_version
from bench.app import App from bench.app import App
from bench.tests.test_base import FRAPPE_BRANCH, TestBenchBase from bench.tests.test_base import FRAPPE_BRANCH, TestBenchBase
from bench.bench import Bench
# changed from frappe_theme because it wasn't maintained and incompatible, # changed from frappe_theme because it wasn't maintained and incompatible,
@ -38,11 +39,14 @@ class TestBenchInit(TestBenchBase):
def test_init(self, bench_name="test-bench", **kwargs): def test_init(self, bench_name="test-bench", **kwargs):
self.init_bench(bench_name, **kwargs) self.init_bench(bench_name, **kwargs)
app = App("file:///tmp/frappe") app = App("file:///tmp/frappe")
self.assertEqual(app.url, "/tmp/frappe") self.assertEqual(app.mount_path, "/tmp/frappe")
self.assertEqual(app.url, "https://github.com/frappe/frappe.git")
self.assert_folders(bench_name) self.assert_folders(bench_name)
self.assert_virtual_env(bench_name) self.assert_virtual_env(bench_name)
self.assert_config(bench_name) self.assert_config(bench_name)
test_bench = Bench(bench_name)
app = App("frappe", bench=test_bench)
self.assertEqual(app.from_apps, True)
def basic(self): def basic(self):
try: try:
@ -107,6 +111,20 @@ class TestBenchInit(TestBenchBase):
app_installed_in_env = TEST_FRAPPE_APP in subprocess.check_output(["bench", "pip", "freeze"], cwd=bench_path).decode('utf8') app_installed_in_env = TEST_FRAPPE_APP in subprocess.check_output(["bench", "pip", "freeze"], cwd=bench_path).decode('utf8')
self.assertTrue(app_installed_in_env) self.assertTrue(app_installed_in_env)
def test_get_app_resolve_deps(self):
FRAPPE_APP = "healthcare"
self.init_bench("test-bench")
bench_path = os.path.join(self.benches_path, "test-bench")
exec_cmd(f"bench get-app {FRAPPE_APP} --resolve-deps", cwd=bench_path)
self.assertTrue(os.path.exists(os.path.join(bench_path, "apps", FRAPPE_APP)))
states_path = os.path.join(bench_path, "sites", "apps.json")
self.assert_(os.path.exists(states_path))
with open(states_path, "r") as f:
states = json.load(f)
self.assert_(FRAPPE_APP in states)
def test_install_app(self): def test_install_app(self):
bench_name = "test-bench" bench_name = "test-bench"

80
bench/tests/test_utils.py Normal file
View File

@ -0,0 +1,80 @@
import os
import shutil
import subprocess
import unittest
from tabnanny import check
from bench.app import App
from bench.bench import Bench
from bench.exceptions import InvalidRemoteException
from bench.utils import is_valid_frappe_branch
class TestUtils(unittest.TestCase):
def test_app_utils(self):
git_url = "https://github.com/frappe/frappe"
branch = "develop"
app = App(name=git_url, branch=branch, bench=Bench("."))
self.assertTrue(
all(
[
app.name == git_url,
app.branch == branch,
app.tag == branch,
app.is_url == True,
app.on_disk == False,
app.org == "frappe",
app.url == git_url,
]
)
)
def test_is_valid_frappe_branch(self):
with self.assertRaises(InvalidRemoteException):
is_valid_frappe_branch("https://github.com/frappe/frappe.git", frappe_branch="random-branch")
is_valid_frappe_branch("https://github.com/random/random.git", frappe_branch="random-branch")
is_valid_frappe_branch("https://github.com/frappe/frappe.git", frappe_branch="develop")
is_valid_frappe_branch("https://github.com/frappe/frappe.git", frappe_branch="v13.29.0")
def test_app_states(self):
bench_dir = "./sandbox"
sites_dir = os.path.join(bench_dir, "sites")
if not os.path.exists(sites_dir):
os.makedirs(sites_dir)
fake_bench = Bench(bench_dir)
self.assertTrue(hasattr(fake_bench.apps, "states"))
fake_bench.apps.states = {
"frappe": {"resolution": {"branch": "develop", "commit_hash": "234rwefd"}, "version": "14.0.0-dev"}
}
fake_bench.apps.update_apps_states()
self.assertEqual(fake_bench.apps.states, {})
frappe_path = os.path.join(bench_dir, "apps", "frappe")
os.makedirs(os.path.join(frappe_path, "frappe"))
subprocess.run(["git", "init"], cwd=frappe_path, capture_output=True, check=True)
with open(os.path.join(frappe_path, "frappe", "__init__.py"), "w+") as f:
f.write("__version__ = '11.0'")
subprocess.run(["git", "add", "."], cwd=frappe_path, capture_output=True, check=True)
subprocess.run(["git", "commit", "-m", "temp"], cwd=frappe_path, capture_output=True, check=True)
fake_bench.apps.update_apps_states(app_name="frappe")
self.assertIn("frappe", fake_bench.apps.states)
self.assertIn("version", fake_bench.apps.states["frappe"])
self.assertEqual("11.0", fake_bench.apps.states["frappe"]["version"])
shutil.rmtree(bench_dir)
def test_ssh_ports(self):
app = App("git@github.com:22:frappe/frappe")
self.assertEqual((app.use_ssh, app.org, app.repo), (True, "frappe", "frappe"))

View File

@ -8,14 +8,16 @@ import sys
from glob import glob from glob import glob
from shlex import split from shlex import split
from typing import List, Tuple from typing import List, Tuple
from functools import lru_cache
# imports - third party imports # imports - third party imports
import click import click
import requests
# imports - module imports # imports - module imports
from bench import PROJECT_NAME, VERSION from bench import PROJECT_NAME, VERSION
from bench.exceptions import CommandFailedError, InvalidRemoteException, ValidationError from bench.exceptions import CommandFailedError, InvalidRemoteException, AppNotInstalledError
logger = logging.getLogger(PROJECT_NAME) logger = logging.getLogger(PROJECT_NAME)
@ -48,6 +50,33 @@ def is_frappe_app(directory: str) -> bool:
return bool(is_frappe_app) return bool(is_frappe_app)
@lru_cache(maxsize=None)
def is_valid_frappe_branch(frappe_path:str, frappe_branch:str):
"""Check if a branch exists in a repo. Throws InvalidRemoteException if branch is not found
Uses native git command to check for branches on a remote.
:param frappe_path: git url
:type frappe_path: str
:param frappe_branch: branch to check
:type frappe_branch: str
:raises InvalidRemoteException: branch for this repo doesn't exist
"""
import git
g = git.cmd.Git()
if frappe_branch:
try:
res = g.ls_remote("--heads", "--tags", frappe_path, frappe_branch)
if not res:
raise InvalidRemoteException(
f"Invalid branch or tag: {frappe_branch} for the remote {frappe_path}"
)
except git.exc.GitCommandError:
raise InvalidRemoteException(f"Invalid frappe path: {frappe_path}")
def log(message, level=0, no_log=False): def log(message, level=0, no_log=False):
import bench import bench
import bench.cli import bench.cli
@ -62,9 +91,7 @@ def log(message, level=0, no_log=False):
color, prefix = levels.get(level, levels[0]) color, prefix = levels.get(level, levels[0])
if bench.cli.from_command_line and bench.cli.dynamic_feed: if bench.cli.from_command_line and bench.cli.dynamic_feed:
bench.LOG_BUFFER.append( bench.LOG_BUFFER.append({"prefix": prefix, "message": message, "color": color})
{"prefix": prefix, "message": message, "color": color}
)
if no_log: if no_log:
click.secho(message, fg=color) click.secho(message, fg=color)
@ -182,9 +209,7 @@ def get_git_version() -> float:
def get_cmd_output(cmd, cwd=".", _raise=True): def get_cmd_output(cmd, cwd=".", _raise=True):
output = "" output = ""
try: try:
output = subprocess.check_output( output = subprocess.check_output(cmd, cwd=cwd, shell=True, stderr=subprocess.PIPE, encoding="utf-8").strip()
cmd, cwd=cwd, shell=True, stderr=subprocess.PIPE, encoding="utf-8"
).strip()
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
if e.output: if e.output:
output = e.output output = e.output
@ -269,7 +294,7 @@ def set_git_remote_url(git_url, bench_path="."):
app = git_url.rsplit("/", 1)[1].rsplit(".", 1)[0] app = git_url.rsplit("/", 1)[1].rsplit(".", 1)[0]
if app not in Bench(bench_path).apps: if app not in Bench(bench_path).apps:
raise ValidationError(f"No app named {app}") raise AppNotInstalledError(f"No app named {app}")
app_dir = get_repo_dir(app, bench_path=bench_path) app_dir = get_repo_dir(app, bench_path=bench_path)
@ -400,10 +425,12 @@ def find_org(org_repo):
for org in ["frappe", "erpnext"]: for org in ["frappe", "erpnext"]:
res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}") res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}")
if res.status_code in (400, 403):
res = requests.head(f"https://github.com/{org}/{org_repo}")
if res.ok: if res.ok:
return org, org_repo return org, org_repo
raise InvalidRemoteException raise InvalidRemoteException(f"{org_repo} not found in frappe or erpnext")
def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]: def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]:

View File

@ -7,8 +7,10 @@ from bench.exceptions import (
InvalidRemoteException, InvalidRemoteException,
InvalidBranchException, InvalidBranchException,
CommandFailedError, CommandFailedError,
VersionNotFound,
) )
from bench.app import get_repo_dir from bench.app import get_repo_dir
from functools import lru_cache
def is_version_upgrade(app="frappe", bench_path=".", branch=None): def is_version_upgrade(app="frappe", bench_path=".", branch=None):
@ -107,7 +109,9 @@ def switch_to_develop(apps=None, bench_path=".", upgrade=True):
def get_version_from_string(contents, field="__version__"): def get_version_from_string(contents, field="__version__"):
match = re.search(r"^(\s*%s\s*=\s*['\\\"])(.+?)(['\"])(?sm)" % field, contents) match = re.search(r"^(\s*%s\s*=\s*['\\\"])(.+?)(['\"])" % field, contents, flags=(re.S | re.M))
if not match:
raise VersionNotFound(f"{contents} is not a valid version")
return match.group(2) return match.group(2)
@ -165,6 +169,31 @@ def get_current_branch(app, bench_path="."):
return get_cmd_output("basename $(git symbolic-ref -q HEAD)", cwd=repo_dir) return get_cmd_output("basename $(git symbolic-ref -q HEAD)", cwd=repo_dir)
@lru_cache(maxsize=5)
def get_required_deps(org, name, branch, deps="hooks.py"):
import requests
import base64
git_api_url = f"https://api.github.com/repos/{org}/{name}/contents/{name}/{deps}"
params = {"branch": branch or "develop"}
res = requests.get(url=git_api_url, params=params).json()
if "message" in res:
git_url = f"https://raw.githubusercontent.com/{org}/{name}/{params['branch']}/{deps}"
return requests.get(git_url).text
return base64.decodebytes(res["content"].encode()).decode()
def required_apps_from_hooks(required_deps, local=False):
if local:
with open(required_deps) as f:
required_deps = f.read()
lines = [x for x in required_deps.split("\n") if x.strip().startswith("required_apps")]
required_apps = eval(lines[0].strip("required_apps").strip().lstrip("=").strip())
return required_apps
def get_remote(app, bench_path="."): def get_remote(app, bench_path="."):
repo_dir = get_repo_dir(app, bench_path=bench_path) repo_dir = get_repo_dir(app, bench_path=bench_path)
contents = subprocess.check_output( contents = subprocess.check_output(
@ -181,25 +210,30 @@ def get_remote(app, bench_path="."):
return contents.splitlines()[0].split()[0] return contents.splitlines()[0].split()[0]
def get_app_name(bench_path, repo_name): def get_app_name(bench_path, folder_name):
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")
config_path = os.path.join(apps_path, repo_name, "setup.cfg") config_path = os.path.join(apps_path, folder_name, "setup.cfg")
if os.path.exists(config_path): if os.path.exists(config_path):
config = read_configuration(config_path) config = read_configuration(config_path)
app_name = config.get("metadata", {}).get("name") app_name = config.get("metadata", {}).get("name")
if not app_name: if not app_name:
# retrieve app name from setup.py as fallback # retrieve app name from setup.py as fallback
app_path = os.path.join(apps_path, repo_name, "setup.py") app_path = os.path.join(apps_path, folder_name, "setup.py")
with open(app_path, "rb") as f: with open(app_path, "rb") as f:
app_name = re.search(r'name\s*=\s*[\'"](.*)[\'"]', f.read().decode("utf-8")).group(1) app_name = re.search(r'name\s*=\s*[\'"](.*)[\'"]', f.read().decode("utf-8")).group(1)
if app_name and repo_name != app_name: if app_name and folder_name != app_name:
os.rename(os.path.join(apps_path, repo_name), os.path.join(apps_path, app_name)) os.rename(os.path.join(apps_path, folder_name), os.path.join(apps_path, app_name))
return app_name return app_name
return repo_name return folder_name
def check_existing_dir(bench_path, repo_name):
cloned_path = os.path.join(bench_path, "apps", repo_name)
dir_already_exists = os.path.isdir(cloned_path)
return dir_already_exists, cloned_path
def get_current_version(app, bench_path="."): def get_current_version(app, bench_path="."):

View File

@ -33,19 +33,25 @@ def get_env_cmd(cmd, bench_path="."):
return os.path.abspath(os.path.join(bench_path, "env", "bin", cmd)) return os.path.abspath(os.path.join(bench_path, "env", "bin", cmd))
def get_venv_path(): def get_virtualenv_path(verbose=False):
venv = which("virtualenv") virtualenv_path = which("virtualenv")
if not venv: if not virtualenv_path and verbose:
current_python = sys.executable log("virtualenv cannot be found", level=2)
with open(os.devnull, "wb") as devnull:
is_venv_installed = not subprocess.call(
[current_python, "-m", "venv", "--help"], stdout=devnull
)
if is_venv_installed:
venv = f"{current_python} -m venv"
return venv or log("virtualenv cannot be found", level=2) return virtualenv_path
def get_venv_path(verbose=False):
current_python = sys.executable
with open(os.devnull, "wb") as devnull:
is_venv_installed = not subprocess.call(
[current_python, "-m", "venv", "--help"], stdout=devnull
)
if is_venv_installed:
return f"{current_python} -m venv"
else:
log("virtualenv cannot be found", level=2)
def update_node_packages(bench_path=".", apps=None): def update_node_packages(bench_path=".", apps=None):
@ -74,7 +80,7 @@ def install_python_dev_dependencies(bench_path=".", apps=None, verbose=False):
if isinstance(apps, str): if isinstance(apps, str):
apps = [apps] apps = [apps]
elif apps is None: elif not apps:
apps = bench.get_installed_apps() apps = bench.get_installed_apps()
for app in apps: for app in apps:

View File

@ -14,6 +14,7 @@ from bench.utils import (
run_frappe_cmd, run_frappe_cmd,
sudoers_file, sudoers_file,
which, which,
is_valid_frappe_branch,
) )
from bench.utils.bench import build_assets, clone_apps_from from bench.utils.bench import build_assets, clone_apps_from
from bench.utils.render import job from bench.utils.render import job
@ -74,9 +75,14 @@ def init(
# remote apps # remote apps
else: else:
frappe_path = frappe_path or "https://github.com/frappe/frappe.git" frappe_path = frappe_path or "https://github.com/frappe/frappe.git"
is_valid_frappe_branch(frappe_path=frappe_path, frappe_branch=frappe_branch)
get_app( get_app(
frappe_path, branch=frappe_branch, bench_path=path, skip_assets=True, verbose=verbose frappe_path,
branch=frappe_branch,
bench_path=path,
skip_assets=True,
verbose=verbose,
resolve_deps=False,
) )
# fetch remote apps using config file - deprecate this! # fetch remote apps using config file - deprecate this!
@ -86,7 +92,12 @@ def init(
# getting app on bench init using --install-app # getting app on bench init using --install-app
if install_app: if install_app:
get_app( get_app(
install_app, branch=frappe_branch, bench_path=path, skip_assets=True, verbose=verbose install_app,
branch=frappe_branch,
bench_path=path,
skip_assets=True,
verbose=verbose,
resolve_deps=False,
) )
if not skip_assets: if not skip_assets:
@ -97,6 +108,8 @@ def init(
def setup_sudoers(user): def setup_sudoers(user):
from bench.config.lets_encrypt import get_certbot_path
if not os.path.exists("/etc/sudoers.d"): if not os.path.exists("/etc/sudoers.d"):
os.makedirs("/etc/sudoers.d") os.makedirs("/etc/sudoers.d")
@ -117,6 +130,7 @@ def setup_sudoers(user):
"service": which("service"), "service": which("service"),
"systemctl": which("systemctl"), "systemctl": which("systemctl"),
"nginx": which("nginx"), "nginx": which("nginx"),
"certbot": get_certbot_path(),
} }
) )

View File

@ -6,4 +6,3 @@ python-crontab~=2.4.0
requests requests
semantic-version~=2.8.2 semantic-version~=2.8.2
setuptools setuptools
virtualenv