| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2022 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import asyncio |
| import json |
| import os |
| import pathlib |
| import shlex |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| async def print_qemu_logs(qemu_reader, qemu_writer): |
| while True: |
| line = await qemu_reader.readline() |
| if not line: |
| break |
| |
| print(line.decode('utf-8'), end='') |
| |
| qemu_writer.close() |
| await qemu_writer.wait_closed() |
| |
| |
| async def poll_process(process): |
| try: |
| await asyncio.wait_for(process.wait(), 1) |
| except asyncio.TimeoutError: |
| pass |
| return process.returncode |
| |
| |
| async def main(): |
| parser = argparse.ArgumentParser( |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| description=''' |
| This command creates a Fuchsia image in the specified directory. As part of |
| this process, it starts up the image in QEMU so that we can copy in our |
| ssh authorized keys file. We do this through QEMU because there is no |
| host-side tooling that lets us inject this file into minfs or fxfs. |
| |
| Once complete, this image will be ready to be used with: |
| |
| $ fx qemu --uefi -D path/to/dir/fuchsia.efi |
| |
| This command delegates to /run-zircon. Flags |
| are documented in that script, and can be discovered by passing -h or |
| --help.''') |
| parser.add_argument( |
| '--arch', |
| default='x64', |
| help='emulator architecture (arm64, riscv64, or x64)') |
| parser.add_argument( |
| '--fuchsia-dir', |
| type=pathlib.Path, |
| default=os.getenv('FUCHSIA_DIR'), |
| help='path to the fuchsia directory (default $FUCHSIA_DIR)') |
| parser.add_argument( |
| '--fuchsia-build-dir', |
| type=pathlib.Path, |
| default=os.environ.get('FUCHSIA_BUILD_DIR'), |
| help='path to the fuchsia build directory (default $FUCHSIA_BUILD_DIR)') |
| parser.add_argument( |
| '--edk2-dir', |
| type=pathlib.Path, |
| default=os.environ.get('PREBUILT_EDK2_DIR'), |
| help='path to the fuchsia build directory (default $PREBUILT_EDK2_DIR)') |
| parser.add_argument( |
| '--image-size', |
| type=int, |
| default=20 * 1024 * 1024 * 1024, |
| help='total image size (default 20 GiB)') |
| parser.add_argument( |
| '--abr-size', |
| type=int, |
| help='ABR slot size (default 256 MiB)') |
| parser.add_argument( |
| '--system-disk-size', |
| type=int, |
| help='system disk size (default 63 MiB)') |
| parser.add_argument( |
| '--disk-type', |
| type=str, |
| help='mount the disk with this type in qemu') |
| parser.add_argument( |
| '--product-bundle-name', |
| required=True, |
| help='The name of the product bundle to use to create the image') |
| parser.add_argument( |
| '--filesystem', |
| choices=['fxfs', 'blobfs'], |
| default='fxfs', |
| help='Use fxfs to create the image') |
| parser.add_argument( |
| 'image', |
| type=pathlib.Path, |
| help='create the image into this path') |
| |
| args = parser.parse_args() |
| |
| if args.fuchsia_dir is None or not args.fuchsia_dir.exists(): |
| print( |
| '--fuchsia-dir or FUCHSIA_DIR must exist', |
| file=sys.stderr) |
| return 1 |
| |
| if args.fuchsia_build_dir is None or not args.fuchsia_build_dir.exists(): |
| print( |
| '--fuchsia-build-dir or FUCHSIA_BUILD_DIR must exist', |
| file=sys.stderr) |
| return 1 |
| |
| if os.path.exists(args.image): |
| print( |
| f'image {args.image} already exists, you should remove it, or run it with `fx qemu --uefi -D {args.image}`', |
| file=sys.stderr) |
| |
| return 1 |
| |
| # Check that the product bundle exists. |
| with open(args.fuchsia_build_dir / 'product_bundles.json') as f: |
| product_bundles = json.load(f) |
| |
| for product_bundle in product_bundles: |
| if product_bundle['name'] == args.product_bundle_name: |
| break |
| else: |
| |
| print(f'Unknown product bundle "{args.product_bundle_name}", must be one of:', |
| file=sys.stderr) |
| |
| for product_bundle in product_bundles: |
| print(f' - {product_bundle["name"]}', |
| file=sys.stderr) |
| |
| return 1 |
| |
| print(f'Creating {args.image}') |
| |
| with tempfile.TemporaryDirectory() as tmp: |
| tmp = pathlib.Path(tmp) |
| cmdline_path = tmp / 'cmdline' |
| |
| with open(cmdline_path, 'w') as f: |
| # Ensure that the output is sent to the serial, and that we boot into |
| # zedboot. |
| f.write( |
| 'kernel.serial=legacy\n' |
| 'TERM=xterm-256color\n' |
| 'kernel.halt-on-panic=true\n' |
| 'bootloader.default=default\n' |
| 'bootloader.timeout=5\n') |
| |
| ## Construct a Fuchsia image. |
| make_fuchsia_vol_args = [ |
| args.fuchsia_build_dir / 'host-tools/make-fuchsia-vol', |
| '--fuchsia-build-dir', args.fuchsia_build_dir, |
| '--arch', args.arch, |
| '--cmdline', cmdline_path, |
| '--resize', str(args.image_size), |
| '--product-bundle-name', args.product_bundle_name, |
| args.image.resolve(), |
| ] |
| |
| if args.system_disk_size is not None: |
| make_fuchsia_vol_args.extend(['--system-disk-size', str(args.system_disk_size)]) |
| |
| if args.abr_size is not None: |
| make_fuchsia_vol_args.extend(['--abr-size', str(args.abr_size)]) |
| |
| if args.filesystem == 'fxfs': |
| make_fuchsia_vol_args.append('--use-fxfs') |
| |
| print(f'Running: {shlex.join(str(s) for s in make_fuchsia_vol_args)}') |
| |
| process = await asyncio.create_subprocess_exec(*make_fuchsia_vol_args) |
| return_code = await process.wait() |
| if return_code != 0: |
| print('Failed to create fuchsia image', file=sys.stderr) |
| return 1 |
| |
| print(f'Created {args.image}') |
| |
| # Emulators always have the same mac address, so they always have this |
| # device name. |
| device_name = 'fuchsia-5254-0063-5e7a' |
| |
| qemu_socket_path = tmp / 'sock' |
| |
| print('Launching QEMU to install the SSH authorized keys. ') |
| |
| run_zircon_args = [ |
| args.fuchsia_dir / 'zircon/scripts/run-zircon', |
| '-a', args.arch, |
| '-N', |
| '--uefi', |
| '-S', f'unix:{qemu_socket_path.resolve()},server,nowait', |
| '-M', 'null', |
| '-D', args.image.resolve(), |
| '-q', args.fuchsia_dir.resolve() / f'prebuilt/third_party/qemu/linux-x64/bin', |
| ] |
| |
| if args.disk_type: |
| run_zircon_args.append(f'--disktype={args.disk_type}') |
| |
| print(f'Running: {shlex.join(str(s) for s in run_zircon_args)}') |
| |
| qemu_process = await asyncio.create_subprocess_exec( |
| *run_zircon_args, |
| stdin=subprocess.DEVNULL, |
| close_fds=True, |
| ) |
| |
| try: |
| print('Waiting to connect to qemu serial...') |
| |
| for i in range(1000): |
| print(f'check {qemu_socket_path}: {qemu_socket_path.exists()}') |
| if await poll_process(qemu_process) is not None: |
| print('qemu exited early', file=sys.stderr) |
| return 1 |
| |
| if qemu_socket_path.exists(): |
| break |
| else: |
| await asyncio.sleep(0.05) |
| else: |
| print('Failed to connect to serial port', file=sys.stderr) |
| return 1 |
| |
| print('Connecting to socket...') |
| |
| qemu_reader, qemu_writer = await asyncio.open_unix_connection(str(qemu_socket_path)) |
| |
| print('Connected to socket') |
| |
| # Enter fastboot. |
| while True: |
| line = await qemu_reader.readline() |
| if not line: |
| break |
| |
| print(line.decode('utf-8'), end='') |
| |
| if line == b'Press f to enter fastboot.\r\n': |
| print('Entering fastboot...') |
| qemu_writer.write(b'f\r\n') |
| await qemu_writer.drain() |
| break |
| |
| log_task = asyncio.create_task(print_qemu_logs(qemu_reader, qemu_writer)) |
| |
| print('Flashing device...') |
| |
| # Flash the device. |
| ffx_path = args.fuchsia_build_dir / 'host-tools/ffx' |
| flash_args = [ |
| ffx_path, |
| '--target', device_name, |
| '--timeout', '60', |
| 'target', 'flash', |
| '--manifest', args.fuchsia_build_dir / 'flash.json', |
| ] |
| print(f'Running: {shlex.join(str(s) for s in flash_args)}') |
| |
| # Sleep a little bit for ffx to notice the device is in fastboot |
| await asyncio.sleep(5) |
| |
| process = await asyncio.create_subprocess_exec(*flash_args) |
| return_code = await process.wait() |
| if return_code != 0: |
| print('Failed to flash target', file=sys.stderr) |
| return return_code |
| |
| print('Flashed device.') |
| print('Connecting to device...') |
| |
| show_args = [ffx_path, '--target', device_name, '--timeout', '60', 'target', 'wait'] |
| print(f'Running: {shlex.join(str(s) for s in show_args)}') |
| |
| process = await asyncio.create_subprocess_exec(*show_args) |
| return_code = await process.wait() |
| if return_code != 0: |
| print('Target failed to come online', file=sys.stderr) |
| return return_code |
| |
| print('Connected to device.') |
| |
| print('Signalling qemu to shut down...') |
| |
| shutdown_args = [ffx_path, '--target', device_name, 'target', 'off'] |
| print(f'Running: {shlex.join(str(s) for s in shutdown_args)}') |
| |
| process = await asyncio.create_subprocess_exec(*shutdown_args) |
| return_code = await process.wait() |
| if return_code != 0: |
| print('Target failed to turn off', file=sys.stderr) |
| return return_code |
| |
| print('Waiting for qemu to shutdown...') |
| |
| for i in range(100): |
| if await poll_process(qemu_process) is not None: |
| break |
| else: |
| await asyncio.sleep(0.5) |
| else: |
| print('Timed out waiting for qemu to shut down', file=sys.stderr) |
| |
| print(f'Successfully created Fuchsia image {args.image}') |
| finally: |
| if await poll_process(qemu_process) is None: |
| print('Killing qemu...', file=sys.stderr) |
| qemu_process.kill() |
| await qemu_process.wait() |
| |
| if __name__ == '__main__': |
| sys.exit(asyncio.run(main())) |