194 lines
5.5 KiB
Bash
Executable File
194 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
apps_dir="${repo_root}/nextcloud/custom_apps"
|
|
nextcloud_image="${NEXTCLOUD_IMAGE:-}"
|
|
list_blocking_apps="0"
|
|
filter_mode="all"
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: ./scripts/check-nextcloud-custom-app-compat.sh [options]
|
|
|
|
Checks custom app appinfo/info.xml Nextcloud dependency ranges against the
|
|
target Nextcloud Docker image major.
|
|
|
|
Options:
|
|
--image <image> Target Nextcloud image, for example nextcloud:34.0.1-apache
|
|
--apps-dir <path> Custom apps directory (default: ./nextcloud/custom_apps)
|
|
--list-blocking-apps Print only incompatible app IDs, one per line
|
|
--tracked-only Check only app manifests tracked by this git repo
|
|
--untracked-only Check only app manifests not tracked by this git repo
|
|
-h, --help Show this help
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--image)
|
|
nextcloud_image="${2:-}"
|
|
shift 2
|
|
;;
|
|
--image=*)
|
|
nextcloud_image="${1#*=}"
|
|
shift
|
|
;;
|
|
--apps-dir)
|
|
apps_dir="${2:-}"
|
|
shift 2
|
|
;;
|
|
--apps-dir=*)
|
|
apps_dir="${1#*=}"
|
|
shift
|
|
;;
|
|
--list-blocking-apps)
|
|
list_blocking_apps="1"
|
|
shift
|
|
;;
|
|
--tracked-only)
|
|
filter_mode="tracked"
|
|
shift
|
|
;;
|
|
--untracked-only)
|
|
filter_mode="untracked"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${nextcloud_image}" ]]; then
|
|
echo "[nextcloud-app-compat] Skipped: --image or NEXTCLOUD_IMAGE is required."
|
|
exit 0
|
|
fi
|
|
|
|
if [[ ! -d "${apps_dir}" ]]; then
|
|
echo "[nextcloud-app-compat] Skipped: custom apps directory not found: ${apps_dir}"
|
|
exit 0
|
|
fi
|
|
|
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
echo "[nextcloud-app-compat] Skipped: python3 is required for app compatibility checks."
|
|
exit 0
|
|
fi
|
|
|
|
python3 - "${nextcloud_image}" "${apps_dir}" "${list_blocking_apps}" "${repo_root}" "${filter_mode}" <<'PY'
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
image = sys.argv[1].strip()
|
|
apps_dir = sys.argv[2]
|
|
list_blocking_apps = sys.argv[3] == "1"
|
|
repo_root = sys.argv[4]
|
|
filter_mode = sys.argv[5]
|
|
prefix = "[nextcloud-app-compat]"
|
|
|
|
|
|
def log(message):
|
|
if list_blocking_apps:
|
|
return
|
|
print(f"{prefix} {message}")
|
|
|
|
|
|
def parse_major_from_image(value):
|
|
tag = "latest"
|
|
last_slash = value.rfind("/")
|
|
last_colon = value.rfind(":")
|
|
if last_colon > last_slash:
|
|
tag = value[last_colon + 1 :]
|
|
match = re.match(r"^(\d+)(?:[.-]|$)", tag)
|
|
if not match:
|
|
return None
|
|
return int(match.group(1))
|
|
|
|
|
|
def is_tracked_manifest(manifest):
|
|
try:
|
|
rel_path = os.path.relpath(manifest, repo_root)
|
|
result = subprocess.run(
|
|
["git", "-C", repo_root, "ls-files", "--error-unmatch", "--", rel_path],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
check=False,
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
target_major = parse_major_from_image(image)
|
|
if target_major is None:
|
|
log(f"Skipped: cannot determine target Nextcloud major from image tag: {image}")
|
|
sys.exit(0)
|
|
|
|
manifests = []
|
|
for root, _dirs, files in os.walk(apps_dir):
|
|
if "info.xml" in files and os.path.basename(root) == "appinfo":
|
|
manifest = os.path.join(root, "info.xml")
|
|
tracked = is_tracked_manifest(manifest)
|
|
if filter_mode == "tracked" and not tracked:
|
|
continue
|
|
if filter_mode == "untracked" and tracked:
|
|
continue
|
|
manifests.append(manifest)
|
|
|
|
if not manifests:
|
|
log(f"Skipped: no appinfo/info.xml files found under {apps_dir}")
|
|
sys.exit(0)
|
|
|
|
failures = []
|
|
checked = 0
|
|
for manifest in sorted(manifests):
|
|
try:
|
|
tree = ET.parse(manifest)
|
|
except ET.ParseError as exc:
|
|
failures.append((os.path.basename(os.path.dirname(os.path.dirname(manifest))), f"{manifest}: invalid XML ({exc})"))
|
|
continue
|
|
|
|
root = tree.getroot()
|
|
app_id = root.findtext("id") or os.path.basename(os.path.dirname(os.path.dirname(manifest)))
|
|
dependency = root.find("./dependencies/nextcloud")
|
|
if dependency is None:
|
|
failures.append((app_id, f"{app_id}: missing <dependencies><nextcloud .../></dependencies>"))
|
|
continue
|
|
|
|
checked += 1
|
|
min_version = dependency.attrib.get("min-version", "").strip()
|
|
max_version = dependency.attrib.get("max-version", "").strip()
|
|
try:
|
|
min_major = int(min_version.split(".", 1)[0]) if min_version else None
|
|
max_major = int(max_version.split(".", 1)[0]) if max_version else None
|
|
except ValueError:
|
|
failures.append((app_id, f"{app_id}: invalid Nextcloud dependency range min={min_version!r} max={max_version!r}"))
|
|
continue
|
|
|
|
if min_major is not None and target_major < min_major:
|
|
failures.append((app_id, f"{app_id}: target major {target_major} is below min-version {min_version}"))
|
|
if max_major is not None and target_major > max_major:
|
|
failures.append((app_id, f"{app_id}: target major {target_major} exceeds max-version {max_version}"))
|
|
|
|
if failures:
|
|
if list_blocking_apps:
|
|
for app_id in sorted({app_id for app_id, _message in failures if app_id}):
|
|
print(app_id)
|
|
sys.exit(1)
|
|
log(f"FAILED for target Nextcloud major {target_major} ({image})")
|
|
for _app_id, failure in failures:
|
|
log(f" - {failure}")
|
|
sys.exit(1)
|
|
|
|
log(f"OK: {checked} custom app manifest(s) declare compatibility with Nextcloud major {target_major}.")
|
|
PY
|