219 lines
8.3 KiB
Python
219 lines
8.3 KiB
Python
"""Entrypoints for osbuild
|
|
|
|
This module contains the application and API entrypoints of `osbuild`, the
|
|
command-line-interface to osbuild. The `osbuild_cli()` entrypoint can be safely
|
|
used from tests to run the cli.
|
|
"""
|
|
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import typing
|
|
from typing import List
|
|
|
|
import osbuild
|
|
import osbuild.meta
|
|
import osbuild.monitor
|
|
from osbuild.meta import ValidationResult
|
|
from osbuild.objectstore import ObjectStore
|
|
from osbuild.pipeline import Manifest
|
|
from osbuild.util.parsing import parse_size
|
|
from osbuild.util.term import fmt as vt
|
|
|
|
|
|
def parse_manifest(path: str) -> dict:
|
|
if path == "-":
|
|
manifest = json.load(sys.stdin)
|
|
else:
|
|
with open(path, encoding="utf8") as f:
|
|
manifest = json.load(f)
|
|
|
|
return manifest
|
|
|
|
|
|
def show_validation(result: ValidationResult, name: str) -> None:
|
|
if name == "-":
|
|
name = "<stdin>"
|
|
|
|
print(f"{vt.bold}{name}{vt.reset} ", end='')
|
|
|
|
if result:
|
|
print(f"is {vt.bold}{vt.green}valid{vt.reset}")
|
|
return
|
|
|
|
print(f"has {vt.bold}{vt.red}errors{vt.reset}:")
|
|
print("")
|
|
|
|
for error in result:
|
|
print(f"{vt.bold}{error.id}{vt.reset}:")
|
|
print(f" {error.message}\n")
|
|
|
|
|
|
def export(name_or_id: str, output_directory: str, store: ObjectStore, manifest: Manifest) -> None:
|
|
pipeline = manifest[name_or_id]
|
|
obj = store.get(pipeline.id)
|
|
dest = os.path.join(output_directory, name_or_id)
|
|
|
|
skip_preserve_owner = \
|
|
os.getenv("OSBUILD_EXPORT_FORCE_NO_PRESERVE_OWNER") == "1"
|
|
os.makedirs(dest, exist_ok=True)
|
|
obj.export(dest, skip_preserve_owner=skip_preserve_owner)
|
|
|
|
|
|
@typing.no_type_check # see https://github.com/python/typeshed/issues/3107
|
|
def parse_arguments(sys_argv: List[str]) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(prog="osbuild",
|
|
description="Build operating system images")
|
|
|
|
parser.add_argument("manifest_path", metavar="MANIFEST",
|
|
help="json file containing the manifest that should be built, or a '-' to read from stdin")
|
|
parser.add_argument("--cache", "--store", metavar="DIRECTORY", type=os.path.abspath,
|
|
default=".osbuild",
|
|
help="directory where sources and intermediary os trees are stored")
|
|
parser.add_argument("-l", "--libdir", metavar="DIRECTORY", type=os.path.abspath, default="/usr/lib/osbuild",
|
|
help="directory containing stages, assemblers, and the osbuild library")
|
|
parser.add_argument("--cache-max-size", metavar="SIZE", type=parse_size, default=None,
|
|
help="maximum size of the cache (bytes) or 'unlimited' for no restriction")
|
|
parser.add_argument(
|
|
"--checkpoint",
|
|
metavar="ID",
|
|
action="append",
|
|
type=str,
|
|
default=None,
|
|
help="stage to commit to the object store during build (can be passed multiple times), accepts globs")
|
|
parser.add_argument("--export", metavar="ID", action="append", type=str, default=[],
|
|
help="object to export, can be passed multiple times")
|
|
parser.add_argument("--json", action="store_true",
|
|
help="output results in JSON format")
|
|
parser.add_argument("--output-directory", metavar="DIRECTORY", type=os.path.abspath,
|
|
help="directory where result objects are stored")
|
|
parser.add_argument("--inspect", action="store_true",
|
|
help="return the manifest in JSON format including all the ids")
|
|
parser.add_argument("--monitor", metavar="NAME", default=None,
|
|
help="name of the monitor to be used")
|
|
parser.add_argument("--monitor-fd", metavar="FD", type=int, default=sys.stdout.fileno(),
|
|
help="file descriptor to be used for the monitor")
|
|
parser.add_argument("--stage-timeout", type=int, default=None,
|
|
help="set the maximal time (in seconds) each stage is allowed to run")
|
|
parser.add_argument("--version", action="version",
|
|
help="return the version of osbuild",
|
|
version="%(prog)s " + osbuild.__version__)
|
|
# nargs='?' const='*' means `--break` is equivalent to `--break=*`
|
|
parser.add_argument("--break", dest='debug_break', type=str, nargs='?', const='*',
|
|
help="open debug shell when executing stage. Accepts stage name or id or * (for all)")
|
|
parser.add_argument("--quiet", "-q", action="store_true",
|
|
help="suppress normal output")
|
|
|
|
return parser.parse_args(sys_argv[1:])
|
|
|
|
|
|
# pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
|
|
def osbuild_cli() -> int:
|
|
args = parse_arguments(sys.argv)
|
|
desc = parse_manifest(args.manifest_path)
|
|
|
|
index = osbuild.meta.Index(args.libdir)
|
|
|
|
# detect the format from the manifest description
|
|
info = index.detect_format_info(desc)
|
|
if not info:
|
|
print("Unsupported manifest format")
|
|
return 2
|
|
fmt = info.module
|
|
|
|
# first thing is validation of the manifest
|
|
res = fmt.validate(desc, index)
|
|
if not res:
|
|
if args.json or args.inspect:
|
|
json.dump(res.as_dict(), sys.stdout)
|
|
sys.stdout.write("\n")
|
|
else:
|
|
show_validation(res, args.manifest_path)
|
|
return 2
|
|
|
|
manifest = fmt.load(desc, index)
|
|
|
|
exports = set(args.export)
|
|
unresolved = [e for e in exports if e not in manifest]
|
|
if unresolved:
|
|
available = list(manifest.pipelines.keys())
|
|
for name in unresolved:
|
|
print(f"Export {vt.bold}{name}{vt.reset} not found in {available}")
|
|
print(f"{vt.reset}{vt.bold}{vt.red}Failed{vt.reset}")
|
|
return 1
|
|
|
|
if args.checkpoint:
|
|
marked = manifest.mark_checkpoints(args.checkpoint)
|
|
if not marked:
|
|
print("No checkpoints matched provided patterns!")
|
|
print(f"{vt.reset}{vt.bold}{vt.red}Failed{vt.reset}")
|
|
return 1
|
|
|
|
if args.inspect:
|
|
result = fmt.describe(manifest, with_id=True)
|
|
json.dump(result, sys.stdout)
|
|
sys.stdout.write("\n")
|
|
return 0
|
|
|
|
output_directory = args.output_directory
|
|
|
|
if exports and not output_directory:
|
|
print("Need --output-directory for --export")
|
|
return 1
|
|
|
|
monitor_name = args.monitor
|
|
if not monitor_name:
|
|
monitor_name = "NullMonitor" if (args.json or args.quiet) else "LogMonitor"
|
|
|
|
try:
|
|
with ObjectStore(args.cache) as object_store:
|
|
if args.cache_max_size is not None:
|
|
object_store.maximum_size = args.cache_max_size
|
|
|
|
stage_timeout = args.stage_timeout
|
|
debug_break = args.debug_break
|
|
|
|
pipelines = manifest.depsolve(object_store, exports)
|
|
total_steps = len(manifest.sources) + len(pipelines)
|
|
monitor = osbuild.monitor.make(monitor_name, args.monitor_fd, total_steps)
|
|
monitor.log(f"starting {args.manifest_path}", origin="osbuild.main_cli")
|
|
|
|
manifest.download(object_store, monitor)
|
|
|
|
r = manifest.build(
|
|
object_store,
|
|
pipelines,
|
|
monitor,
|
|
args.libdir,
|
|
debug_break,
|
|
stage_timeout=stage_timeout
|
|
)
|
|
if r["success"]:
|
|
monitor.log(f"manifest {args.manifest_path} finished successfully\n", origin="osbuild.main_cli")
|
|
else:
|
|
# if we had monitor.error() we could use that here
|
|
monitor.log(f"manifest {args.manifest_path} failed\n", origin="osbuild.main_cli")
|
|
|
|
if r["success"] and exports:
|
|
for pid in exports:
|
|
export(pid, output_directory, object_store, manifest)
|
|
|
|
if args.json:
|
|
r = fmt.output(manifest, r, object_store)
|
|
json.dump(r, sys.stdout)
|
|
sys.stdout.write("\n")
|
|
elif not args.quiet:
|
|
if r["success"]:
|
|
for name, pl in manifest.pipelines.items():
|
|
print(f"{name + ':': <10}\t{pl.id}")
|
|
else:
|
|
print(f"{vt.reset}{vt.bold}{vt.red}Failed{vt.reset}")
|
|
|
|
return 0 if r["success"] else 1
|
|
|
|
except KeyboardInterrupt:
|
|
print()
|
|
print(f"{vt.reset}{vt.bold}{vt.red}Aborted{vt.reset}")
|
|
return 130
|