logo

live-bootstrap

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

generator.py (15743B)


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