diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 72712747f..70e30d75b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ env: kde_deps: procps pyqt6-dev-tools python3-pyqt6 unit_deps: locales python3-systemd python3-zstandard integration_deps: > - binutils dpkg-dev gcc gdb kmod libc6-dev libglib2.0-dev libxml2-utils + binutils gcc gdb kmod libc6-dev libglib2.0-dev libxml2-utils polkitd python3-systemd valgrind xterm system_deps: > chaos-marmosets dbus dbus-x11 dirmngr dpkg-dev gcc gdb gvfs-daemons psmisc diff --git a/apport/package_info.py b/apport/package_info.py index 72a2db7d2..57217f9c1 100644 --- a/apport/package_info.py +++ b/apport/package_info.py @@ -148,23 +148,16 @@ def get_system_architecture() -> str: "this method must be implemented by a concrete subclass" ) - @staticmethod - def get_native_multiarch_triplet() -> str: - """Return the GNU multiarch triplet for of the system architecture, if - applicable, raises NotImplementedError otherwise. - """ - raise NotImplementedError( - "this method must be implemented by a concrete subclass if applicable" - ) + def get_library_paths(self, architecture: str | None = None) -> str: + """Return a list of default library search paths for the architecture. - def get_library_paths(self) -> str: - """Return a list of default library search paths. + If architecture is not specified, the system archictecture is used. The entries should be separated with a colon ':', like for $LD_LIBRARY_PATH. This needs to take any multiarch directories into account. """ - # simple default implementation, pylint: disable=no-self-use + # simple default implementation, pylint: disable=no-self-use,unused-argument return "/lib:/usr/lib" def set_mirror(self, url: str) -> None: diff --git a/apport/packaging_impl/apt_dpkg.py b/apport/packaging_impl/apt_dpkg.py index 83ec35674..05f5fb84d 100644 --- a/apport/packaging_impl/apt_dpkg.py +++ b/apport/packaging_impl/apt_dpkg.py @@ -57,6 +57,15 @@ import apport.logging from apport.package_info import PackageInfo +_ARCH_MULTIARCH_TRIPLET_MAPPING = { + "amd64": "x86_64-linux-gnu", + "arm64": "aarch64-linux-gnu", + "armhf": "arm-linux-gnueabihf", + "i386": "i386-linux-gnu", + "ppc64el": "powerpc64le-linux-gnu", + "riscv64": "riscv64-linux-gnu", + "s390x": "s390x-linux-gnu", +} # The Contents-*.gz files are huge. Loading all data into memory would result in a # dictionary with millions of entries consuming several gigabytes of memory. # Therefore exclude unneeded paths with 100k entries or more. @@ -869,26 +878,24 @@ def get_system_architecture() -> str: assert arch return arch - @staticmethod - @functools.cache - def get_native_multiarch_triplet() -> str: - """Return the multiarch triplet for the system architecture""" - dpkg = subprocess.run( - ["dpkg-architecture", "-qDEB_HOST_MULTIARCH"], - check=True, - text=True, - stdout=subprocess.PIPE, - ) - return dpkg.stdout.strip() + def get_library_paths(self, architecture: str | None = None) -> str: + """Return a list of default library search paths for the architecture. - def get_library_paths(self) -> str: - """Return a list of default library search paths. + If architecture is not specified, the system archictecture is used. The entries should be separated with a colon ':', like for $LD_LIBRARY_PATH. This needs to take any multiarch directories into account. """ - return f"/lib/{self.get_native_multiarch_triplet()}:/lib" + if architecture is None: + architecture = self.get_system_architecture() + multiarch_triplet = _ARCH_MULTIARCH_TRIPLET_MAPPING.get(architecture) + if multiarch_triplet is None: + raise NotImplementedError( + f"Missing multi-arch triplet information" + f" for architecture {architecture}" + ) + return f"/lib/{multiarch_triplet}:/usr/lib/{multiarch_triplet}:/lib:/usr/lib" def set_mirror(self, url: str) -> None: """Explicitly set a distribution mirror URL for operations that need to diff --git a/apport/report.py b/apport/report.py index c828c8b63..f349f2ca1 100644 --- a/apport/report.py +++ b/apport/report.py @@ -341,21 +341,6 @@ def _get_gdb_path(gdb_sandbox: str | None, same_arch: bool) -> str: return gdb_path -def _get_ld_search_paths(ld_so: str = "ld.so") -> list[str]: - """Query the given ld.so binary for the shared library search paths.""" - diagnostics = subprocess.run( - [ld_so, "--list-diagnostics"], check=True, stdout=subprocess.PIPE, text=True - ) - # Example matching line: path.system_dirs[0x0]="/lib/x86_64-linux-gnu/" - matches = re.findall( - r'^path\.system_dirs\[(0x[0-9a-f]+)\]="([^"]+)"$', - diagnostics.stdout, - flags=re.MULTILINE, - ) - ld_search_paths = {int(index, 0): path for (index, path) in matches} - return [path for (index, path) in sorted(ld_search_paths.items())] - - def _run_hook(report, ui, hook): if not os.path.exists(hook): return False @@ -2009,10 +1994,13 @@ def gdb_command( environ: dict[str, str] = {} if sandbox: - host_library_paths = _get_ld_search_paths( - "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" - ) - sandbox_library_paths = [f"{sandbox}{path}" for path in host_library_paths] + arch = self.get("PackageArchitecture") + if not arch or arch == "all": + arch = self.get("Architecture") + sandbox_library_paths = [ + f"{sandbox}{path}" + for path in packaging.get_library_paths(arch).split(":") + ] # N.B. set solib-absolute-prefix is an alias for set sysroot command += [ "--ex", @@ -2027,7 +2015,8 @@ def gdb_command( ] if gdb_sandbox: gdb_sandbox_library_paths = [ - f"{gdb_sandbox}{path}" for path in host_library_paths + f"{gdb_sandbox}{path}" + for path in packaging.get_library_paths().split(":") ] ld_lib_path = ":".join(gdb_sandbox_library_paths) pyhome = f"{gdb_sandbox}/usr" diff --git a/tests/integration/test_packaging_apt_dpkg.py b/tests/integration/test_packaging_apt_dpkg.py index a9af7a4f7..d1c62fb7c 100644 --- a/tests/integration/test_packaging_apt_dpkg.py +++ b/tests/integration/test_packaging_apt_dpkg.py @@ -525,7 +525,6 @@ def test_get_system_architecture(self) -> None: self.assertNotEqual(arch, "") self.assertNotIn("\n", arch) - @skip_if_command_is_missing("dpkg-architecture") def test_get_library_paths(self) -> None: """get_library_paths().""" paths = impl.get_library_paths() diff --git a/tests/unit/test_packaging_apt_dpkg.py b/tests/unit/test_packaging_apt_dpkg.py index 6c9c77618..16d0d9d35 100644 --- a/tests/unit/test_packaging_apt_dpkg.py +++ b/tests/unit/test_packaging_apt_dpkg.py @@ -102,6 +102,11 @@ def test_is_distro_package_system_image( getitem_mock.assert_called_once_with("apport") exists_mock.assert_called_once_with("/etc/system-image/channel.ini") + def test_get_library_paths_unknown_arch(self) -> None: + """get_library_paths() for unknown architecture.""" + with self.assertRaisesRegex(NotImplementedError, "architecture osprey64"): + impl.get_library_paths("osprey64") + def test_map_mirror_to_arch_ports_to_primary(self) -> None: """Test _map_mirror_to_arch() to map ports to primary.""" self.assertEqual( diff --git a/tests/unit/test_report.py b/tests/unit/test_report.py index bfc6e3b40..b0ce6b588 100644 --- a/tests/unit/test_report.py +++ b/tests/unit/test_report.py @@ -1644,49 +1644,3 @@ def test_add_kernel_crash_info_fail(self, run_mock: MagicMock) -> None: tmpfile = called_cmd[-1] self.assertRegex(tmpfile, r"^/.*/apport_vmcore_\w{8}$") self.assertFalse(pathlib.Path(tmpfile).exists()) - - @unittest.mock.patch("subprocess.run") - def test_get_ld_search_paths(self, run_mock: MagicMock) -> None: - run_mock.return_value = subprocess.CompletedProcess( - args=MagicMock(), - returncode=0, - stdout=textwrap.dedent("""\ - path.prefix="/usr" - path.sysconfdir="/etc" - path.system_dirs[0x0]="/lib/x86_64-linux-gnu/" - path.system_dirs[0x1]="/usr/lib/x86_64-linux-gnu/" - path.system_dirs[0x2]="/lib/" - path.system_dirs[0x3]="/usr/lib/" - """), - ) - ld_search_paths = apport.report._get_ld_search_paths() - self.assertEqual( - ld_search_paths, - [ - "/lib/x86_64-linux-gnu/", - "/usr/lib/x86_64-linux-gnu/", - "/lib/", - "/usr/lib/", - ], - ) - run_mock.assert_called_once_with( - ["ld.so", "--list-diagnostics"], - check=True, - stdout=subprocess.PIPE, - text=True, - ) - - @unittest.mock.patch("subprocess.run") - def test_get_ld_search_paths_sorting(self, run_mock: MagicMock) -> None: - run_mock.return_value = subprocess.CompletedProcess( - args=MagicMock(), - returncode=0, - stdout=textwrap.dedent("""\ - path.system_dirs[0x2]="third" - path.system_dirs[0x1]="second" - path.system_dirs[0x0]="first" - """), - ) - ld_search_paths = apport.report._get_ld_search_paths("ld.so") - self.assertEqual(ld_search_paths, ["first", "second", "third"]) - run_mock.assert_called_once()