498 lines
12 KiB
Python
498 lines
12 KiB
Python
"""WizardKit: UFD Functions"""
|
|
# vim: sts=2 sw=2 ts=2
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
|
|
from collections import OrderedDict
|
|
from docopt import docopt
|
|
|
|
from wk import io, log, std
|
|
from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT
|
|
from wk.cfg.ufd import BOOT_ENTRIES, BOOT_FILES, ITEMS, ITEMS_HIDDEN, SOURCES
|
|
from wk.exe import run_program
|
|
from wk.os import linux
|
|
|
|
|
|
# STATIC VARIABLES
|
|
DOCSTRING = '''WizardKit: Build UFD
|
|
|
|
Usage:
|
|
build-ufd [options] --ufd-device PATH --linux PATH
|
|
[--linux-minimal PATH]
|
|
[--main-kit PATH]
|
|
[--winpe PATH]
|
|
[--extra-dir PATH]
|
|
build-ufd (-h | --help)
|
|
|
|
Options:
|
|
-e PATH, --extra-dir PATH
|
|
-k PATH, --main-kit PATH
|
|
-l PATH, --linux PATH
|
|
-m PATH, --linux-minimal PATH
|
|
-u PATH, --ufd-device PATH
|
|
-w PATH, --winpe PATH
|
|
|
|
-h --help Show this page
|
|
-M --use-mbr Use real MBR instead of GPT w/ Protective MBR
|
|
-F --force Bypass all confirmation messages. USE WITH EXTREME CAUTION!
|
|
-U --update Don't format device, just update
|
|
'''
|
|
LOG = logging.getLogger(__name__)
|
|
ISO_LABEL = f'{KIT_NAME_SHORT}_LINUX'
|
|
UFD_LABEL = f'{KIT_NAME_SHORT}_UFD'
|
|
|
|
|
|
# Functions
|
|
def build_ufd():
|
|
"""Build UFD using selected sources."""
|
|
args = docopt(DOCSTRING)
|
|
log.update_log_path(dest_name='build-ufd', timestamp=True)
|
|
try_print = std.TryAndPrint()
|
|
try_print.add_error(FileNotFoundError)
|
|
try_print.catch_all = False
|
|
try_print.verbose = True
|
|
try_print.indent = 2
|
|
|
|
# Show header
|
|
std.print_success(KIT_NAME_FULL)
|
|
std.print_warning('UFD Build Tool')
|
|
std.print_warning(' ')
|
|
|
|
# Verify selections
|
|
ufd_dev = verify_ufd(args['--ufd-device'])
|
|
sources = verify_sources(args, SOURCES)
|
|
show_selections(args, sources, ufd_dev, SOURCES)
|
|
if not args['--force']:
|
|
confirm_selections(update=args['--update'])
|
|
|
|
# Prep UFD
|
|
if not args['--update']:
|
|
std.print_info('Prep UFD')
|
|
try_print.run(
|
|
message='Zeroing first 64MiB...',
|
|
function=zero_device,
|
|
dev_path=ufd_dev,
|
|
)
|
|
try_print.run(
|
|
message='Creating partition table...',
|
|
function=create_table,
|
|
dev_path=ufd_dev,
|
|
use_mbr=args['--use-mbr'],
|
|
)
|
|
try_print.run(
|
|
message='Setting boot flag...',
|
|
function=set_boot_flag,
|
|
dev_path=ufd_dev,
|
|
use_mbr=args['--use-mbr'],
|
|
)
|
|
try_print.run(
|
|
message='Formatting partition...',
|
|
function=format_partition,
|
|
dev_path=ufd_dev,
|
|
label=UFD_LABEL,
|
|
)
|
|
|
|
# Mount UFD
|
|
try_print.run(
|
|
message='Mounting UFD...',
|
|
function=linux.mount,
|
|
source=find_first_partition(ufd_dev),
|
|
mount_point='/mnt/UFD',
|
|
read_write=True,
|
|
)
|
|
|
|
# Remove Arch folder
|
|
if args['--update']:
|
|
try_print.run(
|
|
message='Removing Linux...',
|
|
function=remove_arch,
|
|
)
|
|
|
|
# Copy sources
|
|
std.print_standard(' ')
|
|
std.print_info('Copy Sources')
|
|
for s_label, s_path in sources.items():
|
|
try_print.run(
|
|
message='Copying {}...'.format(s_label),
|
|
function=copy_source,
|
|
source=s_path,
|
|
items=ITEMS[s_label],
|
|
overwrite=True,
|
|
)
|
|
|
|
# Update boot entries
|
|
std.print_standard(' ')
|
|
std.print_info('Boot Setup')
|
|
try_print.run(
|
|
message='Updating boot entries...',
|
|
function=update_boot_entries,
|
|
)
|
|
|
|
# Install syslinux (to partition)
|
|
try_print.run(
|
|
message='Syslinux (partition)...',
|
|
function=install_syslinux_to_partition,
|
|
partition=find_first_partition(ufd_dev),
|
|
)
|
|
|
|
# Unmount UFD
|
|
try_print.run(
|
|
message='Unmounting UFD...',
|
|
function=linux.unmount,
|
|
source_or_mountpoint='/mnt/UFD',
|
|
)
|
|
|
|
# Install syslinux (to device)
|
|
try_print.run(
|
|
message='Syslinux (device)...',
|
|
function=install_syslinux_to_dev,
|
|
ufd_dev=ufd_dev,
|
|
use_mbr=args['--use-mbr'],
|
|
)
|
|
|
|
# Hide items
|
|
std.print_standard(' ')
|
|
std.print_info('Final Touches')
|
|
try_print.run(
|
|
message='Hiding items...',
|
|
function=hide_items,
|
|
ufd_dev=ufd_dev,
|
|
items=ITEMS_HIDDEN,
|
|
)
|
|
|
|
# Done
|
|
std.print_standard('\nDone.')
|
|
if not args['--force']:
|
|
std.pause('Press Enter to exit...')
|
|
|
|
|
|
def confirm_selections(update=False):
|
|
"""Ask tech to confirm selections, twice if necessary."""
|
|
if not std.ask('Is the above information correct?'):
|
|
std.abort()
|
|
|
|
# Safety check
|
|
if not update:
|
|
std.print_standard(' ')
|
|
std.print_warning('SAFETY CHECK')
|
|
std.print_standard(
|
|
'All data will be DELETED from the disk and partition(s) listed above.')
|
|
std.print_colored(
|
|
['This is irreversible and will lead to', 'DATA LOSS'],
|
|
[None, 'RED'],
|
|
)
|
|
if not std.ask('Asking again to confirm, is this correct?'):
|
|
std.abort()
|
|
|
|
std.print_standard(' ')
|
|
|
|
|
|
def copy_source(source, items, overwrite=False):
|
|
"""Copy source items to /mnt/UFD."""
|
|
is_image = source.is_file()
|
|
items_not_found = False
|
|
|
|
# Mount source if necessary
|
|
if is_image:
|
|
linux.mount(source, '/mnt/Source')
|
|
|
|
# Copy items
|
|
for i_source, i_dest in items:
|
|
i_source = f'{"/mnt/Source" if is_image else source}{i_source}'
|
|
i_dest = f'/mnt/UFD{i_dest}'
|
|
try:
|
|
io.recursive_copy(i_source, i_dest, overwrite=overwrite)
|
|
except FileNotFoundError:
|
|
items_not_found = True
|
|
|
|
# Unmount source if necessary
|
|
if is_image:
|
|
linux.unmount('/mnt/Source')
|
|
|
|
# Raise exception if item(s) were not found
|
|
if items_not_found:
|
|
raise FileNotFoundError('One or more items not found')
|
|
|
|
|
|
def create_table(dev_path, use_mbr=False):
|
|
"""Create GPT or DOS partition table."""
|
|
cmd = [
|
|
'sudo',
|
|
'parted', dev_path,
|
|
'--script',
|
|
'--',
|
|
'mklabel', 'msdos' if use_mbr else 'gpt',
|
|
'mkpart', 'primary', 'fat32', '4MiB',
|
|
'-1s' if use_mbr else '-4MiB',
|
|
]
|
|
run_program(cmd)
|
|
|
|
|
|
def find_first_partition(dev_path):
|
|
"""Find path to first partition of dev, returns str.
|
|
|
|
NOTE: This assumes the dev was just partitioned with
|
|
a single partition.
|
|
"""
|
|
cmd = [
|
|
'lsblk',
|
|
'--list',
|
|
'--noheadings',
|
|
'--output', 'name',
|
|
'--paths',
|
|
dev_path,
|
|
]
|
|
|
|
# Run cmd
|
|
proc = run_program(cmd)
|
|
part_path = proc.stdout.splitlines()[-1].strip()
|
|
|
|
# Done
|
|
return part_path
|
|
|
|
|
|
def format_partition(dev_path, label):
|
|
"""Format first partition on device FAT32."""
|
|
cmd = [
|
|
'sudo',
|
|
'mkfs.vfat',
|
|
'-F', '32',
|
|
'-n', label,
|
|
find_first_partition(dev_path),
|
|
]
|
|
run_program(cmd)
|
|
|
|
|
|
def get_uuid(path):
|
|
"""Get filesystem UUID via findmnt, returns str."""
|
|
cmd = [
|
|
'findmnt',
|
|
'--noheadings',
|
|
'--target', path,
|
|
'--output', 'uuid'
|
|
]
|
|
|
|
# Run findmnt
|
|
proc = run_program(cmd, check=False)
|
|
|
|
# Done
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def hide_items(ufd_dev, items):
|
|
"""Set FAT32 hidden flag for items."""
|
|
first_partition = find_first_partition(ufd_dev)
|
|
with open('/root/.mtoolsrc', 'w') as _f:
|
|
_f.write(f'drive U: file="{first_partition}"\n')
|
|
_f.write('mtools_skip_check=1\n')
|
|
|
|
# Hide items
|
|
for item in items:
|
|
cmd = [f'yes | sudo mattrib +h "U:/{item}"']
|
|
run_program(cmd, shell=True)
|
|
|
|
|
|
def install_syslinux_to_dev(ufd_dev, use_mbr):
|
|
"""Install Syslinux to UFD (dev)."""
|
|
cmd = [
|
|
'sudo',
|
|
'dd',
|
|
'bs=440',
|
|
'count=1',
|
|
f'if=/usr/lib/syslinux/bios/{"mbr" if use_mbr else "gptmbr"}.bin',
|
|
f'of={ufd_dev}',
|
|
]
|
|
run_program(cmd)
|
|
|
|
|
|
def install_syslinux_to_partition(partition):
|
|
"""Install Syslinux to UFD (partition)."""
|
|
cmd = [
|
|
'sudo',
|
|
'syslinux',
|
|
'--install',
|
|
'--directory',
|
|
'/arch/boot/syslinux/',
|
|
partition,
|
|
]
|
|
run_program(cmd)
|
|
|
|
|
|
def is_valid_path(path_obj, path_type):
|
|
"""Verify path_obj is valid by type, returns bool."""
|
|
valid_path = False
|
|
if path_type == 'DIR':
|
|
valid_path = path_obj.is_dir()
|
|
elif path_type == 'KIT':
|
|
valid_path = path_obj.is_dir() and path_obj.joinpath('.bin').exists()
|
|
elif path_type == 'IMG':
|
|
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img'
|
|
elif path_type == 'ISO':
|
|
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.iso'
|
|
elif path_type == 'UFD':
|
|
valid_path = path_obj.is_block_device()
|
|
|
|
return valid_path
|
|
|
|
|
|
def set_boot_flag(dev_path, use_mbr=False):
|
|
"""Set modern or legacy boot flag."""
|
|
cmd = [
|
|
'sudo',
|
|
'parted', dev_path,
|
|
'set', '1',
|
|
'boot' if use_mbr else 'legacy_boot',
|
|
'on',
|
|
]
|
|
run_program(cmd)
|
|
|
|
|
|
def remove_arch():
|
|
"""Remove arch dir from UFD.
|
|
|
|
This ensures a clean installation to the UFD and resets the boot files
|
|
"""
|
|
shutil.rmtree(io.case_insensitive_path('/mnt/UFD/arch'))
|
|
|
|
|
|
def show_selections(args, sources, ufd_dev, ufd_sources):
|
|
"""Show selections including non-specified options."""
|
|
|
|
# Sources
|
|
std.print_info('Sources')
|
|
for label in ufd_sources.keys():
|
|
if label in sources:
|
|
std.print_standard(f' {label+":":<18} {sources[label]}')
|
|
else:
|
|
std.print_colored(
|
|
[f' {label+":":<18}', 'Not Specified'],
|
|
[None, 'YELLOW'],
|
|
)
|
|
std.print_standard(' ')
|
|
|
|
# Destination
|
|
std.print_info('Destination')
|
|
cmd = [
|
|
'lsblk', '--nodeps', '--noheadings', '--paths',
|
|
'--output', 'NAME,FSTYPE,TRAN,SIZE,VENDOR,MODEL,SERIAL',
|
|
ufd_dev,
|
|
]
|
|
proc = run_program(cmd, check=False)
|
|
std.print_standard(proc.stdout.strip())
|
|
cmd = [
|
|
'lsblk', '--noheadings', '--paths',
|
|
'--output', 'NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT',
|
|
ufd_dev,
|
|
]
|
|
proc = run_program(cmd, check=False)
|
|
for line in proc.stdout.splitlines()[1:]:
|
|
std.print_standard(line)
|
|
|
|
# Notes
|
|
if args['--update']:
|
|
std.print_warning('Updating kit in-place')
|
|
elif args['--use-mbr']:
|
|
std.print_warning('Formatting using legacy MBR')
|
|
std.print_standard(' ')
|
|
|
|
|
|
def update_boot_entries():
|
|
"""Update boot files for UFD usage"""
|
|
configs = []
|
|
uuid = get_uuid('/mnt/UFD')
|
|
|
|
# Find config files
|
|
for c_path, c_ext in BOOT_FILES.items():
|
|
try:
|
|
c_path = io.case_insensitive_path(f'/mnt/UFD{c_path}')
|
|
except FileNotFoundError:
|
|
# Ignore and continue to next file
|
|
continue
|
|
for item in os.scandir(c_path):
|
|
if item.name.lower().endswith(c_ext.lower()):
|
|
configs.append(item.path)
|
|
|
|
# Use UUID instead of label
|
|
cmd = [
|
|
'sudo',
|
|
'sed',
|
|
'--in-place',
|
|
'--regexp-extended',
|
|
f's#archisolabel={ISO_LABEL}#archisodevice=/dev/disk/by-uuid/{uuid}#',
|
|
*configs,
|
|
]
|
|
run_program(cmd)
|
|
|
|
# Uncomment extra entries if present
|
|
for b_path, b_comment in BOOT_ENTRIES.items():
|
|
try:
|
|
io.case_insensitive_path(f'/mnt/UFD{b_path}')
|
|
except (FileNotFoundError, NotADirectoryError):
|
|
# Entry not found, continue to next entry
|
|
continue
|
|
|
|
# Entry found, update config files
|
|
cmd = [
|
|
'sudo',
|
|
'sed',
|
|
'--in-place',
|
|
f's/#{b_comment}#//',
|
|
*configs,
|
|
]
|
|
run_program(cmd, check=False)
|
|
|
|
|
|
def verify_sources(args, ufd_sources):
|
|
"""Check all sources and abort if necessary, returns dict."""
|
|
sources = OrderedDict()
|
|
|
|
for label, data in ufd_sources.items():
|
|
s_path = args[data['Arg']]
|
|
if s_path:
|
|
try:
|
|
s_path_obj = io.case_insensitive_path(s_path)
|
|
except FileNotFoundError:
|
|
std.print_error(f'ERROR: {label} not found: {s_path}')
|
|
std.abort()
|
|
if not is_valid_path(s_path_obj, data['Type']):
|
|
std.print_error(f'ERROR: Invalid {label} source: {s_path}')
|
|
std.abort()
|
|
sources[label] = s_path_obj
|
|
|
|
return sources
|
|
|
|
|
|
def verify_ufd(dev_path):
|
|
"""Check that dev_path is a valid UFD, returns pathlib.Path obj."""
|
|
ufd_dev = None
|
|
|
|
try:
|
|
ufd_dev = io.case_insensitive_path(dev_path)
|
|
except FileNotFoundError:
|
|
std.print_error(f'ERROR: UFD device not found: {dev_path}')
|
|
std.abort()
|
|
|
|
if not is_valid_path(ufd_dev, 'UFD'):
|
|
std.print_error(f'ERROR: Invalid UFD device: {ufd_dev}')
|
|
std.abort()
|
|
|
|
return ufd_dev
|
|
|
|
|
|
def zero_device(dev_path):
|
|
"""Zero-out first 64MB of device."""
|
|
cmd = [
|
|
'sudo',
|
|
'dd',
|
|
'bs=4M',
|
|
'count=16',
|
|
'if=/dev/zero',
|
|
f'of={dev_path}',
|
|
]
|
|
run_program(cmd)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("This file is not meant to be called directly.")
|