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.
93 lines
3 KiB
Python
Executable file
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)))
|