#!/usr/bin/env python3 import argparse import itertools import os import pwd import re import shutil import subprocess import sys import threading import time from pathlib import Path DEFAULT_USER = "rtorrent" DEFAULT_GROUP = "rtorrent" DEFAULT_HOME = "/home/rtorrent" DEFAULT_BASE_DIR = "/opt/rtorrent_build" DEFAULT_LIBTORRENT_REF = "v0.15.7" DEFAULT_RTORRENT_REF = "v0.15.7" DEFAULT_XMLRPC_REF = "latest-stable" DEFAULT_CARES_REF = "1.34.6" DEFAULT_CURL_REF = "8.19.0" DEFAULT_SERVICE_PATH = "/etc/systemd/system/rtorrent@.service" DEFAULT_SCGI_PORT = 5000 DEFAULT_TORRENT_PORT = 51300 class InstallError(Exception): pass class Spinner: FRAMES = ["|", "/", "-", "\\"] def __init__(self, message, enabled=True): self.message = message self.enabled = enabled and sys.stdout.isatty() self._stop = threading.Event() self._thread = None self._start = None def _run(self): for frame in itertools.cycle(self.FRAMES): if self._stop.is_set(): break elapsed = time.time() - self._start sys.stdout.write(f"\r[ {frame} ] {self.message} ({elapsed:.1f}s)") sys.stdout.flush() time.sleep(0.12) def __enter__(self): self._start = time.time() if self.enabled: self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() return self def __exit__(self, exc_type, exc, tb): elapsed = time.time() - self._start if self.enabled: self._stop.set() self._thread.join(timeout=0.5) status = "ERR" if exc else "OK " sys.stdout.write(f"\r[ {status} ] {self.message} ({elapsed:.1f}s)\n") sys.stdout.flush() def run(cmd, *, cwd=None, env=None, check=True, debug=False, capture_output=False): if debug: print(f"\n>>> {' '.join(cmd)}") stdout = subprocess.PIPE if capture_output else (None if debug else subprocess.DEVNULL) stderr = subprocess.PIPE if capture_output else (None if debug else subprocess.DEVNULL) result = subprocess.run(cmd, cwd=cwd, env=env, check=False, text=True, stdout=stdout, stderr=stderr) if check and result.returncode != 0: stderr_text = "" if capture_output and result.stderr: stderr_text = f"\n{result.stderr.strip()}" raise InstallError(f"Command failed with exit code {result.returncode}: {' '.join(cmd)}{stderr_text}") return result def capture(cmd, **kwargs): result = run(cmd, capture_output=True, **kwargs) out = (result.stdout or "").strip() err = (result.stderr or "").strip() return out if out else err def require_root(): if os.geteuid() != 0: raise InstallError("This script must be run as root (use sudo).") def detect_debian(): os_release = Path("/etc/os-release") if not os_release.exists(): raise InstallError("Cannot detect operating system: /etc/os-release is missing.") data = {} for line in os_release.read_text().splitlines(): if "=" in line: k, v = line.split("=", 1) data[k] = v.strip().strip('"') distro_id = data.get("ID", "").lower() distro_like = data.get("ID_LIKE", "").lower() if distro_id != "debian" and "debian" not in distro_like: raise InstallError( f"Unsupported distribution: ID={data.get('ID', 'unknown')}, " f"ID_LIKE={data.get('ID_LIKE', 'unknown')}. This installer currently supports Debian only." ) print(f"Detected Debian-compatible system: {data.get('PRETTY_NAME', distro_id)}") def prompt_yes_no(question, default=True, assume_yes=False): if assume_yes: print(f"{question} [{'Y/n' if default else 'y/N'}] -> auto-yes") return True suffix = "[Y/n]" if default else "[y/N]" while True: reply = input(f"{question} {suffix} ").strip().lower() if not reply: return default if reply in {"y", "yes"}: return True if reply in {"n", "no"}: return False print("Please answer yes or no.") def parse_version(version): parts = [int(x) for x in re.findall(r"\d+", version)] return tuple(parts[:3]) if parts else (0,) def ensure_packages(packages, *, debug=False): print("Updating APT metadata...") run(["apt-get", "update"], debug=debug) print("Installing build and runtime dependencies...") run(["apt-get", "install", "-y", *packages], debug=debug) def ensure_dir(path, owner=None, group=None, mode=None): Path(path).mkdir(parents=True, exist_ok=True) if owner is not None or group is not None: shutil.chown(path, user=owner, group=group) if mode is not None: os.chmod(path, mode) def create_system_user(user, group, home, assume_yes=False, debug=False): try: pwd.getpwnam(user) print(f"User '{user}' already exists.") except KeyError: if not prompt_yes_no(f"Create system user '{user}' with home '{home}'?", default=True, assume_yes=assume_yes): raise InstallError("User creation declined.") run(["groupadd", "--system", group], check=False, debug=debug) run([ "useradd", "--system", "--home-dir", home, "--create-home", "--shell", "/usr/sbin/nologin", "--gid", group, user, ], debug=debug) def clone_or_update_repo(repo_url, repo_dir, ref, *, debug=False): repo_dir = Path(repo_dir) if not repo_dir.exists(): with Spinner(f"Cloning {repo_dir.name}", enabled=not debug): run(["git", "clone", repo_url, str(repo_dir)], debug=debug) else: print(f"Repository already exists: {repo_dir}") with Spinner(f"Checking out {repo_dir.name} -> {ref}", enabled=not debug): run(["git", "fetch", "--all", "--tags"], cwd=str(repo_dir), debug=debug) run(["git", "checkout", ref], cwd=str(repo_dir), debug=debug) run(["git", "pull", "--ff-only"], cwd=str(repo_dir), check=False, debug=debug) def download_file(url, destination, *, debug=False): run(["curl", "-fL", url, "-o", str(destination)], debug=debug) def extract_tarball(tarball, destination, *, debug=False): if destination.exists(): shutil.rmtree(destination) destination.mkdir(parents=True, exist_ok=True) run(["tar", "-xzf", str(tarball), "-C", str(destination), "--strip-components=1"], debug=debug) def find_xmlrpc_config(base_dir, preferred_install=None): candidates = [] if preferred_install is not None: preferred = Path(preferred_install) / "bin" / "xmlrpc-c-config" if preferred.exists(): candidates.append(preferred.resolve()) root = Path(base_dir) if root.exists(): for match in root.rglob("xmlrpc-c-config"): if match.is_file(): candidates.append(match.resolve()) unique = [] seen = set() for candidate in candidates: if candidate not in seen: seen.add(candidate) unique.append(candidate) if preferred_install is not None: preferred_prefix = str(Path(preferred_install).resolve()) for candidate in unique: if str(candidate).startswith(preferred_prefix): return candidate return unique[0] if unique else None def verify_xmlrpc_environment(xmlrpc_config_path, *, debug=False): tool = Path(xmlrpc_config_path) if not tool.exists(): raise InstallError(f"xmlrpc-c-config was not found: {tool}") version = capture([str(tool), "--version"], check=True, debug=debug) if parse_version(version) < (1, 11): raise InstallError(f"xmlrpc-c version is too old: {version}. Version 1.11 or newer is required.") print(f"Detected xmlrpc-c version: {version} ({tool})") return version def build_env(*prefixes, extra_env=None): env = os.environ.copy() include_dirs = [] lib_dirs = [] pkg_dirs = [] bin_dirs = [] for prefix in prefixes: if not prefix: continue prefix = str(prefix) include_dirs.append(f"-I{prefix}/include") lib_dirs.append(f"-L{prefix}/lib") pkg_dirs.append(f"{prefix}/lib/pkgconfig") bin_dirs.append(f"{prefix}/bin") if include_dirs: env["CPPFLAGS"] = " ".join(include_dirs + [env.get("CPPFLAGS", "")]).strip() env["CFLAGS"] = " ".join(include_dirs + [env.get("CFLAGS", "")]).strip() if lib_dirs: rpaths = [f"-Wl,-rpath,{d[2:]}" for d in lib_dirs] env["LDFLAGS"] = " ".join(lib_dirs + rpaths + [env.get("LDFLAGS", "")]).strip() if pkg_dirs: env["PKG_CONFIG_PATH"] = ":".join(pkg_dirs + ([env.get("PKG_CONFIG_PATH")] if env.get("PKG_CONFIG_PATH") else [])) if bin_dirs: env["PATH"] = ":".join(bin_dirs + [env.get("PATH", "")]) if extra_env: env.update(extra_env) return env def build_xmlrpc_c(base_dir, xmlrpc_ref, *, debug=False): source_root = Path(base_dir) / "xmlrpc-c-src" install_dir = Path(base_dir) / "xmlrpc-c_install" build_root = Path(base_dir) / "_sources" tarball = build_root / "xmlrpc-c.tar.gz" existing_config = find_xmlrpc_config(base_dir, install_dir) if existing_config and str(existing_config).startswith(str(install_dir.resolve())): print(f"Reusing existing xmlrpc-c installation: {existing_config}") version = verify_xmlrpc_environment(existing_config, debug=debug) return install_dir, version ensure_dir(build_root) if xmlrpc_ref == "latest-stable": url = "https://sourceforge.net/projects/xmlrpc-c/files/latest/download" elif re.match(r"^\d+\.\d+\.\d+$", xmlrpc_ref): url = ( "https://downloads.sourceforge.net/project/xmlrpc-c/Xmlrpc-c%20Super%20Stable/" f"{xmlrpc_ref}/xmlrpc-c-{xmlrpc_ref}.tgz" ) else: url = xmlrpc_ref with Spinner("Downloading xmlrpc-c", enabled=not debug): download_file(url, tarball, debug=debug) extract_tarball(tarball, source_root, debug=debug) with Spinner("Configuring xmlrpc-c", enabled=not debug): run(["./configure", f"--prefix={install_dir}"], cwd=str(source_root), debug=debug) with Spinner("Building xmlrpc-c", enabled=not debug): run(["make", "-j", str(os.cpu_count() or 1)], cwd=str(source_root), debug=debug) with Spinner("Installing xmlrpc-c", enabled=not debug): run(["make", "install"], cwd=str(source_root), debug=debug) xmlrpc_config = find_xmlrpc_config(base_dir, install_dir) if not xmlrpc_config or not str(xmlrpc_config).startswith(str(install_dir.resolve())): raise InstallError(f"Custom xmlrpc-c build finished, but xmlrpc-c-config was not found under {install_dir}.") version = verify_xmlrpc_environment(xmlrpc_config, debug=debug) return install_dir, version def build_cares(base_dir, cares_version, *, debug=False): source_root = Path(base_dir) / "c-ares-src" install_dir = Path(base_dir) / "c-ares_install" build_root = Path(base_dir) / "_sources" tarball = build_root / f"c-ares-{cares_version}.tar.gz" url = f"https://github.com/c-ares/c-ares/releases/download/v{cares_version}/c-ares-{cares_version}.tar.gz" ensure_dir(build_root) with Spinner("Downloading c-ares", enabled=not debug): download_file(url, tarball, debug=debug) extract_tarball(tarball, source_root, debug=debug) with Spinner("Configuring c-ares", enabled=not debug): run([ "cmake", "-S", str(source_root), "-B", str(source_root / "build"), f"-DCMAKE_INSTALL_PREFIX={install_dir}", "-DCARES_SHARED=ON", "-DCARES_STATIC=OFF", "-DCMAKE_BUILD_TYPE=Release", ], debug=debug) with Spinner("Building c-ares", enabled=not debug): run(["cmake", "--build", str(source_root / "build"), "--parallel", str(os.cpu_count() or 1)], debug=debug) with Spinner("Installing c-ares", enabled=not debug): run(["cmake", "--install", str(source_root / "build")], debug=debug) return install_dir, cares_version def build_curl(base_dir, curl_version, cares_install, *, debug=False): source_root = Path(base_dir) / "curl-src" install_dir = Path(base_dir) / "curl_install" build_root = Path(base_dir) / "_sources" tarball = build_root / f"curl-{curl_version}.tar.gz" url = f"https://curl.se/download/curl-{curl_version}.tar.gz" ensure_dir(build_root) with Spinner("Downloading curl", enabled=not debug): download_file(url, tarball, debug=debug) extract_tarball(tarball, source_root, debug=debug) env = build_env(cares_install) buildconf_script = source_root / "buildconf" with Spinner("Preparing curl build system", enabled=not debug): if buildconf_script.exists(): run(["./buildconf"], cwd=str(source_root), env=env, debug=debug) run(["make", "distclean"], cwd=str(source_root), env=env, check=False, debug=debug) with Spinner("Configuring curl with c-ares", enabled=not debug): run([ "./configure", f"--prefix={install_dir}", "--with-openssl", f"--enable-ares={cares_install}", "--disable-static", "--enable-shared", ], cwd=str(source_root), env=env, debug=debug) with Spinner("Building curl", enabled=not debug): run(["make", "-j", str(os.cpu_count() or 1)], cwd=str(source_root), env=env, debug=debug) with Spinner("Installing curl", enabled=not debug): run(["make", "install"], cwd=str(source_root), env=env, debug=debug) version = capture([str(install_dir / "bin" / "curl"), "--version"], env=build_env(install_dir, cares_install), debug=debug) return install_dir, version def build_libtorrent(base_dir, libtorrent_ref, curl_install=None, cares_install=None, *, debug=False): source_dir = Path(base_dir) / "libtorrent" install_dir = Path(base_dir) / "libtorrent_install" clone_or_update_repo("https://github.com/rakshasa/libtorrent.git", source_dir, libtorrent_ref, debug=debug) prefixes = [] if curl_install: prefixes.append(curl_install) if cares_install: prefixes.append(cares_install) env = build_env(*prefixes) configure_cmd = ["./configure", f"--prefix={install_dir}"] if curl_install: curl_config = str(Path(curl_install) / "bin" / "curl-config") env["CURL_CONFIG"] = curl_config if Path(curl_config).exists(): configure_cmd.append(f"--with-curl={curl_config}") env["LIBS"] = f"-L{Path(curl_install) / 'lib'} -lcurl " + env.get("LIBS", "") if cares_install: env["LIBS"] = f"-L{Path(cares_install) / 'lib'} -lcares " + env.get("LIBS", "") with Spinner("Preparing libtorrent build system", enabled=not debug): run(["autoreconf", "-i"], cwd=str(source_dir), env=env, debug=debug) run(["make", "distclean"], cwd=str(source_dir), env=env, check=False, debug=debug) with Spinner("Configuring libtorrent", enabled=not debug): run(configure_cmd, cwd=str(source_dir), env=env, debug=debug) with Spinner("Building libtorrent", enabled=not debug): run(["make", "-j", str(os.cpu_count() or 1)], cwd=str(source_dir), env=env, debug=debug) with Spinner("Installing libtorrent", enabled=not debug): run(["make", "install"], cwd=str(source_dir), env=env, debug=debug) version = capture(["git", "describe", "--tags", "--always"], cwd=str(source_dir), debug=debug) return install_dir, version def build_rtorrent(base_dir, rtorrent_ref, libtorrent_install, xmlrpc_install, curl_install=None, cares_install=None, *, debug=False): source_dir = Path(base_dir) / "rtorrent" install_dir = Path(base_dir) / "rtorrent_install" clone_or_update_repo("https://github.com/rakshasa/rtorrent.git", source_dir, rtorrent_ref, debug=debug) xmlrpc_config = find_xmlrpc_config(base_dir, xmlrpc_install) if not xmlrpc_config: raise InstallError(f"Could not find custom xmlrpc-c-config under {base_dir}.") if not str(xmlrpc_config).startswith(str(Path(xmlrpc_install).resolve())): raise InstallError(f"Wrong xmlrpc-c-config selected: {xmlrpc_config}. Expected one under: {xmlrpc_install}") verify_xmlrpc_environment(xmlrpc_config, debug=debug) prefixes = [libtorrent_install, xmlrpc_install] if curl_install: prefixes.append(curl_install) if cares_install: prefixes.append(cares_install) env = build_env(*prefixes) env["PATH"] = f"{xmlrpc_config.parent}:" + env.get("PATH", "") env["XMLRPC_C_CONFIG"] = str(xmlrpc_config) with Spinner("Preparing rTorrent build system", enabled=not debug): run(["autoreconf", "-i"], cwd=str(source_dir), env=env, debug=debug) run(["make", "distclean"], cwd=str(source_dir), env=env, check=False, debug=debug) configure_cmd = ["./configure", f"--prefix={install_dir}", "--with-xmlrpc-c"] with Spinner("Configuring rTorrent", enabled=not debug): run(configure_cmd, cwd=str(source_dir), env=env, debug=debug) with Spinner("Building rTorrent", enabled=not debug): run(["make", "-j", str(os.cpu_count() or 1)], cwd=str(source_dir), env=env, debug=debug) with Spinner("Installing rTorrent", enabled=not debug): run(["make", "install"], cwd=str(source_dir), env=env, debug=debug) runtime_prefixes = [libtorrent_install, xmlrpc_install] if curl_install: runtime_prefixes.append(curl_install) if cares_install: runtime_prefixes.append(cares_install) runtime_env = build_env(*runtime_prefixes) runtime_env["LD_LIBRARY_PATH"] = ":".join([f"{p}/lib" for p in runtime_prefixes]) version = capture([str(install_dir / "bin" / "rtorrent"), "-h"], env=runtime_env, check=False, debug=debug) return install_dir, version def install_symlinks(rtorrent_install, libtorrent_install, xmlrpc_install, curl_install=None, cares_install=None, *, debug=False): rtorrent_bin = Path(rtorrent_install) / "bin" / "rtorrent" if not rtorrent_bin.exists(): raise InstallError(f"Compiled rtorrent binary not found: {rtorrent_bin}") usr_local_bin = Path("/usr/local/bin/rtorrent") if usr_local_bin.exists() or usr_local_bin.is_symlink(): usr_local_bin.unlink() usr_local_bin.symlink_to(rtorrent_bin) print(f"Symlinked {usr_local_bin} -> {rtorrent_bin}") lib_dirs = [f"{libtorrent_install}/lib", f"{xmlrpc_install}/lib"] if curl_install: lib_dirs.append(f"{curl_install}/lib") if cares_install: lib_dirs.append(f"{cares_install}/lib") ld_conf = Path("/etc/ld.so.conf.d/rtorrent-custom-libs.conf") ld_conf.write_text("\n".join(lib_dirs) + "\n") run(["ldconfig"], debug=debug) def write_service(service_path, binary_path, runtime_lib_dirs): service_content = f"""[Unit] Description=rTorrent for %I After=network.target [Service] Type=simple User=%I Group=%I KillMode=process WorkingDirectory=/home/%I ExecStartPre=-/bin/rm -f /home/%I/.session/rtorrent.lock ExecStart={binary_path} -o system.daemon.set=true -n -o import=/home/%I/.rtorrent.rc ExecStop=/bin/kill -9 $MAINPID Restart=always RestartSec=3 Environment=LD_LIBRARY_PATH={runtime_lib_dirs} [Install] WantedBy=multi-user.target """ Path(service_path).write_text(service_content) print(f"Wrote systemd unit: {service_path}") run(["systemctl", "daemon-reload"]) def write_rtorrent_config(user_home, username): config_path = Path(user_home) / ".rtorrent.rc" config_content = f""" ## https://git.linuxiarz.pl/gru/tools_scripts/_edit/master/install_rtorrent.py directory = /home/{username}/downloads session = /home/{username}/.session port_random = yes encoding_list = UTF-8 execute.nothrow = chmod,777,/home/rtorrent/downloads network.scgi.open_port = 127.0.0.1:5000 network.port_range.set = 51300-51310 network.port_random.set = no network.bind_address.set = 0.0.0.0 system.file.allocate.set = 1 system.umask.set = 0022 dht.mode.set = disable protocol.pex.set = no trackers.use_udp.set = no protocol.encryption.set = allow_incoming,enable_retry,prefer_plaintext schedule2 = session_save,1200,43200,((session.save)) schedule2 = tied_directory,6,5,start_tied= schedule2 = untied_directory,7,5,stop_untied= schedule2 = watch_directory,5,5,load.normal=/home/rtorrent/watch/*.torrent ratio.max.set=-1 """ config_path.write_text(config_content) shutil.chown(config_path, user=username, group=username) print(f"Wrote config: {config_path}") def prepare_user_dirs(user_home, username): for d in [Path(user_home) / "downloads", Path(user_home) / ".session", Path(user_home) / "watch"]: ensure_dir(d, owner=username, group=username, mode=0o755) shutil.chown(Path(user_home), user=username, group=username) def enable_service(user, *, debug=False): unit_name = f"rtorrent@{user}.service" run(["systemctl", "enable", "--now", unit_name], debug=debug) print(f"Enabled and started {unit_name}") def print_link_lines(title, lines): print(title) for line in lines: print(line) def verify_libtorrent_curl_integration(base_dir, libtorrent_install, curl_install, cares_install, *, debug=False): libtorrent_so = next((p for p in sorted((Path(libtorrent_install) / "lib").glob("libtorrent.so*")) if p.is_file() and not p.is_symlink()), None) if not libtorrent_so: raise InstallError("Could not find compiled libtorrent shared object for verification.") libtorrent_linked = capture(["ldd", str(libtorrent_so)], check=True, debug=debug) curl_lines = [line for line in libtorrent_linked.splitlines() if "libcurl" in line.lower()] print_link_lines("Linked libcurl lines (from libtorrent):", curl_lines) expected_curl = str(Path(curl_install) / "lib") if curl_lines: if not any(expected_curl in line for line in curl_lines): raise InstallError(f"libtorrent does not appear to be linked against the compiled libcurl from {expected_curl}.") else: config_log = Path(base_dir) / "libtorrent" / "config.log" config_text = config_log.read_text(errors="ignore") if config_log.exists() else "" curl_config = str(Path(curl_install) / "bin" / "curl-config") if curl_config not in config_text and expected_curl not in config_text: raise InstallError( "libtorrent does not expose libcurl in ldd, and config.log does not show the custom curl path either. " "The build likely used the system curl or no curl integration." ) print("libtorrent does not show libcurl in ldd; accepting config.log evidence of custom curl usage.") custom_curl = Path(curl_install) / "bin" / "curl" curl_version = capture([str(custom_curl), "--version"], env=build_env(curl_install, cares_install), check=True, debug=debug) print("Custom curl version:") print(curl_version.splitlines()[0]) lower = curl_version.lower() if "asynchdns" not in lower: raise InstallError("Custom curl does not report AsynchDNS support.") if "c-ares" not in lower and "ares" not in lower: print("Warning: curl --version does not explicitly show c-ares. Continuing because AsynchDNS is present.") if cares_install: cares_lines = [line for line in libtorrent_linked.splitlines() if "cares" in line.lower()] print_link_lines("Linked c-ares lines (from libtorrent):", cares_lines) if not cares_lines: print("c-ares is not visible in libtorrent ldd; this can still be valid when libcurl is resolved differently.") def verify_install(base_dir, rtorrent_install, libtorrent_install, xmlrpc_install, curl_install=None, cares_install=None, *, debug=False): rtorrent_bin = Path(rtorrent_install) / "bin" / "rtorrent" which_rtorrent = capture(["which", "rtorrent"], check=False, debug=debug) or "not found in PATH" print(f"Resolved rtorrent from PATH: {which_rtorrent}") linked = capture(["ldd", str(rtorrent_bin)], check=True, debug=debug) for libname, expected in [("libtorrent", str(Path(libtorrent_install) / "lib")), ("xmlrpc", str(Path(xmlrpc_install) / "lib"))]: lines = [line for line in linked.splitlines() if libname in line] print_link_lines(f"Linked {libname} lines:", lines) if not any(expected in line for line in lines): raise InstallError(f"rtorrent does not appear to be linked against the compiled {libname} from {expected}.") if curl_install: verify_libtorrent_curl_integration(base_dir, libtorrent_install, curl_install, cares_install, debug=debug) env = build_env(libtorrent_install, xmlrpc_install, curl_install, cares_install) env["LANG"] = "C" env["LC_ALL"] = "C" env["TERM"] = env.get("TERM", "xterm") ld_paths = [str(Path(libtorrent_install) / "lib"), str(Path(xmlrpc_install) / "lib")] if curl_install: ld_paths.append(str(Path(curl_install) / "lib")) if cares_install: ld_paths.append(str(Path(cares_install) / "lib")) env["LD_LIBRARY_PATH"] = ":".join(ld_paths) probe = subprocess.run([str(rtorrent_bin), "-h"], env=env, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) help_output = ((probe.stdout or "") + "\n" + (probe.stderr or "")).lower() if "xmlrpc-c" in help_output and "i8" in help_output: raise InstallError( "rTorrent was built against an xmlrpc-c library without i8 support. " "Make sure the custom xmlrpc-c build is used and that no older local installation shadows it." ) def build_parser(): parser = argparse.ArgumentParser(description="Debian installer for xmlrpc-c + libtorrent + rTorrent under /opt with optional c-ares/custom curl support.") parser.add_argument("--base-dir", default=DEFAULT_BASE_DIR, help=f"Base build/install directory (default: {DEFAULT_BASE_DIR})") parser.add_argument("--libtorrent-ref", default=DEFAULT_LIBTORRENT_REF, help=f"Git branch, tag or commit for libtorrent (default: {DEFAULT_LIBTORRENT_REF})") parser.add_argument("--rtorrent-ref", default=DEFAULT_RTORRENT_REF, help=f"Git branch, tag or commit for rtorrent (default: {DEFAULT_RTORRENT_REF})") parser.add_argument("--xmlrpc-ref", default=DEFAULT_XMLRPC_REF, help="xmlrpc-c source version or URL (default: latest-stable)") parser.add_argument("--cares-ref", default=DEFAULT_CARES_REF, help=f"c-ares release version (default: {DEFAULT_CARES_REF})") parser.add_argument("--curl-ref", default=DEFAULT_CURL_REF, help=f"curl release version (default: {DEFAULT_CURL_REF})") parser.add_argument("--user", default=DEFAULT_USER, help=f"System user for the service (default: {DEFAULT_USER})") parser.add_argument("--group", default=DEFAULT_GROUP, help=f"System group for the service (default: {DEFAULT_GROUP})") parser.add_argument("--home", default=DEFAULT_HOME, help=f"Home directory for the service user (default: {DEFAULT_HOME})") parser.add_argument("--only-build", action="store_true", help="Only build and install libtorrent/rTorrent under /opt. Skip user, config and systemd.") parser.add_argument("--yes", action="store_true", help="Assume yes for interactive prompts.") parser.add_argument("--debug", action="store_true", help="Show full command output during build steps.") parser.add_argument("--without-cares", dest="use_cares", action="store_false", help="Skip building c-ares and custom curl.") parser.set_defaults(use_cares=True) return parser def main(): parser = build_parser() args = parser.parse_args() require_root() detect_debian() packages = [ "build-essential", "pkg-config", "libtool", "autoconf", "automake", "git", "ca-certificates", "libssl-dev", "libncurses5-dev", "libncursesw5-dev", "libexpat1-dev", "curl", "tar", "cmake", "libpsl-dev", "zlib1g-dev", "libbrotli-dev", "libzstd-dev" ] print("This script will:") print(f" - build xmlrpc-c from '{args.xmlrpc_ref}'") print(f" - build libtorrent from '{args.libtorrent_ref}'") print(f" - build rtorrent from '{args.rtorrent_ref}'") if args.use_cares: print(f" - build c-ares from '{args.cares_ref}'") print(f" - build curl from '{args.curl_ref}' with c-ares") else: print(" - skip c-ares/custom curl and use system curl") print(f" - install everything under '{args.base_dir}'") if args.only_build: print(" - skip service user, config and systemd setup") else: print(f" - configure systemd service for user '{args.user}'") if not prompt_yes_no("Continue?", default=True, assume_yes=args.yes): print("Aborted by user.") return 1 ensure_packages(packages, debug=args.debug) ensure_dir(args.base_dir) xmlrpc_install, xmlrpc_version = build_xmlrpc_c(args.base_dir, args.xmlrpc_ref, debug=args.debug) cares_install = None cares_version = None curl_install = None curl_version = None if args.use_cares: cares_install, cares_version = build_cares(args.base_dir, args.cares_ref, debug=args.debug) curl_install, curl_version = build_curl(args.base_dir, args.curl_ref, cares_install, debug=args.debug) libtorrent_install, libtorrent_version = build_libtorrent( args.base_dir, args.libtorrent_ref, curl_install=curl_install, cares_install=cares_install, debug=args.debug ) rtorrent_install, rtorrent_version = build_rtorrent( args.base_dir, args.rtorrent_ref, libtorrent_install, xmlrpc_install, curl_install=curl_install, cares_install=cares_install, debug=args.debug ) install_symlinks(rtorrent_install, libtorrent_install, xmlrpc_install, curl_install=curl_install, cares_install=cares_install, debug=args.debug) verify_install(args.base_dir, rtorrent_install, libtorrent_install, xmlrpc_install, curl_install=curl_install, cares_install=cares_install, debug=args.debug) if not args.only_build: create_system_user(args.user, args.group, args.home, assume_yes=args.yes, debug=args.debug) prepare_user_dirs(args.home, args.user) write_rtorrent_config(args.home, args.user) runtime_lib_dirs = [f"{libtorrent_install}/lib", f"{xmlrpc_install}/lib"] if curl_install: runtime_lib_dirs.append(f"{curl_install}/lib") if cares_install: runtime_lib_dirs.append(f"{cares_install}/lib") write_service(DEFAULT_SERVICE_PATH, "/usr/local/bin/rtorrent", ":".join(runtime_lib_dirs)) enable_service(args.user, debug=args.debug) print(f"\nService status hint: systemctl status rtorrent@{args.user}.service") print("\nBuild summary") print("-------------") print(f"xmlrpc-c: {xmlrpc_version}") print(f"libtorrent: {libtorrent_version}") print(f"rtorrent: {rtorrent_version.splitlines()[0] if rtorrent_version else args.rtorrent_ref}") if args.use_cares: print(f"c-ares: {cares_version}") print(f"curl: {curl_version.splitlines()[0] if curl_version else args.curl_ref}") else: print("c-ares: disabled") print("curl: system") print("binary: /usr/local/bin/rtorrent") print(f"base dir: {args.base_dir}") print("\nDone.") return 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: print("\nInterrupted.") sys.exit(130) except InstallError as exc: print(f"\nERROR: {exc}", file=sys.stderr) sys.exit(1)