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.
166 lines
7.6 KiB
Python
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)
|