logo

live-bootstrap

Mirror of <https://github.com/fosslinux/live-bootstrap>

rootfs.py (14194B)


  1. #!/usr/bin/env python3
  2. """
  3. A helper application used to start bootstrapping process.
  4. It has a few modes of operation, you can create initramfs with
  5. binary seeds and sources that you can boot into or alternatively
  6. you can run bootstap inside chroot.
  7. """
  8. # SPDX-License-Identifier: GPL-3.0-or-later
  9. # SPDX-FileCopyrightText: 2022 Dor Askayo <dor.askayo@gmail.com>
  10. # SPDX-FileCopyrightText: 2021 Andrius Štikonas <andrius@stikonas.eu>
  11. # SPDX-FileCopyrightText: 2021 Bastian Bittorf <bb@npl.de>
  12. # SPDX-FileCopyrightText: 2021 Melg Eight <public.melg8@gmail.com>
  13. # SPDX-FileCopyrightText: 2021-23 fosslinux <fosslinux@aussies.space>
  14. # SPDX-FileCopyrightText: 2023-24 Gábor Stefanik <netrolller.3d@gmail.com>
  15. import argparse
  16. import os
  17. import signal
  18. import threading
  19. from lib.generator import Generator, stage0_arch_map
  20. from lib.simple_mirror import SimpleMirror
  21. from lib.target import Target
  22. from lib.utils import run, run_as_root
  23. def create_configuration_file(args):
  24. """
  25. Creates bootstrap.cfg file which would contain options used to
  26. customize bootstrap.
  27. """
  28. config_path = os.path.join('steps', 'bootstrap.cfg')
  29. with open(config_path, "w", encoding="utf_8") as config:
  30. config.write(f"ARCH={args.arch}\n")
  31. config.write(f"ARCH_DIR={stage0_arch_map.get(args.arch, args.arch)}\n")
  32. config.write(f"FORCE_TIMESTAMPS={args.force_timestamps}\n")
  33. config.write(f"CHROOT={args.chroot or args.bwrap}\n")
  34. config.write(f"UPDATE_CHECKSUMS={args.update_checksums}\n")
  35. config.write(f"JOBS={args.cores}\n")
  36. config.write(f"SWAP_SIZE={args.swap}\n")
  37. config.write(f"FINAL_JOBS={args.cores}\n")
  38. config.write(f"INTERNAL_CI={args.internal_ci or False}\n")
  39. config.write(f"INTERACTIVE={args.interactive}\n")
  40. config.write(f"BARE_METAL={args.bare_metal or (args.qemu and args.interactive)}\n")
  41. if (args.bare_metal or args.qemu) and not args.kernel:
  42. if args.repo or args.external_sources:
  43. config.write("DISK=sdb1\n")
  44. else:
  45. config.write("DISK=sda\n")
  46. config.write("KERNEL_BOOTSTRAP=True\n")
  47. else:
  48. config.write("DISK=sda1\n")
  49. config.write("KERNEL_BOOTSTRAP=False\n")
  50. config.write(f"BUILD_KERNELS={args.update_checksums or args.build_kernels}\n")
  51. config.write(f"CONFIGURATOR={args.configurator}\n")
  52. if not args.external_sources:
  53. if args.mirrors:
  54. config.write(f'MIRRORS="{" ".join(args.mirrors)}"\n')
  55. config.write(f"MIRRORS_LEN={len(args.mirrors)}\n")
  56. else:
  57. config.write("MIRRORS_LEN=0\n")
  58. # pylint: disable=too-many-statements,too-many-branches
  59. def main():
  60. """
  61. A few command line arguments to customize bootstrap.
  62. This function also creates object which prepares directory
  63. structure with bootstrap seeds and all sources.
  64. """
  65. parser = argparse.ArgumentParser()
  66. parser.add_argument("-a", "--arch", help="Bootstrap architecture",
  67. default="x86")
  68. parser.add_argument("-c", "--chroot", help="Run inside chroot",
  69. action="store_true")
  70. parser.add_argument("-bw", "--bwrap", help="Run inside a bwrap sandbox",
  71. action="store_true")
  72. parser.add_argument("-t", "--target", help="Target directory",
  73. default="target")
  74. parser.add_argument("--tmpfs", help="Use a tmpfs on target",
  75. action="store_true")
  76. parser.add_argument("--tmpfs-size", help="Size of the tmpfs",
  77. default="8G")
  78. parser.add_argument("--cores", help="Cores to use for building",
  79. default=2)
  80. parser.add_argument("--force-timestamps",
  81. help="Force all files timestamps to be 0 unix time",
  82. action="store_true")
  83. parser.add_argument("--update-checksums",
  84. help="Update checksum files",
  85. action="store_true")
  86. parser.add_argument("--external-sources",
  87. help="Download sources externally from live-bootstrap",
  88. action="store_true")
  89. parser.add_argument("--build-kernels",
  90. help="Also build kernels in chroot and bwrap builds",
  91. action="store_true")
  92. parser.add_argument("--no-create-config",
  93. help="Do not automatically create config file",
  94. action="store_true")
  95. parser.add_argument("-i", "--interactive",
  96. help="Use interactive prompts to resolve issues during bootstrap",
  97. action="store_true")
  98. parser.add_argument("--configurator",
  99. help="Run the interactive configurator",
  100. action="store_true")
  101. parser.add_argument("-m", "--mirrors",
  102. help="Mirrors to download distfiles from",
  103. nargs='+')
  104. parser.add_argument("-r", "--repo",
  105. help="Path to prebuilt binary packages", nargs=None)
  106. parser.add_argument("--early-preseed",
  107. help="Skip early stages of live-bootstrap", nargs=None)
  108. parser.add_argument("--internal-ci", help="INTERNAL for github CI")
  109. parser.add_argument("-s", "--swap", help="Swap space to allocate in Linux",
  110. default=0)
  111. # QEMU arguments
  112. parser.add_argument("-q", "--qemu", help="Use QEMU",
  113. action="store_true")
  114. parser.add_argument("-qc", "--qemu-cmd", help="QEMU command to run",
  115. default="qemu-system-x86_64")
  116. parser.add_argument("-qr", "--qemu-ram", help="Memory (in megabytes) allocated to QEMU VM",
  117. default=4096)
  118. parser.add_argument("-qs", "--target-size", help="Size of the target image (for QEMU only)",
  119. default="16G")
  120. parser.add_argument("-qk", "--kernel", help="Custom early kernel to use")
  121. parser.add_argument("-b", "--bare-metal", help="Build images for bare metal",
  122. action="store_true")
  123. args = parser.parse_args()
  124. # Mode validation
  125. def check_types():
  126. count = 0
  127. if args.qemu:
  128. count += 1
  129. if args.chroot:
  130. count += 1
  131. if args.bwrap:
  132. count += 1
  133. if args.bare_metal:
  134. count += 1
  135. return count
  136. if check_types() > 1:
  137. raise ValueError("No more than one of qemu, chroot, bwrap, bare metal"
  138. "may be used.")
  139. if check_types() == 0:
  140. raise ValueError("One of qemu, chroot, bwrap, or bare metal must be selected.")
  141. # Arch validation
  142. if args.arch != "x86":
  143. print("Only x86 is supported at the moment, other arches are for development only.")
  144. # Tmpfs validation
  145. if args.bwrap and args.tmpfs:
  146. raise ValueError("tmpfs cannot be used with bwrap.")
  147. # Cores validation
  148. if int(args.cores) < 1:
  149. raise ValueError("Must use one or more cores.")
  150. # Target image size validation
  151. if args.qemu:
  152. if int(str(args.target_size).rstrip('gGmM')) < 1:
  153. raise ValueError("Please specify a positive target size for qemu.")
  154. args.target_size = (int(str(args.target_size).rstrip('gGmM')) *
  155. (1024 if str(args.target_size).lower().endswith('g') else 1))
  156. else:
  157. args.target_size = 0
  158. # Swap file size validation
  159. if args.qemu or args.bare_metal:
  160. args.swap = (int(str(args.swap).rstrip('gGmM')) *
  161. (1024 if str(args.swap).lower().endswith('g') else 1))
  162. else:
  163. args.swap = 0
  164. # Set constant umask
  165. os.umask(0o022)
  166. # file:// mirrors
  167. mirror_servers = []
  168. if args.mirrors:
  169. for i, mirror in enumerate(args.mirrors):
  170. if mirror.startswith("file://"):
  171. path = mirror.removeprefix("file://")
  172. if not path.startswith("/"):
  173. raise ValueError("A file:// mirror must be an absolute path.")
  174. server = SimpleMirror(path)
  175. args.mirrors[i] = f"http://127.0.0.1:{server.port}"
  176. mirror_servers.append(server)
  177. # bootstrap.cfg
  178. try:
  179. os.remove(os.path.join('steps', 'bootstrap.cfg'))
  180. except FileNotFoundError:
  181. pass
  182. if not args.no_create_config:
  183. create_configuration_file(args)
  184. else:
  185. with open(os.path.join('steps', 'bootstrap.cfg'), 'a', encoding='UTF-8'):
  186. pass
  187. # target
  188. target = Target(path=args.target)
  189. if args.tmpfs:
  190. target.tmpfs(size=args.tmpfs_size)
  191. for server in mirror_servers:
  192. thread = threading.Thread(target=server.serve_forever)
  193. thread.start()
  194. def cleanup(*_):
  195. for server in mirror_servers:
  196. server.shutdown()
  197. signal.signal(signal.SIGINT, cleanup)
  198. generator = Generator(arch=args.arch,
  199. external_sources=args.external_sources,
  200. repo_path=args.repo,
  201. early_preseed=args.early_preseed,
  202. mirrors=args.mirrors)
  203. bootstrap(args, generator, target, args.target_size, cleanup)
  204. cleanup()
  205. def bootstrap(args, generator, target, size, cleanup):
  206. """Kick off bootstrap process."""
  207. print(f"Bootstrapping {args.arch}", flush=True)
  208. if args.chroot:
  209. find_chroot = """
  210. import shutil
  211. print(shutil.which('chroot'))
  212. """
  213. chroot_binary = run_as_root('python3', '-c', find_chroot,
  214. capture_output=True).stdout.decode().strip()
  215. generator.prepare(target, using_kernel=False)
  216. arch = stage0_arch_map.get(args.arch, args.arch)
  217. init = os.path.join(os.sep, 'bootstrap-seeds', 'POSIX', arch, 'kaem-optional-seed')
  218. run_as_root('env', '-i', 'PATH=/bin', chroot_binary, generator.target_dir, init,
  219. cleanup=cleanup)
  220. elif args.bwrap:
  221. init = '/init'
  222. if not args.internal_ci or args.internal_ci == "pass1":
  223. generator.prepare(target, using_kernel=False)
  224. arch = stage0_arch_map.get(args.arch, args.arch)
  225. init = os.path.join(os.sep, 'bootstrap-seeds', 'POSIX', arch, 'kaem-optional-seed')
  226. else:
  227. generator.reuse(target)
  228. run('env', '-i', 'bwrap', '--unshare-user',
  229. '--uid', '0',
  230. '--gid', '0',
  231. '--unshare-net' if args.external_sources else None,
  232. '--setenv', 'PATH', '/usr/bin',
  233. '--bind', generator.target_dir, '/',
  234. '--dir', '/dev',
  235. '--dev-bind', '/dev/null', '/dev/null',
  236. '--dev-bind', '/dev/zero', '/dev/zero',
  237. '--dev-bind', '/dev/random', '/dev/random',
  238. '--dev-bind', '/dev/urandom', '/dev/urandom',
  239. '--dev-bind', '/dev/ptmx', '/dev/ptmx',
  240. '--dev-bind', '/dev/tty', '/dev/tty',
  241. '--tmpfs', '/dev/shm',
  242. '--proc', '/proc',
  243. '--bind', '/sys', '/sys',
  244. '--tmpfs', '/tmp',
  245. init,
  246. cleanup=cleanup)
  247. elif args.bare_metal:
  248. if args.kernel:
  249. generator.prepare(target, using_kernel=True, target_size=size)
  250. path = os.path.join(args.target, os.path.relpath(generator.target_dir, args.target))
  251. print("Please:")
  252. print(f" 1. Take {path}/initramfs and your kernel, boot using this.")
  253. print(f" 2. Take {path}/disk.img and put this on a writable storage medium.")
  254. else:
  255. generator.prepare(target, kernel_bootstrap=True, target_size=size)
  256. path = os.path.join(args.target, os.path.relpath(generator.target_dir, args.target))
  257. print("Please:")
  258. print(f" 1. Take {path}.img and write it to a boot drive and then boot it.")
  259. else:
  260. if args.kernel:
  261. generator.prepare(target, using_kernel=True, target_size=size)
  262. arg_list = [
  263. '-enable-kvm',
  264. '-m', str(args.qemu_ram) + 'M',
  265. '-smp', str(args.cores),
  266. '-drive', 'file=' + target.get_disk("disk") + ',format=raw'
  267. ]
  268. if target.get_disk("external") is not None:
  269. arg_list += [
  270. '-drive', 'file=' + target.get_disk("external") + ',format=raw',
  271. ]
  272. arg_list += [
  273. '-nic', 'user,ipv6=off,model=e1000',
  274. '-kernel', args.kernel,
  275. '-append',
  276. ]
  277. if args.interactive:
  278. arg_list += ['consoleblank=0 earlyprintk=vga root=/dev/sda1 '
  279. 'rootfstype=ext3 init=/init rw']
  280. else:
  281. arg_list += ['console=ttyS0 earlycon=uart8250,io,0x3f8,115200n8 '
  282. 'root=/dev/sda1 rootfstype=ext3 init=/init rw']
  283. else:
  284. generator.prepare(target, kernel_bootstrap=True, target_size=size)
  285. arg_list = [
  286. '-enable-kvm',
  287. '-m', str(args.qemu_ram) + 'M',
  288. '-smp', str(args.cores),
  289. '-drive', 'file=' + generator.target_dir + '.img' + ',format=raw'
  290. ]
  291. if target.get_disk("external") is not None:
  292. arg_list += [
  293. '-drive', 'file=' + target.get_disk("external") + ',format=raw',
  294. ]
  295. arg_list += [
  296. '-machine', 'kernel-irqchip=split',
  297. '-nic', 'user,ipv6=off,model=e1000'
  298. ]
  299. if not args.interactive:
  300. arg_list += ['-no-reboot', '-nographic']
  301. run(args.qemu_cmd, *arg_list, cleanup=cleanup)
  302. if __name__ == "__main__":
  303. main()