diff --git a/Dockerfile b/Dockerfile index 8900047..58fb34d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ RUN pacman -Syu --noconfirm \ python python-pip \ devtools rsync \ aarch64-linux-gnu-gcc aarch64-linux-gnu-binutils aarch64-linux-gnu-glibc aarch64-linux-gnu-linux-api-headers \ - git + git \ + android-tools RUN sed -i "s/EUID == 0/EUID == -1/g" $(which makepkg) @@ -18,7 +19,6 @@ RUN yes | pacman -Scc RUN sed -i "s/SigLevel.*/SigLevel = Never/g" /etc/pacman.conf RUN sed -i "s|run_pacman |run_pacman --root \"/chroot/copy\" --arch aarch64 --config \"/app/src/pacman_copy.conf\" |g" $(which makepkg) -RUN echo "Server = http://mirror.archlinuxarm.org/\$arch/\$repo" > /etc/pacman.d/aarch64_mirrorlist RUN mkdir -p /app/bin RUN ln -sf $(which aarch64-linux-gnu-strip) /app/bin/strip diff --git a/boot.py b/boot.py new file mode 100644 index 0000000..ac25c8d --- /dev/null +++ b/boot.py @@ -0,0 +1,54 @@ +import os +import urllib.request +from image import get_device_and_flavour, get_image_name +from logger import * +import click +import subprocess + + +FASTBOOT = 'fastboot' + +JUMPDRIVE = 'jumpdrive' +jumpdrive_version = '0.8' + +boot_strategies = { + 'oneplus-enchilada': FASTBOOT, + 'xiaomi-beryllium-ebbg': FASTBOOT, + 'xiaomi-beryllium-tianma': FASTBOOT, +} + + +def dump_bootimg(image_name: str) -> str: + path = '/tmp/boot.img' + result = subprocess.run(['debugfs', + image_name, + '-R', f'dump /boot/boot.img {path}']) + if result.returncode != 0: + logging.fatal(f'Faild to dump boot.img') + exit(1) + return path + + +@click.command(name='boot', help=f'Leave TYPE empty or choose \'{JUMPDRIVE}\'') +@verbose_option +@click.argument('type', required=False) +def cmd_boot(verbose, type): + setup_logging(verbose) + + device, flavour = get_device_and_flavour() + image_name = get_image_name(device, flavour) + strategy = boot_strategies[device] + + if strategy == FASTBOOT: + if type == JUMPDRIVE: + file = f'boot-{device}.img' + path = os.path.join('/var/cache/jumpdrive', file) + urllib.request.urlretrieve( + f'https://github.com/dreemurrs-embedded/Jumpdrive/releases/download/{jumpdrive_version}/{file}', path) + else: + path = dump_bootimg(image_name) + + result = subprocess.run(['fastboot', 'boot', path]) + if result.returncode != 0: + logging.fatal(f'Failed to boot {path} using fastboot') + exit(1) diff --git a/cache.py b/cache.py index 48603fb..e3d4c29 100644 --- a/cache.py +++ b/cache.py @@ -14,7 +14,7 @@ def cmd_cache(): def cmd_clean(verbose): setup_logging(verbose) - for dir in ['/chroot', '/var/cache/pacman/pkg']: + for dir in ['/chroot', '/var/cache/pacman/pkg', '/var/cache/jumpdrive']: for file in os.listdir(dir): path = os.path.join(dir, file) if os.path.isdir(path): diff --git a/flash.py b/flash.py new file mode 100644 index 0000000..29ebbda --- /dev/null +++ b/flash.py @@ -0,0 +1,81 @@ +from image import get_device_and_flavour, get_image_name +from boot import dump_bootimg +import os +import subprocess +import click +from logger import * + +ROOTFS = 'rootfs' +BOOTIMG = 'bootimg' + +EMMC = 'emmc' +EMMCFILE = 'emmc-file' +MICROSD = 'microsd' +locations = [EMMC, EMMCFILE, MICROSD] + + +@click.command(name='flash') +@verbose_option +@click.argument('what') +@click.argument('location', required=False) +def cmd_flash(verbose, what, location): + setup_logging(verbose) + + device, flavour = get_device_and_flavour() + image_name = get_image_name(device, flavour) + + if what == ROOTFS: + if location == None: + logging.info(f'You need to specify a location to flash {what} to') + exit(1) + if location not in locations: + logging.info( + f'Invalid location {location}. Choose one of {", ".join(locations)} for location') + exit(1) + + dir = '/dev/disk/by-id' + for file in os.listdir(dir): + sanitized_file = file.replace('-', '').replace('_', '').lower() + if f'jumpdrive{location.split("-")[0]}' in sanitized_file: + path = os.path.realpath(os.path.join(dir, file)) + result = subprocess.run(['lsblk', + path, + '-o', 'SIZE'], + capture_output=True) + if result.returncode != 0: + logging.info(f'Failed to lsblk {path}') + exit(1) + if result.stdout == b'SIZE\n 0B\n': + logging.info( + f'Disk {path} has a size of 0B. That probably means it is not available (e.g. no microSD inserted or no microSD card slot installed in the device) or corrupt or defect') + exit(1) + + if location.endswith('-file'): + logging.fatal('Not implemented yet') + exit() + else: + result = subprocess.run(['dd', + f'if={image_name}', + f'of={path}', + 'bs=20M', + 'iflag=direct', + 'oflag=direct', + 'status=progress']) + if result.returncode != 0: + logging.info(f'Failed to flash {image_name} to {path}') + exit(1) + + elif what == BOOTIMG: + result = subprocess.run(['fastboot', 'erase', 'dtbo']) + if result.returncode != 0: + logging.info(f'Failed to erase dtbo') + exit(1) + + path = dump_bootimg(image_name) + result = subprocess.run(['fastboot', 'flash', 'boot', path]) + if result.returncode != 0: + logging.info(f'Failed to flash boot.img') + exit(1) + else: + logging.fatal(f'Unknown what {what}') + exit(1) diff --git a/forwarding.py b/forwarding.py new file mode 100644 index 0000000..439705c --- /dev/null +++ b/forwarding.py @@ -0,0 +1,38 @@ +import click +import subprocess +from logger import * + + +@click.command(name='forwarding') +@verbose_option +def cmd_forwarding(verbose): + setup_logging(verbose) + + result = subprocess.run(['sysctl', 'net.ipv4.ip_forward=1']) + if result.returncode != 0: + logging.fatal(f'Failed to enable ipv4 forward via sysctl') + exit(1) + + result = subprocess.run(['iptables', '-P', 'FORWARD', 'ACCEPT']) + if result.returncode != 0: + logging.fatal(f'Failed set iptables rule') + exit(1) + + result = subprocess.run(['iptables', + '-A', 'POSTROUTING', + '-t', 'nat', + '-j', 'MASQUERADE', + '-s', '172.16.42.0/24']) + if result.returncode != 0: + logging.fatal(f'Failed set iptables rule') + exit(1) + + result = subprocess.run(['ssh', + '-o', 'GlobalKnownHostsFile=/dev/null', + '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'StrictHostKeyChecking=no', + '-t', 'kupfer@172.16.42.1', + 'sudo route add default gw 172.16.42.2']) + if result.returncode != 0: + logging.fatal(f'Failed to add gateway over ssh') + exit(1) diff --git a/image.py b/image.py new file mode 100644 index 0000000..3d00da0 --- /dev/null +++ b/image.py @@ -0,0 +1,198 @@ +import atexit +from logging import root +import os +import shutil +import signal +import subprocess +import time +import click +from logger import * + +devices = { + 'oneplus-enchilada': ['sdm845-oneplus-enchilada'], + 'xiaomi-beryllium-ebbg': ['sdm845-xiaomi-beryllium-ebbg'], + 'xiaomi-beryllium-tianma': ['sdm845-xiaomi-beryllium-tianma'], +} + +flavours = { + 'barebone': [], + 'phosh': [], + 'plasma-mobile': [], +} + + +def get_device_and_flavour() -> tuple[str, str]: + if not os.path.exists('.device'): + logging.fatal( + f'Please set the device using \'kupferbootstrap image device ...\'') + exit(1) + if not os.path.exists('.flavour'): + logging.fatal( + f'Please set the flavour using \'kupferbootstrap image flavour ...\'') + exit(1) + + with open('.device', 'r') as file: + device = file.read() + with open('.flavour', 'r') as file: + flavour = file.read() + + return (device, flavour) + + +def get_image_name(device, flavour): + return f'{device}-{flavour}-rootfs.img' + + +def mount_rootfs_image(path): + rootfs_mount = '/mnt/kupfer/rootfs' + if not os.path.exists(rootfs_mount): + os.makedirs(rootfs_mount) + + def umount(): + subprocess.run(['umount', '-lc', rootfs_mount], + stderr=subprocess.DEVNULL) + atexit.register(umount) + + result = subprocess.run(['mount', + '-o', 'loop', + path, + rootfs_mount]) + if result.returncode != 0: + logging.fatal(f'Failed to loop mount {path} to {rootfs_mount}') + exit(1) + + return rootfs_mount + + +@click.group(name='image') +def cmd_image(): + pass + + +@click.command(name='device') +@verbose_option +@click.argument('device') +def cmd_device(verbose, device): + setup_logging(verbose) + + for key in devices.keys(): + if '-'.join(key.split('-')[1:]) == device: + device = key + break + + if device not in devices: + logging.fatal( + f'Unknown device {device}. Pick one from:\n{", ".join(devices.keys())}') + exit(1) + + logging.info(f'Setting device to {device}') + + with open('.device', 'w') as file: + file.write(device) + + +@click.command(name='flavour') +@verbose_option +@click.argument('flavour') +def cmd_flavour(verbose, flavour): + setup_logging(verbose) + + if flavour not in flavours: + logging.fatal( + f'Unknown flavour {flavour}. Pick one from:\n{", ".join(flavours.keys())}') + exit(1) + + logging.info(f'Setting flavour to {flavour}') + + with open('.flavour', 'w') as file: + file.write(flavour) + + +@click.command(name='build') +@verbose_option +def cmd_build(verbose): + setup_logging(verbose) + + device, flavour = get_device_and_flavour() + image_name = get_image_name(device, flavour) + + shutil.copyfile('/app/src/pacman.conf', '/app/src/pacman_copy.conf') + with open('/app/src/pacman_copy.conf', 'a') as file: + file.write( + '\n\n[main]\nServer = https://gitlab.com/kupfer/packages/prebuilts/-/raw/main/$repo') + file.write( + '\n\n[device]\nServer = https://gitlab.com/kupfer/packages/prebuilts/-/raw/main/$repo') + + if not os.path.exists(image_name): + result = subprocess.run(['fallocate', + '-l', '4G', + image_name]) + if result.returncode != 0: + logging.fatal(f'Failed to allocate {image_name}') + exit(1) + + result = subprocess.run(['mkfs.ext4', + '-L', 'kupfer', + image_name]) + if result.returncode != 0: + logging.fatal(f'Failed to create ext4 filesystem on {image_name}') + exit(1) + + rootfs_mount = mount_rootfs_image(image_name) + + result = subprocess.run(['pacstrap', + '-C', '/app/src/pacman_copy.conf', + '-c', + '-G', + rootfs_mount, + 'base', 'base-kupfer'] + + devices[device] + + flavours[flavour] + + ['--needed', '--overwrite=*', '-yyuu']) + + with open(os.path.join(rootfs_mount, 'install'), 'w') as file: + user = 'kupfer' + password = '123456' + groups = ['network', 'video', 'audio', 'optical', 'storage', + 'input', 'scanner', 'games', 'lp', 'rfkill', 'wheel'] + file.write('\n'.join([ + f'if ! id -u "{user}" >/dev/null 2>&1; then', + f' useradd -m {user}', + f'fi', + f'usermod -a -G {",".join(groups)} {user}', + f'echo "{user}:{password}" | chpasswd', + f'chown {user}:{user} /home/{user} -R', + ])) + result = subprocess.run(['arch-chroot', + rootfs_mount, + '/bin/bash', '/install']) + os.unlink(os.path.join(rootfs_mount, 'install')) + if result.returncode != 0: + logging.fatal('Failed to setup user') + exit(1) + + +""" +This doesn't work, because the mount isn't passed through to the real host +""" + +""" +@click.command(name='inspect') +@verbose_option +def cmd_inspect(verbose): + setup_logging(verbose) + + device, flavour = get_device_and_flavour() + image_name = get_image_name(device, flavour) + + rootfs_mount = mount_rootfs_image(image_name) + + logging.info(f'Inspect the rootfs image at {rootfs_mount}') + + signal.pause() +""" + +cmd_image.add_command(cmd_device) +cmd_image.add_command(cmd_flavour) +cmd_image.add_command(cmd_build) +# cmd_image.add_command(cmd_inspect) diff --git a/main.py b/main.py index 62c738b..fdd5cd9 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,10 @@ from packages import cmd_packages from cache import cmd_cache +from image import cmd_image +from boot import cmd_boot +from flash import cmd_flash +from ssh import cmd_ssh +from forwarding import cmd_forwarding import click @@ -10,3 +15,8 @@ def cli(): cli.add_command(cmd_cache) cli.add_command(cmd_packages) +cli.add_command(cmd_image) +cli.add_command(cmd_boot) +cli.add_command(cmd_flash) +cli.add_command(cmd_ssh) +cli.add_command(cmd_forwarding) diff --git a/packages.py b/packages.py index 05dccbe..e22d571 100644 --- a/packages.py +++ b/packages.py @@ -123,8 +123,6 @@ def setup_chroot(): file.write('\n\n[device]\nServer = file:///src/prebuilts/device') shutil.copyfile('/app/src/pacman_copy.conf', '/chroot/root/etc/pacman.conf') - shutil.copyfile('/etc/pacman.d/aarch64_mirrorlist', - '/chroot/root/etc/pacman.d/aarch64_mirrorlist') result = subprocess.run(pacman_cmd + ['-Sy', '--root', '/chroot/root', diff --git a/src/pacman.conf b/src/pacman.conf index b9843aa..b2e4eb0 100644 --- a/src/pacman.conf +++ b/src/pacman.conf @@ -70,19 +70,19 @@ LocalFileSigLevel = Never # after the header, and they will be used before the default mirrors. [core] -Include = /etc/pacman.d/aarch64_mirrorlist +Server = http://mirror.archlinuxarm.org/$arch/$repo [extra] -Include = /etc/pacman.d/aarch64_mirrorlist +Server = http://mirror.archlinuxarm.org/$arch/$repo [community] -Include = /etc/pacman.d/aarch64_mirrorlist +Server = http://mirror.archlinuxarm.org/$arch/$repo [alarm] -Include = /etc/pacman.d/aarch64_mirrorlist +Server = http://mirror.archlinuxarm.org/$arch/$repo [aur] -Include = /etc/pacman.d/aarch64_mirrorlist +Server = http://mirror.archlinuxarm.org/$arch/$repo # An example of a custom package repository. See the pacman manpage for # tips on creating your own repositories. diff --git a/src/update-pacman-files.sh b/src/update-pacman-files.sh index c499ce3..143a86e 100755 --- a/src/update-pacman-files.sh +++ b/src/update-pacman-files.sh @@ -28,4 +28,4 @@ sed -i "s/@CARCH@/aarch64/g" pacman.conf sed -i "s/#ParallelDownloads.*/ParallelDownloads = 8/g" pacman.conf sed -i "s/SigLevel.*/SigLevel = Never/g" pacman.conf sed -i "s/^CheckSpace/#CheckSpace/g" pacman.conf -sed -i "s|/mirrorlist|/aarch64_mirrorlist|g" pacman.conf +sed -i "s|Include = /etc/pacman.d/mirrorlist|Server = http://mirror.archlinuxarm.org/\$arch/\$repo|g" pacman.conf diff --git a/ssh.py b/ssh.py new file mode 100644 index 0000000..2f1d005 --- /dev/null +++ b/ssh.py @@ -0,0 +1,15 @@ +import subprocess +import click +from logger import * + + +@click.command(name='ssh') +@verbose_option +def cmd_ssh(verbose): + setup_logging(verbose) + + subprocess.run(['ssh', + '-o', 'GlobalKnownHostsFile=/dev/null', + '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'StrictHostKeyChecking=no', + 'kupfer@172.16.42.1']) diff --git a/wrapper.py b/wrapper.py index ed7eb65..794b3eb 100644 --- a/wrapper.py +++ b/wrapper.py @@ -43,10 +43,15 @@ else: 'run', '--name', 'kupferbootstrap', '--rm', + '--interactive', + '--tty', '--privileged', '-v', f'{os.getcwd()}:/src:z', '-v', f'{os.path.join(appdirs.user_cache_dir("kupfer"),"chroot")}:/chroot:z', '-v', f'{os.path.join(appdirs.user_cache_dir("kupfer"),"pacman")}:/var/cache/pacman/pkg:z', + '-v', f'{os.path.join(appdirs.user_cache_dir("kupfer"),"jumpdrive")}:/var/cache/jumpdrive:z', + '-v', '/dev:/dev', + #'-v', '/mnt/kupfer:/mnt/kupfer:z', '-v', '/usr/share/i18n/locales:/usr/share/i18n/locales:ro'] + [tag, 'kupferbootstrap']