nspawn can provide a minimal PID1 implementation, avoiding stages to themselves do things like reap zobies etc. Use that.
106 lines
3.7 KiB
Python
Executable file
106 lines
3.7 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
import argparse
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
RESET = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
RED = "\033[31m"
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def tmpfs(save=None):
|
|
"""A contextmanager that mounts a tmpfs and returns its location.
|
|
|
|
If `save` is given, it should contain the name of an .tar.gz archive to
|
|
which the contents of the tmpfs will be written.
|
|
"""
|
|
|
|
with tempfile.TemporaryDirectory(prefix="osbuild-tree-", dir=os.getcwd()) as path:
|
|
subprocess.run(["mount", "-t", "tmpfs", "tmpfs", path], check=True)
|
|
try:
|
|
yield path
|
|
if save:
|
|
print(f"Saving tree to {save}...")
|
|
subprocess.run(["tar", "-czf", save, "-C", path, "."], stdout=subprocess.DEVNULL, check=True)
|
|
finally:
|
|
subprocess.run(["umount", path], check=True)
|
|
|
|
@contextlib.contextmanager
|
|
def bindmnt(src_path):
|
|
"""A contextmanager that mindmounts a path read-only and returns its location.
|
|
"""
|
|
|
|
with tempfile.TemporaryDirectory(prefix="osbuild-build-", dir=os.getcwd()) as dst_path:
|
|
subprocess.run(["mount", "-o", "bind,ro", src_path, dst_path], check=True)
|
|
try:
|
|
yield dst_path
|
|
finally:
|
|
subprocess.run(["umount", dst_path], check=True)
|
|
|
|
|
|
def main(pipeline_path, from_archive, save):
|
|
with open(pipeline_path) as f:
|
|
pipeline = json.load(f)
|
|
|
|
with tmpfs(save) as tree, bindmnt("/") as root:
|
|
if from_archive:
|
|
r = subprocess.run(["tar", "-xzf", from_archive, "-C", tree])
|
|
if r.returncode != 0:
|
|
return
|
|
|
|
for i, stage in enumerate(pipeline["stages"], start=1):
|
|
name = stage["name"]
|
|
options = stage.get("options", {})
|
|
options["tree"] = "/tmp/tree"
|
|
options["state"] = "/tmp/state"
|
|
|
|
options_str = json.dumps(options, indent=2)
|
|
|
|
r = subprocess.run(["mkdir", "-p", f"{os.getcwd()}/state/{name}"])
|
|
if r.returncode != 0:
|
|
return
|
|
|
|
print()
|
|
print(f"{RESET}{BOLD}{i}. {name}{RESET} {options_str}")
|
|
print()
|
|
|
|
try:
|
|
subprocess.run(["systemd-nspawn",
|
|
"--as-pid2",
|
|
"--link-journal=no",
|
|
"--volatile=yes",
|
|
f"--directory={root}",
|
|
f"--bind={tree}:/tmp/tree",
|
|
f"--bind={os.getcwd()}/state/{name}:/tmp/state",
|
|
f"--bind={os.getcwd()}/stages/{name}:/tmp/stage",
|
|
"--bind=/etc/pki",
|
|
"/tmp/stage"],
|
|
input=options_str, encoding="utf-8", check=True)
|
|
except KeyboardInterrupt:
|
|
print()
|
|
print(f"{RESET}{BOLD}{RED}Aborted{RESET}")
|
|
return 130
|
|
except subprocess.CalledProcessError as error:
|
|
print()
|
|
print(f"{RESET}{BOLD}{RED}{name} failed with code {error.returncode}{RESET}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Build operating system images")
|
|
parser.add_argument("pipeline_path", metavar="PIPELINE",
|
|
help="json file containing the pipeline that should be built")
|
|
parser.add_argument("--save", metavar="ARCHIVE",
|
|
help="save the resulting tree to ARCHIVE")
|
|
parser.add_argument("--from", dest="from_archive", metavar="ARCHIVE",
|
|
help="initialize the tree from ARCHIVE")
|
|
args = parser.parse_args()
|
|
|
|
sys.exit(main(**vars(args)))
|