* [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py
@ 2026-06-26 12:45 Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 2/5] test: move interactive QEMU launcher out of pytest Ahmad Fatoum
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Ahmad Fatoum @ 2026-06-26 12:45 UTC (permalink / raw)
To: barebox; +Cc: Ahmad Fatoum
To make the change splitting pytest --interactive into its own script
easier to follow, duplicate the existing code under the new name.
No functional change.
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
Change can be squashed into follow-up commit. This doesn't yet work,
but doesn't break anything.
---
scripts/qemu_interactive.py | 304 ++++++++++++++++++++++++++++++++++++
1 file changed, 304 insertions(+)
create mode 100644 scripts/qemu_interactive.py
diff --git a/scripts/qemu_interactive.py b/scripts/qemu_interactive.py
new file mode 100644
index 000000000000..6f76586e010b
--- /dev/null
+++ b/scripts/qemu_interactive.py
@@ -0,0 +1,304 @@
+# SPDX-License-Identifier: GPL-2.0-only
+import pytest
+import os
+import argparse
+from labgrid.exceptions import NoDriverFoundError
+from test.py import helper
+
+
+def transition_to_barebox(request, strategy):
+ try:
+ strategy.transition('barebox')
+ except Exception as e:
+ # If a normal strategy transition fails, there's no point in
+ # continuing the test. Let's print stderr and exit.
+ capmanager = request.config.pluginmanager.getplugin("capturemanager")
+ with capmanager.global_and_fixture_disabled():
+ _, stderr = capmanager.read_global_capture()
+ pytest.exit(f"{type(e).__name__}(\"{e}\"). Standard error:\n{stderr}",
+ returncode=3)
+
+
+@pytest.fixture(scope='function')
+def barebox(request, strategy, target):
+ transition_to_barebox(request, strategy)
+ return target.get_driver('BareboxDriver')
+
+
+@pytest.fixture(scope='function')
+def shell(strategy, target):
+ strategy.transition('shell')
+ return target.get_driver('ShellDriver')
+
+
+@pytest.fixture(scope="session")
+def barebox_config(request, strategy, target):
+ transition_to_barebox(request, strategy)
+ command = target.get_driver("BareboxDriver")
+ return helper.get_config(command)
+
+
+def get_enabled_arch(config):
+ # Get the absolute path to the directory containing this script
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+
+ # Path to the 'arch' directory relative to this script
+ arch_dir = os.path.join(base_dir, "arch")
+
+ if not os.path.isdir(arch_dir):
+ return None
+
+ # Optional mapping from directory names to config key suffixes
+ arch_map = {"powerpc": "ppc"}
+
+ for entry in os.listdir(arch_dir):
+ path = os.path.join(arch_dir, entry)
+ if os.path.isdir(path):
+ key_suffix = arch_map.get(entry, entry).upper()
+ config_key = f"CONFIG_{key_suffix}"
+ if config.get(config_key):
+ return entry
+
+ return None
+
+
+def guess_lg_env():
+ config_file = helper.open_config_file(os.environ['LG_BUILDDIR'] + "/.config")
+ config = helper.parse_config(config_file)
+ if not config or not config.get('CONFIG_NAME'):
+ return None
+ arch = get_enabled_arch(config)
+ if not arch:
+ return None
+ filename = os.path.join("test", arch, f"{config['CONFIG_NAME']}.yaml")
+ if os.path.exists(filename):
+ return filename
+ return None
+
+
+lg_env = lg_builddir = None
+
+
+def pytest_configure(config):
+ if 'LG_BUILDDIR' not in os.environ:
+ if 'KBUILD_OUTPUT' in os.environ:
+ os.environ['LG_BUILDDIR'] = os.environ['KBUILD_OUTPUT']
+ elif os.path.isdir('build'):
+ os.environ['LG_BUILDDIR'] = os.path.realpath('build')
+ else:
+ os.environ['LG_BUILDDIR'] = os.getcwd()
+
+ if os.environ['LG_BUILDDIR'] is not None:
+ os.environ['LG_BUILDDIR'] = os.path.realpath(os.environ['LG_BUILDDIR'])
+
+ global lg_env
+ global lg_builddir
+
+ lg_builddir = os.environ['LG_BUILDDIR']
+ lg_env = config.option.lg_env
+ if lg_env is None:
+ lg_env = os.environ.get('LG_ENV')
+ if lg_env is None:
+ if lg_env := guess_lg_env():
+ os.environ['LG_ENV'] = lg_env
+
+
+def pytest_report_header(config):
+ report = []
+ if lg_builddir is not None:
+ report += [f"Build diectory: {lg_builddir}"]
+ if lg_env is not None:
+ report += [f"Labgrid Environment: {lg_env}"]
+ return "\n".join(report)
+
+
+def pytest_addoption(parser):
+ def assignment(arg):
+ return arg.split('=', 1)
+
+ parser.addoption('--interactive', action='store_const', const='qemu_interactive',
+ dest='lg_initial_state',
+ help=('(for debugging) skip tests and just start Qemu interactively'))
+ parser.addoption('--dry-run', action='store_const', const='qemu_dry_run',
+ dest='lg_initial_state',
+ help=('(for debugging) skip tests and just print Qemu command line'))
+ parser.addoption('--dump-dtb', action='store_const', const='qemu_dump_dtb',
+ dest='lg_initial_state',
+ help=('(for debugging) skip tests and just dump the Qemu device tree'))
+ parser.addoption('--graphic', '--graphics', action='store_true', dest='qemu_graphics',
+ help=('enable QEMU graphics output'))
+ parser.addoption('--rng', action='count', dest='qemu_rng',
+ help=('instantiate Virt I/O random number generator'))
+ parser.addoption('--console', action='count', dest='qemu_console', default=0,
+ help=('Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox'))
+ parser.addoption('--fs', action='append', dest='qemu_fs',
+ default=[], metavar="[tag=]DIR", type=assignment,
+ help=('Pass directory trees to emulated barebox. Can be specified more than once')) # noqa
+ parser.addoption('--blk', action='append', dest='qemu_block',
+ default=[], metavar="FILE",
+ help=('Pass block device to emulated barebox. Can be specified more than once')) # noqa
+ parser.addoption('--env', action='append', dest='qemu_fw_cfg', type=assignment,
+ default=[], metavar="[envpath=]content | [envpath=]@filepath",
+ help=('Pass barebox environment files to barebox. Can be specified more than once')) # noqa
+ parser.addoption('--qemu', dest='qemu_arg', nargs=argparse.REMAINDER, default=[],
+ help=('Pass all remaining options to QEMU as is'))
+ parser.addoption('--bootarg', action='append', dest='bootarg', default=[],
+ help=('Pass boot arguments to barebox for debugging purposes'))
+ parser.addoption('--port-forward', metavar="PORT", action='append', dest='qemu_port', default=[],
+ help=('Forward incoming TCP or UDP connections on specified PORT'))
+
+
+@pytest.fixture(scope="session")
+def strategy(request, target, pytestconfig): # noqa: max-complexity=30
+ try:
+ strategy = target.get_driver("Strategy")
+ except NoDriverFoundError as e:
+ pytest.exit(e)
+
+ try:
+ main = target.env.config.data["targets"]["main"]
+ features = main["features"]
+ except KeyError:
+ features = []
+
+ try:
+ main = target.env.config.data["targets"]["main"]
+ yaml_env = main["env"]
+ except KeyError:
+ yaml_env = {}
+
+ try:
+ main = target.env.config.data["targets"]["main"]
+ qemu_bin = main["drivers"]["QEMUDriver"]["qemu_bin"]
+ features.append("qemu")
+ except KeyError:
+ pass
+
+ virtio = None
+
+ if "virtio-mmio" in features:
+ virtio = "device"
+ strategy.append_qemu_args('-global virtio-mmio.force-legacy=false')
+ if "virtio-pci" in features:
+ virtio = "pci,disable-modern=off"
+ features.append("pci")
+
+ if virtio and pytestconfig.option.qemu_rng:
+ for i in range(pytestconfig.option.qemu_rng):
+ strategy.append_qemu_args("-device", f"virtio-rng-{virtio}")
+
+ for i in range(pytestconfig.option.qemu_console):
+ if virtio and i == 0:
+ strategy.append_qemu_args(
+ "-device", f"virtio-serial-{virtio}",
+ "-chardev", f"pty,id=virtcon{i}",
+ "-device", f"virtconsole,chardev=virtcon{i},name=console.virtcon{i}"
+ )
+ continue
+
+ # ns16550 serial driver only works with x86 so far
+ if 'pci' in features:
+ strategy.append_qemu_args(
+ "-chardev", f"pty,id=pcicon{i}",
+ "-device", f"pci-serial,chardev=pcicon{i}"
+ )
+ else:
+ pytest.exit("barebox currently supports only a single extra virtio console\n", 1)
+
+ if "qemu" in features:
+ if not pytestconfig.option.qemu_graphics:
+ graphics = '-nographic'
+ elif qemu_bin == "qemu-system-x86_64":
+ graphics = '-device isa-vga'
+ elif 'pci' in features:
+ graphics = '-device VGA'
+ elif virtio:
+ graphics = '-vga none -device ramfb'
+ graphics += f' -device virtio-keyboard-{virtio}'
+ else:
+ pytest.exit("--graphics unsupported for target\n", 1)
+
+ if graphics is not None and \
+ pytestconfig.option.lg_initial_state != 'qemu_interactive':
+ graphics += ' -display none'
+
+ strategy.append_qemu_args(graphics)
+
+ for i, blk in enumerate(pytestconfig.option.qemu_block):
+ if virtio:
+ strategy.append_qemu_args(
+ "-drive", f"if=none,format=raw,id=hd{i},file={blk}",
+ "-device", f"virtio-blk-{virtio},drive=hd{i}"
+ )
+ else:
+ pytest.exit("--blk unsupported for target\n", 1)
+
+ envopts = {}
+
+ for i, fw_cfg in enumerate(pytestconfig.option.qemu_fw_cfg):
+ value = fw_cfg.pop()
+ envpath = fw_cfg.pop() if fw_cfg else f"data/fw_cfg{i}"
+
+ envopts[envpath] = value
+
+ for envpath, value in (yaml_env | envopts).items():
+ if virtio:
+ if isinstance(value, str) and value.startswith('@'):
+ source = f"file='{value[1:]}'"
+ else:
+ source = f"string='{value}'"
+
+ strategy.append_qemu_args(
+ '-fw_cfg', f'name=opt/org.barebox.env/{envpath},{source}'
+ )
+ else:
+ pytest.exit("env unsupported for target\n", 1)
+
+ if len(pytestconfig.option.bootarg) > 0:
+ strategy.append_qemu_bootargs(pytestconfig.option.bootarg)
+
+ for arg in pytestconfig.option.qemu_arg:
+ strategy.append_qemu_args(arg)
+
+ qemu_nic = "user,id=net0"
+
+ for port in pytestconfig.option.qemu_port:
+ qemu_nic += f",hostfwd=udp:127.0.0.2:{port}-:{port}"
+ qemu_nic += f",hostfwd=tcp:127.0.0.2:{port}-:{port}"
+
+ if "testfs" in features:
+ if not any(fs and fs[0] == "testfs" for fs in pytestconfig.option.qemu_fs):
+ testfs_path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
+ pytestconfig.option.qemu_fs.append(["testfs", testfs_path])
+ os.makedirs(testfs_path, exist_ok=True)
+ qemu_nic += f",tftp={testfs_path}"
+
+ if "qemu" in features:
+ strategy.append_qemu_args("-nic", qemu_nic)
+
+ for i, fs in enumerate(pytestconfig.option.qemu_fs):
+ if virtio:
+ path = fs.pop()
+ tag = fs.pop() if fs else f"fs{i}"
+
+ strategy.append_qemu_args(
+ "-fsdev", f"local,security_model=none,id=fs{i},path={path}",
+ "-device", f"virtio-9p-{virtio},id=fs{i},fsdev=fs{i},mount_tag={tag}"
+ )
+ else:
+ pytest.exit("--fs unsupported for target\n", 1)
+
+ state = request.config.option.lg_initial_state
+ if state is not None:
+ strategy.force(state)
+
+ return strategy
+
+
+@pytest.fixture(scope="session")
+def testfs(strategy, env):
+ if "testfs" not in env.get_target_features():
+ pytest.skip("testfs not supported on this platform")
+
+ path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
+ return path
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 2/5] test: move interactive QEMU launcher out of pytest
2026-06-26 12:45 [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py Ahmad Fatoum
@ 2026-06-26 12:45 ` Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 3/5] usb: xhci-hcd: add XHCI over PCI driver Ahmad Fatoum
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Ahmad Fatoum @ 2026-06-26 12:45 UTC (permalink / raw)
To: barebox; +Cc: Ahmad Fatoum
pytest --interactive apparently starts two QEMUs, the interactive one in
addition to the one started directly by Labgrid. This leads to issues
when QEMU does something mutually exclusive like listening on a port or
taking a file lock.
Arguably, running an interactive QEMU with pytest as a runner was
a strange idea in the first place. Let's stop doing that and add a
dedicated qemu_interactive.py script.
The new script is imported by conftest.py to get support for custom
options like --blk or --rng. --interactive becomes an error.
To avoid cross-dependencies between test suite and the new script, the
CONFIG_NAME resolution is reimplemented without test/py/helper.py, but
apart from that it's mostly copy-paste and adding some boilerplate.
In the future[1], we can adapt this to use the new QEMUDriver.interact().
[1]: https://github.com/labgrid-project/labgrid/pull/1892
Assisted-by: Codex:gpt-5.5
Reported-by: Marc Kleine-Budde <mkl@pengutronix.de>
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
Documentation/boards/emulated.rst | 10 +-
Documentation/devel/contributing.rst | 8 +-
Documentation/user/security-policies.rst | 2 +-
conftest.py | 243 ++---------
scripts/qemu_interactive.py | 534 +++++++++++++++--------
5 files changed, 375 insertions(+), 422 deletions(-)
mode change 100644 => 100755 scripts/qemu_interactive.py
diff --git a/Documentation/boards/emulated.rst b/Documentation/boards/emulated.rst
index 0d6dd85dc759..bc3e2566eaec 100644
--- a/Documentation/boards/emulated.rst
+++ b/Documentation/boards/emulated.rst
@@ -23,15 +23,15 @@ VirtIO over PCI.
Running in QEMU
---------------
-Emulated targets can be started interactively with ``pytest --interactive``::
+Emulated targets can be started interactively with ``scripts/qemu_interactive.py``::
# Run x86 VM runnig the EFI payload from efi_defconfig
- pytest --lg-env test/x86/efi_defconfig.yaml --interactive
+ scripts/qemu_interactive.py test/x86/efi_defconfig.yaml
# Identical to above, provided the CONFIG_NAME=efi_defconfig
- pytest --interactive
+ scripts/qemu_interactive.py
-The test suite can be run by omitting the ``--interactive``.
+The test suite can be run with ``pytest`` instead.
For more information, see the :ref:`labgrid` section in the
:ref:`contributing` guide.
@@ -43,7 +43,7 @@ be used in combination with QEMU. With user-mode networking (SLIRP),
guest-to-host UDP works via NAT out of the box,
but unsolicited host-to-guest UDP requires an explicit port forward::
- pytest --lg-env test/arm/multi_v8_defconfig.yaml --interactive \
+ scripts/qemu_interactive.py test/arm/multi_v8_defconfig.yaml \
--env nv/dev.netconsole.ip=10.0.2.2 \
--env nv/dev.netconsole.port=6666 \
--env init/netconsole="ifup -a1; netconsole.active=ioe" \
diff --git a/Documentation/devel/contributing.rst b/Documentation/devel/contributing.rst
index cb3820f23c63..8b92c6f483aa 100644
--- a/Documentation/devel/contributing.rst
+++ b/Documentation/devel/contributing.rst
@@ -134,7 +134,7 @@ it directly from PyPI instead of your distro's package repositories::
Example usage::
# Run x86 VM runnig the EFI payload from efi_defconfig
- pytest --lg-env test/x86/efi_defconfig.yaml --interactive
+ scripts/qemu_interactive.py test/x86/efi_defconfig.yaml
# Run the test suite against the same
pytest --lg-env test/x86/efi_defconfig.yaml
@@ -145,10 +145,10 @@ built out-of-tree, the build directory must be pointed at by
``LG_BUILDDIR``, ``KBUILD_OUTPUT`` or a ``build`` symlink.
Additional QEMU command-line options can be added by specifying
-them after the ``--qemu`` option::
+them after ``--qemu``::
# appends -device ? to the command line. Add --dry-run to see the final result
- pytest --lg-env test/riscv/rv64i_defconfig.yaml --interactive --qemu -device '?'
+ scripts/qemu_interactive.py test/riscv/rv64i_defconfig.yaml --qemu -device '?'
Some of the QEMU options that are used more often also have explicit
support in the test runner, so paravirtualized devices can be added
@@ -158,7 +158,7 @@ more easily::
pytest --lg-env test/arm/virt@multi_v8_defconfig.yaml --blk=rootfs.ext4
# Run interactively with graphics output
- pytest --lg-env test/mips/qemu-malta_defconfig.yaml --interactive --graphics
+ scripts/qemu_interactive.py test/mips/qemu-malta_defconfig.yaml --graphics
For testing, the QEMU fw_cfg and virtfs support is particularly useful::
diff --git a/Documentation/user/security-policies.rst b/Documentation/user/security-policies.rst
index 8d515508d106..296191c4f8e5 100644
--- a/Documentation/user/security-policies.rst
+++ b/Documentation/user/security-policies.rst
@@ -97,7 +97,7 @@ policy development and evaluation. ``images/barebox-dt-2nd.img`` that results
from building it can be passed as argument to ``qemu-system-arm -M virt -kernel``.
The easiest way to do this is probably installing labgrid and running
-``pytest --interactive`` after having built the config.
+``scripts/qemu_interactive.py`` after having built the config.
Differences from Kconfig
------------------------
diff --git a/conftest.py b/conftest.py
index 6f76586e010b..4ab76b295e59 100644
--- a/conftest.py
+++ b/conftest.py
@@ -3,6 +3,7 @@ import pytest
import os
import argparse
from labgrid.exceptions import NoDriverFoundError
+from scripts import qemu_interactive
from test.py import helper
@@ -38,68 +39,24 @@ def barebox_config(request, strategy, target):
return helper.get_config(command)
-def get_enabled_arch(config):
- # Get the absolute path to the directory containing this script
- base_dir = os.path.dirname(os.path.abspath(__file__))
-
- # Path to the 'arch' directory relative to this script
- arch_dir = os.path.join(base_dir, "arch")
-
- if not os.path.isdir(arch_dir):
- return None
-
- # Optional mapping from directory names to config key suffixes
- arch_map = {"powerpc": "ppc"}
-
- for entry in os.listdir(arch_dir):
- path = os.path.join(arch_dir, entry)
- if os.path.isdir(path):
- key_suffix = arch_map.get(entry, entry).upper()
- config_key = f"CONFIG_{key_suffix}"
- if config.get(config_key):
- return entry
-
- return None
-
-
-def guess_lg_env():
- config_file = helper.open_config_file(os.environ['LG_BUILDDIR'] + "/.config")
- config = helper.parse_config(config_file)
- if not config or not config.get('CONFIG_NAME'):
- return None
- arch = get_enabled_arch(config)
- if not arch:
- return None
- filename = os.path.join("test", arch, f"{config['CONFIG_NAME']}.yaml")
- if os.path.exists(filename):
- return filename
- return None
-
-
lg_env = lg_builddir = None
def pytest_configure(config):
- if 'LG_BUILDDIR' not in os.environ:
- if 'KBUILD_OUTPUT' in os.environ:
- os.environ['LG_BUILDDIR'] = os.environ['KBUILD_OUTPUT']
- elif os.path.isdir('build'):
- os.environ['LG_BUILDDIR'] = os.path.realpath('build')
- else:
- os.environ['LG_BUILDDIR'] = os.getcwd()
-
- if os.environ['LG_BUILDDIR'] is not None:
- os.environ['LG_BUILDDIR'] = os.path.realpath(os.environ['LG_BUILDDIR'])
+ removed_mode = getattr(config.option, 'qemu_interactive_removed', None)
+ if removed_mode is not None:
+ pytest.exit(qemu_interactive.replacement_message(removed_mode),
+ returncode=2)
global lg_env
global lg_builddir
- lg_builddir = os.environ['LG_BUILDDIR']
+ lg_builddir = qemu_interactive.resolve_lg_builddir()
lg_env = config.option.lg_env
if lg_env is None:
lg_env = os.environ.get('LG_ENV')
if lg_env is None:
- if lg_env := guess_lg_env():
+ if lg_env := qemu_interactive.guess_lg_env(lg_builddir):
os.environ['LG_ENV'] = lg_env
@@ -113,39 +70,22 @@ def pytest_report_header(config):
def pytest_addoption(parser):
- def assignment(arg):
- return arg.split('=', 1)
+ parser.addoption('--interactive', action='store_const',
+ const='--interactive', dest='qemu_interactive_removed',
+ help=argparse.SUPPRESS)
+ parser.addoption('--dry-run', action='store_const',
+ const='--dry-run', dest='qemu_interactive_removed',
+ help=argparse.SUPPRESS)
+ parser.addoption('--dump-dtb', action='store_const',
+ const='--dump-dtb', dest='qemu_interactive_removed',
+ help=argparse.SUPPRESS)
- parser.addoption('--interactive', action='store_const', const='qemu_interactive',
- dest='lg_initial_state',
- help=('(for debugging) skip tests and just start Qemu interactively'))
- parser.addoption('--dry-run', action='store_const', const='qemu_dry_run',
- dest='lg_initial_state',
- help=('(for debugging) skip tests and just print Qemu command line'))
- parser.addoption('--dump-dtb', action='store_const', const='qemu_dump_dtb',
- dest='lg_initial_state',
- help=('(for debugging) skip tests and just dump the Qemu device tree'))
- parser.addoption('--graphic', '--graphics', action='store_true', dest='qemu_graphics',
- help=('enable QEMU graphics output'))
- parser.addoption('--rng', action='count', dest='qemu_rng',
- help=('instantiate Virt I/O random number generator'))
- parser.addoption('--console', action='count', dest='qemu_console', default=0,
- help=('Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox'))
- parser.addoption('--fs', action='append', dest='qemu_fs',
- default=[], metavar="[tag=]DIR", type=assignment,
- help=('Pass directory trees to emulated barebox. Can be specified more than once')) # noqa
- parser.addoption('--blk', action='append', dest='qemu_block',
- default=[], metavar="FILE",
- help=('Pass block device to emulated barebox. Can be specified more than once')) # noqa
- parser.addoption('--env', action='append', dest='qemu_fw_cfg', type=assignment,
- default=[], metavar="[envpath=]content | [envpath=]@filepath",
- help=('Pass barebox environment files to barebox. Can be specified more than once')) # noqa
- parser.addoption('--qemu', dest='qemu_arg', nargs=argparse.REMAINDER, default=[],
- help=('Pass all remaining options to QEMU as is'))
- parser.addoption('--bootarg', action='append', dest='bootarg', default=[],
- help=('Pass boot arguments to barebox for debugging purposes'))
- parser.addoption('--port-forward', metavar="PORT", action='append', dest='qemu_port', default=[],
- help=('Forward incoming TCP or UDP connections on specified PORT'))
+ group = parser.getgroup('barebox-qemu', 'barebox QEMU options')
+ qemu_interactive.register_shared_options(group)
+
+
+def _pytest_exit(message, returncode=1):
+ pytest.exit(message, returncode=returncode)
@pytest.fixture(scope="session")
@@ -155,142 +95,9 @@ def strategy(request, target, pytestconfig): # noqa: max-complexity=30
except NoDriverFoundError as e:
pytest.exit(e)
- try:
- main = target.env.config.data["targets"]["main"]
- features = main["features"]
- except KeyError:
- features = []
-
- try:
- main = target.env.config.data["targets"]["main"]
- yaml_env = main["env"]
- except KeyError:
- yaml_env = {}
-
- try:
- main = target.env.config.data["targets"]["main"]
- qemu_bin = main["drivers"]["QEMUDriver"]["qemu_bin"]
- features.append("qemu")
- except KeyError:
- pass
-
- virtio = None
-
- if "virtio-mmio" in features:
- virtio = "device"
- strategy.append_qemu_args('-global virtio-mmio.force-legacy=false')
- if "virtio-pci" in features:
- virtio = "pci,disable-modern=off"
- features.append("pci")
-
- if virtio and pytestconfig.option.qemu_rng:
- for i in range(pytestconfig.option.qemu_rng):
- strategy.append_qemu_args("-device", f"virtio-rng-{virtio}")
-
- for i in range(pytestconfig.option.qemu_console):
- if virtio and i == 0:
- strategy.append_qemu_args(
- "-device", f"virtio-serial-{virtio}",
- "-chardev", f"pty,id=virtcon{i}",
- "-device", f"virtconsole,chardev=virtcon{i},name=console.virtcon{i}"
- )
- continue
-
- # ns16550 serial driver only works with x86 so far
- if 'pci' in features:
- strategy.append_qemu_args(
- "-chardev", f"pty,id=pcicon{i}",
- "-device", f"pci-serial,chardev=pcicon{i}"
- )
- else:
- pytest.exit("barebox currently supports only a single extra virtio console\n", 1)
-
- if "qemu" in features:
- if not pytestconfig.option.qemu_graphics:
- graphics = '-nographic'
- elif qemu_bin == "qemu-system-x86_64":
- graphics = '-device isa-vga'
- elif 'pci' in features:
- graphics = '-device VGA'
- elif virtio:
- graphics = '-vga none -device ramfb'
- graphics += f' -device virtio-keyboard-{virtio}'
- else:
- pytest.exit("--graphics unsupported for target\n", 1)
-
- if graphics is not None and \
- pytestconfig.option.lg_initial_state != 'qemu_interactive':
- graphics += ' -display none'
-
- strategy.append_qemu_args(graphics)
-
- for i, blk in enumerate(pytestconfig.option.qemu_block):
- if virtio:
- strategy.append_qemu_args(
- "-drive", f"if=none,format=raw,id=hd{i},file={blk}",
- "-device", f"virtio-blk-{virtio},drive=hd{i}"
- )
- else:
- pytest.exit("--blk unsupported for target\n", 1)
-
- envopts = {}
-
- for i, fw_cfg in enumerate(pytestconfig.option.qemu_fw_cfg):
- value = fw_cfg.pop()
- envpath = fw_cfg.pop() if fw_cfg else f"data/fw_cfg{i}"
-
- envopts[envpath] = value
-
- for envpath, value in (yaml_env | envopts).items():
- if virtio:
- if isinstance(value, str) and value.startswith('@'):
- source = f"file='{value[1:]}'"
- else:
- source = f"string='{value}'"
-
- strategy.append_qemu_args(
- '-fw_cfg', f'name=opt/org.barebox.env/{envpath},{source}'
- )
- else:
- pytest.exit("env unsupported for target\n", 1)
-
- if len(pytestconfig.option.bootarg) > 0:
- strategy.append_qemu_bootargs(pytestconfig.option.bootarg)
-
- for arg in pytestconfig.option.qemu_arg:
- strategy.append_qemu_args(arg)
-
- qemu_nic = "user,id=net0"
-
- for port in pytestconfig.option.qemu_port:
- qemu_nic += f",hostfwd=udp:127.0.0.2:{port}-:{port}"
- qemu_nic += f",hostfwd=tcp:127.0.0.2:{port}-:{port}"
-
- if "testfs" in features:
- if not any(fs and fs[0] == "testfs" for fs in pytestconfig.option.qemu_fs):
- testfs_path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
- pytestconfig.option.qemu_fs.append(["testfs", testfs_path])
- os.makedirs(testfs_path, exist_ok=True)
- qemu_nic += f",tftp={testfs_path}"
-
- if "qemu" in features:
- strategy.append_qemu_args("-nic", qemu_nic)
-
- for i, fs in enumerate(pytestconfig.option.qemu_fs):
- if virtio:
- path = fs.pop()
- tag = fs.pop() if fs else f"fs{i}"
-
- strategy.append_qemu_args(
- "-fsdev", f"local,security_model=none,id=fs{i},path={path}",
- "-device", f"virtio-9p-{virtio},id=fs{i},fsdev=fs{i},mount_tag={tag}"
- )
- else:
- pytest.exit("--fs unsupported for target\n", 1)
-
- state = request.config.option.lg_initial_state
- if state is not None:
- strategy.force(state)
+ qemu_interactive.apply_shared_options(
+ strategy, target, pytestconfig.option, interactive=False,
+ fail=_pytest_exit)
return strategy
diff --git a/scripts/qemu_interactive.py b/scripts/qemu_interactive.py
old mode 100644
new mode 100755
index 6f76586e010b..a8605d23b132
--- a/scripts/qemu_interactive.py
+++ b/scripts/qemu_interactive.py
@@ -1,195 +1,218 @@
+#!/usr/bin/env python3
+# /// script
+# requires-python = ">=3.9"
+# dependencies = ["labgrid"]
+# ///
# SPDX-License-Identifier: GPL-2.0-only
-import pytest
-import os
+
import argparse
-from labgrid.exceptions import NoDriverFoundError
-from test.py import helper
+import glob
+import os
+import re
+import subprocess
+import sys
-def transition_to_barebox(request, strategy):
- try:
- strategy.transition('barebox')
- except Exception as e:
- # If a normal strategy transition fails, there's no point in
- # continuing the test. Let's print stderr and exit.
- capmanager = request.config.pluginmanager.getplugin("capturemanager")
- with capmanager.global_and_fixture_disabled():
- _, stderr = capmanager.read_global_capture()
- pytest.exit(f"{type(e).__name__}(\"{e}\"). Standard error:\n{stderr}",
- returncode=3)
+MODE_INTERACTIVE = "qemu_interactive"
+MODE_DRY_RUN = "qemu_dry_run"
+MODE_DUMP_DTB = "qemu_dump_dtb"
-@pytest.fixture(scope='function')
-def barebox(request, strategy, target):
- transition_to_barebox(request, strategy)
- return target.get_driver('BareboxDriver')
+class QemuInteractiveError(Exception):
+ def __init__(self, message, returncode=1):
+ super().__init__(message)
+ self.returncode = returncode
-@pytest.fixture(scope='function')
-def shell(strategy, target):
- strategy.transition('shell')
- return target.get_driver('ShellDriver')
+def _add_option(parser, *opts, **attrs):
+ if hasattr(parser, "addoption"):
+ return parser.addoption(*opts, **attrs)
+
+ return parser.add_argument(*opts, **attrs)
-@pytest.fixture(scope="session")
-def barebox_config(request, strategy, target):
- transition_to_barebox(request, strategy)
- command = target.get_driver("BareboxDriver")
- return helper.get_config(command)
+def _assignment(arg):
+ return arg.split("=", 1)
-def get_enabled_arch(config):
- # Get the absolute path to the directory containing this script
- base_dir = os.path.dirname(os.path.abspath(__file__))
-
- # Path to the 'arch' directory relative to this script
- arch_dir = os.path.join(base_dir, "arch")
-
- if not os.path.isdir(arch_dir):
- return None
-
- # Optional mapping from directory names to config key suffixes
- arch_map = {"powerpc": "ppc"}
-
- for entry in os.listdir(arch_dir):
- path = os.path.join(arch_dir, entry)
- if os.path.isdir(path):
- key_suffix = arch_map.get(entry, entry).upper()
- config_key = f"CONFIG_{key_suffix}"
- if config.get(config_key):
- return entry
-
- return None
+def register_shared_options(parser):
+ _add_option(parser, "--graphic", "--graphics", action="store_true",
+ dest="qemu_graphics", help="enable QEMU graphics output")
+ _add_option(parser, "--rng", action="count", dest="qemu_rng",
+ help="instantiate Virt I/O random number generator")
+ _add_option(parser, "--console", action="count", dest="qemu_console",
+ default=0,
+ help="Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox")
+ _add_option(parser, "--fs", action="append", dest="qemu_fs",
+ default=[], metavar="[tag=]DIR", type=_assignment,
+ help="Pass directory trees to emulated barebox. Can be specified more than once")
+ _add_option(parser, "--blk", action="append", dest="qemu_block",
+ default=[], metavar="FILE",
+ help="Pass block device to emulated barebox. Can be specified more than once")
+ _add_option(parser, "--env", action="append", dest="qemu_fw_cfg",
+ type=_assignment, default=[],
+ metavar="[envpath=]content | [envpath=]@filepath",
+ help="Pass barebox environment files to barebox. Can be specified more than once")
+ _add_option(parser, "--qemu", dest="qemu_arg",
+ nargs=argparse.REMAINDER, default=[],
+ help="Pass all remaining options to QEMU as is")
+ _add_option(parser, "--bootarg", action="append", dest="bootarg",
+ default=[],
+ help="Pass boot arguments to barebox for debugging purposes")
+ _add_option(parser, "--port-forward", metavar="PORT", action="append",
+ dest="qemu_port", default=[],
+ help="Forward incoming TCP or UDP connections on specified PORT")
-def guess_lg_env():
- config_file = helper.open_config_file(os.environ['LG_BUILDDIR'] + "/.config")
- config = helper.parse_config(config_file)
- if not config or not config.get('CONFIG_NAME'):
- return None
- arch = get_enabled_arch(config)
- if not arch:
- return None
- filename = os.path.join("test", arch, f"{config['CONFIG_NAME']}.yaml")
- if os.path.exists(filename):
- return filename
- return None
+def register_interactive_options(parser):
+ _add_option(parser, "--lg-env", dest="lg_env", metavar="LG_ENV",
+ help="labgrid environment config file")
+
+ if hasattr(parser, "add_mutually_exclusive_group"):
+ mode_parser = parser.add_mutually_exclusive_group()
+ else:
+ mode_parser = parser
+
+ _add_option(mode_parser, "--dry-run", action="store_const",
+ const=MODE_DRY_RUN, default=MODE_INTERACTIVE,
+ dest="qemu_mode",
+ help="print the QEMU command line instead of executing it")
+ _add_option(mode_parser, "--dump-dtb", action="store_const",
+ const=MODE_DUMP_DTB, dest="qemu_mode",
+ help="run QEMU only to dump the device tree")
-lg_env = lg_builddir = None
+def repo_root():
+ return os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir))
-def pytest_configure(config):
- if 'LG_BUILDDIR' not in os.environ:
- if 'KBUILD_OUTPUT' in os.environ:
- os.environ['LG_BUILDDIR'] = os.environ['KBUILD_OUTPUT']
- elif os.path.isdir('build'):
- os.environ['LG_BUILDDIR'] = os.path.realpath('build')
+def resolve_lg_builddir():
+ if "LG_BUILDDIR" not in os.environ:
+ if "KBUILD_OUTPUT" in os.environ:
+ os.environ["LG_BUILDDIR"] = os.environ["KBUILD_OUTPUT"]
+ elif os.path.isdir("build"):
+ os.environ["LG_BUILDDIR"] = os.path.realpath("build")
else:
- os.environ['LG_BUILDDIR'] = os.getcwd()
+ os.environ["LG_BUILDDIR"] = os.getcwd()
- if os.environ['LG_BUILDDIR'] is not None:
- os.environ['LG_BUILDDIR'] = os.path.realpath(os.environ['LG_BUILDDIR'])
-
- global lg_env
- global lg_builddir
-
- lg_builddir = os.environ['LG_BUILDDIR']
- lg_env = config.option.lg_env
- if lg_env is None:
- lg_env = os.environ.get('LG_ENV')
- if lg_env is None:
- if lg_env := guess_lg_env():
- os.environ['LG_ENV'] = lg_env
+ os.environ["LG_BUILDDIR"] = os.path.realpath(os.environ["LG_BUILDDIR"])
+ return os.environ["LG_BUILDDIR"]
-def pytest_report_header(config):
- report = []
- if lg_builddir is not None:
- report += [f"Build diectory: {lg_builddir}"]
- if lg_env is not None:
- report += [f"Labgrid Environment: {lg_env}"]
- return "\n".join(report)
-
-
-def pytest_addoption(parser):
- def assignment(arg):
- return arg.split('=', 1)
-
- parser.addoption('--interactive', action='store_const', const='qemu_interactive',
- dest='lg_initial_state',
- help=('(for debugging) skip tests and just start Qemu interactively'))
- parser.addoption('--dry-run', action='store_const', const='qemu_dry_run',
- dest='lg_initial_state',
- help=('(for debugging) skip tests and just print Qemu command line'))
- parser.addoption('--dump-dtb', action='store_const', const='qemu_dump_dtb',
- dest='lg_initial_state',
- help=('(for debugging) skip tests and just dump the Qemu device tree'))
- parser.addoption('--graphic', '--graphics', action='store_true', dest='qemu_graphics',
- help=('enable QEMU graphics output'))
- parser.addoption('--rng', action='count', dest='qemu_rng',
- help=('instantiate Virt I/O random number generator'))
- parser.addoption('--console', action='count', dest='qemu_console', default=0,
- help=('Pass an extra console (Virt I/O or ns16550_pci) to emulated barebox'))
- parser.addoption('--fs', action='append', dest='qemu_fs',
- default=[], metavar="[tag=]DIR", type=assignment,
- help=('Pass directory trees to emulated barebox. Can be specified more than once')) # noqa
- parser.addoption('--blk', action='append', dest='qemu_block',
- default=[], metavar="FILE",
- help=('Pass block device to emulated barebox. Can be specified more than once')) # noqa
- parser.addoption('--env', action='append', dest='qemu_fw_cfg', type=assignment,
- default=[], metavar="[envpath=]content | [envpath=]@filepath",
- help=('Pass barebox environment files to barebox. Can be specified more than once')) # noqa
- parser.addoption('--qemu', dest='qemu_arg', nargs=argparse.REMAINDER, default=[],
- help=('Pass all remaining options to QEMU as is'))
- parser.addoption('--bootarg', action='append', dest='bootarg', default=[],
- help=('Pass boot arguments to barebox for debugging purposes'))
- parser.addoption('--port-forward', metavar="PORT", action='append', dest='qemu_port', default=[],
- help=('Forward incoming TCP or UDP connections on specified PORT'))
-
-
-@pytest.fixture(scope="session")
-def strategy(request, target, pytestconfig): # noqa: max-complexity=30
+def get_config_name(builddir):
try:
- strategy = target.get_driver("Strategy")
- except NoDriverFoundError as e:
- pytest.exit(e)
-
- try:
- main = target.env.config.data["targets"]["main"]
- features = main["features"]
- except KeyError:
- features = []
-
- try:
- main = target.env.config.data["targets"]["main"]
- yaml_env = main["env"]
- except KeyError:
- yaml_env = {}
-
- try:
- main = target.env.config.data["targets"]["main"]
- qemu_bin = main["drivers"]["QEMUDriver"]["qemu_bin"]
- features.append("qemu")
- except KeyError:
+ with open(os.path.join(builddir, ".config")) as config:
+ for line in config:
+ match = re.match(r'^CONFIG_NAME="(.*)"$', line)
+ if match:
+ return match.group(1)
+ except OSError:
pass
+ return None
+
+
+def guess_lg_env(builddir=None):
+ if builddir is None:
+ builddir = os.environ.get("LG_BUILDDIR")
+ if builddir is None:
+ return None
+
+ config_name = get_config_name(builddir)
+ if config_name is None:
+ return None
+
+ matches = glob.glob(os.path.join(repo_root(), "test", "*",
+ f"{config_name}.yaml"))
+ if matches:
+ return matches[0]
+
+ return None
+
+
+def resolve_lg_env(lg_env=None):
+ if lg_env is None:
+ lg_env = os.environ.get("LG_ENV")
+ if lg_env is None:
+ lg_env = guess_lg_env()
+
+ if lg_env is not None:
+ os.environ["LG_ENV"] = lg_env
+
+ return lg_env
+
+
+def _get_option(options, name, default=None):
+ if isinstance(options, dict):
+ return options.get(name, default)
+
+ return getattr(options, name, default)
+
+
+def _get_list_option(options, name):
+ value = _get_option(options, name, [])
+ if value is None:
+ return []
+ return list(value)
+
+
+def _fail(message, returncode=1):
+ raise QemuInteractiveError(message, returncode)
+
+
+def _append_qemu_args(strategy, fail, *args):
+ if strategy.qemu is None:
+ fail("Qemu option supplied for non-Qemu target")
+
+ for arg in args:
+ strategy.console.extra_args += " " + arg
+
+
+def _append_qemu_bootargs(strategy, fail, args):
+ if strategy.qemu is None:
+ fail("Qemu option supplied for non-Qemu target")
+ if strategy.console.boot_args is None:
+ strategy.console.boot_args = ""
+ strategy.console.boot_args += " ".join(args)
+
+
+def apply_shared_options(strategy, target, options, *, interactive, fail=None): # noqa: max-complexity=30
+ if fail is None:
+ fail = _fail
+
+ try:
+ main = target.env.config.data["targets"]["main"]
+ except KeyError:
+ main = {}
+
+ features = list(main.get("features", []))
+ yaml_env = dict(main.get("env", {}))
+ qemu_driver = main.get("drivers", {}).get("QEMUDriver")
+ qemu_bin = None
+
+ if qemu_driver is not None:
+ qemu_bin = qemu_driver.get("qemu_bin")
+ features.append("qemu")
+
virtio = None
if "virtio-mmio" in features:
virtio = "device"
- strategy.append_qemu_args('-global virtio-mmio.force-legacy=false')
+ _append_qemu_args(strategy, fail, '-global virtio-mmio.force-legacy=false')
if "virtio-pci" in features:
virtio = "pci,disable-modern=off"
features.append("pci")
- if virtio and pytestconfig.option.qemu_rng:
- for i in range(pytestconfig.option.qemu_rng):
- strategy.append_qemu_args("-device", f"virtio-rng-{virtio}")
+ qemu_rng = _get_option(options, "qemu_rng", 0) or 0
+ for _ in range(qemu_rng):
+ if virtio:
+ _append_qemu_args(strategy, fail, "-device", f"virtio-rng-{virtio}")
- for i in range(pytestconfig.option.qemu_console):
+ qemu_console = _get_option(options, "qemu_console", 0) or 0
+ for i in range(qemu_console):
if virtio and i == 0:
- strategy.append_qemu_args(
+ _append_qemu_args(
+ strategy, fail,
"-device", f"virtio-serial-{virtio}",
"-chardev", f"pty,id=virtcon{i}",
"-device", f"virtconsole,chardev=virtcon{i},name=console.virtcon{i}"
@@ -198,15 +221,16 @@ def strategy(request, target, pytestconfig): # noqa: max-complexity=30
# ns16550 serial driver only works with x86 so far
if 'pci' in features:
- strategy.append_qemu_args(
+ _append_qemu_args(
+ strategy, fail,
"-chardev", f"pty,id=pcicon{i}",
"-device", f"pci-serial,chardev=pcicon{i}"
)
else:
- pytest.exit("barebox currently supports only a single extra virtio console\n", 1)
+ fail("barebox currently supports only a single extra virtio console\n")
if "qemu" in features:
- if not pytestconfig.option.qemu_graphics:
+ if not _get_option(options, "qemu_graphics", False):
graphics = '-nographic'
elif qemu_bin == "qemu-system-x86_64":
graphics = '-device isa-vga'
@@ -216,89 +240,211 @@ def strategy(request, target, pytestconfig): # noqa: max-complexity=30
graphics = '-vga none -device ramfb'
graphics += f' -device virtio-keyboard-{virtio}'
else:
- pytest.exit("--graphics unsupported for target\n", 1)
+ fail("--graphics unsupported for target\n")
- if graphics is not None and \
- pytestconfig.option.lg_initial_state != 'qemu_interactive':
+ if graphics is not None and not interactive:
graphics += ' -display none'
- strategy.append_qemu_args(graphics)
+ _append_qemu_args(strategy, fail, graphics)
- for i, blk in enumerate(pytestconfig.option.qemu_block):
+ for i, blk in enumerate(_get_list_option(options, "qemu_block")):
if virtio:
- strategy.append_qemu_args(
+ _append_qemu_args(
+ strategy, fail,
"-drive", f"if=none,format=raw,id=hd{i},file={blk}",
"-device", f"virtio-blk-{virtio},drive=hd{i}"
)
else:
- pytest.exit("--blk unsupported for target\n", 1)
+ fail("--blk unsupported for target\n")
envopts = {}
- for i, fw_cfg in enumerate(pytestconfig.option.qemu_fw_cfg):
+ for i, fw_cfg in enumerate(_get_list_option(options, "qemu_fw_cfg")):
value = fw_cfg.pop()
envpath = fw_cfg.pop() if fw_cfg else f"data/fw_cfg{i}"
envopts[envpath] = value
- for envpath, value in (yaml_env | envopts).items():
+ for envpath, value in {**yaml_env, **envopts}.items():
if virtio:
if isinstance(value, str) and value.startswith('@'):
source = f"file='{value[1:]}'"
else:
source = f"string='{value}'"
- strategy.append_qemu_args(
+ _append_qemu_args(
+ strategy, fail,
'-fw_cfg', f'name=opt/org.barebox.env/{envpath},{source}'
)
else:
- pytest.exit("env unsupported for target\n", 1)
+ fail("env unsupported for target\n")
- if len(pytestconfig.option.bootarg) > 0:
- strategy.append_qemu_bootargs(pytestconfig.option.bootarg)
+ bootargs = _get_list_option(options, "bootarg")
+ if bootargs:
+ _append_qemu_bootargs(strategy, fail, bootargs)
- for arg in pytestconfig.option.qemu_arg:
- strategy.append_qemu_args(arg)
+ for arg in _get_list_option(options, "qemu_arg"):
+ _append_qemu_args(strategy, fail, arg)
qemu_nic = "user,id=net0"
- for port in pytestconfig.option.qemu_port:
+ for port in _get_list_option(options, "qemu_port"):
qemu_nic += f",hostfwd=udp:127.0.0.2:{port}-:{port}"
qemu_nic += f",hostfwd=tcp:127.0.0.2:{port}-:{port}"
+ qemu_fs = [list(fs) for fs in _get_list_option(options, "qemu_fs")]
+
if "testfs" in features:
- if not any(fs and fs[0] == "testfs" for fs in pytestconfig.option.qemu_fs):
+ if not any(fs and fs[0] == "testfs" for fs in qemu_fs):
testfs_path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
- pytestconfig.option.qemu_fs.append(["testfs", testfs_path])
+ qemu_fs.append(["testfs", testfs_path])
os.makedirs(testfs_path, exist_ok=True)
qemu_nic += f",tftp={testfs_path}"
if "qemu" in features:
- strategy.append_qemu_args("-nic", qemu_nic)
+ _append_qemu_args(strategy, fail, "-nic", qemu_nic)
- for i, fs in enumerate(pytestconfig.option.qemu_fs):
+ for i, fs in enumerate(qemu_fs):
if virtio:
path = fs.pop()
tag = fs.pop() if fs else f"fs{i}"
- strategy.append_qemu_args(
+ _append_qemu_args(
+ strategy, fail,
"-fsdev", f"local,security_model=none,id=fs{i},path={path}",
"-device", f"virtio-9p-{virtio},id=fs{i},fsdev=fs{i},mount_tag={tag}"
)
else:
- pytest.exit("--fs unsupported for target\n", 1)
-
- state = request.config.option.lg_initial_state
- if state is not None:
- strategy.force(state)
-
- return strategy
+ fail("--fs unsupported for target\n")
-@pytest.fixture(scope="session")
-def testfs(strategy, env):
- if "testfs" not in env.get_target_features():
- pytest.skip("testfs not supported on this platform")
+def replacement_message(mode):
+ return (f"pytest {mode} is no longer supported. Use "
+ "scripts/qemu_interactive.py instead.")
- path = os.path.join(os.environ["LG_BUILDDIR"], "testfs")
- return path
+
+def _split_qemu_args(argv):
+ for i, arg in enumerate(argv):
+ if arg in ("--qemu", "--"):
+ return argv[:i], argv[i + 1:]
+ if arg.startswith("--qemu="):
+ return argv[:i], [arg.split("=", 1)[1]] + argv[i + 1:]
+
+ return argv, []
+
+
+def build_arg_parser():
+ parser = argparse.ArgumentParser(
+ description="Start a barebox labgrid QEMU target interactively",
+ epilog="Raw QEMU arguments may follow either --qemu or --."
+ )
+
+ register_interactive_options(
+ parser.add_argument_group("qemu_interactive.py options"))
+ register_shared_options(parser.add_argument_group("QEMU options"))
+ parser.add_argument("lg_env_pos", nargs="*", metavar="LG_ENV",
+ help="labgrid environment config file")
+
+ return parser
+
+
+def parse_args(argv=None):
+ if argv is None:
+ argv = sys.argv[1:]
+
+ parser = build_arg_parser()
+ argv, qemu_args = _split_qemu_args(list(argv))
+ args = parser.parse_args(argv)
+
+ if len(args.lg_env_pos) > 1:
+ parser.error("only one positional labgrid environment may be specified")
+
+ if args.lg_env_pos:
+ if args.lg_env is not None:
+ parser.error("labgrid environment specified both positionally and with --lg-env")
+ args.lg_env = args.lg_env_pos[0]
+
+ del args.lg_env_pos
+ args.qemu_arg = qemu_args
+
+ return args
+
+
+def quote_cmd(cmd):
+ quoted = map(lambda s: s if s.find(" ") == -1 else "'" + s + "'", cmd)
+ return " ".join(quoted)
+
+
+def build_qemu_command(strategy, mode):
+ if strategy.qemu is None:
+ raise QemuInteractiveError(f"Can't enter {mode} for non-QEMU target")
+
+ if mode != MODE_DRY_RUN:
+ strategy.transition("off")
+
+ if mode == MODE_DUMP_DTB:
+ strategy.qemu.machine += f",dumpdtb={strategy.target.name}.dtb"
+
+ cmd = strategy.qemu.get_qemu_base_args()
+ cmd.extend(["-serial", "mon:stdio", "-trace", "file=/dev/null"])
+
+ return cmd
+
+
+def run_qemu(strategy, mode):
+ if mode not in (MODE_INTERACTIVE, MODE_DRY_RUN, MODE_DUMP_DTB):
+ raise QemuInteractiveError(
+ "Can only force to: qemu_dry_run, qemu_interactive, qemu_dump_dtb")
+
+ cmd = build_qemu_command(strategy, mode)
+ command = "running: \n{}\n".format(quote_cmd(cmd))
+
+ if mode == MODE_DRY_RUN:
+ print(command, end="")
+ return 0
+
+ with open("/dev/tty", "r+b", buffering=0) as tty:
+ tty.write(bytes(command, "UTF-8"))
+ return subprocess.run(cmd, stdin=tty, stdout=tty, stderr=tty).returncode
+
+
+def main(argv=None):
+ from labgrid import Environment
+ from labgrid.exceptions import NoDriverFoundError
+
+ args = parse_args(argv)
+ env = None
+
+ try:
+ resolve_lg_builddir()
+ lg_env = resolve_lg_env(args.lg_env)
+ if lg_env is None:
+ raise QemuInteractiveError(
+ "no labgrid environment specified; pass --lg-env, "
+ "a positional LG_ENV, set LG_ENV, or build a config with CONFIG_NAME",
+ 2)
+
+ env = Environment(lg_env)
+ target = env.get_target("main")
+ if target is None:
+ raise QemuInteractiveError("labgrid environment has no main target")
+
+ try:
+ strategy = target.get_driver("Strategy")
+ except NoDriverFoundError as e:
+ raise QemuInteractiveError(str(e)) from e
+
+ apply_shared_options(strategy, target, args,
+ interactive=args.qemu_mode != MODE_DUMP_DTB)
+
+ return run_qemu(strategy, args.qemu_mode)
+ except QemuInteractiveError as e:
+ print(f"error: {e}", file=sys.stderr)
+ return e.returncode
+ finally:
+ if env is not None:
+ env.cleanup()
+
+
+if __name__ == "__main__":
+ sys.exit(main())
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 3/5] usb: xhci-hcd: add XHCI over PCI driver
2026-06-26 12:45 [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 2/5] test: move interactive QEMU launcher out of pytest Ahmad Fatoum
@ 2026-06-26 12:45 ` Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 4/5] scripts: qemu_interactive.py: add new --usbblk option Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 5/5] scripts: qemu_interactive.py: add new --nvmeblk option Ahmad Fatoum
3 siblings, 0 replies; 5+ messages in thread
From: Ahmad Fatoum @ 2026-06-26 12:45 UTC (permalink / raw)
To: barebox; +Cc: Ahmad Fatoum
In order to support USB in QEMU Virt, add support for registering over
PCI as well. The probe function looks nearly identical to the DT-probed
xhci_probe with the difference that we map a PCI MMIO region.
To make it readily usable, also enable it in multi_v8_defconfig.
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
arch/arm/configs/multi_v8_defconfig | 1 +
drivers/usb/host/Kconfig | 7 +++
drivers/usb/host/Makefile | 1 +
drivers/usb/host/xhci-pci.c | 97 +++++++++++++++++++++++++++++
drivers/usb/host/xhci.h | 1 +
5 files changed, 107 insertions(+)
create mode 100644 drivers/usb/host/xhci-pci.c
diff --git a/arch/arm/configs/multi_v8_defconfig b/arch/arm/configs/multi_v8_defconfig
index e62dbc96fd03..9712113bf1cd 100644
--- a/arch/arm/configs/multi_v8_defconfig
+++ b/arch/arm/configs/multi_v8_defconfig
@@ -213,6 +213,7 @@ CONFIG_USB_IMX_CHIPIDEA=y
CONFIG_USB_DWC3=y
CONFIG_USB_DWC3_DUAL_ROLE=y
CONFIG_USB_EHCI=y
+CONFIG_USB_XHCI_PCI=y
CONFIG_USB_STORAGE=y
CONFIG_USB_ONBOARD_DEV=y
CONFIG_USB_GADGET=y
diff --git a/drivers/usb/host/Kconfig b/drivers/usb/host/Kconfig
index 58f276cdb45a..21a99dd85b28 100644
--- a/drivers/usb/host/Kconfig
+++ b/drivers/usb/host/Kconfig
@@ -41,3 +41,10 @@ config USB_XHCI
This driver currently only supports virtual USB 2.0 ports, if you
plan to use USB 3.0 devices, use a USB 2.0 cable in between.
+
+config USB_XHCI_PCI
+ tristate "xHCI over PCI driver"
+ depends on USB_XHCI
+ help
+ Say y here if your USB 3.0 controller is connected via PCI and
+ you wish to access the USB devices on the bus from barebox.
diff --git a/drivers/usb/host/Makefile b/drivers/usb/host/Makefile
index cbddfbe9232e..ed446d0d801c 100644
--- a/drivers/usb/host/Makefile
+++ b/drivers/usb/host/Makefile
@@ -6,3 +6,4 @@ obj-$(CONFIG_USB_EHCI_ZYNQ) += ehci-zynq.o
obj-$(CONFIG_USB_OHCI) += ohci-hcd.o
obj-$(CONFIG_USB_OHCI_AT91) += ohci-at91.o
obj-$(CONFIG_USB_XHCI) += xhci.o xhci-mem.o xhci-ring.o
+obj-$(CONFIG_USB_XHCI_PCI) += xhci-pci.o
diff --git a/drivers/usb/host/xhci-pci.c b/drivers/usb/host/xhci-pci.c
new file mode 100644
index 000000000000..ebd64b347430
--- /dev/null
+++ b/drivers/usb/host/xhci-pci.c
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/pci.h>
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/reset.h>
+#include <module.h>
+
+#include "xhci.h"
+
+static const char hcd_name[] = "xhci_hcd";
+
+static int xhci_pci_common_probe(struct pci_dev *pdev, const struct pci_device_id *id)
+{
+ struct device *dev = &pdev->dev;
+ struct xhci_ctrl *ctrl;
+ void __iomem *base;
+ int ret;
+
+ (void)id;
+
+ ret = pci_enable_device(pdev);
+ if (ret)
+ return ret;
+
+ pci_set_master(pdev);
+
+ base = pci_iomap(pdev, 0);
+ if (!base) {
+ ret = dev_err_probe(dev, -EBUSY, "failed to map BAR0\n");
+ goto err_clear_master;
+ }
+
+ ctrl = xzalloc(sizeof(*ctrl));
+ ctrl->dev = dev;
+ ctrl->hccr = base;
+ ctrl->hcor = (struct xhci_hcor *)((uintptr_t)ctrl->hccr +
+ HC_LENGTH(xhci_readl(&(ctrl->hccr)->cr_capbase)));
+
+ dev->priv = ctrl;
+
+ ret = xhci_register(ctrl);
+ if (ret)
+ goto err_free_ctrl;
+
+ return 0;
+
+err_free_ctrl:
+ dev->priv = NULL;
+ free(ctrl);
+err_clear_master:
+ pci_clear_master(pdev);
+ return ret;
+}
+
+static int xhci_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
+{
+ return xhci_pci_common_probe(dev, id);
+}
+
+static void xhci_pci_remove(struct pci_dev *dev)
+{
+ struct xhci_ctrl *ctrl = dev->dev.priv;
+
+ xhci_deregister(ctrl);
+ pci_clear_master(dev);
+ free(ctrl);
+}
+
+/*-------------------------------------------------------------------------*/
+
+/* PCI driver selection metadata; PCI hotplugging uses this */
+static const struct pci_device_id pci_ids[] = {
+ /* handle any USB 3.0 xHCI controller */
+ { PCI_DEVICE_CLASS(PCI_CLASS_SERIAL_USB_XHCI, ~0),
+ },
+ { /* end: all zeroes */ }
+};
+MODULE_DEVICE_TABLE(pci, pci_ids);
+
+/* pci driver glue; this is a "new style" PCI driver module */
+static struct pci_driver xhci_pci_driver = {
+ .name = hcd_name,
+ .id_table = pci_ids,
+
+ .probe = xhci_pci_probe,
+ .remove = xhci_pci_remove,
+};
+
+static int __init xhci_pci_init(void)
+{
+ return pci_register_driver(&xhci_pci_driver);
+}
+module_init(xhci_pci_init);
+
+MODULE_DESCRIPTION("xHCI PCI Host Controller Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/usb/host/xhci.h b/drivers/usb/host/xhci.h
index 37e8cee843cf..e5feb4f3d5a1 100644
--- a/drivers/usb/host/xhci.h
+++ b/drivers/usb/host/xhci.h
@@ -20,6 +20,7 @@
#include <io.h>
#include <io-64-nonatomic-lo-hi.h>
#include <linux/list.h>
+#include <linux/usb/usb.h>
#define MAX_EP_CTX_NUM 31
#define XHCI_ALIGNMENT 64
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 4/5] scripts: qemu_interactive.py: add new --usbblk option
2026-06-26 12:45 [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 2/5] test: move interactive QEMU launcher out of pytest Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 3/5] usb: xhci-hcd: add XHCI over PCI driver Ahmad Fatoum
@ 2026-06-26 12:45 ` Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 5/5] scripts: qemu_interactive.py: add new --nvmeblk option Ahmad Fatoum
3 siblings, 0 replies; 5+ messages in thread
From: Ahmad Fatoum @ 2026-06-26 12:45 UTC (permalink / raw)
To: barebox; +Cc: Ahmad Fatoum
To be able to easily exercise the new xhci-pci support, add into
qemu_interactive.py a new --usbblk option that behaves like --blk, but
instead of creating a Virt I/O block device, creates a removable USB
storage.
As the ARM 64-bit Virt machine has no USB host by default, enable
xhci-pci there, so it can be readily used.
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
scripts/qemu_interactive.py | 10 ++++++++++
test/arm/virt@multi_v8_defconfig.yaml | 1 +
2 files changed, 11 insertions(+)
diff --git a/scripts/qemu_interactive.py b/scripts/qemu_interactive.py
index a8605d23b132..827a831725ba 100755
--- a/scripts/qemu_interactive.py
+++ b/scripts/qemu_interactive.py
@@ -49,6 +49,9 @@ def register_shared_options(parser):
_add_option(parser, "--blk", action="append", dest="qemu_block",
default=[], metavar="FILE",
help="Pass block device to emulated barebox. Can be specified more than once")
+ _add_option(parser, "--usbblk", action="append", dest="qemu_usbblock",
+ default=[], metavar="FILE",
+ help="Pass USB block device to emulated barebox. Can be specified more than once")
_add_option(parser, "--env", action="append", dest="qemu_fw_cfg",
type=_assignment, default=[],
metavar="[envpath=]content | [envpath=]@filepath",
@@ -257,6 +260,13 @@ def apply_shared_options(strategy, target, options, *, interactive, fail=None):
else:
fail("--blk unsupported for target\n")
+ for i, blk in enumerate(_get_list_option(options, "qemu_usbblock")):
+ _append_qemu_args(
+ strategy, fail,
+ "-drive", f"if=none,format=raw,id=usbstorage{i},file={blk}",
+ "-device", f"usb-storage,drive=usbstorage{i},bus=usb-bus.0,removable=on"
+ )
+
envopts = {}
for i, fw_cfg in enumerate(_get_list_option(options, "qemu_fw_cfg")):
diff --git a/test/arm/virt@multi_v8_defconfig.yaml b/test/arm/virt@multi_v8_defconfig.yaml
index 4eb75da4610e..60707c951c2e 100644
--- a/test/arm/virt@multi_v8_defconfig.yaml
+++ b/test/arm/virt@multi_v8_defconfig.yaml
@@ -8,6 +8,7 @@ targets:
memory: 1024M
bios: barebox-qemu-virt.img
display: qemu-default
+ extra_args: '-device qemu-xhci'
BareboxDriver:
prompt: 'barebox@[^:]+:[^ ]+ '
bootstring: 'commandline:'
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 5/5] scripts: qemu_interactive.py: add new --nvmeblk option
2026-06-26 12:45 [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py Ahmad Fatoum
` (2 preceding siblings ...)
2026-06-26 12:45 ` [PATCH 4/5] scripts: qemu_interactive.py: add new --usbblk option Ahmad Fatoum
@ 2026-06-26 12:45 ` Ahmad Fatoum
3 siblings, 0 replies; 5+ messages in thread
From: Ahmad Fatoum @ 2026-06-26 12:45 UTC (permalink / raw)
To: barebox; +Cc: Ahmad Fatoum
To be able to test the recent non-512-byte block support, add a
--nvmeblk option that behaves like --blk, but appears to the guest as
block device.
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
arch/arm/configs/multi_v8_defconfig | 2 ++
scripts/qemu_interactive.py | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/arch/arm/configs/multi_v8_defconfig b/arch/arm/configs/multi_v8_defconfig
index 9712113bf1cd..5ea63372d050 100644
--- a/arch/arm/configs/multi_v8_defconfig
+++ b/arch/arm/configs/multi_v8_defconfig
@@ -103,6 +103,7 @@ CONFIG_CMD_MAGICVAR_HELP=y
CONFIG_CMD_SAVEENV=y
CONFIG_CMD_FILETYPE=y
CONFIG_CMD_LN=y
+CONFIG_CMD_STAT=y
CONFIG_CMD_MD5SUM=y
CONFIG_CMD_SHA1SUM=y
CONFIG_CMD_SHA224SUM=y
@@ -284,6 +285,7 @@ CONFIG_PHY_ROCKCHIP_USBDP=y
CONFIG_ROCKCHIP_IODOMAIN=y
CONFIG_ROCKCHIP_PM_DOMAINS=y
CONFIG_TI_SCI_PM_DOMAINS=y
+CONFIG_BLK_DEV_NVME=y
CONFIG_NVMEM_REBOOT_MODE=y
CONFIG_VIRTIO_MMIO=y
CONFIG_VIRTIO_PCI=y
diff --git a/scripts/qemu_interactive.py b/scripts/qemu_interactive.py
index 827a831725ba..77ff6f9fc8b4 100755
--- a/scripts/qemu_interactive.py
+++ b/scripts/qemu_interactive.py
@@ -49,6 +49,9 @@ def register_shared_options(parser):
_add_option(parser, "--blk", action="append", dest="qemu_block",
default=[], metavar="FILE",
help="Pass block device to emulated barebox. Can be specified more than once")
+ _add_option(parser, "--nvmeblk", action="append", dest="qemu_nvmeblock",
+ default=[], metavar="FILE",
+ help="Pass NVMe block device with 4K sector size to emulated barebox. Can be specified more than once")
_add_option(parser, "--usbblk", action="append", dest="qemu_usbblock",
default=[], metavar="FILE",
help="Pass USB block device to emulated barebox. Can be specified more than once")
@@ -260,6 +263,13 @@ def apply_shared_options(strategy, target, options, *, interactive, fail=None):
else:
fail("--blk unsupported for target\n")
+ for i, blk in enumerate(_get_list_option(options, "qemu_nvmeblock")):
+ _append_qemu_args(
+ strategy, fail,
+ "-drive", f"if=none,format=raw,id=nvme{i},file={blk}",
+ "-device", f"nvme,drive=nvme{i},serial=0ba2eb08,logical_block_size=4096,physical_block_size=4096"
+ )
+
for i, blk in enumerate(_get_list_option(options, "qemu_usbblock")):
_append_qemu_args(
strategy, fail,
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-06-26 12:47 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-26 12:45 [PATCH 1/5] scripts: duplicate conftest.py as qemu_interactive.py Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 2/5] test: move interactive QEMU launcher out of pytest Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 3/5] usb: xhci-hcd: add XHCI over PCI driver Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 4/5] scripts: qemu_interactive.py: add new --usbblk option Ahmad Fatoum
2026-06-26 12:45 ` [PATCH 5/5] scripts: qemu_interactive.py: add new --nvmeblk option Ahmad Fatoum
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox