Files
UniXNU/scripts/unixnu/cli.py
T
bluseliu50 647db5f144 feat(scripts): add the unixnu build driver
A Python 3 driver with defconfig, fetch, tree, build, bootimg, status
and clean. Device repositories supply board.toml and configs/*_defconfig;
external sources come from feeds.toml and are fetched with shallow,
blobless, sparse git checkouts pinned in unixnu.lock. Modules can come
from local directories through --local, --local-<kind> and
--local-module. The boot image step packs BootShim, the firmware volume
and the device tree for a Mu-Silicium checkout given with --uefi.
2026-10-19 19:46:45 +08:00

166 lines
7.6 KiB
Python

"""Command line of the UniXNU build driver."""
import argparse
import platform
import shutil
import sys
from . import bootimg, feeds, firmware, kernel, package, tree
from .config import LOCAL_KINDS, Context, parse_config
from .util import Error, expand, git, log
STEPS = ('kernel', 'package', 'firmware')
def common():
p = argparse.ArgumentParser(add_help=False)
p.add_argument('-D', '--device', metavar='DIR', help='device repository (remembered; or UNIXNU_DEVICE)')
p.add_argument('--out', metavar='DIR', help='output root (default: out/ here, or UNIXNU_OUT)')
p.add_argument('-j', '--jobs', type=int, help='parallel jobs')
p.add_argument('--local', metavar='DIR', help='directory laid out like this repository; its drivers, kits, patches, scripts and package are used')
for kind in LOCAL_KINDS:
p.add_argument(f'--local-{kind}', metavar='DIR', help=f'use DIR as the {kind} directory (beats --local)')
p.add_argument('--local-module', action='append', metavar='[KIND:]NAME=PATH', help='use PATH for one module (beats everything)')
p.add_argument('--uefi', metavar='DIR', help='Mu-Silicium checkout for the device (uefi-sheng); or UNIXNU_UEFI')
p.add_argument('--edk2', metavar='DIR', help='Mu_Basecore checkout; or UNIXNU_EDK2')
p.add_argument('--kernelcache', metavar='FILE', help='original decompressed kernelcache; or UNIXNU_KERNELCACHE')
p.add_argument('--target', default='DEBUG', choices=('DEBUG', 'RELEASE'), help='EDK II build target')
return p
def parser():
top = argparse.ArgumentParser(prog='unixnu', description='Build UniXNU, the XNU kernel and the boot image for a device repository.')
sub = top.add_subparsers(dest='command', required=True)
c = common()
d = sub.add_parser('defconfig', parents=[c], help='select configs/NAME in the device repository')
d.add_argument('name', nargs='?', help='defconfig name; omit to list')
f = sub.add_parser('fetch', parents=[c], help='shallow-fetch git sources and write unixnu.lock')
f.add_argument('names', nargs='*', help='sources to fetch (default: all)')
f.add_argument('--update', action='store_true', help='ignore the lock and move to the newest ref')
sub.add_parser('tree', parents=[c], help='assemble the build tree')
b = sub.add_parser('build', parents=[c], help='build kernel, package and firmware')
b.add_argument('steps', nargs='*', choices=STEPS + ('all', 'prelink'), default=['all'])
i = sub.add_parser('bootimg', parents=[c], help='pack the boot image from the firmware volume and a device tree')
i.add_argument('--fd', metavar='FILE', help='firmware volume (default: the build output under --uefi)')
i.add_argument('--dtb', metavar='FILE', help='compiled device tree; or UNIXNU_DTB')
i.add_argument('--ramdisk', metavar='FILE', help='ramdisk / payload image (default: empty)')
i.add_argument('-o', '--output', metavar='FILE', help='output (default: out/<device>/boot/boot.img)')
sub.add_parser('status', parents=[c], help='show the device, configuration and where each module comes from')
sub.add_parser('clean', parents=[c], help='remove the build output of the device (keeps fetched sources)')
return top
def need_uefi(ctx):
uefi = ctx.option('uefi', 'UNIXNU_UEFI')
if not uefi:
raise Error('the firmware needs a Mu-Silicium checkout: --uefi DIR (or UNIXNU_UEFI)')
if not (uefi / 'Resources/Scripts/mkbootimg.py').is_file():
raise Error(f'{uefi} is not a Mu-Silicium checkout')
return uefi
REFERENCES = {'kernel': ('xnu', 'libplatform', 'libdispatch', 'AvailabilityVersions'), 'package': ('edk2',), 'firmware': ('edk2',)}
def prepare(ctx, steps=()):
"""Resolve modules, fetch the git ones (references only for the steps that need them) and assemble the tree."""
modules = feeds.resolve(ctx)
wanted = {name for step in steps for name in REFERENCES.get(step, ())}
feeds.fetch(ctx, modules, only={m.name for m in modules.values() if m.origin == 'git' and (m.kind != 'refer' or m.name in wanted)})
tree.assemble(ctx, modules)
return modules
def cmd_defconfig(ctx):
configs = ctx.device / 'configs'
names = sorted(p.name for p in configs.glob('*_defconfig')) if configs.is_dir() else []
if not ctx.args.name:
print('\n'.join(names) or 'no defconfigs')
return
source = configs / ctx.args.name
if not source.is_file():
raise Error(f'{source} not found; available: {", ".join(names) or "none"}')
parse_config(source.read_text())
ctx.work.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, ctx.config_path)
log(f'configuration {ctx.args.name} -> {ctx.config_path}')
def cmd_fetch(ctx):
modules = feeds.resolve(ctx)
commits = feeds.fetch(ctx, modules, update=ctx.args.update, only=set(ctx.args.names) or None)
if commits:
feeds.lock_write(ctx, {**feeds.lock_read(ctx), **commits})
log(f'wrote {ctx.lock_path}')
def cmd_build(ctx):
steps = ctx.args.steps
if 'all' in steps:
steps = list(STEPS)
ctx.need_config()
if 'firmware' in steps:
need_uefi(ctx) # fail before fetching anything
modules = prepare(ctx, steps + (['kernel'] if 'prelink' in steps else []))
if 'kernel' in steps:
kernel.build(ctx)
kernel.prelink(ctx)
elif 'prelink' in steps:
kernel.prelink(ctx)
if 'package' in steps:
package.build(ctx, modules, ctx.args.target)
if 'firmware' in steps:
firmware.build(ctx, modules, need_uefi(ctx), package.edk2_dir(ctx, modules), ctx.args.target)
def cmd_bootimg(ctx):
uefi = need_uefi(ctx)
fd = expand(ctx.args.fd) if ctx.args.fd else firmware.fd_path(ctx, uefi, ctx.args.target)
dtb = ctx.option('dtb', 'UNIXNU_DTB')
ramdisk = expand(ctx.args.ramdisk) if ctx.args.ramdisk else None
bootimg.build(ctx, uefi, fd, dtb, ramdisk, expand(ctx.args.output) if ctx.args.output else None)
def cmd_status(ctx):
print(f'device {ctx.device} ({ctx.name})')
print(f'output {ctx.work}')
print(f'config {ctx.config_path if ctx.config_path.is_file() else "none (run defconfig)"}')
modules = feeds.resolve(ctx)
locked = feeds.lock_read(ctx)
unpinned = []
for (kind, name), module in sorted(modules.items()):
extra = ''
if module.origin == 'git':
pinned = locked.get(name) or module.source.get('commit')
extra = f' pinned {pinned[:12]}' if pinned else ' NOT LOCKED'
elif module.local:
try:
extra = ' (dirty)' if git(module.path, 'status', '--porcelain', '--', '.').strip() else ' (clean)'
except Error:
extra = ' (not a git work tree)'
unpinned.append(f'{kind}:{name}')
print(f'{kind:8} {name:24} {module.origin}{extra}')
if unpinned:
print(f'\nnot reproducible: {len(unpinned)} local source(s) are not pinned by unixnu.lock')
def cmd_clean(ctx):
for name in ('tree', 'logs', 'kernel', 'package', 'boot', 'edk2-conf'):
shutil.rmtree(ctx.work / name, ignore_errors=True)
log(f'cleaned {ctx.work}')
def main(argv=None):
if sys.version_info < (3, 11):
sys.exit('unixnu needs Python 3.11 or newer')
if platform.system() != 'Darwin':
sys.exit('unixnu supports macOS hosts only')
args = parser().parse_args(argv)
try:
ctx = Context(args)
{'defconfig': cmd_defconfig, 'fetch': cmd_fetch, 'tree': lambda c: prepare(c), 'build': cmd_build,
'bootimg': cmd_bootimg, 'status': cmd_status, 'clean': cmd_clean}[args.command](ctx)
except Error as error:
sys.exit(f'unixnu: {error}')
except KeyboardInterrupt:
sys.exit(130)