blob: f82bed7f18c77ff4a9a6712ef38f76586a59815d [file] [log] [blame]
Austin Schuh51014832023-10-20 17:44:45 -07001#!/usr/bin/python3
2
3import contextlib
4import pathlib
5import collections
6import subprocess
7import shlex
8import os
9
10IMAGE = "arm64_bookworm_debian_yocto.img"
11YOCTO = "/home/austin/local/jetpack/robot-yocto/build"
12
13REQUIRED_DEPS = ["debootstrap", "u-boot-tools"]
14
15
16@contextlib.contextmanager
17def scoped_loopback(image):
18 """Mounts an image as a loop back device."""
19 result = subprocess.run(["sudo", "losetup", "--show", "-f", image],
20 check=True,
21 stdout=subprocess.PIPE)
22 device = result.stdout.decode('utf-8').strip()
23 print("Mounted", image, "to", repr(device))
24 try:
25 yield device
26 finally:
27 subprocess.run(["sudo", "losetup", "-d", device], check=True)
28
29
30@contextlib.contextmanager
31def scoped_mount(image):
32 """Mounts an image as a partition."""
33 partition = f"{image}.partition"
34 try:
35 os.mkdir(partition)
36 except FileExistsError:
37 pass
38
39 result = subprocess.run(["sudo", "mount", "-o", "loop", image, partition],
40 check=True)
41
42 try:
43 yield partition
44 finally:
45 subprocess.run(
46 ["sudo", "rm", f"{partition}/usr/bin/qemu-aarch64-static"])
47 subprocess.run(["sudo", "umount", partition], check=True)
48
49
50def check_required_deps(deps):
51 """Checks if the provided list of dependencies is installed."""
52 missing_deps = []
53 for dep in deps:
54 result = subprocess.run(["dpkg-query", "-W", "-f='${Status}'", dep],
55 check=True,
56 stdout=subprocess.PIPE)
57
58 if "install ok installed" not in result.stdout.decode('utf-8'):
59 missing_deps.append(dep)
60
61 if len(missing_deps) > 0:
62 print("Missing dependencies, please install:")
63 print("sudo apt-get install", " ".join(missing_deps))
64
65
66def make_image(image):
67 """Makes an image and creates an xfs filesystem on it."""
68 result = subprocess.run([
69 "dd", "if=/dev/zero", f"of={image}", "bs=1", "count=0",
70 "seek=8589934592"
71 ],
72 check=True)
73
74 with scoped_loopback(image) as loopback:
75 subprocess.run([
76 "sudo", "mkfs.xfs", "-d", "su=128k", "-d", "sw=1", "-L", "rootfs",
77 loopback
78 ],
79 check=True)
80
81
82def target_unescaped(cmd):
83 """Runs a command as root with bash -c cmd, ie without escaping."""
84 subprocess.run([
85 "sudo", "chroot", "--userspec=0:0", f"{PARTITION}",
86 "qemu-aarch64-static", "/bin/bash", "-c", cmd
87 ],
88 check=True)
89
90
91def target(cmd):
92 """Runs a command as root with escaping."""
93 target_unescaped(shlex.join([shlex.quote(c) for c in cmd]))
94
95
96def pi_target_unescaped(cmd):
97 """Runs a command as pi with bash -c cmd, ie without escaping."""
98 subprocess.run([
99 "sudo", "chroot", "--userspec=pi:pi", "--groups=pi", f"{PARTITION}",
100 "qemu-aarch64-static", "/bin/bash", "-c", cmd
101 ],
102 check=True)
103
104
105def pi_target(cmd):
106 """Runs a command as pi with escaping."""
107 pi_target_unescaped(shlex.join([shlex.quote(c) for c in cmd]))
108
109
110def copyfile(owner, permissions, file):
111 """Copies a file from contents/{file} with the provided owner and permissions."""
112 print("copyfile", owner, permissions, file)
113 subprocess.run(["sudo", "cp", f"contents/{file}", f"{PARTITION}/{file}"],
114 check=True)
115 subprocess.run(["sudo", "chmod", permissions, f"{PARTITION}/{file}"],
116 check=True)
117 target(["chown", owner, f"/{file}"])
118
119
120def target_mkdir(owner_group, permissions, folder):
121 """Creates a directory recursively with the provided permissions and ownership."""
122 print("target_mkdir", owner_group, permissions, folder)
123 owner, group = owner_group.split('.')
124 target(
125 ["install", "-d", "-m", permissions, "-o", owner, "-g", group, folder])
126
127
128def list_packages():
129 """Lists all installed packages.
130
131 Returns:
132 A dictionary with keys as packages, and values as versions.
133 """
134 result = subprocess.run([
135 "sudo", "chroot", "--userspec=0:0", f"{PARTITION}",
136 "qemu-aarch64-static", "/bin/bash", "-c",
137 "dpkg-query -W -f='${Package} ${Version}\n'"
138 ],
139 check=True,
140 stdout=subprocess.PIPE)
141
142 device = result.stdout.decode('utf-8').strip()
143
144 r = {}
145 for line in result.stdout.decode('utf-8').strip().split('\n'):
146 package, version = line.split(' ')
147 r[package] = version
148
149 return r
150
151
152def list_yocto_packages():
153 """Lists all packages in the Yocto folder.
154
155 Returns:
156 list of Package classes.
157 """
158 Package = collections.namedtuple(
159 'Package', ['path', 'name', 'version', 'architecture'])
160 result = []
161 pathlist = pathlib.Path(f"{YOCTO}/tmp/deploy/deb").glob('**/*.deb')
162 for path in pathlist:
163 # Strip off the path, .deb, and split on _ to parse the package info.
164 s = os.path.basename(str(path))[:-4].split('_')
165 result.append(Package(str(path), s[0], s[1], s[2]))
166
167 return result
168
169
170def install_packages(new_packages, existing_packages):
171 """Installs the provided yocto packages, if they are new."""
172 # To install the yocto packages, first copy them into a folder in /tmp, then install them, then clean the folder up.
173 target(["mkdir", "-p", "/tmp/yocto_packages"])
174 try:
175 to_install = []
176 for package in new_packages:
177 if package.name in existing_packages and existing_packages[
178 package.name] == package.version:
179 print('Skipping', package)
180 continue
181
182 subprocess.run([
183 "sudo", "cp", package.path,
184 f"{PARTITION}/tmp/yocto_packages/{os.path.basename(package.path)}"
185 ],
186 check=True)
187 to_install.append(package)
188
189 if len(to_install) > 0:
190 target(["dpkg", "-i"] + [
191 f"/tmp/yocto_packages/{os.path.basename(package.path)}"
192 for package in to_install
193 ])
194
195 finally:
196 target(["rm", "-rf", "/tmp/yocto_packages"])
197
198
199def install_virtual_packages(virtual_packages):
200 """Builds and installs the provided virtual packages."""
201 try:
202 target(["mkdir", "-p", "/tmp/yocto_packages"])
203 for virtual_package in virtual_packages:
204 subprocess.run(
205 ["dpkg-deb", "--build", f"virtual_packages/{virtual_package}"],
206 check=True)
207 subprocess.run([
208 "sudo", "cp", f"virtual_packages/{virtual_package}.deb",
209 f"{PARTITION}/tmp/yocto_packages/{virtual_package}.deb"
210 ],
211 check=True)
212
213 target(["dpkg", "-i"] + [
214 f"/tmp/yocto_packages/{package}.deb"
215 for package in virtual_packages
216 ])
217
218 finally:
219 target(["rm", "-rf", "/tmp/yocto_packages"])
220
221
222def main():
223 check_required_deps(REQUIRED_DEPS)
224
225 new_image = not os.path.exists(IMAGE)
226 if new_image:
227 make_image(IMAGE)
228
229 with scoped_mount(IMAGE) as partition:
230 if new_image:
231 subprocess.run([
232 "sudo", "debootstrap", "--arch=arm64", "--no-check-gpg",
233 "--foreign", "bookworm", partition,
234 "http://deb.debian.org/debian/"
235 ],
236 check=True)
237
238 subprocess.run([
239 "sudo", "cp", "/usr/bin/qemu-aarch64-static",
240 f"{partition}/usr/bin/"
241 ],
242 check=True)
243
244 global PARTITION
245 PARTITION = partition
246
247 if new_image:
248 target(["/debootstrap/debootstrap", "--second-stage"])
249
250 target([
251 "useradd", "-m", "-p",
252 '$y$j9T$85lzhdky63CTj.two7Zj20$pVY53UR0VebErMlm8peyrEjmxeiRw/rfXfx..9.xet1',
253 '-s', '/bin/bash', 'pi'
254 ])
255 target(["addgroup", "debug"])
256 target(["addgroup", "crypto"])
257 target(["addgroup", "trusty"])
258
259 if not os.path.exists(
260 f"{partition}/etc/apt/sources.list.d/bullseye-backports.list"):
261 copyfile("root.root", "644",
262 "etc/apt/sources.list.d/bullseye-backports.list")
263 target(["apt-get", "update"])
264
265 target([
266 "apt-get", "-y", "install", "gnupg", "wget", "systemd",
267 "systemd-resolved", "locales"
268 ])
269
270 target(["localedef", "-i", "en_US", "-f", "UTF-8", "en_US.UTF-8"])
271
272 target_mkdir("root.root", "755", "run/systemd")
273 target_mkdir("systemd-resolve.systemd-resolve", "755",
274 "run/systemd/resolve")
275 copyfile("systemd-resolve.systemd-resolve", "644",
276 "run/systemd/resolve/stub-resolv.conf")
277 target(["systemctl", "enable", "systemd-resolved"])
278
279 target([
280 "apt-get", "-y", "install", "bpfcc-tools", "sudo",
281 "openssh-server", "python3", "bash-completion", "git", "v4l-utils",
282 "cpufrequtils", "pmount", "rsync", "vim-nox", "chrony",
283 "libopencv-calib3d406", "libopencv-contrib406",
284 "libopencv-core406", "libopencv-features2d406",
285 "libopencv-flann406", "libopencv-highgui406",
286 "libopencv-imgcodecs406", "libopencv-imgproc406",
287 "libopencv-ml406", "libopencv-objdetect406", "libopencv-photo406",
288 "libopencv-shape406", "libopencv-stitching406",
289 "libopencv-superres406", "libopencv-video406",
290 "libopencv-videoio406", "libopencv-videostab406",
291 "libopencv-viz406", "libnice10", "pmount", "libnice-dev", "feh",
292 "libgstreamer1.0-0", "libgstreamer-plugins-base1.0-0",
293 "libgstreamer-plugins-bad1.0-0", "gstreamer1.0-plugins-base",
294 "gstreamer1.0-plugins-good", "gstreamer1.0-plugins-bad",
295 "gstreamer1.0-plugins-ugly", "gstreamer1.0-nice", "usbutils",
296 "locales", "trace-cmd", "clinfo", "jq", "strace", "sysstat",
297 "lm-sensors", "can-utils", "xfsprogs", "gstreamer1.0-tools",
298 "bridge-utils", "net-tools", "apt-file", "parted", "xxd"
299 ])
300 target(["apt-get", "clean"])
301
302 target(["usermod", "-a", "-G", "sudo", "pi"])
303 target(["usermod", "-a", "-G", "video", "pi"])
304 target(["usermod", "-a", "-G", "systemd-journal", "pi"])
305 target(["usermod", "-a", "-G", "dialout", "pi"])
306
307 virtual_packages = [
308 'libglib-2.0-0', 'libglvnd', 'libgtk-3-0', 'libxcb-glx', 'wayland'
309 ]
310
311 install_virtual_packages(virtual_packages)
312
313 yocto_package_names = [
314 'tegra-argus-daemon', 'tegra-firmware', 'tegra-firmware-tegra234',
315 'tegra-firmware-vic', 'tegra-firmware-xusb',
316 'tegra-libraries-argus-daemon-base', 'tegra-libraries-camera',
317 'tegra-libraries-core', 'tegra-libraries-cuda',
318 'tegra-libraries-eglcore', 'tegra-libraries-glescore',
319 'tegra-libraries-glxcore', 'tegra-libraries-multimedia',
320 'tegra-libraries-multimedia-utils',
321 'tegra-libraries-multimedia-v4l', 'tegra-libraries-nvsci',
322 'tegra-libraries-vulkan', 'tegra-nvphs', 'tegra-nvphs-base',
323 'libnvidia-egl-wayland1'
324 ]
325 yocto_packages = list_yocto_packages()
326 packages = list_packages()
327
328 install_packages([
329 package for package in yocto_packages
330 if package.name in yocto_package_names
331 ], packages)
332
333 # Now, install the kernel and modules after all the normal packages are in.
334 yocto_packages_to_install = [
335 package for package in yocto_packages
336 if (package.name.startswith('kernel-module-') or package.name.
337 startswith('kernel-5.10') or package.name == 'kernel-modules')
338 ]
339
340 packages_to_remove = []
341
342 # Remove kernel-module-* packages + kernel- package.
343 for key in packages:
344 if key.startswith('kernel-module') or key.startswith(
345 'kernel-5.10'):
346 already_installed = False
347 for index, yocto_package in enumerate(
348 yocto_packages_to_install):
349 if key == yocto_package.name and packages[
350 key] == yocto_package.version:
351 print('Found already installed package', key,
352 yocto_package)
353 already_installed = True
354 del yocto_packages_to_install[index]
355 break
356 if not already_installed:
357 packages_to_remove.append(key)
358
359 print("Removing", packages_to_remove)
360 if len(packages_to_remove) > 0:
361 target(['dpkg', '--purge'] + packages_to_remove)
362 print("Installing",
363 [package.name for package in yocto_packages_to_install])
364
365 install_packages(yocto_packages_to_install, packages)
366
367 target(["systemctl", "enable", "nvargus-daemon.service"])
368
369 copyfile("root.root", "644", "etc/sysctl.d/sctp.conf")
370 copyfile("root.root", "644", "etc/systemd/logind.conf")
371 copyfile("root.root", "555",
372 "etc/bash_completion.d/aos_dump_autocomplete")
373 copyfile("root.root", "644", "etc/security/limits.d/rt.conf")
374 copyfile("root.root", "644", "etc/systemd/system/usb-mount@.service")
375 copyfile("root.root", "644", "etc/chrony/chrony.conf")
376 target_mkdir("root.root", "700", "root/bin")
377 target_mkdir("pi.pi", "755", "home/pi/.ssh")
378 copyfile("pi.pi", "600", "home/pi/.ssh/authorized_keys")
379 target_mkdir("root.root", "700", "root/bin")
380 copyfile("root.root", "644", "etc/systemd/system/grow-rootfs.service")
381 copyfile("root.root", "500", "root/bin/change_hostname.sh")
382 copyfile("root.root", "700", "root/trace.sh")
383 copyfile("root.root", "440", "etc/sudoers")
384 copyfile("root.root", "644", "etc/fstab")
385 copyfile("root.root", "644",
386 "var/nvidia/nvcam/settings/camera_overrides.isp")
387
388 target_mkdir("root.root", "755", "etc/systemd/network")
389 copyfile("root.root", "644", "etc/systemd/network/eth0.network")
390 copyfile("root.root", "644", "etc/systemd/network/80-can.network")
391 target(["/root/bin/change_hostname.sh", "pi-971-1"])
392
393 target(["systemctl", "enable", "systemd-networkd"])
394 target(["systemctl", "enable", "grow-rootfs"])
395 target(["apt-file", "update"])
396
397 target(["ldconfig"])
398
399 if not os.path.exists(f"{partition}/home/pi/.dotfiles"):
400 pi_target_unescaped(
401 "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"
402 )
403 pi_target(["vim", "-c", "\":qa!\""])
404
405 target_unescaped(
406 "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"
407 )
408 target(["vim", "-c", "\":qa!\""])
409
410
411if __name__ == '__main__':
412 main()