diff mbox series

[1/3] oeqa/sdk: run the SDK's own copy of a host tool

Message ID 20260830183616.4026354-1-twoerner@gmail.com
State New
Headers show
Series [1/3] oeqa/sdk: run the SDK's own copy of a host tool | expand

Commit Message

Trevor Woerner Aug. 30, 2026, 6:36 p.m. UTC
ensure_host_package() decides whether a test can run by looking for a
package name in the SDK's host manifest, which establishes neither that
the package is present nor that the command the test runs is the SDK's
own. On an extensible SDK the manifest lists what the shipped sstate
could provide, so a case can pass the check and then run a command that
is not there, or one belonging to the build host.

Ask the SDK for the command instead. An eSDK ships sstate for more than
it installs, so when the command is missing devtool installs the recipe
and the lookup runs again. A command that resolves outside the SDK now
fails rather than reporting a result about the build host.

AI-Generated: codex/claude-opus 5 (xhigh)
Signed-off-by: Trevor Woerner <twoerner@gmail.com>
---
 meta/lib/oeqa/sdk/case.py | 96 ++++++++++++++++++++++++++++-----------
 1 file changed, 69 insertions(+), 27 deletions(-)
diff mbox series

Patch

diff --git a/meta/lib/oeqa/sdk/case.py b/meta/lib/oeqa/sdk/case.py
index 03cfde88ff42..5f269161f4e1 100644
--- a/meta/lib/oeqa/sdk/case.py
+++ b/meta/lib/oeqa/sdk/case.py
@@ -18,38 +18,80 @@  class OESDKTestCase(OETestCase):
                 (self.tc.sdk_env, cmd), shell=True, executable="/bin/bash",
                 stderr=subprocess.STDOUT, universal_newlines=True)
 
-    def ensure_host_package(self, *packages, recipe=None):
+    def find_sdk_binary(self, binaries):
         """
-        Check that the host variation of one of the packages listed is available
-        in the SDK (nativesdk-foo for SDK, foo-native for eSDK). The package is
-        a list for the case where debian-renaming may have occured, and the
-        manifest could contain 'foo' or 'libfoo'.
+        (name, outside) for the first of binaries that resolves.
 
-        If testing an eSDK and the package is not found, then try to install the
-        specified recipe to install it from sstate.
+        name is the command to run when it came from the SDK, and outside
+        lists what resolved beyond it otherwise. Callers run the name
+        returned: an eSDK stages native recipes, which need not name a
+        command the way their nativesdk counterparts do. perl-native keeps
+        its interpreter at bindir/perl-native/perl, reached as nativeperl.
+        """
+        if isinstance(binaries, str):
+            binaries = (binaries,)
+        sdk_root = os.path.dirname(self.tc.sdk_env)
+
+        outside = []
+        for binary in binaries:
+            try:
+                path = self._run("command -v %s" % binary).strip()
+            except subprocess.CalledProcessError:
+                continue
+            if path.startswith(sdk_root + os.sep):
+                return binary, []
+            outside.append("%s is %s" % (binary, path))
+        return None, outside
+
+    def ensure_sdk_binary(self, binaries, packages=()):
         """
+        Return the first of binaries that resolves inside the SDK.
 
-        # In a SDK the manifest is correct. In an eSDK the manifest may be
-        # correct (type=full) or not include packages that exist in sstate but
-        # not installed yet (minimal) so we should try to install the recipe.
-        for package in packages:
-            if isinstance(self.tc, OESDKExtTestContext):
-                package = package + "-native"
-            else:
-                package = "nativesdk-" + package
+        Fail when one resolves outside it, because a result from the build
+        host describes the host and not the SDK. Skip when none resolves.
+        """
+        if isinstance(binaries, str):
+            binaries = (binaries,)
+        if isinstance(packages, str):
+            packages = (packages,)
+        name, outside = self.find_sdk_binary(binaries)
+        if name:
+            return name
+        need = ", ".join(packages) or ", ".join(binaries)
+        if outside:
+            self.fail("Test %s needs one of %s from the SDK, but found %s"
+                      % (self.id(), need, "; ".join(outside)))
+        raise unittest.SkipTest("Test %s needs one of %s: the SDK has no %s"
+                                % (self.id(), need, " or ".join(binaries)))
+
+    def ensure_host_package(self, *packages, recipe=None, binary=None):
+        """
+        Return the command the test should run, having confirmed it comes
+        from the SDK rather than the build host.
 
-            if self.tc.hasHostPackage(package):
-                break
-        else:
-            if isinstance(self.tc, OESDKExtTestContext):
-                recipe = (recipe or packages[0]) + "-native"
-                print("Trying to install %s..." % recipe)
-                try:
-                    self._run('devtool sdk-install %s' % recipe)
-                except subprocess.CalledProcessError:
-                    raise unittest.SkipTest("Test %s needs one of %s" % (self.id(), ", ".join(packages)))
-            else:
-                raise unittest.SkipTest("Test %s needs one of %s" % (self.id(), ", ".join(packages)))
+        An eSDK ships sstate for more than it installs, so a tool can be
+        available without being present. When the command is missing, ask
+        devtool to install the recipe and look again.
+
+        binary names the commands the packages provide, and is needed only
+        where a command is not named after its package: pkgconf provides
+        pkg-config, and an eSDK reaches perl as nativeperl.
+        """
+        binaries = binary or packages
+
+        name, _ = self.find_sdk_binary(binaries)
+        if name:
+            return name
+
+        if isinstance(self.tc, OESDKExtTestContext):
+            install = (recipe or packages[0]) + "-native"
+            print("Trying to install %s..." % install)
+            try:
+                self._run('devtool sdk-install %s' % install)
+            except subprocess.CalledProcessError:
+                pass
+
+        return self.ensure_sdk_binary(binaries, packages)
 
     def ensure_target_package(self, *packages, multilib=False, recipe=None):
         """