Add script to build Bookworm rootfs with libargus support

This is the minimal amount needed to build a bookworm rootfs which boots
and is able to take pictures with the imx296 sensor

Note: the paths are pretty hard-coded right now, they need to be
improved.

Flashing is done using ./doflash_frc971.sh

Change-Id: Ibf480d3f5b0aed553ba71431f8a94a8e5290a98f
Signed-off-by: Austin Schuh <austin.linux@gmail.com>
diff --git a/frc971/orin/build_rootfs.py b/frc971/orin/build_rootfs.py
new file mode 100755
index 0000000..f82bed7
--- /dev/null
+++ b/frc971/orin/build_rootfs.py
@@ -0,0 +1,412 @@
+#!/usr/bin/python3
+
+import contextlib
+import pathlib
+import collections
+import subprocess
+import shlex
+import os
+
+IMAGE = "arm64_bookworm_debian_yocto.img"
+YOCTO = "/home/austin/local/jetpack/robot-yocto/build"
+
+REQUIRED_DEPS = ["debootstrap", "u-boot-tools"]
+
+
+@contextlib.contextmanager
+def scoped_loopback(image):
+    """Mounts an image as a loop back device."""
+    result = subprocess.run(["sudo", "losetup", "--show", "-f", image],
+                            check=True,
+                            stdout=subprocess.PIPE)
+    device = result.stdout.decode('utf-8').strip()
+    print("Mounted", image, "to", repr(device))
+    try:
+        yield device
+    finally:
+        subprocess.run(["sudo", "losetup", "-d", device], check=True)
+
+
+@contextlib.contextmanager
+def scoped_mount(image):
+    """Mounts an image as a partition."""
+    partition = f"{image}.partition"
+    try:
+        os.mkdir(partition)
+    except FileExistsError:
+        pass
+
+    result = subprocess.run(["sudo", "mount", "-o", "loop", image, partition],
+                            check=True)
+
+    try:
+        yield partition
+    finally:
+        subprocess.run(
+            ["sudo", "rm", f"{partition}/usr/bin/qemu-aarch64-static"])
+        subprocess.run(["sudo", "umount", partition], check=True)
+
+
+def check_required_deps(deps):
+    """Checks if the provided list of dependencies is installed."""
+    missing_deps = []
+    for dep in deps:
+        result = subprocess.run(["dpkg-query", "-W", "-f='${Status}'", dep],
+                                check=True,
+                                stdout=subprocess.PIPE)
+
+        if "install ok installed" not in result.stdout.decode('utf-8'):
+            missing_deps.append(dep)
+
+    if len(missing_deps) > 0:
+        print("Missing dependencies, please install:")
+        print("sudo apt-get install", " ".join(missing_deps))
+
+
+def make_image(image):
+    """Makes an image and creates an xfs filesystem on it."""
+    result = subprocess.run([
+        "dd", "if=/dev/zero", f"of={image}", "bs=1", "count=0",
+        "seek=8589934592"
+    ],
+                            check=True)
+
+    with scoped_loopback(image) as loopback:
+        subprocess.run([
+            "sudo", "mkfs.xfs", "-d", "su=128k", "-d", "sw=1", "-L", "rootfs",
+            loopback
+        ],
+                       check=True)
+
+
+def target_unescaped(cmd):
+    """Runs a command as root with bash -c cmd, ie without escaping."""
+    subprocess.run([
+        "sudo", "chroot", "--userspec=0:0", f"{PARTITION}",
+        "qemu-aarch64-static", "/bin/bash", "-c", cmd
+    ],
+                   check=True)
+
+
+def target(cmd):
+    """Runs a command as root with escaping."""
+    target_unescaped(shlex.join([shlex.quote(c) for c in cmd]))
+
+
+def pi_target_unescaped(cmd):
+    """Runs a command as pi with bash -c cmd, ie without escaping."""
+    subprocess.run([
+        "sudo", "chroot", "--userspec=pi:pi", "--groups=pi", f"{PARTITION}",
+        "qemu-aarch64-static", "/bin/bash", "-c", cmd
+    ],
+                   check=True)
+
+
+def pi_target(cmd):
+    """Runs a command as pi with escaping."""
+    pi_target_unescaped(shlex.join([shlex.quote(c) for c in cmd]))
+
+
+def copyfile(owner, permissions, file):
+    """Copies a file from contents/{file} with the provided owner and permissions."""
+    print("copyfile", owner, permissions, file)
+    subprocess.run(["sudo", "cp", f"contents/{file}", f"{PARTITION}/{file}"],
+                   check=True)
+    subprocess.run(["sudo", "chmod", permissions, f"{PARTITION}/{file}"],
+                   check=True)
+    target(["chown", owner, f"/{file}"])
+
+
+def target_mkdir(owner_group, permissions, folder):
+    """Creates a directory recursively with the provided permissions and ownership."""
+    print("target_mkdir", owner_group, permissions, folder)
+    owner, group = owner_group.split('.')
+    target(
+        ["install", "-d", "-m", permissions, "-o", owner, "-g", group, folder])
+
+
+def list_packages():
+    """Lists all installed packages.
+
+    Returns:
+      A dictionary with keys as packages, and values as versions.
+    """
+    result = subprocess.run([
+        "sudo", "chroot", "--userspec=0:0", f"{PARTITION}",
+        "qemu-aarch64-static", "/bin/bash", "-c",
+        "dpkg-query -W -f='${Package} ${Version}\n'"
+    ],
+                            check=True,
+                            stdout=subprocess.PIPE)
+
+    device = result.stdout.decode('utf-8').strip()
+
+    r = {}
+    for line in result.stdout.decode('utf-8').strip().split('\n'):
+        package, version = line.split(' ')
+        r[package] = version
+
+    return r
+
+
+def list_yocto_packages():
+    """Lists all packages in the Yocto folder.
+
+    Returns:
+      list of Package classes.
+    """
+    Package = collections.namedtuple(
+        'Package', ['path', 'name', 'version', 'architecture'])
+    result = []
+    pathlist = pathlib.Path(f"{YOCTO}/tmp/deploy/deb").glob('**/*.deb')
+    for path in pathlist:
+        # Strip off the path, .deb, and split on _ to parse the package info.
+        s = os.path.basename(str(path))[:-4].split('_')
+        result.append(Package(str(path), s[0], s[1], s[2]))
+
+    return result
+
+
+def install_packages(new_packages, existing_packages):
+    """Installs the provided yocto packages, if they are new."""
+    # To install the yocto packages, first copy them into a folder in /tmp, then install them, then clean the folder up.
+    target(["mkdir", "-p", "/tmp/yocto_packages"])
+    try:
+        to_install = []
+        for package in new_packages:
+            if package.name in existing_packages and existing_packages[
+                    package.name] == package.version:
+                print('Skipping', package)
+                continue
+
+            subprocess.run([
+                "sudo", "cp", package.path,
+                f"{PARTITION}/tmp/yocto_packages/{os.path.basename(package.path)}"
+            ],
+                           check=True)
+            to_install.append(package)
+
+        if len(to_install) > 0:
+            target(["dpkg", "-i"] + [
+                f"/tmp/yocto_packages/{os.path.basename(package.path)}"
+                for package in to_install
+            ])
+
+    finally:
+        target(["rm", "-rf", "/tmp/yocto_packages"])
+
+
+def install_virtual_packages(virtual_packages):
+    """Builds and installs the provided virtual packages."""
+    try:
+        target(["mkdir", "-p", "/tmp/yocto_packages"])
+        for virtual_package in virtual_packages:
+            subprocess.run(
+                ["dpkg-deb", "--build", f"virtual_packages/{virtual_package}"],
+                check=True)
+            subprocess.run([
+                "sudo", "cp", f"virtual_packages/{virtual_package}.deb",
+                f"{PARTITION}/tmp/yocto_packages/{virtual_package}.deb"
+            ],
+                           check=True)
+
+        target(["dpkg", "-i"] + [
+            f"/tmp/yocto_packages/{package}.deb"
+            for package in virtual_packages
+        ])
+
+    finally:
+        target(["rm", "-rf", "/tmp/yocto_packages"])
+
+
+def main():
+    check_required_deps(REQUIRED_DEPS)
+
+    new_image = not os.path.exists(IMAGE)
+    if new_image:
+        make_image(IMAGE)
+
+    with scoped_mount(IMAGE) as partition:
+        if new_image:
+            subprocess.run([
+                "sudo", "debootstrap", "--arch=arm64", "--no-check-gpg",
+                "--foreign", "bookworm", partition,
+                "http://deb.debian.org/debian/"
+            ],
+                           check=True)
+
+        subprocess.run([
+            "sudo", "cp", "/usr/bin/qemu-aarch64-static",
+            f"{partition}/usr/bin/"
+        ],
+                       check=True)
+
+        global PARTITION
+        PARTITION = partition
+
+        if new_image:
+            target(["/debootstrap/debootstrap", "--second-stage"])
+
+            target([
+                "useradd", "-m", "-p",
+                '$y$j9T$85lzhdky63CTj.two7Zj20$pVY53UR0VebErMlm8peyrEjmxeiRw/rfXfx..9.xet1',
+                '-s', '/bin/bash', 'pi'
+            ])
+            target(["addgroup", "debug"])
+            target(["addgroup", "crypto"])
+            target(["addgroup", "trusty"])
+
+        if not os.path.exists(
+                f"{partition}/etc/apt/sources.list.d/bullseye-backports.list"):
+            copyfile("root.root", "644",
+                     "etc/apt/sources.list.d/bullseye-backports.list")
+            target(["apt-get", "update"])
+
+        target([
+            "apt-get", "-y", "install", "gnupg", "wget", "systemd",
+            "systemd-resolved", "locales"
+        ])
+
+        target(["localedef", "-i", "en_US", "-f", "UTF-8", "en_US.UTF-8"])
+
+        target_mkdir("root.root", "755", "run/systemd")
+        target_mkdir("systemd-resolve.systemd-resolve", "755",
+                     "run/systemd/resolve")
+        copyfile("systemd-resolve.systemd-resolve", "644",
+                 "run/systemd/resolve/stub-resolv.conf")
+        target(["systemctl", "enable", "systemd-resolved"])
+
+        target([
+            "apt-get", "-y", "install", "bpfcc-tools", "sudo",
+            "openssh-server", "python3", "bash-completion", "git", "v4l-utils",
+            "cpufrequtils", "pmount", "rsync", "vim-nox", "chrony",
+            "libopencv-calib3d406", "libopencv-contrib406",
+            "libopencv-core406", "libopencv-features2d406",
+            "libopencv-flann406", "libopencv-highgui406",
+            "libopencv-imgcodecs406", "libopencv-imgproc406",
+            "libopencv-ml406", "libopencv-objdetect406", "libopencv-photo406",
+            "libopencv-shape406", "libopencv-stitching406",
+            "libopencv-superres406", "libopencv-video406",
+            "libopencv-videoio406", "libopencv-videostab406",
+            "libopencv-viz406", "libnice10", "pmount", "libnice-dev", "feh",
+            "libgstreamer1.0-0", "libgstreamer-plugins-base1.0-0",
+            "libgstreamer-plugins-bad1.0-0", "gstreamer1.0-plugins-base",
+            "gstreamer1.0-plugins-good", "gstreamer1.0-plugins-bad",
+            "gstreamer1.0-plugins-ugly", "gstreamer1.0-nice", "usbutils",
+            "locales", "trace-cmd", "clinfo", "jq", "strace", "sysstat",
+            "lm-sensors", "can-utils", "xfsprogs", "gstreamer1.0-tools",
+            "bridge-utils", "net-tools", "apt-file", "parted", "xxd"
+        ])
+        target(["apt-get", "clean"])
+
+        target(["usermod", "-a", "-G", "sudo", "pi"])
+        target(["usermod", "-a", "-G", "video", "pi"])
+        target(["usermod", "-a", "-G", "systemd-journal", "pi"])
+        target(["usermod", "-a", "-G", "dialout", "pi"])
+
+        virtual_packages = [
+            'libglib-2.0-0', 'libglvnd', 'libgtk-3-0', 'libxcb-glx', 'wayland'
+        ]
+
+        install_virtual_packages(virtual_packages)
+
+        yocto_package_names = [
+            'tegra-argus-daemon', 'tegra-firmware', 'tegra-firmware-tegra234',
+            'tegra-firmware-vic', 'tegra-firmware-xusb',
+            'tegra-libraries-argus-daemon-base', 'tegra-libraries-camera',
+            'tegra-libraries-core', 'tegra-libraries-cuda',
+            'tegra-libraries-eglcore', 'tegra-libraries-glescore',
+            'tegra-libraries-glxcore', 'tegra-libraries-multimedia',
+            'tegra-libraries-multimedia-utils',
+            'tegra-libraries-multimedia-v4l', 'tegra-libraries-nvsci',
+            'tegra-libraries-vulkan', 'tegra-nvphs', 'tegra-nvphs-base',
+            'libnvidia-egl-wayland1'
+        ]
+        yocto_packages = list_yocto_packages()
+        packages = list_packages()
+
+        install_packages([
+            package for package in yocto_packages
+            if package.name in yocto_package_names
+        ], packages)
+
+        # Now, install the kernel and modules after all the normal packages are in.
+        yocto_packages_to_install = [
+            package for package in yocto_packages
+            if (package.name.startswith('kernel-module-') or package.name.
+                startswith('kernel-5.10') or package.name == 'kernel-modules')
+        ]
+
+        packages_to_remove = []
+
+        # Remove kernel-module-* packages + kernel- package.
+        for key in packages:
+            if key.startswith('kernel-module') or key.startswith(
+                    'kernel-5.10'):
+                already_installed = False
+                for index, yocto_package in enumerate(
+                        yocto_packages_to_install):
+                    if key == yocto_package.name and packages[
+                            key] == yocto_package.version:
+                        print('Found already installed package', key,
+                              yocto_package)
+                        already_installed = True
+                        del yocto_packages_to_install[index]
+                        break
+                if not already_installed:
+                    packages_to_remove.append(key)
+
+        print("Removing", packages_to_remove)
+        if len(packages_to_remove) > 0:
+            target(['dpkg', '--purge'] + packages_to_remove)
+        print("Installing",
+              [package.name for package in yocto_packages_to_install])
+
+        install_packages(yocto_packages_to_install, packages)
+
+        target(["systemctl", "enable", "nvargus-daemon.service"])
+
+        copyfile("root.root", "644", "etc/sysctl.d/sctp.conf")
+        copyfile("root.root", "644", "etc/systemd/logind.conf")
+        copyfile("root.root", "555",
+                 "etc/bash_completion.d/aos_dump_autocomplete")
+        copyfile("root.root", "644", "etc/security/limits.d/rt.conf")
+        copyfile("root.root", "644", "etc/systemd/system/usb-mount@.service")
+        copyfile("root.root", "644", "etc/chrony/chrony.conf")
+        target_mkdir("root.root", "700", "root/bin")
+        target_mkdir("pi.pi", "755", "home/pi/.ssh")
+        copyfile("pi.pi", "600", "home/pi/.ssh/authorized_keys")
+        target_mkdir("root.root", "700", "root/bin")
+        copyfile("root.root", "644", "etc/systemd/system/grow-rootfs.service")
+        copyfile("root.root", "500", "root/bin/change_hostname.sh")
+        copyfile("root.root", "700", "root/trace.sh")
+        copyfile("root.root", "440", "etc/sudoers")
+        copyfile("root.root", "644", "etc/fstab")
+        copyfile("root.root", "644",
+                 "var/nvidia/nvcam/settings/camera_overrides.isp")
+
+        target_mkdir("root.root", "755", "etc/systemd/network")
+        copyfile("root.root", "644", "etc/systemd/network/eth0.network")
+        copyfile("root.root", "644", "etc/systemd/network/80-can.network")
+        target(["/root/bin/change_hostname.sh", "pi-971-1"])
+
+        target(["systemctl", "enable", "systemd-networkd"])
+        target(["systemctl", "enable", "grow-rootfs"])
+        target(["apt-file", "update"])
+
+        target(["ldconfig"])
+
+        if not os.path.exists(f"{partition}/home/pi/.dotfiles"):
+            pi_target_unescaped(
+                "cd /home/pi/ && git clone --separate-git-dir=/home/pi/.dotfiles https://github.com/AustinSchuh/.dotfiles.git tmpdotfiles && rsync --recursive --verbose --exclude .git tmpdotfiles/ /home/pi/ && rm -r tmpdotfiles && git --git-dir=/home/pi/.dotfiles/ --work-tree=/home/pi/ config --local status.showUntrackedFiles no"
+            )
+            pi_target(["vim", "-c", "\":qa!\""])
+
+            target_unescaped(
+                "cd /root/ && git clone --separate-git-dir=/root/.dotfiles https://github.com/AustinSchuh/.dotfiles.git tmpdotfiles && rsync --recursive --verbose --exclude .git tmpdotfiles/ /root/ && rm -r tmpdotfiles && git --git-dir=/root/.dotfiles/ --work-tree=/root/ config --local status.showUntrackedFiles no"
+            )
+            target(["vim", "-c", "\":qa!\""])
+
+
+if __name__ == '__main__':
+    main()