diff options
Diffstat (limited to 'scripts/runqemu')
-rwxr-xr-x | scripts/runqemu | 159 |
1 files changed, 97 insertions, 62 deletions
diff --git a/scripts/runqemu b/scripts/runqemu index 69cd44864e..2be7a0f286 100755 --- a/scripts/runqemu +++ b/scripts/runqemu | |||
@@ -197,11 +197,13 @@ class BaseConfig(object): | |||
197 | self.portlocks = {} | 197 | self.portlocks = {} |
198 | self.bitbake_e = '' | 198 | self.bitbake_e = '' |
199 | self.snapshot = False | 199 | self.snapshot = False |
200 | self.wictypes = ('wic', 'wic.vmdk', 'wic.qcow2', 'wic.vdi', "wic.vhd", "wic.vhdx") | 200 | self.wictypes = ('wic.zst', 'wic', 'wic.vmdk', 'wic.qcow2', 'wic.vdi', "wic.vhd", "wic.vhdx") |
201 | self.fstypes = ('ext2', 'ext3', 'ext4', 'jffs2', 'nfs', 'btrfs', | 201 | self.fstypes = ('ext2', 'ext3', 'ext4', 'ext2.zst', 'ext3.zst', 'ext4.zst', |
202 | 'cpio.gz', 'cpio', 'ramfs', 'tar.bz2', 'tar.gz', | 202 | 'jffs2', 'nfs', 'btrfs', 'cpio.gz', 'cpio', 'ramfs', |
203 | 'tar.bz2', 'tar.gz', 'tar.zst', | ||
203 | 'squashfs', 'squashfs-xz', 'squashfs-lzo', | 204 | 'squashfs', 'squashfs-xz', 'squashfs-lzo', |
204 | 'squashfs-lz4', 'squashfs-zst') | 205 | 'squashfs-lz4', 'squashfs-zst', |
206 | 'erofs', 'erofs-lz4', 'erofs-lz4hc') | ||
205 | self.vmtypes = ('hddimg', 'iso') | 207 | self.vmtypes = ('hddimg', 'iso') |
206 | self.fsinfo = {} | 208 | self.fsinfo = {} |
207 | self.network_device = "-device e1000,netdev=net0,mac=@MAC@" | 209 | self.network_device = "-device e1000,netdev=net0,mac=@MAC@" |
@@ -368,19 +370,20 @@ class BaseConfig(object): | |||
368 | - Check whether it is an NFS dir | 370 | - Check whether it is an NFS dir |
369 | - Check whether it is an OVMF flash file | 371 | - Check whether it is an OVMF flash file |
370 | """ | 372 | """ |
373 | n = os.path.basename(p) | ||
371 | if p.endswith('.qemuboot.conf'): | 374 | if p.endswith('.qemuboot.conf'): |
372 | self.qemuboot = p | 375 | self.qemuboot = p |
373 | self.qbconfload = True | 376 | self.qbconfload = True |
374 | elif re.search('\\.bin$', p) or re.search('bzImage', p) or \ | 377 | elif re.search('\\.bin$', n) or re.search('bzImage', n) or \ |
375 | re.search('zImage', p) or re.search('vmlinux', p) or \ | 378 | re.search('zImage', n) or re.search('vmlinux', n) or \ |
376 | re.search('fitImage', p) or re.search('uImage', p): | 379 | re.search('fitImage', n) or re.search('uImage', n): |
377 | self.kernel = p | 380 | self.kernel = p |
378 | elif os.path.isfile(p) and ('-image-' in os.path.basename(p) or '.rootfs.' in os.path.basename(p)): | 381 | elif os.path.isfile(p) and ('-image-' in os.path.basename(p) or '.rootfs.' in os.path.basename(p)): |
379 | self.rootfs = p | 382 | self.rootfs = p |
380 | # Check filename against self.fstypes can handle <file>.cpio.gz, | 383 | # Check filename against self.fstypes can handle <file>.cpio.gz, |
381 | # otherwise, its type would be "gz", which is incorrect. | 384 | # otherwise, its type would be "gz", which is incorrect. |
382 | fst = "" | 385 | fst = "" |
383 | for t in self.fstypes: | 386 | for t in self.fstypes + self.vmtypes + self.wictypes: |
384 | if p.endswith(t): | 387 | if p.endswith(t): |
385 | fst = t | 388 | fst = t |
386 | break | 389 | break |
@@ -418,6 +421,46 @@ class BaseConfig(object): | |||
418 | else: | 421 | else: |
419 | raise RunQemuError("Unknown path arg %s" % p) | 422 | raise RunQemuError("Unknown path arg %s" % p) |
420 | 423 | ||
424 | def uncompress_rootfs(self): | ||
425 | """Decompress ZST rootfs image if needed""" | ||
426 | if not self.rootfs or not self.fstype.endswith('.zst'): | ||
427 | return | ||
428 | |||
429 | # Ensure snapshot mode is active before allowing decompression. | ||
430 | if not self.snapshot: | ||
431 | raise RunQemuError(".zst images are only supported with snapshot mode. " \ | ||
432 | "You can either use the \"snapshot\" option or use an uncompressed image.") | ||
433 | |||
434 | # Get the real path to the image to avoid issues when a symbolic link is passed. | ||
435 | # This ensures we always operate on the actual file. | ||
436 | image_path = os.path.realpath(self.rootfs) | ||
437 | # Extract target filename by removing .zst | ||
438 | image_dir = os.path.dirname(image_path) | ||
439 | uncompressed_name = os.path.basename(image_path).replace(".zst", "") | ||
440 | uncompressed_path = os.path.join(image_dir, uncompressed_name) | ||
441 | |||
442 | # If the decompressed image already exists (e.g., in the deploy directory), | ||
443 | # we use it directly to avoid overwriting artifacts generated by the build system. | ||
444 | # This prevents redundant decompression and preserves build outputs. | ||
445 | if os.path.exists(uncompressed_path): | ||
446 | logger.warning(f"Found existing decompressed image: {uncompressed_path}, Using it directly.") | ||
447 | else: | ||
448 | logger.info(f"Decompressing {self.rootfs} to {uncompressed_path}") | ||
449 | # Ensure the 'zstd' tool is installed before attempting to decompress. | ||
450 | if not shutil.which('zstd'): | ||
451 | raise RunQemuError(f"'zstd' is required to decompress {self.rootfs} but was not found in PATH") | ||
452 | try: | ||
453 | subprocess.check_call(['zstd', '-d', image_path, '-o', uncompressed_path]) | ||
454 | except subprocess.CalledProcessError as e: | ||
455 | raise RunQemuError(f"Failed to decompress {self.rootfs}: {e}") | ||
456 | finally: | ||
457 | # Mark temporary file for deletion | ||
458 | self.cleanup_files.append(uncompressed_path) | ||
459 | |||
460 | # Use the decompressed image as the rootfs | ||
461 | self.rootfs = uncompressed_path | ||
462 | self.fstype = self.fstype.removesuffix(".zst") | ||
463 | |||
421 | def check_arg_machine(self, arg): | 464 | def check_arg_machine(self, arg): |
422 | """Check whether it is a machine""" | 465 | """Check whether it is a machine""" |
423 | if self.get('MACHINE') == arg: | 466 | if self.get('MACHINE') == arg: |
@@ -468,9 +511,11 @@ class BaseConfig(object): | |||
468 | self.set("IMAGE_LINK_NAME", image_link_name) | 511 | self.set("IMAGE_LINK_NAME", image_link_name) |
469 | logger.debug('Using IMAGE_LINK_NAME = "%s"' % image_link_name) | 512 | logger.debug('Using IMAGE_LINK_NAME = "%s"' % image_link_name) |
470 | 513 | ||
471 | def set_dri_path(self): | 514 | def set_mesa_paths(self): |
472 | drivers_path = os.path.join(self.bindir_native, '../lib/dri') | 515 | drivers_path = os.path.join(self.bindir_native, '../lib/dri') |
473 | if not os.path.exists(drivers_path) or not os.listdir(drivers_path): | 516 | gbm_path = os.path.join(self.bindir_native, '../lib/gbm') |
517 | if not os.path.exists(drivers_path) or not os.listdir(drivers_path) \ | ||
518 | or not os.path.exists(gbm_path) or not os.listdir(gbm_path): | ||
474 | raise RunQemuError(""" | 519 | raise RunQemuError(""" |
475 | qemu has been built without opengl support and accelerated graphics support is not available. | 520 | qemu has been built without opengl support and accelerated graphics support is not available. |
476 | To enable it, add: | 521 | To enable it, add: |
@@ -479,6 +524,7 @@ DISTRO_FEATURES_NATIVESDK:append = " opengl" | |||
479 | to your build configuration. | 524 | to your build configuration. |
480 | """) | 525 | """) |
481 | self.qemu_environ['LIBGL_DRIVERS_PATH'] = drivers_path | 526 | self.qemu_environ['LIBGL_DRIVERS_PATH'] = drivers_path |
527 | self.qemu_environ['GBM_BACKENDS_PATH'] = gbm_path | ||
482 | 528 | ||
483 | def check_args(self): | 529 | def check_args(self): |
484 | for debug in ("-d", "--debug"): | 530 | for debug in ("-d", "--debug"): |
@@ -961,34 +1007,12 @@ to your build configuration. | |||
961 | if not self.bitbake_e: | 1007 | if not self.bitbake_e: |
962 | self.load_bitbake_env() | 1008 | self.load_bitbake_env() |
963 | 1009 | ||
964 | if self.bitbake_e: | 1010 | native_vars = ['STAGING_DIR_NATIVE'] |
965 | native_vars = ['STAGING_DIR_NATIVE'] | 1011 | for nv in native_vars: |
966 | for nv in native_vars: | 1012 | s = re.search('^%s="(.*)"' % nv, self.bitbake_e, re.M) |
967 | s = re.search('^%s="(.*)"' % nv, self.bitbake_e, re.M) | 1013 | if s and s.group(1) != self.get(nv): |
968 | if s and s.group(1) != self.get(nv): | 1014 | logger.info('Overriding conf file setting of %s to %s from Bitbake environment' % (nv, s.group(1))) |
969 | logger.info('Overriding conf file setting of %s to %s from Bitbake environment' % (nv, s.group(1))) | 1015 | self.set(nv, s.group(1)) |
970 | self.set(nv, s.group(1)) | ||
971 | else: | ||
972 | # when we're invoked from a running bitbake instance we won't | ||
973 | # be able to call `bitbake -e`, then try: | ||
974 | # - get OE_TMPDIR from environment and guess paths based on it | ||
975 | # - get OECORE_NATIVE_SYSROOT from environment (for sdk) | ||
976 | tmpdir = self.get('OE_TMPDIR') | ||
977 | oecore_native_sysroot = self.get('OECORE_NATIVE_SYSROOT') | ||
978 | if tmpdir: | ||
979 | logger.info('Setting STAGING_DIR_NATIVE and STAGING_BINDIR_NATIVE relative to OE_TMPDIR (%s)' % tmpdir) | ||
980 | hostos, _, _, _, machine = os.uname() | ||
981 | buildsys = '%s-%s' % (machine, hostos.lower()) | ||
982 | staging_dir_native = '%s/sysroots/%s' % (tmpdir, buildsys) | ||
983 | self.set('STAGING_DIR_NATIVE', staging_dir_native) | ||
984 | elif oecore_native_sysroot: | ||
985 | logger.info('Setting STAGING_DIR_NATIVE to OECORE_NATIVE_SYSROOT (%s)' % oecore_native_sysroot) | ||
986 | self.set('STAGING_DIR_NATIVE', oecore_native_sysroot) | ||
987 | if self.get('STAGING_DIR_NATIVE'): | ||
988 | # we have to assume that STAGING_BINDIR_NATIVE is at usr/bin | ||
989 | staging_bindir_native = '%s/usr/bin' % self.get('STAGING_DIR_NATIVE') | ||
990 | logger.info('Setting STAGING_BINDIR_NATIVE to %s' % staging_bindir_native) | ||
991 | self.set('STAGING_BINDIR_NATIVE', '%s/usr/bin' % self.get('STAGING_DIR_NATIVE')) | ||
992 | 1016 | ||
993 | def print_config(self): | 1017 | def print_config(self): |
994 | logoutput = ['Continuing with the following parameters:'] | 1018 | logoutput = ['Continuing with the following parameters:'] |
@@ -1008,6 +1032,9 @@ to your build configuration. | |||
1008 | logoutput.append('NFS_DIR: [%s]' % self.rootfs) | 1032 | logoutput.append('NFS_DIR: [%s]' % self.rootfs) |
1009 | else: | 1033 | else: |
1010 | logoutput.append('ROOTFS: [%s]' % self.rootfs) | 1034 | logoutput.append('ROOTFS: [%s]' % self.rootfs) |
1035 | logoutput.append('SNAPSHOT: [%s]' % | ||
1036 | "Enabled. Changes on rootfs won't be kept after QEMU shutdown." if self.snapshot | ||
1037 | else "Disabled. Changes on rootfs will be kept after QEMU shutdown.") | ||
1011 | if self.ovmf_bios: | 1038 | if self.ovmf_bios: |
1012 | logoutput.append('OVMF: %s' % self.ovmf_bios) | 1039 | logoutput.append('OVMF: %s' % self.ovmf_bios) |
1013 | if (self.ovmf_secboot_pkkek1): | 1040 | if (self.ovmf_secboot_pkkek1): |
@@ -1192,19 +1219,22 @@ to your build configuration. | |||
1192 | raise RunQemuError("a new one with sudo.") | 1219 | raise RunQemuError("a new one with sudo.") |
1193 | 1220 | ||
1194 | gid = os.getgid() | 1221 | gid = os.getgid() |
1195 | uid = os.getuid() | ||
1196 | logger.info("Setting up tap interface under sudo") | 1222 | logger.info("Setting up tap interface under sudo") |
1197 | cmd = ('sudo', self.qemuifup, str(gid)) | 1223 | cmd = ('sudo', self.qemuifup, str(gid)) |
1198 | try: | 1224 | for _ in range(5): |
1199 | tap = subprocess.check_output(cmd).decode('utf-8').strip() | 1225 | try: |
1200 | except subprocess.CalledProcessError as e: | 1226 | tap = subprocess.check_output(cmd).decode('utf-8').strip() |
1201 | logger.error('Setting up tap device failed:\n%s\nRun runqemu-gen-tapdevs to manually create one.' % str(e)) | 1227 | except subprocess.CalledProcessError as e: |
1202 | sys.exit(1) | 1228 | logger.error('Setting up tap device failed:\n%s\nRun runqemu-gen-tapdevs to manually create one.' % str(e)) |
1203 | lockfile = os.path.join(lockdir, tap) | 1229 | sys.exit(1) |
1204 | self.taplock = lockfile + '.lock' | 1230 | lockfile = os.path.join(lockdir, tap) |
1205 | self.acquire_taplock() | 1231 | self.taplock = lockfile + '.lock' |
1206 | self.cleantap = True | 1232 | if self.acquire_taplock(): |
1207 | logger.debug('Created tap: %s' % tap) | 1233 | self.cleantap = True |
1234 | logger.debug('Created tap: %s' % tap) | ||
1235 | break | ||
1236 | else: | ||
1237 | tap = None | ||
1208 | 1238 | ||
1209 | if not tap: | 1239 | if not tap: |
1210 | logger.error("Failed to setup tap device. Run runqemu-gen-tapdevs to manually create.") | 1240 | logger.error("Failed to setup tap device. Run runqemu-gen-tapdevs to manually create.") |
@@ -1295,6 +1325,10 @@ to your build configuration. | |||
1295 | elif drive_type.startswith("/dev/hd"): | 1325 | elif drive_type.startswith("/dev/hd"): |
1296 | logger.info('Using ide drive') | 1326 | logger.info('Using ide drive') |
1297 | vm_drive = "-drive file=%s,format=%s" % (self.rootfs, rootfs_format) | 1327 | vm_drive = "-drive file=%s,format=%s" % (self.rootfs, rootfs_format) |
1328 | elif drive_type.startswith("/dev/mmcblk"): | ||
1329 | logger.info('Using sdcard drive') | ||
1330 | vm_drive = '-drive id=sdcard0,if=none,file=%s,format=%s -device sdhci-pci -device sd-card,drive=sdcard0' \ | ||
1331 | % (self.rootfs, rootfs_format) | ||
1298 | elif drive_type.startswith("/dev/vdb"): | 1332 | elif drive_type.startswith("/dev/vdb"): |
1299 | logger.info('Using block virtio drive'); | 1333 | logger.info('Using block virtio drive'); |
1300 | vm_drive = '-drive id=disk0,file=%s,if=none,format=%s -device virtio-blk-device,drive=disk0%s' \ | 1334 | vm_drive = '-drive id=disk0,file=%s,if=none,format=%s -device virtio-blk-device,drive=disk0%s' \ |
@@ -1454,7 +1488,7 @@ to your build configuration. | |||
1454 | self.qemu_opt += ' -display ' | 1488 | self.qemu_opt += ' -display ' |
1455 | if self.egl_headless == True: | 1489 | if self.egl_headless == True: |
1456 | self.check_render_nodes() | 1490 | self.check_render_nodes() |
1457 | self.set_dri_path() | 1491 | self.set_mesa_paths() |
1458 | self.qemu_opt += 'egl-headless,' | 1492 | self.qemu_opt += 'egl-headless,' |
1459 | else: | 1493 | else: |
1460 | if self.sdl == True: | 1494 | if self.sdl == True: |
@@ -1464,10 +1498,10 @@ to your build configuration. | |||
1464 | self.qemu_opt += 'gtk,' | 1498 | self.qemu_opt += 'gtk,' |
1465 | 1499 | ||
1466 | if self.gl == True: | 1500 | if self.gl == True: |
1467 | self.set_dri_path() | 1501 | self.set_mesa_paths() |
1468 | self.qemu_opt += 'gl=on,' | 1502 | self.qemu_opt += 'gl=on,' |
1469 | elif self.gl_es == True: | 1503 | elif self.gl_es == True: |
1470 | self.set_dri_path() | 1504 | self.set_mesa_paths() |
1471 | self.qemu_opt += 'gl=es,' | 1505 | self.qemu_opt += 'gl=es,' |
1472 | self.qemu_opt += 'show-cursor=on' | 1506 | self.qemu_opt += 'show-cursor=on' |
1473 | 1507 | ||
@@ -1483,7 +1517,7 @@ to your build configuration. | |||
1483 | # If no serial or serialtcp options were specified, only ttyS0 is created | 1517 | # If no serial or serialtcp options were specified, only ttyS0 is created |
1484 | # and sysvinit shows an error trying to enable ttyS1: | 1518 | # and sysvinit shows an error trying to enable ttyS1: |
1485 | # INIT: Id "S1" respawning too fast: disabled for 5 minutes | 1519 | # INIT: Id "S1" respawning too fast: disabled for 5 minutes |
1486 | serial_num = len(re.findall("-serial", self.qemu_opt)) | 1520 | serial_num = len(re.findall("(^| )-serial ", self.qemu_opt)) |
1487 | 1521 | ||
1488 | # Assume if the user passed serial options, they know what they want | 1522 | # Assume if the user passed serial options, they know what they want |
1489 | # and pad to two devices | 1523 | # and pad to two devices |
@@ -1503,7 +1537,7 @@ to your build configuration. | |||
1503 | 1537 | ||
1504 | self.qemu_opt += " %s" % self.get("QB_SERIAL_OPT") | 1538 | self.qemu_opt += " %s" % self.get("QB_SERIAL_OPT") |
1505 | 1539 | ||
1506 | serial_num = len(re.findall("-serial", self.qemu_opt)) | 1540 | serial_num = len(re.findall("(^| )-serial ", self.qemu_opt)) |
1507 | if serial_num < 2: | 1541 | if serial_num < 2: |
1508 | self.qemu_opt += " -serial null" | 1542 | self.qemu_opt += " -serial null" |
1509 | 1543 | ||
@@ -1658,9 +1692,6 @@ to your build configuration. | |||
1658 | self.cleaned = True | 1692 | self.cleaned = True |
1659 | 1693 | ||
1660 | def run_bitbake_env(self, mach=None, target=''): | 1694 | def run_bitbake_env(self, mach=None, target=''): |
1661 | bitbake = shutil.which('bitbake') | ||
1662 | if not bitbake: | ||
1663 | return | ||
1664 | 1695 | ||
1665 | if not mach: | 1696 | if not mach: |
1666 | mach = self.get('MACHINE') | 1697 | mach = self.get('MACHINE') |
@@ -1669,11 +1700,18 @@ to your build configuration. | |||
1669 | if multiconfig: | 1700 | if multiconfig: |
1670 | multiconfig = "mc:%s" % multiconfig | 1701 | multiconfig = "mc:%s" % multiconfig |
1671 | 1702 | ||
1703 | if self.rootfs and not target: | ||
1704 | target = self.rootfs | ||
1705 | |||
1672 | if mach: | 1706 | if mach: |
1673 | cmd = 'MACHINE=%s bitbake -e %s %s' % (mach, multiconfig, target) | 1707 | cmd = 'MACHINE=%s bitbake -e %s %s' % (mach, multiconfig, target) |
1674 | else: | 1708 | else: |
1675 | cmd = 'bitbake -e %s %s' % (multiconfig, target) | 1709 | cmd = 'bitbake -e %s %s' % (multiconfig, target) |
1676 | 1710 | ||
1711 | bitbake = shutil.which('bitbake') | ||
1712 | if not bitbake: | ||
1713 | raise OEPathError("Bitbake is needed to run '%s', but it is not found in PATH. Please source the bitbake build environment." % cmd.strip()) | ||
1714 | |||
1677 | logger.info('Running %s...' % cmd) | 1715 | logger.info('Running %s...' % cmd) |
1678 | try: | 1716 | try: |
1679 | return subprocess.check_output(cmd, shell=True).decode('utf-8') | 1717 | return subprocess.check_output(cmd, shell=True).decode('utf-8') |
@@ -1685,11 +1723,7 @@ to your build configuration. | |||
1685 | cmd = 'MACHINE=%s bitbake -e %s %s' % (mach, multiconfig, target) | 1723 | cmd = 'MACHINE=%s bitbake -e %s %s' % (mach, multiconfig, target) |
1686 | else: | 1724 | else: |
1687 | cmd = 'bitbake -e %s %s' % (multiconfig, target) | 1725 | cmd = 'bitbake -e %s %s' % (multiconfig, target) |
1688 | try: | 1726 | return subprocess.check_output(cmd, shell=True).decode('utf-8') |
1689 | return subprocess.check_output(cmd, shell=True).decode('utf-8') | ||
1690 | except subprocess.CalledProcessError as err: | ||
1691 | logger.warning("Couldn't run '%s' to gather environment information, giving up with 'bitbake -e':\n%s" % (cmd, err.output.decode('utf-8'))) | ||
1692 | return '' | ||
1693 | 1727 | ||
1694 | 1728 | ||
1695 | def load_bitbake_env(self, mach=None, target=None): | 1729 | def load_bitbake_env(self, mach=None, target=None): |
@@ -1749,6 +1783,7 @@ def main(): | |||
1749 | config.check_args() | 1783 | config.check_args() |
1750 | config.read_qemuboot() | 1784 | config.read_qemuboot() |
1751 | config.check_and_set() | 1785 | config.check_and_set() |
1786 | config.uncompress_rootfs() | ||
1752 | # Check whether the combos is valid or not | 1787 | # Check whether the combos is valid or not |
1753 | config.validate_combos() | 1788 | config.validate_combos() |
1754 | config.print_config() | 1789 | config.print_config() |