From 2432100455d604cfd8f0e9adc2bb3bd1ec27c7c6 Mon Sep 17 00:00:00 2001 From: Amadeus Date: Thu, 12 Nov 2026 14:46:01 -0300 Subject: [PATCH] mintupdate-cli.py: Don't remove packages when running with --yes With --assume-yes apt-get accepts its own solution to a Breaks or Conflicts, including removing installed packages. Automatic upgrades run "mintupdate-cli upgrade --yes", so a package published before the packages it breaks were updated made the automatic upgrade remove them without anybody seeing it (#1111). Pass --no-remove along with --assume-yes. apt-get then aborts with "Packages need to be removed but remove is disabled" and installs nothing, and the next run picks the updates up once the repository is consistent. Without --yes nothing changes: apt-get shows the removals and asks. Co-Authored-By: Claude Opus 5.5 --- doc/man/mintupdate-cli.8 | 2 +- tests/test_cli_upgrade.py | 42 +++++++++++++++++++ .../linuxmint/mintUpdate/mintupdate-cli.py | 5 ++- 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tests/test_cli_upgrade.py diff --git a/doc/man/mintupdate-cli.8 b/doc/man/mintupdate-cli.8 index 66eb8906..356c52b9 100644 --- a/doc/man/mintupdate-cli.8 +++ b/doc/man/mintupdate-cli.8 @@ -55,7 +55,7 @@ Refresh the APT cache Simulation mode, don't upgrade anything .TP \fB-y\fR, \fB--yes\fR -Automatically answer yes to all questions and always install new configuration files (unless you also use "--keep-configuration" option). +Automatically answer yes to all questions and always install new configuration files (unless you also use "--keep-configuration" option). The upgrade is aborted if it would remove installed packages. .TP \fB--install-recommends\fR Install recommended packages regardless of system defaults. diff --git a/tests/test_cli_upgrade.py b/tests/test_cli_upgrade.py new file mode 100644 index 00000000..d3a01615 --- /dev/null +++ b/tests/test_cli_upgrade.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +# apt-get command line built by "mintupdate-cli upgrade". +# Run with: /usr/bin/python3 -m unittest discover -s tests + +import os +import runpy +import sys +import unittest +from unittest import mock + +from test_blacklist import FakeAptUpdater, libPath +import aptUpdater + +def apt_arguments(*args): + # Runs "mintupdate-cli upgrade" on the stub packages and returns the + # apt-get arguments instead of running apt-get + real_exists = os.path.exists + def exists(path): + return False if path == "/etc/mintupdate.blacklist" else real_exists(path) + FakeAptUpdater.gui_blacklist = [] + with mock.patch.object(aptUpdater, "AptUpdater", FakeAptUpdater), \ + mock.patch("os.path.exists", exists), \ + mock.patch.dict(os.environ), \ + mock.patch.object(sys, "argv", ["mintupdate-cli", "upgrade"] + list(args)), \ + mock.patch("subprocess.call", return_value=0) as call: + runpy.run_path(os.path.join(libPath, "mintupdate-cli.py"), run_name="__main__") + return call.call_args.args[0] + +class TestUpgrade(unittest.TestCase): + + def test_yes_never_removes(self): + arguments = apt_arguments("--yes") + self.assertIn("--assume-yes", arguments) + self.assertIn("--no-remove", arguments) + + def test_interactive_asks(self): + # Without --yes apt-get shows the removals and asks + self.assertNotIn("--no-remove", apt_arguments()) + +if __name__ == "__main__": + unittest.main() diff --git a/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py b/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py index c06805ca..d75943e9 100755 --- a/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py +++ b/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py @@ -20,7 +20,7 @@ parser.add_argument("-i", "--ignore", help="list of updates to ignore (comma-separated list of source package names). Note: You can also blacklist updates by adding them to /etc/mintupdate.blacklist, one source package per line. To ignore a specific version, use the format package=version.") parser.add_argument("-r", "--refresh-cache", action="store_true", help="refresh the APT cache") parser.add_argument("-d", "--dry-run", action="store_true", help="simulation mode, don't upgrade anything") - parser.add_argument("-y", "--yes", action="store_true", help="automatically answer yes to all questions and always install new configuration files (unless you also use \"--keep-configuration\" option)") + parser.add_argument("-y", "--yes", action="store_true", help="automatically answer yes to all questions and always install new configuration files (unless you also use \"--keep-configuration\" option). The upgrade is aborted if it would remove installed packages.") parser.add_argument("--install-recommends", action="store_true", help="install recommended packages") parser.add_argument("--keep-configuration", action="store_true", default=False, help="always keep local changes in configuration files (use with caution)") parser.add_argument("-v", "--version", action="version", version="__DEB_VERSION__", help="Display the current version") @@ -72,7 +72,8 @@ if args.yes: environment = os.environ environment.update({"DEBIAN_FRONTEND": "noninteractive"}) - arguments.append("--assume-yes") + # never let apt remove packages without asking + arguments.extend(["--assume-yes", "--no-remove"]) if not args.keep_configuration: arguments.extend(["--option", "Dpkg::Options::=--force-confnew"]) else: