Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ repos:
additional_dependencies: ['flake8-bugbear']

- repo: https://github.com/agritheory/test_utils
rev: v1.25.1
rev: v1.28.0
hooks:
- id: update_pre_commit_config
- id: validate_frappe_project
Expand Down
3 changes: 3 additions & 0 deletions cloud_storage/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
# before_install = "cloud_storage.install.before_install"
after_install = "cloud_storage.install.after_install"

# Documented for operators; install hooks read DEBIAN_PACKAGES in install.py.
debian_packages = ["libmagic1", "libreoffice"]

# Uninstallation
# ------------

Expand Down
45 changes: 3 additions & 42 deletions cloud_storage/install.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,10 @@
# Copyright (c) 2025, AgriTheory and contributors
# For license information, please see license.txt

import os
import subprocess
from getpass import getpass
from sys import platform
from cloud_storage.system_packages import ensure_debian_packages


def is_root():
return os.geteuid() == 0


def test_sudo():
args = "sudo -S echo OK".split()
kwargs = dict(stdout=subprocess.PIPE, encoding="ascii")
cmd = subprocess.run(args, **kwargs)
return "OK" in cmd.stdout


def install(module, pwd=""):
args = f"sudo -S apt-get -y install {module}".split()
kwargs = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="ascii")
if pwd:
kwargs.update(input=pwd)
cmd = subprocess.run(args, **kwargs)
return cmd.stdout, cmd.stderr
DEBIAN_PACKAGES = ["libmagic1", "libreoffice"]


def after_install():
modules = ["libmagic1", "libreoffice"]
if platform != "linux":
print(f"You need to manually install the following modules: {', '.join(modules)}.")
return

has_sudo_permissions = is_root() or test_sudo()
pwd = ""
if not has_sudo_permissions:
pwd = getpass(f"Provide sudo password to install {', '.join(modules)}: ")

for module in modules:
try:
out, err = install(module, pwd)
if err:
print(f"There was an error installing {module}: {err}.")
if out:
print(f"{module}: {out}")
except Exception as e:
print(f"There was an error installing {module}: {e}.")
ensure_debian_packages(DEBIAN_PACKAGES)
78 changes: 78 additions & 0 deletions cloud_storage/system_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (c) 2025, AgriTheory and contributors
# For license information, please see license.txt

"""Debian/Ubuntu system package helpers for Frappe install hooks.

Used from before_install / after_install so ``bench --site … install-app``
can satisfy OS dependencies without duplicating package lists in Ansible.
Set FRAPPE_INSTALL_NONINTERACTIVE=1 (or run without a TTY) for automation.
"""

from __future__ import annotations

import os
import subprocess
import sys

ENV_NONINTERACTIVE = "FRAPPE_INSTALL_NONINTERACTIVE"


def is_noninteractive() -> bool:
return os.environ.get(ENV_NONINTERACTIVE) == "1" or not sys.stdin.isatty()


def can_sudo() -> bool:
if os.geteuid() == 0:
return True
return (
subprocess.run(
["sudo", "-n", "true"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)


def dpkg_installed(package: str) -> bool:
return (
subprocess.run(
["dpkg", "-s", package],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)


def ensure_debian_packages(packages: list[str], *, required: bool = True) -> None:
"""Install missing Debian packages via apt when sudo is available."""
if sys.platform != "linux":
message = f"Install system packages manually: {', '.join(packages)}"
if required:
raise RuntimeError(message)
print(message)
return

missing = [pkg for pkg in packages if not dpkg_installed(pkg)]
if not missing:
return

if not can_sudo():
message = (
f"Need passwordless sudo to install: {', '.join(missing)}. "
f"Run: sudo apt-get install -y {' '.join(missing)}"
)
if required or is_noninteractive():
raise RuntimeError(message)
print(message)
return

env = os.environ.copy()
env["DEBIAN_FRONTEND"] = "noninteractive"
subprocess.run(["sudo", "apt-get", "update"], check=required, env=env)
subprocess.run(
["sudo", "apt-get", "install", "-y", *missing],
check=required,
env=env,
)
Loading