#!/usr/bin/env python3 import os import subprocess import sys from pathlib import Path # The public, read-only copy machines install from. The forge is behind a # login, so a bare machine could neither fetch this script nor clone from it. DEFAULT_DISTRIBUTION = "https://loudout.serlo.lu" # loudout reads its manifests with tomllib, which arrived in 3.11. This script # stays parseable by older interpreters so they can say so instead of crashing. MINIMUM_PYTHON = (3, 11) def fail(message): print(f"bootstrap: {message}", file=sys.stderr) sys.exit(1) def main(): if sys.version_info[:2] < MINIMUM_PYTHON: found = "{}.{}".format(*sys.version_info[:2]) fail( f"loudout needs Python 3.11 or newer, but {sys.executable} is {found}. " f"Run this again with a newer python3; nothing was installed." ) home = Path(os.environ.get("HOME", Path.home())) target = home / ".loudout" # Where did this script run from? # Piped from curl, the script arrives on stdin and has no file of its own, # so there is no checkout it could be inside. script_file = globals().get("__file__") script_dir = Path(script_file).resolve().parent if script_file and script_file != "" else None # Is it inside a dev checkout? We know it is if bin/loudout exists next to it. is_dev_checkout = script_dir is not None and (script_dir / "bin" / "loudout").exists() # A dev checkout that moved leaves its link behind. exists() says no, but # the name is taken, so neither linking nor cloning could succeed. if target.is_symlink() and not target.exists(): fail( f"{target} links to {os.readlink(target)}, which no longer exists. " f"Remove the link, or point it at your checkout, and run this again." ) if target.exists(): print(f"bootstrap: {target} already exists, skipping clone/link.") else: if is_dev_checkout and script_dir != target: print(f"bootstrap: linking dev checkout from {script_dir} to {target}") target.symlink_to(script_dir) else: source = os.environ.get("LOUDOUT_DISTRIBUTION", DEFAULT_DISTRIBUTION) print(f"bootstrap: cloning from {source} to {target}", flush=True) try: subprocess.run(["git", "clone", "-q", source, str(target)], check=True) except FileNotFoundError: fail("git was not found. Install it and run this again.") except subprocess.CalledProcessError: fail(f"could not clone {source} (git's message is above); nothing was installed.") executable = target / "bin" / "loudout" if not executable.exists(): fail(f"could not find {executable}") # ~/.loudout is the install path on every machine, even when it is a link # to a dev checkout, so that is what the timer and targets are pointed at. env = dict(os.environ, LOUDOUT_ROOT=str(target)) print("bootstrap: running install", flush=True) installed = subprocess.run([sys.executable, str(executable), "install"], env=env) if installed.returncode != 0: fail(f"install failed (see above). Once it is fixed, run `{executable} install`.") # Without the timer the machine never updates itself, but a box with no # launchd or user systemd (a container, say) should still come up. print("bootstrap: installing the sync timer", flush=True) timer = subprocess.run([sys.executable, str(executable), "timer", "install"], env=env) if timer.returncode != 0: print( f"bootstrap: timer install failed, so this machine will not update itself — " f"run `{executable} timer install` once launchd or systemd is available.", file=sys.stderr, ) if __name__ == "__main__": main()