debian-forge/osbuild
Tom Gundersen cdcfa1277e osbuild: make state handling generic
Rather than treating the dnf-cache specially, give each stage its
own state directory that they can reuse. This should obviously be
used with care by the stages in order to make the builds
reproducible.
2019-06-06 19:37:49 +02:00

93 lines
3 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)
def main(pipeline_path, from_archive, save):
with open(pipeline_path) as f:
pipeline = json.load(f)
with tmpfs(save) as tree:
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"] = os.path.abspath(tree)
options["state"] = f"{os.getcwd()}/state/{name}"
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()
script = f"""
set -e
mount -t tmpfs tmpfs /run
mount -t tmpfs tmpfs /tmp
mount -t tmpfs tmpfs /var/tmp
stages/{name}
"""
try:
subprocess.run(["unshare", "--pid", "--kill-child", "--mount", "sh", "-c", script],
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)))