553 lines
16 KiB
Bash
Executable File
553 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -u
|
|
|
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
CONFIG_DIR="${FFX_CONFIG_DIR:-${HOME}/.local/etc}"
|
|
CONFIG_FILE="${FFX_CONFIG_FILE:-${CONFIG_DIR}/ffx.json}"
|
|
VAR_DIR="${FFX_VAR_DIR:-${HOME}/.local/var/ffx}"
|
|
LOG_DIR="${FFX_LOG_DIR:-${HOME}/.local/var/log}"
|
|
DATABASE_FILE="${FFX_DATABASE_FILE:-${VAR_DIR}/ffx.db}"
|
|
SUBTITLES_BASE_DIR="${FFX_SUBTITLES_BASE_DIR:-${HOME}/.local/var/sync/subtitles}"
|
|
FFX_PYTHON="${FFX_PYTHON:-${HOME}/.local/share/ffx.venv/bin/python}"
|
|
CONFIG_TEMPLATE_FILE="${FFX_CONFIG_TEMPLATE:-${ROOT_DIR}/assets/ffx.json.j2}"
|
|
|
|
CHECK_ONLY=0
|
|
WITH_TESTS=0
|
|
|
|
MUTATIONS=0
|
|
INSTALL_FAILURES=0
|
|
READINESS_FAILURES=0
|
|
|
|
MISSING_REQUIRED_SYSTEM=()
|
|
MISSING_OPTIONAL_SYSTEM=()
|
|
|
|
COLOR_RESET=""
|
|
COLOR_GREEN=""
|
|
COLOR_YELLOW=""
|
|
COLOR_RED=""
|
|
|
|
if [ -t 1 ]; then
|
|
COLOR_RESET="$(printf '\033[0m')"
|
|
COLOR_GREEN="$(printf '\033[32m')"
|
|
COLOR_YELLOW="$(printf '\033[33m')"
|
|
COLOR_RED="$(printf '\033[31m')"
|
|
fi
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [--check] [--with-tests] [--help]
|
|
|
|
Prepare the local workstation environment for an already installed FFX bundle.
|
|
|
|
Options:
|
|
--check Report readiness only. Do not create, install, or modify.
|
|
--with-tests Include test-related notes while preparing system dependencies and local config.
|
|
--help Show this help text.
|
|
|
|
Environment overrides:
|
|
FFX_CONFIG_DIR Override the parent directory for the seeded ffx.json file.
|
|
FFX_CONFIG_FILE Override the seeded config file path directly.
|
|
FFX_VAR_DIR Override the default data directory.
|
|
FFX_LOG_DIR Override the default log directory.
|
|
FFX_DATABASE_FILE Override the database path written into a newly seeded config.
|
|
FFX_SUBTITLES_BASE_DIR Override the default subtitles base directory written into a newly seeded config.
|
|
FFX_PYTHON Override the bundle venv Python used to render the seeded config.
|
|
FFX_CONFIG_TEMPLATE Override the Jinja2 template path used to seed the config.
|
|
|
|
Notes:
|
|
- tools/setup.sh is the first installation step and owns bundle venv setup.
|
|
- This script is the second step and owns system dependencies plus local config.
|
|
- After the bundle is installed, the aligned CLI wrapper is: ffx configure_workstation
|
|
- Python test packages are installed by tools/setup.sh --with-tests, not here.
|
|
EOF
|
|
}
|
|
|
|
status_ok() {
|
|
printf '%sok%s' "${COLOR_GREEN}" "${COLOR_RESET}"
|
|
}
|
|
|
|
status_warn() {
|
|
printf '%swarn%s' "${COLOR_YELLOW}" "${COLOR_RESET}"
|
|
}
|
|
|
|
status_fail() {
|
|
printf '%sfailed%s' "${COLOR_RED}" "${COLOR_RESET}"
|
|
}
|
|
|
|
report_component() {
|
|
local level="$1"
|
|
local label="$2"
|
|
local detail="$3"
|
|
local rendered_status=""
|
|
|
|
case "${level}" in
|
|
ok)
|
|
rendered_status="$(status_ok)"
|
|
;;
|
|
warn)
|
|
rendered_status="$(status_warn)"
|
|
;;
|
|
*)
|
|
rendered_status="$(status_fail)"
|
|
;;
|
|
esac
|
|
|
|
printf '[%s] %s%s\n' "${rendered_status}" "${label}" "${detail:+: $detail}"
|
|
}
|
|
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
check_command_component() {
|
|
command_exists "$2"
|
|
}
|
|
|
|
check_tmdb_key() {
|
|
[ -n "${TMDB_API_KEY:-}" ]
|
|
}
|
|
|
|
check_seeded_dir() {
|
|
[ -d "$1" ]
|
|
}
|
|
|
|
check_seeded_file() {
|
|
[ -f "$1" ]
|
|
}
|
|
|
|
component_detail() {
|
|
case "$1" in
|
|
git|python3|ffmpeg|ffprobe|cpulimit)
|
|
command -v "$1" || printf "command '%s' not found" "$1"
|
|
;;
|
|
tmdb-key)
|
|
if check_tmdb_key; then
|
|
printf 'TMDB_API_KEY is set'
|
|
else
|
|
printf 'TMDB_API_KEY is unset; TMDB-backed flows will be skipped or fail'
|
|
fi
|
|
;;
|
|
config-dir)
|
|
if check_seeded_dir "${CONFIG_DIR}"; then
|
|
printf '%s' "${CONFIG_DIR}"
|
|
else
|
|
printf 'missing; prep can create it'
|
|
fi
|
|
;;
|
|
var-dir)
|
|
if check_seeded_dir "${VAR_DIR}"; then
|
|
printf '%s' "${VAR_DIR}"
|
|
else
|
|
printf 'missing; prep can create it'
|
|
fi
|
|
;;
|
|
log-dir)
|
|
if check_seeded_dir "${LOG_DIR}"; then
|
|
printf '%s' "${LOG_DIR}"
|
|
else
|
|
printf 'missing; prep can create it'
|
|
fi
|
|
;;
|
|
subtitles-base-dir)
|
|
if check_seeded_dir "${SUBTITLES_BASE_DIR}"; then
|
|
printf '%s' "${SUBTITLES_BASE_DIR}"
|
|
else
|
|
printf 'missing; prep can create it'
|
|
fi
|
|
;;
|
|
ffx-config)
|
|
if check_seeded_file "${CONFIG_FILE}"; then
|
|
printf '%s' "${CONFIG_FILE}"
|
|
else
|
|
printf 'missing; prep can seed a default non-destructively'
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
report_toolchain_component() {
|
|
local label="$1"
|
|
local command_name="$2"
|
|
local required="$3"
|
|
|
|
if check_command_component "${label}" "${command_name}" "${required}"; then
|
|
report_component ok "${label}" "$(component_detail "${command_name}")"
|
|
else
|
|
if [ "${required}" = "required" ]; then
|
|
report_component failed "${label}" "$(component_detail "${command_name}")"
|
|
MISSING_REQUIRED_SYSTEM+=("${command_name}")
|
|
READINESS_FAILURES=$((READINESS_FAILURES + 1))
|
|
else
|
|
report_component warn "${label}" "$(component_detail "${command_name}")"
|
|
MISSING_OPTIONAL_SYSTEM+=("${command_name}")
|
|
fi
|
|
fi
|
|
}
|
|
|
|
report_tmdb_component() {
|
|
if check_tmdb_key; then
|
|
report_component ok "TMDB API key" "$(component_detail tmdb-key)"
|
|
else
|
|
report_component warn "TMDB API key" "$(component_detail tmdb-key)"
|
|
fi
|
|
}
|
|
|
|
report_seeded_component() {
|
|
local label="$1"
|
|
local key="$2"
|
|
local required="$3"
|
|
local ok=1
|
|
|
|
case "${key}" in
|
|
config-dir)
|
|
check_seeded_dir "${CONFIG_DIR}" || ok=0
|
|
;;
|
|
var-dir)
|
|
check_seeded_dir "${VAR_DIR}" || ok=0
|
|
;;
|
|
log-dir)
|
|
check_seeded_dir "${LOG_DIR}" || ok=0
|
|
;;
|
|
subtitles-base-dir)
|
|
check_seeded_dir "${SUBTITLES_BASE_DIR}" || ok=0
|
|
;;
|
|
ffx-config)
|
|
check_seeded_file "${CONFIG_FILE}" || ok=0
|
|
;;
|
|
esac
|
|
|
|
if [ "${ok}" -eq 1 ]; then
|
|
report_component ok "${label}" "$(component_detail "${key}")"
|
|
else
|
|
if [ "${required}" = "required" ]; then
|
|
report_component failed "${label}" "$(component_detail "${key}")"
|
|
READINESS_FAILURES=$((READINESS_FAILURES + 1))
|
|
else
|
|
report_component warn "${label}" "$(component_detail "${key}")"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
print_dependency_status() {
|
|
READINESS_FAILURES=0
|
|
MISSING_REQUIRED_SYSTEM=()
|
|
MISSING_OPTIONAL_SYSTEM=()
|
|
|
|
echo "Dependency status:"
|
|
report_toolchain_component "git" "git" "required"
|
|
report_toolchain_component "python3" "python3" "required"
|
|
report_toolchain_component "ffmpeg" "ffmpeg" "required"
|
|
report_toolchain_component "ffprobe" "ffprobe" "required"
|
|
report_toolchain_component "cpulimit" "cpulimit" "required"
|
|
report_tmdb_component
|
|
}
|
|
|
|
print_seeded_file_status() {
|
|
echo "Seeded local files:"
|
|
report_seeded_component "Config dir" "config-dir" "optional"
|
|
report_seeded_component "Var dir" "var-dir" "optional"
|
|
report_seeded_component "Log dir" "log-dir" "optional"
|
|
report_seeded_component "Subtitles base dir" "subtitles-base-dir" "optional"
|
|
report_seeded_component "ffx config" "ffx-config" "optional"
|
|
}
|
|
|
|
print_test_package_status() {
|
|
if [ "${WITH_TESTS}" -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
|
|
echo "Test environment notes:"
|
|
report_component ok "system test dependencies" "no extra system packages beyond the standard runtime toolchain"
|
|
report_component ok "Python test packages" "install via tools/setup.sh --with-tests"
|
|
}
|
|
|
|
detect_package_manager() {
|
|
if command_exists apt-get; then
|
|
printf 'apt-get\n'
|
|
return 0
|
|
fi
|
|
if command_exists pacman; then
|
|
printf 'pacman\n'
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
run_root_command() {
|
|
if [ "${EUID}" -eq 0 ]; then
|
|
"$@"
|
|
elif command_exists sudo; then
|
|
sudo "$@"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_system_requirements() {
|
|
local package_manager
|
|
|
|
if ! package_manager="$(detect_package_manager)"; then
|
|
printf 'No supported package manager found for automatic preparation.\n' >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
|
|
case "${package_manager}" in
|
|
apt-get)
|
|
printf 'Installing missing system dependencies via apt-get...\n'
|
|
if ! run_root_command apt-get update; then
|
|
printf 'apt-get update failed.\n' >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
if ! run_root_command apt-get install -y git python3 ffmpeg cpulimit; then
|
|
printf 'apt-get install failed.\n' >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
;;
|
|
pacman)
|
|
printf 'Installing missing system dependencies via pacman...\n'
|
|
if ! run_root_command pacman -Sy --noconfirm git python ffmpeg cpulimit; then
|
|
printf 'pacman install failed.\n' >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
MUTATIONS=$((MUTATIONS + 1))
|
|
return 0
|
|
}
|
|
|
|
render_default_config() {
|
|
local output_path="$1"
|
|
local temporary_output_path=""
|
|
|
|
if [ ! -x "${FFX_PYTHON}" ]; then
|
|
printf 'Missing bundle Python interpreter at %s.\n' "${FFX_PYTHON}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
|
|
if [ ! -f "${CONFIG_TEMPLATE_FILE}" ]; then
|
|
printf 'Missing FFX config template at %s.\n' "${CONFIG_TEMPLATE_FILE}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
|
|
if ! temporary_output_path="$(mktemp "${output_path}.tmp.XXXXXX")"; then
|
|
printf 'Failed to create a temporary config file next to %s.\n' "${output_path}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
|
|
if ! FFX_CONFIG_TEMPLATE_FILE="${CONFIG_TEMPLATE_FILE}" \
|
|
FFX_REPO_ROOT="${ROOT_DIR}" \
|
|
FFX_DATABASE_PATH="${DATABASE_FILE}" \
|
|
FFX_LOG_DIRECTORY="${LOG_DIR}" \
|
|
FFX_SUBTITLES_DIRECTORY="${SUBTITLES_BASE_DIR}" \
|
|
"${FFX_PYTHON}" - >"${temporary_output_path}" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
|
|
repo_root = Path(os.environ["FFX_REPO_ROOT"])
|
|
src_root = repo_root / "src"
|
|
if str(src_root) not in sys.path:
|
|
sys.path.insert(0, str(src_root))
|
|
|
|
from ffx.constants import (
|
|
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
|
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
|
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
|
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
|
)
|
|
from ffx.i18n import resolve_application_language
|
|
|
|
template_path = Path(os.environ["FFX_CONFIG_TEMPLATE_FILE"])
|
|
environment = Environment(
|
|
loader=FileSystemLoader(str(template_path.parent)),
|
|
undefined=StrictUndefined,
|
|
autoescape=False,
|
|
keep_trailing_newline=True,
|
|
)
|
|
template = environment.get_template(template_path.name)
|
|
|
|
sys.stdout.write(
|
|
template.render(
|
|
language_json=json.dumps(resolve_application_language()),
|
|
database_path_json=json.dumps(os.environ["FFX_DATABASE_PATH"]),
|
|
log_directory_json=json.dumps(os.environ["FFX_LOG_DIRECTORY"]),
|
|
subtitles_directory_json=json.dumps(os.environ["FFX_SUBTITLES_DIRECTORY"]),
|
|
default_index_season_digits=DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
|
default_index_episode_digits=DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
|
default_indicator_season_digits=DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
|
default_indicator_episode_digits=DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
|
)
|
|
)
|
|
PY
|
|
then
|
|
rm -f "${temporary_output_path}"
|
|
printf 'Failed to render ffx config from template %s.\n' "${CONFIG_TEMPLATE_FILE}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
|
|
if ! mv "${temporary_output_path}" "${output_path}"; then
|
|
rm -f "${temporary_output_path}"
|
|
printf 'Failed to move rendered ffx config into place at %s.\n' "${output_path}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
seed_default_config() {
|
|
if [ "${CHECK_ONLY}" -eq 1 ]; then
|
|
return 0
|
|
fi
|
|
|
|
local created_any=0
|
|
|
|
if [ ! -d "${CONFIG_DIR}" ]; then
|
|
printf 'Creating config dir at %s...\n' "${CONFIG_DIR}"
|
|
if ! mkdir -p "${CONFIG_DIR}"; then
|
|
printf 'Failed to create config dir at %s.\n' "${CONFIG_DIR}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
created_any=1
|
|
fi
|
|
|
|
if [ ! -d "${VAR_DIR}" ]; then
|
|
printf 'Creating var dir at %s...\n' "${VAR_DIR}"
|
|
if ! mkdir -p "${VAR_DIR}"; then
|
|
printf 'Failed to create var dir at %s.\n' "${VAR_DIR}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
created_any=1
|
|
fi
|
|
|
|
if [ ! -d "${LOG_DIR}" ]; then
|
|
printf 'Creating log dir at %s...\n' "${LOG_DIR}"
|
|
if ! mkdir -p "${LOG_DIR}"; then
|
|
printf 'Failed to create log dir at %s.\n' "${LOG_DIR}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
created_any=1
|
|
fi
|
|
|
|
if [ ! -d "${SUBTITLES_BASE_DIR}" ]; then
|
|
printf 'Creating subtitles base dir at %s...\n' "${SUBTITLES_BASE_DIR}"
|
|
if ! mkdir -p "${SUBTITLES_BASE_DIR}"; then
|
|
printf 'Failed to create subtitles base dir at %s.\n' "${SUBTITLES_BASE_DIR}" >&2
|
|
INSTALL_FAILURES=$((INSTALL_FAILURES + 1))
|
|
return 1
|
|
fi
|
|
created_any=1
|
|
fi
|
|
|
|
if [ ! -f "${CONFIG_FILE}" ]; then
|
|
printf 'Seeding ffx config at %s...\n' "${CONFIG_FILE}"
|
|
if ! render_default_config "${CONFIG_FILE}"; then
|
|
return 1
|
|
fi
|
|
created_any=1
|
|
fi
|
|
|
|
if [ "${created_any}" -eq 1 ]; then
|
|
MUTATIONS=$((MUTATIONS + 1))
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
parse_args() {
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--check)
|
|
CHECK_ONLY=1
|
|
;;
|
|
--with-tests)
|
|
WITH_TESTS=1
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
printf 'Unknown option: %s\n\n' "$1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
|
|
print_dependency_status
|
|
|
|
if [ "${CHECK_ONLY}" -eq 0 ] && [ "${#MISSING_REQUIRED_SYSTEM[@]}" -gt 0 ]; then
|
|
install_system_requirements
|
|
|
|
echo
|
|
print_dependency_status
|
|
fi
|
|
|
|
echo
|
|
print_seeded_file_status
|
|
|
|
echo
|
|
print_test_package_status
|
|
|
|
if [ "${CHECK_ONLY}" -eq 0 ]; then
|
|
seed_default_config
|
|
echo
|
|
print_dependency_status
|
|
echo
|
|
print_seeded_file_status
|
|
echo
|
|
print_test_package_status
|
|
fi
|
|
|
|
echo
|
|
if [ "${INSTALL_FAILURES}" -gt 0 ]; then
|
|
echo "One or more install steps failed; see the status checks above." >&2
|
|
return 1
|
|
fi
|
|
|
|
if [ "${READINESS_FAILURES}" -gt 0 ]; then
|
|
if [ "${CHECK_ONLY}" -eq 1 ]; then
|
|
echo "Required system prerequisites are incomplete." >&2
|
|
else
|
|
echo "Required components are still missing after preparation." >&2
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
if [ "${CHECK_ONLY}" -eq 1 ]; then
|
|
echo "The FFX preparation environment is ready."
|
|
elif [ "${MUTATIONS}" -gt 0 ]; then
|
|
echo "The FFX preparation environment is ready."
|
|
else
|
|
echo "The FFX preparation environment is already prepared."
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
main "$@"
|