logo

live-bootstrap

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

generator.py (14863B)


  1. #!/usr/bin/env python3
  2. """
  3. This file contains all code required to generate the boot image for live-bootstrap
  4. """
  5. # SPDX-License-Identifier: GPL-3.0-or-later
  6. # SPDX-FileCopyrightText: 2022-2023 Dor Askayo <dor.askayo@gmail.com>
  7. # SPDX-FileCopyrightText: 2021 Andrius Štikonas <andrius@stikonas.eu>
  8. # SPDX-FileCopyrightText: 2021 Melg Eight <public.melg8@gmail.com>
  9. # SPDX-FileCopyrightText: 2021-23 fosslinux <fosslinux@aussies.space>
  10. import hashlib
  11. import os
  12. import shutil
  13. import tarfile
  14. import traceback
  15. import requests
  16. # pylint: disable=too-many-instance-attributes
  17. class Generator():
  18. """
  19. Class responsible for generating the basic media to be consumed.
  20. """
  21. git_dir = os.path.join(os.path.dirname(os.path.join(__file__)), '..')
  22. distfiles_dir = os.path.join(git_dir, 'distfiles')
  23. def __init__(self, arch, external_sources, early_preseed, repo_path):
  24. self.arch = arch
  25. self.early_preseed = early_preseed
  26. self.external_sources = external_sources
  27. self.repo_path = repo_path
  28. self.source_manifest = self.get_source_manifest(not self.external_sources)
  29. self.early_source_manifest = self.get_source_manifest(True)
  30. self.target_dir = None
  31. self.external_dir = None
  32. def reuse(self, target):
  33. """
  34. Reuse a previously prepared bwrap environment for further stages.
  35. """
  36. self.target_dir = target.path
  37. self.external_dir = os.path.join(self.target_dir, 'external')
  38. self.distfiles()
  39. def prepare(self, target, using_kernel=False, kernel_bootstrap=False, target_size=0):
  40. """
  41. Prepare basic media of live-bootstrap.
  42. /steps -- contains steps to be built
  43. / -- contains seed to allow steps to be built, containing custom
  44. scripts and stage0-posix
  45. """
  46. self.target_dir = target.path
  47. self.external_dir = os.path.join(self.target_dir, 'external')
  48. # We use ext3 here; ext4 actually has a variety of extensions that
  49. # have been added with varying levels of recency
  50. # Linux 4.9.10 does not support a bunch of them
  51. # Attempting to disable extensions that a particular e2fsprogs
  52. # is *unaware* of causes the filesystem creation to fail
  53. # We could hypothetically detect e2fsprogs version and create an
  54. # argument matrix ... or we could just use ext3 instead which
  55. # is effectively universally the same
  56. if kernel_bootstrap:
  57. self.target_dir = os.path.join(self.target_dir, 'init')
  58. os.mkdir(self.target_dir)
  59. if not self.repo_path and not self.external_sources:
  60. self.external_dir = os.path.join(self.target_dir, 'external')
  61. elif using_kernel:
  62. self.target_dir = os.path.join(self.target_dir, 'disk')
  63. self.external_dir = os.path.join(self.target_dir, 'external')
  64. if self.early_preseed:
  65. # Extract tar containing preseed
  66. with tarfile.open(self.early_preseed, "r") as seed:
  67. seed.extractall(self.target_dir)
  68. if os.path.exists(os.path.join(self.target_dir, 'steps')):
  69. shutil.rmtree(os.path.join(self.target_dir, 'steps'))
  70. if os.path.exists(self.external_dir):
  71. shutil.rmtree(self.external_dir)
  72. shutil.copy2(os.path.join(self.git_dir, 'seed', 'preseeded.kaem'),
  73. os.path.join(self.target_dir, 'kaem.x86'))
  74. else:
  75. self.stage0_posix(kernel_bootstrap)
  76. self.seed()
  77. os.makedirs(self.external_dir)
  78. self.steps()
  79. self.distfiles()
  80. if self.repo_path:
  81. repo_dir = os.path.join(self.external_dir, 'repo-preseeded')
  82. shutil.copytree(self.repo_path, repo_dir)
  83. if kernel_bootstrap:
  84. self.create_builder_hex0_disk_image(self.target_dir + '.img', target_size)
  85. if self.repo_path or self.external_sources:
  86. mkfs_args = ['-d', os.path.join(target.path, 'external')]
  87. target.add_disk("external", filesystem="ext3", mkfs_args=mkfs_args)
  88. elif using_kernel:
  89. mkfs_args = ['-F', '-d', os.path.join(target.path, 'disk')]
  90. target.add_disk("disk",
  91. filesystem="ext3",
  92. size=(str(target_size) + "M") if target_size else "16G",
  93. bootable=True,
  94. mkfs_args=mkfs_args)
  95. def steps(self):
  96. """Copy in steps."""
  97. self.get_packages()
  98. shutil.copytree(os.path.join(self.git_dir, 'steps'), os.path.join(self.target_dir, 'steps'))
  99. def stage0_posix(self, kernel_bootstrap=False):
  100. """Copy in all of the stage0-posix"""
  101. stage0_posix_base_dir = os.path.join(self.git_dir, 'seed', 'stage0-posix')
  102. for entry in os.listdir(stage0_posix_base_dir):
  103. if kernel_bootstrap and entry == 'bootstrap-seeds':
  104. continue
  105. orig = os.path.join(stage0_posix_base_dir, entry)
  106. target = os.path.join(self.target_dir, entry)
  107. if os.path.isfile(orig):
  108. shutil.copy2(orig, target)
  109. else:
  110. shutil.copytree(orig, target)
  111. if not kernel_bootstrap:
  112. arch = stage0_arch_map.get(self.arch, self.arch)
  113. kaem_optional_seed = os.path.join(self.git_dir, 'seed', 'stage0-posix',
  114. 'bootstrap-seeds', 'POSIX', arch,
  115. 'kaem-optional-seed')
  116. shutil.copy2(kaem_optional_seed, os.path.join(self.target_dir, 'init'))
  117. def seed(self):
  118. """Copy in extra seed files"""
  119. seed_dir = os.path.join(self.git_dir, 'seed')
  120. for entry in os.listdir(seed_dir):
  121. if os.path.isfile(os.path.join(seed_dir, entry)):
  122. shutil.copy2(os.path.join(seed_dir, entry), os.path.join(self.target_dir, entry))
  123. def distfiles(self):
  124. """Copy in distfiles"""
  125. def copy_no_network_distfiles(out, early):
  126. # Note that "no disk" implies "no network" for kernel bootstrap mode
  127. manifest = self.early_source_manifest if early else self.source_manifest
  128. for file in manifest:
  129. file = file[3].strip()
  130. shutil.copy2(os.path.join(self.distfiles_dir, file),
  131. os.path.join(out, file))
  132. early_distfile_dir = os.path.join(self.target_dir, 'external', 'distfiles')
  133. main_distfile_dir = os.path.join(self.external_dir, 'distfiles')
  134. if early_distfile_dir != main_distfile_dir:
  135. os.makedirs(early_distfile_dir, exist_ok=True)
  136. copy_no_network_distfiles(early_distfile_dir, True)
  137. if self.external_sources:
  138. shutil.copytree(self.distfiles_dir, main_distfile_dir, dirs_exist_ok=True)
  139. else:
  140. os.mkdir(main_distfile_dir)
  141. copy_no_network_distfiles(main_distfile_dir, False)
  142. @staticmethod
  143. def output_dir(srcfs_file, dirpath):
  144. """Add a directory to srcfs file system"""
  145. srcline = f"src 0 {dirpath}\n"
  146. srcfs_file.write(srcline.encode())
  147. @staticmethod
  148. def output_file(srcfs_file, filepath):
  149. """Add a file to srcfs file system"""
  150. srcline = f"src {os.path.getsize(filepath)} {filepath}\n"
  151. srcfs_file.write(srcline.encode())
  152. with open(filepath, 'rb') as srcfile:
  153. srcfs_file.write(srcfile.read())
  154. def output_tree(self, srcfs_file, treepath):
  155. """Add a tree of files to srcfs file system"""
  156. self.output_dir(srcfs_file, treepath)
  157. for root, dirs, files in os.walk(treepath):
  158. if ".git" in root:
  159. continue
  160. for dirpath in dirs:
  161. if ".git" in dirpath:
  162. continue
  163. self.output_dir(srcfs_file, os.path.join(root, dirpath))
  164. for filepath in files:
  165. if ".git" in filepath:
  166. continue
  167. self.output_file(srcfs_file, os.path.join(root, filepath))
  168. def append_srcfs(self, image_file):
  169. """Append srcfs file system to disk image"""
  170. save_cwd = os.getcwd()
  171. os.chdir(self.target_dir)
  172. self.output_tree(image_file, '.')
  173. # Add commands to kick off stage0-posix
  174. cmd = ' '.join(['src',
  175. '0',
  176. '/bootstrap-seeds\n'])
  177. image_file.write(cmd.encode())
  178. cmd = ' '.join(['src',
  179. '0',
  180. '/bootstrap-seeds/POSIX\n'])
  181. image_file.write(cmd.encode())
  182. cmd = ' '.join(['src',
  183. '0',
  184. '/bootstrap-seeds/POSIX/x86\n'])
  185. image_file.write(cmd.encode())
  186. cmd = ' '.join(['hex0',
  187. '/x86/hex0_x86.hex0',
  188. '/bootstrap-seeds/POSIX/x86/hex0-seed\n'])
  189. image_file.write(cmd.encode())
  190. cmd = ' '.join(['hex0',
  191. '/x86/kaem-minimal.hex0',
  192. '/bootstrap-seeds/POSIX/x86/kaem-optional-seed\n'])
  193. image_file.write(cmd.encode())
  194. cmd = ' '.join(['hex0',
  195. '/x86/kaem-minimal.hex0',
  196. '/init\n'])
  197. image_file.write(cmd.encode())
  198. cmd = ' '.join(['/bootstrap-seeds/POSIX/x86/kaem-optional-seed', '/kaem.x86\n'])
  199. image_file.write(cmd.encode())
  200. os.chdir(save_cwd)
  201. def create_builder_hex0_disk_image(self, image_file_name, size):
  202. """Create builder-hex0 disk image"""
  203. with open(image_file_name, 'ab') as image_file:
  204. # Compile and write stage1 binary seed
  205. with open(os.path.join('builder-hex0', 'builder-hex0-x86-stage1.hex0'),
  206. encoding="utf-8") as infile:
  207. for line in infile:
  208. image_file.write(bytes.fromhex(line.split('#')[0].split(';')[0].strip()))
  209. # Append stage2 hex0 source
  210. with open(os.path.join('builder-hex0', 'builder-hex0-x86-stage2.hex0'),
  211. encoding="utf-8") as infile:
  212. image_file.write(infile.read().encode())
  213. # Pad to next sector
  214. current_size = os.stat(image_file_name).st_size
  215. while current_size % 512 != 0:
  216. image_file.write(b'\0')
  217. current_size += 1
  218. self.append_srcfs(image_file)
  219. current_size = os.stat(image_file_name).st_size
  220. megabyte = 1024 * 1024
  221. # fill file with zeros up to next megabyte
  222. extra = current_size % megabyte
  223. round_up = megabyte - extra
  224. with open(image_file_name, 'ab') as image_file:
  225. image_file.write(b'\0' * round_up)
  226. current_size += round_up
  227. # extend file up to desired size
  228. if current_size < size * megabyte:
  229. with open(image_file_name, 'ab') as image_file:
  230. image_file.truncate(size * megabyte)
  231. @staticmethod
  232. def check_file(file_name, expected_hash):
  233. """Check hash of downloaded source file."""
  234. with open(file_name, "rb") as downloaded_file:
  235. downloaded_content = downloaded_file.read() # read entire file as bytes
  236. readable_hash = hashlib.sha256(downloaded_content).hexdigest()
  237. if expected_hash == readable_hash:
  238. return
  239. raise ValueError(f"Checksum mismatch for file {os.path.basename(file_name)}:\n\
  240. expected: {expected_hash}\n\
  241. actual: {readable_hash}\n\
  242. When in doubt, try deleting the file in question -- it will be downloaded again when running \
  243. this script the next time")
  244. @staticmethod
  245. def download_file(url, directory, file_name, silent=False):
  246. """
  247. Download a single source archive.
  248. """
  249. abs_file_name = os.path.join(directory, file_name)
  250. # Create a directory for downloaded file
  251. if not os.path.isdir(directory):
  252. os.mkdir(directory)
  253. # Actually download the file
  254. headers = {
  255. "Accept-Encoding": "identity",
  256. "User-Agent": "curl/7.88.1"
  257. }
  258. if not os.path.isfile(abs_file_name):
  259. if not silent:
  260. print(f"Downloading: {file_name}")
  261. response = requests.get(url, allow_redirects=True, stream=True,
  262. headers=headers, timeout=20)
  263. if response.status_code == 200:
  264. with open(abs_file_name, 'wb') as target_file:
  265. target_file.write(response.raw.read())
  266. else:
  267. raise requests.HTTPError("Download failed: HTTP " +
  268. str(response.status_code) + " " + response.reason)
  269. return abs_file_name
  270. def get_packages(self):
  271. """Prepare remaining sources"""
  272. for line in self.source_manifest:
  273. try:
  274. path = self.download_file(line[2], line[1], line[3])
  275. except requests.HTTPError:
  276. print(traceback.format_exc())
  277. for line in self.source_manifest:
  278. path = os.path.join(line[1], line[3])
  279. self.check_file(path, line[0])
  280. @classmethod
  281. def get_source_manifest(cls, pre_network=False):
  282. """
  283. Generate a source manifest for the system.
  284. """
  285. entries = []
  286. directory = os.path.relpath(cls.distfiles_dir, cls.git_dir)
  287. # Find all source files
  288. steps_dir = os.path.join(cls.git_dir, 'steps')
  289. with open(os.path.join(steps_dir, 'manifest'), 'r', encoding="utf_8") as file:
  290. for line in file:
  291. if pre_network and line.strip().startswith("improve: ") and "network" in line:
  292. break
  293. if not line.strip().startswith("build: "):
  294. continue
  295. step = line.split(" ")[1].split("#")[0].strip()
  296. sourcef = os.path.join(steps_dir, step, "sources")
  297. if os.path.exists(sourcef):
  298. # Read sources from the source file
  299. with open(sourcef, "r", encoding="utf_8") as sources:
  300. for source in sources.readlines():
  301. source = source.strip().split(" ")
  302. if len(source) > 2:
  303. file_name = source[2]
  304. else:
  305. # Automatically determine file name based on URL.
  306. file_name = os.path.basename(source[0])
  307. entry = (source[1], directory, source[0], file_name)
  308. if entry not in entries:
  309. entries.append(entry)
  310. return entries
  311. stage0_arch_map = {
  312. "amd64": "AMD64",
  313. }