553 lines
21 KiB
Bash
Executable File
553 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
GITEA_BASE_URL="${ERP_GITEA_BASE_URL:-}"
|
|
REPOSITORY="${ERP_UPDATE_REPOSITORY:-awaioi/ERP}"
|
|
REQUESTED_VERSION="${ERP_INSTALL_VERSION:-}"
|
|
INSTALL_ROOT="${ERP_INSTALL_ROOT:-}"
|
|
ALLOW_INSECURE="${ERP_UPDATE_ALLOW_INSECURE_HTTP:-0}"
|
|
NO_SERVICE="${ERP_INSTALL_NO_SERVICE:-0}"
|
|
TOKEN="${ERP_GITEA_TOKEN:-${OA_UPDATE_TOKEN:-}}"
|
|
PUBLIC_KEY='-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEAaErhcY8WZIZvPILmYnfjndVBAdOuWkvhaoHIWqNNdxI=
|
|
-----END PUBLIC KEY-----'
|
|
TMP_DIR=""
|
|
|
|
say() { printf '[ERP Install] %s\n' "$*"; }
|
|
fail() { printf '[ERP Install] ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: install.sh [options]
|
|
--gitea-url URL Gitea public base URL
|
|
--repository O/R Release repository (default: awaioi/ERP)
|
|
--version VERSION Install one exact stable release
|
|
--install-root PATH Override installation directory
|
|
--allow-insecure Development only: allow plain HTTP release URLs
|
|
--no-service Start without systemd/launchd; online update stays disabled
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--gitea-url) [[ $# -ge 2 ]] || fail '--gitea-url requires a value'; GITEA_BASE_URL="$2"; shift 2 ;;
|
|
--repository) [[ $# -ge 2 ]] || fail '--repository requires a value'; REPOSITORY="$2"; shift 2 ;;
|
|
--version) [[ $# -ge 2 ]] || fail '--version requires a value'; REQUESTED_VERSION="$2"; shift 2 ;;
|
|
--install-root) [[ $# -ge 2 ]] || fail '--install-root requires a value'; INSTALL_ROOT="$2"; shift 2 ;;
|
|
--allow-insecure) ALLOW_INSECURE=1; shift ;;
|
|
--no-service) NO_SERVICE=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) fail "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
cleanup() {
|
|
if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
|
|
rm -rf "$TMP_DIR"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
detect_platform() {
|
|
case "$(uname -s)" in
|
|
Linux) PLATFORM=linux ;;
|
|
Darwin) PLATFORM=darwin ;;
|
|
*) fail "unsupported operating system: $(uname -s)" ;;
|
|
esac
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) ARCH=amd64 ;;
|
|
arm64|aarch64) ARCH=arm64 ;;
|
|
*) fail "unsupported CPU architecture: $(uname -m); 32-bit systems are not supported" ;;
|
|
esac
|
|
if [[ "$PLATFORM" == "linux" && "$(id -u)" != "0" ]]; then
|
|
fail 'Linux installation requires root; use: curl ... | sudo -E bash'
|
|
fi
|
|
}
|
|
|
|
validate_service_manager() {
|
|
[[ "$PLATFORM" != "linux" || "$NO_SERVICE" == "1" ]] && return 0
|
|
command -v systemctl >/dev/null 2>&1 \
|
|
|| fail 'systemd is required for a production Linux install; use --no-service only for development'
|
|
[[ -d /run/systemd/system ]] \
|
|
|| fail 'systemd is not running on this Linux host; use --no-service only for development'
|
|
}
|
|
|
|
java_major() {
|
|
local java_bin="${1:-java}"
|
|
"$java_bin" -version 2>&1 | awk -F'[".]' '/version/ {print $2; exit}'
|
|
}
|
|
|
|
has_java_17() {
|
|
command -v java >/dev/null 2>&1 || return 1
|
|
local major
|
|
major="$(java_major "$(command -v java)")"
|
|
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 17 ]]
|
|
}
|
|
|
|
has_openssl_3_ed25519() {
|
|
command -v openssl >/dev/null 2>&1 || return 1
|
|
local version algorithms
|
|
version="$(openssl version 2>/dev/null || true)"
|
|
[[ "$version" =~ ^OpenSSL[[:space:]]3\. ]] || return 1
|
|
algorithms="$(openssl list -public-key-algorithms 2>/dev/null || true)"
|
|
grep -qi ED25519 <<< "$algorithms"
|
|
}
|
|
|
|
install_linux_dependencies() {
|
|
local need_java="$1"
|
|
local packages=(ca-certificates)
|
|
command -v curl >/dev/null 2>&1 || packages+=(curl)
|
|
command -v tar >/dev/null 2>&1 || packages+=(tar)
|
|
command -v python3 >/dev/null 2>&1 || packages+=(python3)
|
|
has_openssl_3_ed25519 || packages+=(openssl)
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
[[ "$need_java" == "0" ]] || packages+=(openjdk-17-jre-headless)
|
|
say 'Installing missing runtime packages with apt-get...'
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update
|
|
apt-get install -y "${packages[@]}"
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
[[ "$need_java" == "0" ]] || packages+=(java-17-openjdk-headless)
|
|
say 'Installing missing runtime packages with dnf...'
|
|
dnf install -y "${packages[@]}"
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
[[ "$need_java" == "0" ]] || packages+=(java-17-openjdk-headless)
|
|
say 'Installing missing runtime packages with yum...'
|
|
yum install -y "${packages[@]}"
|
|
elif command -v zypper >/dev/null 2>&1; then
|
|
[[ "$need_java" == "0" ]] || packages+=(java-17-openjdk-headless)
|
|
say 'Installing missing runtime packages with zypper...'
|
|
zypper --non-interactive install "${packages[@]}"
|
|
else
|
|
fail 'no supported package manager found (apt-get, dnf, yum, or zypper is required)'
|
|
fi
|
|
}
|
|
|
|
install_macos_dependencies() {
|
|
command -v brew >/dev/null 2>&1 || fail 'Homebrew is required to install missing macOS packages'
|
|
local packages=()
|
|
has_java_17 || packages+=(openjdk@17)
|
|
command -v python3 >/dev/null 2>&1 || packages+=(python@3)
|
|
has_openssl_3_ed25519 || packages+=(openssl@3)
|
|
if (( ${#packages[@]} > 0 )); then
|
|
say 'Installing missing runtime packages with Homebrew...'
|
|
brew install "${packages[@]}"
|
|
fi
|
|
local java_prefix openssl_prefix
|
|
java_prefix="$(brew --prefix openjdk@17 2>/dev/null || true)"
|
|
openssl_prefix="$(brew --prefix openssl@3 2>/dev/null || true)"
|
|
[[ -z "$java_prefix" ]] || export PATH="$java_prefix/bin:$PATH"
|
|
[[ -z "$openssl_prefix" ]] || export PATH="$openssl_prefix/bin:$PATH"
|
|
}
|
|
|
|
check_and_install_dependencies() {
|
|
local need_install=0 need_java=0
|
|
has_java_17 || { need_install=1; need_java=1; }
|
|
command -v curl >/dev/null 2>&1 || need_install=1
|
|
command -v tar >/dev/null 2>&1 || need_install=1
|
|
command -v python3 >/dev/null 2>&1 || need_install=1
|
|
has_openssl_3_ed25519 || need_install=1
|
|
|
|
if [[ "$need_install" == "1" ]]; then
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
install_linux_dependencies "$need_java"
|
|
else
|
|
install_macos_dependencies
|
|
fi
|
|
fi
|
|
|
|
for command in curl tar python3 openssl java; do
|
|
command -v "$command" >/dev/null 2>&1 || fail "missing command after installation: $command"
|
|
done
|
|
has_java_17 || fail 'Java 17 or newer is required and could not be installed'
|
|
has_openssl_3_ed25519 || fail 'OpenSSL 3 with Ed25519 support is required and could not be installed'
|
|
JAVA_BIN="$(command -v java)"
|
|
say "Using Java $(java_major "$JAVA_BIN") at $JAVA_BIN"
|
|
}
|
|
|
|
validate_download_url() {
|
|
case "$1" in
|
|
https://*) return 0 ;;
|
|
http://*) [[ "$ALLOW_INSECURE" == "1" || "$ALLOW_INSECURE" == "true" ]] && return 0 ;;
|
|
esac
|
|
fail 'release server must use HTTPS (use --allow-insecure only for an isolated test server)'
|
|
}
|
|
|
|
create_curl_config() {
|
|
CURL_CONFIG="$TMP_DIR/curl.conf"
|
|
{
|
|
printf 'silent\nshow-error\nfail\nlocation\nconnect-timeout = 15\nmax-time = 900\n'
|
|
if [[ "$ALLOW_INSECURE" == "1" || "$ALLOW_INSECURE" == "true" ]]; then
|
|
printf 'proto = "=http,https"\nproto-redir = "=http,https"\n'
|
|
else
|
|
printf 'proto = "=https"\nproto-redir = "=https"\n'
|
|
fi
|
|
if [[ -n "$TOKEN" ]]; then
|
|
[[ "$TOKEN" =~ ^[A-Za-z0-9._-]+$ ]] || fail 'invalid Gitea token format'
|
|
printf 'header = "Authorization: token %s"\n' "$TOKEN"
|
|
fi
|
|
} > "$CURL_CONFIG"
|
|
chmod 600 "$CURL_CONFIG"
|
|
}
|
|
|
|
download() {
|
|
validate_download_url "$1"
|
|
curl --config "$CURL_CONFIG" --output "$2" "$1"
|
|
}
|
|
|
|
download_release() {
|
|
[[ "$REPOSITORY" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]] || fail 'invalid Gitea repository'
|
|
local owner="${REPOSITORY%%/*}" repo="${REPOSITORY#*/}"
|
|
local release_json="$TMP_DIR/release.json"
|
|
download "${GITEA_BASE_URL%/}/api/v1/repos/$owner/$repo/releases/latest" "$release_json" \
|
|
|| fail 'unable to read the latest Gitea Release'
|
|
local selection="$TMP_DIR/selection" selection_error="$TMP_DIR/selection-error"
|
|
if ! python3 - "$release_json" "$REQUESTED_VERSION" > "$selection" 2> "$selection_error" <<'PY'
|
|
import json, re, sys
|
|
try:
|
|
with open(sys.argv[1], encoding="utf-8") as handle:
|
|
release = json.load(handle)
|
|
except (OSError, json.JSONDecodeError):
|
|
raise SystemExit("invalid release JSON")
|
|
tag = str(release.get("tag_name") or "").strip()
|
|
match = re.fullmatch(r"v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)", tag)
|
|
if not match or release.get("draft") or release.get("prerelease"):
|
|
raise SystemExit("invalid stable release")
|
|
version = match.group(1)
|
|
if "-" in version.split("+", 1)[0]:
|
|
raise SystemExit("prerelease rejected by stable installer")
|
|
requested = sys.argv[2].removeprefix("v")
|
|
if requested and requested != version:
|
|
raise SystemExit("requested version does not match latest release")
|
|
assets = {str(a.get("name")): str(a.get("browser_download_url") or "") for a in release.get("assets", [])}
|
|
names = [
|
|
f"kaidi-erp-{version}.tar.gz",
|
|
f"kaidi-erp-installer-{version}.jar",
|
|
"SHA256SUMS",
|
|
"SHA256SUMS.sig",
|
|
]
|
|
urls = [assets.get(name, "") for name in names]
|
|
if any(not url or any(ch.isspace() for ch in url) for url in urls):
|
|
raise SystemExit("release assets missing")
|
|
print("\t".join([version, *urls]))
|
|
PY
|
|
then
|
|
local reason="invalid Release metadata"
|
|
[[ ! -s "$selection_error" ]] || reason="$(<"$selection_error")"
|
|
fail "$reason"
|
|
fi
|
|
|
|
local archive_url installer_url sums_url signature_url
|
|
IFS=$'\t' read -r VERSION archive_url installer_url sums_url signature_url < "$selection"
|
|
ARCHIVE_NAME="kaidi-erp-${VERSION}.tar.gz"
|
|
INSTALLER_NAME="kaidi-erp-installer-${VERSION}.jar"
|
|
ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_NAME"
|
|
INSTALLER_PATH="$TMP_DIR/$INSTALLER_NAME"
|
|
download "$archive_url" "$ARCHIVE_PATH"
|
|
download "$installer_url" "$INSTALLER_PATH"
|
|
download "$sums_url" "$TMP_DIR/SHA256SUMS"
|
|
download "$signature_url" "$TMP_DIR/SHA256SUMS.sig"
|
|
}
|
|
|
|
checksum_for() {
|
|
python3 - "$TMP_DIR/SHA256SUMS" "$1" <<'PY'
|
|
import re, sys
|
|
for line in open(sys.argv[1], encoding="utf-8"):
|
|
match = re.fullmatch(r"([0-9a-fA-F]{64})\s+\*?(.+?)\s*", line)
|
|
if match and match.group(2) == sys.argv[2]:
|
|
print(match.group(1).lower())
|
|
break
|
|
PY
|
|
}
|
|
|
|
actual_checksum() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$1" | awk '{print $1}'
|
|
else
|
|
shasum -a 256 "$1" | awk '{print $1}'
|
|
fi
|
|
}
|
|
|
|
verify_release() {
|
|
printf '%s\n' "$PUBLIC_KEY" > "$TMP_DIR/release-public-key.pem"
|
|
openssl pkeyutl -verify -rawin -pubin -inkey "$TMP_DIR/release-public-key.pem" \
|
|
-sigfile "$TMP_DIR/SHA256SUMS.sig" -in "$TMP_DIR/SHA256SUMS" >/dev/null \
|
|
|| fail 'Release Ed25519 signature verification failed'
|
|
|
|
local name path expected actual
|
|
for name in "$ARCHIVE_NAME" "$INSTALLER_NAME"; do
|
|
if [[ "$name" == "$ARCHIVE_NAME" ]]; then path="$ARCHIVE_PATH"; else path="$INSTALLER_PATH"; fi
|
|
expected="$(checksum_for "$name")"
|
|
[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || fail "release checksum is missing for $name"
|
|
actual="$(actual_checksum "$path")"
|
|
[[ "$actual" == "$expected" ]] || fail "release checksum verification failed for $name"
|
|
done
|
|
|
|
python3 - "$ARCHIVE_PATH" <<'PY'
|
|
import pathlib, sys, tarfile
|
|
with tarfile.open(sys.argv[1], "r:gz") as archive:
|
|
for item in archive.getmembers():
|
|
path = pathlib.PurePosixPath(item.name)
|
|
if path.is_absolute() or ".." in path.parts or item.issym() or item.islnk():
|
|
raise SystemExit(f"unsafe archive member: {item.name}")
|
|
PY
|
|
}
|
|
|
|
shell_setting() {
|
|
printf '%s=' "$1"
|
|
printf '%q\n' "$2"
|
|
}
|
|
|
|
prepare_layout() {
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
ERP_USER="${ERP_SERVICE_USER:-kaidi-erp}"
|
|
[[ "$ERP_USER" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]] || fail 'invalid Linux service user name'
|
|
id "$ERP_USER" >/dev/null 2>&1 \
|
|
|| useradd --system --home-dir "$INSTALL_ROOT" --shell /usr/sbin/nologin "$ERP_USER"
|
|
ERP_GROUP="$(id -gn "$ERP_USER" 2>/dev/null)" \
|
|
|| fail "unable to resolve the primary group for Linux service user: $ERP_USER"
|
|
CONFIG_ROOT="${ERP_CONFIG_ROOT:-/etc/kaidi-erp}"
|
|
STATE_ROOT="${ERP_STATE_ROOT:-/var/lib/kaidi-erp}"
|
|
LOG_ROOT="${ERP_LOG_ROOT:-/var/log/kaidi-erp}"
|
|
local path
|
|
for path in "$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"; do
|
|
[[ "$path" =~ ^/[A-Za-z0-9._/@:+-]+$ ]] \
|
|
|| fail "Linux installation paths must be absolute and contain only safe characters: $path"
|
|
done
|
|
else
|
|
ERP_USER="$(id -un)"
|
|
ERP_GROUP="$(id -gn)"
|
|
CONFIG_ROOT="${ERP_CONFIG_ROOT:-$INSTALL_ROOT/config}"
|
|
STATE_ROOT="${ERP_STATE_ROOT:-$INSTALL_ROOT/state}"
|
|
LOG_ROOT="${ERP_LOG_ROOT:-$HOME/Library/Logs/KaidiERP}"
|
|
fi
|
|
|
|
mkdir -p "$INSTALL_ROOT/releases" "$INSTALL_ROOT/installer" "$INSTALL_ROOT/run" \
|
|
"$INSTALL_ROOT/backups" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
chown -R "$ERP_USER:$ERP_GROUP" "$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
|
chmod 750 "$CONFIG_ROOT" "$STATE_ROOT"
|
|
fi
|
|
[[ ! -e "$STATE_ROOT/install.lock" ]] || fail 'Kaidi ERP is already installed; use the system update page'
|
|
[[ ! -e "$STATE_ROOT/install.pending" ]] || fail 'a previous installation is still pending; inspect the service before retrying'
|
|
}
|
|
|
|
install_release_files() {
|
|
local unpack="$TMP_DIR/unpack"
|
|
mkdir -p "$unpack"
|
|
tar -xzf "$ARCHIVE_PATH" -C "$unpack"
|
|
local source="$unpack/kaidi-erp-${VERSION}"
|
|
[[ -r "$source/app/kaidi-erp.jar" && -x "$source/bin/erp-run" && -x "$source/bin/erp-update" ]] \
|
|
|| fail 'release archive is incomplete'
|
|
[[ -r "$source/VERSION" && "$(<"$source/VERSION")" == "$VERSION" ]] \
|
|
|| fail 'release archive version mismatch'
|
|
|
|
local destination="$INSTALL_ROOT/releases/$VERSION" replaced=""
|
|
if [[ -e "$destination" ]]; then
|
|
replaced="${destination}.replaced-$$"
|
|
mv "$destination" "$replaced"
|
|
fi
|
|
if ! mv "$source" "$destination"; then
|
|
[[ -z "$replaced" || ! -e "$replaced" ]] || mv "$replaced" "$destination"
|
|
fail 'unable to write the release directory'
|
|
fi
|
|
[[ -z "$replaced" ]] || rm -rf "$replaced"
|
|
|
|
local installer_tmp="$INSTALL_ROOT/installer/.kaidi-erp-installer.jar.tmp-$$"
|
|
cp "$INSTALLER_PATH" "$installer_tmp"
|
|
chmod 600 "$installer_tmp"
|
|
mv "$installer_tmp" "$INSTALL_ROOT/installer/kaidi-erp-installer.jar"
|
|
|
|
python3 - "$INSTALL_ROOT/current" "releases/$VERSION" <<'PY'
|
|
import os, sys
|
|
link, target = sys.argv[1:]
|
|
temporary = f"{link}.new-{os.getpid()}"
|
|
try:
|
|
os.symlink(target, temporary)
|
|
os.replace(temporary, link)
|
|
finally:
|
|
if os.path.lexists(temporary):
|
|
os.unlink(temporary)
|
|
PY
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
chown -R "$ERP_USER:$ERP_GROUP" "$destination" "$INSTALL_ROOT/installer" "$INSTALL_ROOT/current"
|
|
fi
|
|
}
|
|
|
|
write_bootstrap_configuration() {
|
|
CONFIG_FILE="$CONFIG_ROOT/erp.env"
|
|
PUBLIC_KEY_FILE="$CONFIG_ROOT/release-public-key.pem"
|
|
PENDING_FILE="$STATE_ROOT/install.pending"
|
|
INSTALL_LOCK_FILE="$STATE_ROOT/install.lock"
|
|
OPERATION_LOCK_FILE="$STATE_ROOT/install-operation.lock"
|
|
SETUP_TOKEN="$(openssl rand -hex 32)" || fail 'unable to generate the one-time setup token'
|
|
local update_enabled=true port="${ERP_SERVER_PORT:-8091}"
|
|
[[ "$NO_SERVICE" != "1" ]] || update_enabled=false
|
|
|
|
printf '%s\n' "$PUBLIC_KEY" > "$PUBLIC_KEY_FILE"
|
|
{
|
|
shell_setting ERP_INSTALL_ROOT "$INSTALL_ROOT"
|
|
shell_setting ERP_CONFIG_FILE "$CONFIG_FILE"
|
|
shell_setting ERP_RUN_DIR "$INSTALL_ROOT/run"
|
|
shell_setting ERP_STATE_FILE "$STATE_ROOT/update-state.json"
|
|
shell_setting ERP_INSTALL_PENDING_FILE "$PENDING_FILE"
|
|
shell_setting ERP_INSTALL_LOCK_FILE "$INSTALL_LOCK_FILE"
|
|
shell_setting ERP_INSTALL_OPERATION_LOCK_FILE "$OPERATION_LOCK_FILE"
|
|
shell_setting ERP_SETUP_TOKEN "$SETUP_TOKEN"
|
|
shell_setting ERP_INSTALLER_JAR "$INSTALL_ROOT/installer/kaidi-erp-installer.jar"
|
|
shell_setting ERP_JAR_PATH "$INSTALL_ROOT/current/app/kaidi-erp.jar"
|
|
shell_setting ERP_JAVA_BIN "$JAVA_BIN"
|
|
shell_setting ERP_RELEASE_VERSION "$VERSION"
|
|
shell_setting ERP_HEALTH_URL "http://127.0.0.1:${port}/api/oa/health"
|
|
shell_setting ERP_UPDATE_PUBLIC_KEY_FILE "$PUBLIC_KEY_FILE"
|
|
shell_setting ERP_UPDATE_REQUIRE_SIGNATURE true
|
|
shell_setting ERP_UPDATE_BACKUP_MODE "${ERP_UPDATE_BACKUP_MODE:-none}"
|
|
shell_setting ERP_UPDATE_HEALTH_TIMEOUT_SECONDS "${ERP_UPDATE_HEALTH_TIMEOUT_SECONDS:-120}"
|
|
shell_setting ERP_UPDATE_HEALTH_POLL_SECONDS "${ERP_UPDATE_HEALTH_POLL_SECONDS:-2}"
|
|
shell_setting SERVER_PORT "$port"
|
|
shell_setting OA_UPDATE_ENABLED "$update_enabled"
|
|
shell_setting OA_UPDATE_GITEA_BASE_URL "$GITEA_BASE_URL"
|
|
shell_setting OA_UPDATE_REPOSITORY "$REPOSITORY"
|
|
shell_setting OA_UPDATE_CHANNEL stable
|
|
shell_setting OA_UPDATE_TOKEN "$TOKEN"
|
|
shell_setting OA_UPDATE_HELPER_COMMAND "$INSTALL_ROOT/current/bin/erp-update"
|
|
shell_setting OA_UPDATE_STATE_FILE "$STATE_ROOT/update-state.json"
|
|
shell_setting OA_UPDATE_ALLOW_INSECURE_HTTP "$ALLOW_INSECURE"
|
|
} > "$CONFIG_FILE"
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
chown "$ERP_USER:$ERP_GROUP" "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
|
fi
|
|
chmod 600 "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
|
}
|
|
|
|
start_service() {
|
|
if [[ "$NO_SERVICE" == "1" ]]; then
|
|
nohup env ERP_INSTALL_ROOT="$INSTALL_ROOT" ERP_CONFIG_FILE="$CONFIG_FILE" \
|
|
"$INSTALL_ROOT/current/bin/erp-run" >> "$LOG_ROOT/erp.log" 2>> "$LOG_ROOT/erp-error.log" &
|
|
printf '%s\n' "$!" > "$INSTALL_ROOT/run/launcher.pid"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
cat > /etc/systemd/system/kaidi-erp.service <<EOF
|
|
[Unit]
|
|
Description=Kaidi ERP
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$ERP_USER
|
|
Group=$ERP_GROUP
|
|
Environment="ERP_INSTALL_ROOT=$INSTALL_ROOT"
|
|
Environment="ERP_CONFIG_FILE=$CONFIG_FILE"
|
|
WorkingDirectory="$INSTALL_ROOT"
|
|
ExecStart="$INSTALL_ROOT/current/bin/erp-run"
|
|
Restart=always
|
|
RestartSec=3
|
|
TimeoutStopSec=90
|
|
SuccessExitStatus=143
|
|
UMask=0077
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ReadWritePaths="$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
systemctl daemon-reload
|
|
systemctl enable kaidi-erp.service
|
|
systemctl restart kaidi-erp.service
|
|
else
|
|
local agents="$HOME/Library/LaunchAgents" plist="$HOME/Library/LaunchAgents/com.kaidi.erp.plist"
|
|
mkdir -p "$agents"
|
|
python3 - "$plist" "$INSTALL_ROOT" "$CONFIG_FILE" "$LOG_ROOT" <<'PY'
|
|
import plistlib, sys
|
|
path, root, config, logs = sys.argv[1:]
|
|
payload = {
|
|
"Label": "com.kaidi.erp",
|
|
"ProgramArguments": [f"{root}/current/bin/erp-run"],
|
|
"EnvironmentVariables": {"ERP_INSTALL_ROOT": root, "ERP_CONFIG_FILE": config},
|
|
"WorkingDirectory": root,
|
|
"RunAtLoad": True,
|
|
"KeepAlive": True,
|
|
"ThrottleInterval": 3,
|
|
"StandardOutPath": f"{logs}/erp.log",
|
|
"StandardErrorPath": f"{logs}/erp-error.log",
|
|
}
|
|
with open(path, "wb") as handle:
|
|
plistlib.dump(payload, handle)
|
|
PY
|
|
launchctl bootout "gui/$(id -u)/com.kaidi.erp" >/dev/null 2>&1 || true
|
|
launchctl bootstrap "gui/$(id -u)" "$plist"
|
|
fi
|
|
}
|
|
|
|
wait_for_installer() {
|
|
local url="http://127.0.0.1:${ERP_SERVER_PORT:-8091}/api/install/status"
|
|
local deadline=$((SECONDS + 120))
|
|
while (( SECONDS < deadline )); do
|
|
if curl -fsS --connect-timeout 2 --max-time 5 \
|
|
-H "X-Setup-Token: $SETUP_TOKEN" "$url" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
fail 'the web installer did not start within 120 seconds; inspect the kaidi-erp service log'
|
|
}
|
|
|
|
detect_lan_address() {
|
|
local address=""
|
|
if command -v hostname >/dev/null 2>&1; then
|
|
address="$(hostname -I 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i ~ /^[0-9]+\./) {print $i; exit}}' || true)"
|
|
fi
|
|
if [[ -z "$address" && "$PLATFORM" == "linux" ]] && command -v ip >/dev/null 2>&1; then
|
|
address="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {for (i=1; i<=NF; i++) if ($i=="src") {print $(i+1); exit}}' || true)"
|
|
fi
|
|
if [[ -z "$address" && "$PLATFORM" == "darwin" ]] && command -v ipconfig >/dev/null 2>&1; then
|
|
address="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)"
|
|
fi
|
|
printf '%s' "${address:-127.0.0.1}"
|
|
}
|
|
|
|
main() {
|
|
detect_platform
|
|
validate_service_manager
|
|
if [[ -z "$INSTALL_ROOT" ]]; then
|
|
if [[ "$PLATFORM" == "linux" ]]; then
|
|
INSTALL_ROOT=/opt/kaidi-erp
|
|
else
|
|
INSTALL_ROOT="$HOME/Library/Application Support/KaidiERP"
|
|
fi
|
|
fi
|
|
[[ -n "$GITEA_BASE_URL" ]] || fail 'Gitea URL is required; use --gitea-url or ERP_GITEA_BASE_URL'
|
|
validate_download_url "$GITEA_BASE_URL"
|
|
|
|
say "Detected $PLATFORM/$ARCH"
|
|
check_and_install_dependencies
|
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/kaidi-erp-install.XXXXXX")"
|
|
create_curl_config
|
|
say 'Downloading and verifying the signed release...'
|
|
download_release
|
|
verify_release
|
|
prepare_layout
|
|
install_release_files
|
|
write_bootstrap_configuration
|
|
start_service
|
|
wait_for_installer
|
|
|
|
local port="${ERP_SERVER_PORT:-8091}" address
|
|
address="$(detect_lan_address)"
|
|
say "Kaidi ERP $VERSION installer is running"
|
|
say "Setup URL: http://${address}:${port}/?token=${SETUP_TOKEN}"
|
|
say "Local URL: http://127.0.0.1:${port}/?token=${SETUP_TOKEN}"
|
|
say 'Complete PostgreSQL and administrator setup in the browser. The installer will remove itself after the formal service is healthy.'
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then
|
|
main "$@"
|
|
fi
|