diff --git a/changelogs/fragments/apt_virtual_pkg_fix.yml b/changelogs/fragments/apt_virtual_pkg_fix.yml new file mode 100644 index 00000000000..90089c5c0f5 --- /dev/null +++ b/changelogs/fragments/apt_virtual_pkg_fix.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - apt - add support for installing ``.deb`` files whose dependencies include virtual packages (e.g. ``libglib2.0-0`` on Ubuntu 24.04) by recognizing installed providers such as ``libglib2.0-0t64``. This resolves errors like ``Dependency is not satisfiable`` when installing VS Code and other packages (https://github.com/ansible/ansible/issues/85807). diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py index 705616bef00..c3f22680fd1 100644 --- a/lib/ansible/modules/apt.py +++ b/lib/ansible/modules/apt.py @@ -877,34 +877,48 @@ def install_deb( installed_pkg = apt.Cache()[pkg_key] installed_version = installed_pkg.installed.version if package_version_compare(pkg_version, installed_version) == 0: - # Does not need to down-/upgrade, move on to next package continue except Exception: - # Must not be installed, continue with installation pass - # Check if package is installable if not pkg.check(): - if force or ("later version" in pkg._failure_string and allow_downgrade): + missing = getattr(pkg, "missing_deps", []) + all_virtual = all(cache.is_virtual_package(dep) for dep in missing) + if all_virtual: + pass + elif force or ("later version" in pkg._failure_string and allow_downgrade): pass else: m.fail_json(msg=pkg._failure_string) - # add any missing deps to the list of deps we need - # to install so they're all done in one shot - deps_to_install.extend(pkg.missing_deps) + # handle virtual package dependencies properly + missing_deps = [] + for dep in pkg.missing_deps: + try: + if cache.is_virtual_package(dep): + providers = cache.get_providing_packages(dep) + if providers: + if any(cache[p].installed for p in providers if p in cache): + continue + else: + missing_deps.append(dep) + else: + missing_deps.append(dep) + else: + missing_deps.append(dep) + except Exception as e: + missing_deps.append(dep) + + deps_to_install.extend(missing_deps) except Exception as e: m.fail_json(msg="Unable to install package: %s" % to_native(e)) - # Install 'Recommends' of this deb file if install_recommends: pkg_recommends = get_field_of_deb(m, deb_file, "Recommends") deps_to_install.extend([pkg_name.strip() for pkg_name in pkg_recommends.split()]) - # and add this deb to the list of packages to install pkgs_to_install.append(deb_file) - # install the deps through apt retvals = {} if deps_to_install: install_dpkg_options = f"{expand_dpkg_options(dpkg_options)} -o DPkg::Lock::Timeout={lock_timeout}" @@ -934,6 +948,44 @@ def install_deb( with PolicyRcD(m): rc, out, err = m.run_command(cmd) + # handle missing virtual dependencies + if rc != 0 and "dependency problems" in err: + missing_deps = [] + for line in err.splitlines(): + if "depends on" in line and "however" in line: + dep = line.split("depends on", 1)[1].split(";", 1)[0].strip() + if dep: + missing_deps.append(dep) + + resolved = [] + for dep in missing_deps: + try: + if cache.is_virtual_package(dep): + providers = cache.get_providing_packages(dep) + if providers: + provider = providers[0] + resolved.append(provider.name) + except Exception: + continue + + if resolved: + m.warn("dpkg dependency error — virtual deps: %s → installing providers: %s" + % (', '.join(missing_deps), ', '.join(resolved))) + (success, retvals) = install( + m=m, pkgspec=resolved, cache=cache, + install_recommends=True, + fail_on_autoremove=fail_on_autoremove, + allow_unauthenticated=allow_unauthenticated, + allow_downgrade=allow_downgrade, + allow_change_held_packages=allow_change_held_packages, + dpkg_options=expand_dpkg_options(dpkg_options), + ) + if not success: + m.warn("Provider install failed, attempting apt --fix-broken install") + m.run_command("/usr/bin/apt-get -y --fix-broken install") + + rc, out, err = m.run_command(cmd) + if "stdout" in retvals: stdout = retvals["stdout"] + out else: diff --git a/test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml b/test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml new file mode 100644 index 00000000000..a3600379370 --- /dev/null +++ b/test/integration/targets/apt/tasks/apt_virtual_pkg_fix.yml @@ -0,0 +1,60 @@ +--- +- block: + - name: Create directory for test .deb + ansible.builtin.file: + path: /tmp/virtual-test/DEBIAN + state: directory + + - name: Create a test .deb file that depends on a virtual package + ansible.builtin.shell: | + cat > /tmp/virtual-test/DEBIAN/control << EOF + Package: virtual-test-pkg + Version: 1.0 + Architecture: all + Description: Test package depending on virtual package + Depends: mail-transport-agent + EOF + dpkg-deb --build /tmp/virtual-test /tmp/virtual-test.deb + args: + creates: /tmp/virtual-test.deb + + - name: Remove any existing MTA providers + ansible.builtin.apt: + name: + - exim4 + - postfix + - sendmail + state: absent + + - name: Test the patch - install .deb with virtual package dependency + ansible.builtin.apt: + deb: /tmp/virtual-test.deb + state: present + register: patch_test_result + + - name: Debug - Show patch test result + ansible.builtin.debug: + var: patch_test_result + + - name: Check if a virtual package provider was automatically installed + ansible.builtin.shell: | + dpkg -l | grep -E "^ii" | grep -E "(exim|postfix|sendmail)" && echo "SUCCESS: Virtual package provider was installed" || echo "FAIL: No provider installed" + register: provider_check + changed_when: false + + - name: Show provider check result + ansible.builtin.debug: + var: provider_check.stdout + + - name: Assert the patch worked + ansible.builtin.assert: + that: + - patch_test_result is succeeded + - "'SUCCESS' in provider_check.stdout" + fail_msg: "The patch failed to properly resolve virtual package dependencies when installing .deb files" + + always: + - name: Clean up test package + ansible.builtin.file: + path: /tmp/virtual-test.deb + state: absent diff --git a/test/integration/targets/apt/tasks/main.yml b/test/integration/targets/apt/tasks/main.yml index 4ae93219d69..3faeb9cd3be 100644 --- a/test/integration/targets/apt/tasks/main.yml +++ b/test/integration/targets/apt/tasks/main.yml @@ -30,6 +30,8 @@ - import_tasks: 'apt-builddep.yml' + - import_tasks: apt_virtual_pkg_fix.yml + - block: - import_tasks: 'repo.yml' always: diff --git a/test/units/modules/test_apt.py b/test/units/modules/test_apt.py index d207320c82a..2d87d2f1b25 100644 --- a/test/units/modules/test_apt.py +++ b/test/units/modules/test_apt.py @@ -4,8 +4,9 @@ from __future__ import annotations import collections -from ansible.modules.apt import expand_pkgspec_from_fnmatches import pytest +from ansible.modules.apt import expand_pkgspec_from_fnmatches + FakePackage = collections.namedtuple("Package", ("name",)) fake_cache = [