241 lines
6.9 KiB
Python
241 lines
6.9 KiB
Python
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from src import (
|
|
builder,
|
|
container,
|
|
log,
|
|
plan,
|
|
profile as profile_mod,
|
|
recipe as recipe_mod,
|
|
)
|
|
from src.layout import Layout, find_repo_root
|
|
|
|
|
|
def _resolve_build_dir(p: str | None) -> Path:
|
|
if p:
|
|
return Path(p).resolve()
|
|
env = os.environ.get("ORCHID_BUILD")
|
|
if env:
|
|
return Path(env).resolve()
|
|
cwd = Path.cwd()
|
|
if (cwd / "profile").is_symlink() or (cwd / "profile").exists():
|
|
return cwd
|
|
raise SystemExit("error: -C <build-dir> required (or run inside a build dir)")
|
|
|
|
|
|
def _layout(build_dir: Path) -> Layout:
|
|
repo = find_repo_root(build_dir)
|
|
return Layout(repo=repo, build=build_dir)
|
|
|
|
|
|
def _load(layout: Layout):
|
|
prof = profile_mod.load_profile(layout)
|
|
rs = recipe_mod.load_recipes(layout, prof)
|
|
return prof, rs
|
|
|
|
|
|
def cmd_init(args) -> int:
|
|
target = Path(args.build_dir).resolve()
|
|
repo = find_repo_root(Path.cwd())
|
|
profile_mod.init_build_dir(target, repo, args.profile)
|
|
log.ok(f"initialized {target} (profile: {args.profile})")
|
|
return 0
|
|
|
|
|
|
def cmd_image(args) -> int:
|
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
|
layout.ensure()
|
|
prof = profile_mod.load_profile(layout)
|
|
container.ensure_image(
|
|
layout.dockerfile, prof["container_image"], layout.image_hash_file
|
|
)
|
|
return 0
|
|
|
|
|
|
def _recipe_version(r) -> str:
|
|
return f"{r.version}-r{r.revision}"
|
|
|
|
|
|
def _print_plan(p: plan.Plan) -> None:
|
|
out = sys.stdout
|
|
if not p.order:
|
|
print(
|
|
f"{log.tag('info', stream=out)} "
|
|
f"{log.field('plan', 'nothing to do', stream=out)}"
|
|
)
|
|
return
|
|
|
|
count = len(p.order)
|
|
suffix = "" if count == 1 else "s"
|
|
print(
|
|
f"{log.tag('info', stream=out)} "
|
|
f"{log.field('plan', f'{count} recipe{suffix}', stream=out)}"
|
|
)
|
|
|
|
rows: list[tuple[str, str, str, str]] = []
|
|
for i, k in enumerate(p.order, start=1):
|
|
r = p.recipes[k]
|
|
rows.append((str(i), k, _recipe_version(r), ", ".join(p.stages.get(k, ()))))
|
|
|
|
widths = [
|
|
max(len(row[0]) for row in rows),
|
|
max(len("recipe"), *(len(row[1]) for row in rows)),
|
|
max(len("version"), *(len(row[2]) for row in rows)),
|
|
]
|
|
header = (
|
|
f" {'#':>{widths[0]}} "
|
|
f"{'recipe':<{widths[1]}} "
|
|
f"{'version':<{widths[2]}} "
|
|
"stages"
|
|
)
|
|
print(log.bold(header, stream=out))
|
|
for num, name, version, stages in rows:
|
|
print(
|
|
f" {num:>{widths[0]}} "
|
|
f"{name:<{widths[1]}} "
|
|
f"{version:<{widths[2]}} "
|
|
f"{stages}"
|
|
)
|
|
|
|
|
|
def cmd_plan(args) -> int:
|
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
|
layout.ensure()
|
|
prof, rs = _load(layout)
|
|
p = plan.build_plan(rs, layout, args.recipes or None, rebuild=args.rebuild)
|
|
_print_plan(p)
|
|
return 0
|
|
|
|
|
|
def cmd_build(args) -> int:
|
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
|
layout.ensure()
|
|
prof, rs = _load(layout)
|
|
container.ensure_image(
|
|
layout.dockerfile, prof["container_image"], layout.image_hash_file
|
|
)
|
|
p = plan.build_plan(rs, layout, args.recipes or None, rebuild=args.rebuild)
|
|
if args.dry_run:
|
|
return cmd_plan(args)
|
|
builder.execute(p, rs, layout, prof)
|
|
return 0
|
|
|
|
|
|
def cmd_install(args) -> int:
|
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
|
layout.ensure()
|
|
prof, rs = _load(layout)
|
|
container.ensure_image(
|
|
layout.dockerfile, prof["container_image"], layout.image_hash_file
|
|
)
|
|
dest = Path(args.dest).resolve()
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
|
|
if args.recipes:
|
|
pkgs: list[str] = []
|
|
for k in args.recipes:
|
|
r = rs.get(k)
|
|
if r.kind != "target":
|
|
raise SystemExit(f"error: cannot install host recipe {k!r}")
|
|
pkgs.extend(r.outputs)
|
|
else:
|
|
pkgs = [r.name for r in rs.target.values() if r.enabled]
|
|
|
|
builder.install_to(layout, prof, dest, pkgs, initdb=args.initdb)
|
|
log.ok(f"installed {len(pkgs)} package(s) to {dest}")
|
|
return 0
|
|
|
|
|
|
def cmd_fetch(args) -> int:
|
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
|
layout.ensure()
|
|
prof, rs = _load(layout)
|
|
from . import fetch as fetch_mod
|
|
|
|
targets = args.recipes or [r.key for r in rs.all() if r.enabled]
|
|
for k in targets:
|
|
r = rs.get(k)
|
|
for key, src in r.sources.items():
|
|
fetch_mod.fetch(layout, src)
|
|
return 0
|
|
|
|
|
|
def _common(p: argparse.ArgumentParser, with_recipes: bool = True) -> None:
|
|
p.add_argument(
|
|
"-C", "--build-dir", help="build directory (defaults to $ORCHID_BUILD or cwd)"
|
|
)
|
|
if with_recipes:
|
|
p.add_argument(
|
|
"recipes", nargs="*", help="recipe keys (target name or host:<name>)"
|
|
)
|
|
|
|
|
|
def make_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="orchid", description="Orchid distribution builder"
|
|
)
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
p_init = sub.add_parser("init", help="create a build directory")
|
|
p_init.add_argument("build_dir")
|
|
p_init.add_argument("--profile", required=True)
|
|
p_init.set_defaults(func=cmd_init)
|
|
|
|
p_img = sub.add_parser("image", help="build/refresh the container image")
|
|
_common(p_img, with_recipes=False)
|
|
p_img.set_defaults(func=cmd_image)
|
|
|
|
p_plan = sub.add_parser("plan", help="print build plan")
|
|
_common(p_plan)
|
|
p_plan.add_argument("--rebuild", action="store_true")
|
|
p_plan.set_defaults(func=cmd_plan)
|
|
|
|
p_build = sub.add_parser("build", help="build recipes")
|
|
_common(p_build)
|
|
p_build.add_argument("--rebuild", action="store_true")
|
|
p_build.add_argument("-n", "--dry-run", action="store_true")
|
|
p_build.set_defaults(func=cmd_build)
|
|
|
|
p_inst = sub.add_parser(
|
|
"install", help="install built packages into a sysroot directory"
|
|
)
|
|
p_inst.add_argument(
|
|
"-C", "--build-dir", help="build directory (defaults to $ORCHID_BUILD or cwd)"
|
|
)
|
|
p_inst.add_argument("dest", help="destination sysroot directory")
|
|
p_inst.add_argument(
|
|
"recipes",
|
|
nargs="*",
|
|
help="target recipes to install (defaults to all enabled targets)",
|
|
)
|
|
p_inst.add_argument(
|
|
"--no-initdb",
|
|
dest="initdb",
|
|
action="store_false",
|
|
help="do not initialize the apk database (use for incremental installs)",
|
|
)
|
|
p_inst.set_defaults(func=cmd_install, initdb=True)
|
|
|
|
p_fetch = sub.add_parser("fetch", help="fetch sources only")
|
|
_common(p_fetch)
|
|
p_fetch.set_defaults(func=cmd_fetch)
|
|
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = make_parser()
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
return args.func(args)
|
|
except (SystemExit, KeyboardInterrupt):
|
|
raise
|
|
except Exception as e:
|
|
log.error(f"{type(e).__name__}: {e}")
|
|
if os.environ.get("ORCHID_DEBUG"):
|
|
raise
|
|
return 1
|