├── .github └── workflows │ └── build.yml ├── .gitignore ├── Faces-Credit.txt ├── debian ├── changelog ├── control ├── copyright ├── mint-common.install ├── rules └── source │ └── format ├── installer-interact ├── makepot ├── mint-common.pot ├── test └── usr ├── bin ├── apt-changelog ├── cinnamon-remove-application ├── mint-remove-application └── mint-remove-flatpak ├── lib ├── linuxmint │ └── common │ │ └── mint-remove-application.py └── python3 │ └── dist-packages │ └── mintcommon │ ├── __init__.py │ ├── additionalfiles.py │ ├── apt_changelog.py │ ├── aptdaemon.py │ └── installer │ ├── __init__.py │ ├── _apt.py │ ├── _flatpak.py │ ├── appstream_pool.py │ ├── cache.py │ ├── dialogs.py │ ├── installer.py │ ├── misc.py │ └── pkgInfo.py └── share ├── linuxmint ├── icons │ └── flatpak-symbolic.svg └── logo.png ├── pixmaps └── faces │ ├── 0_cars.jpg │ ├── 0_chess.jpg │ ├── 0_coffee.jpg │ ├── 0_guitar.jpg │ ├── 2_10.png │ ├── 2_11.png │ ├── 2_12.png │ ├── 2_13.png │ ├── 3_lightning.jpg │ ├── 3_mountain.jpg │ ├── 3_sky.jpg │ ├── 3_sunset.jpg │ ├── 4_cinnamon.jpg │ ├── 4_flower.jpg │ ├── 4_leaf.jpg │ ├── 4_sunflower.jpg │ ├── 5_fish.jpg │ ├── 5_kitten.jpg │ ├── 5_penguin.jpg │ ├── 5_puppy.jpg │ ├── 6_astronaut.jpg │ ├── 6_butterfly.png │ ├── 6_flake.jpg │ ├── 6_grapes.jpg │ ├── 7_bat.png │ ├── 7_dog.png │ ├── 7_elephant.png │ ├── 7_fox.png │ ├── 7_lion.png │ ├── 7_panda.png │ ├── 7_penguin.png │ └── 7_tucan.png └── polkit-1 └── actions └── com.linuxmint.mintcommon.policy /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | uses: linuxmint/github-actions/.github/workflows/do-builds.yml@master 15 | with: 16 | commit_id: master 17 | ############################## Comma separated list - like 'linuxmint/xapp, linuxmint/cinnamon-desktop' 18 | dependencies: > 19 | linuxmint/xapp 20 | ############################## 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debian/mint-common 2 | debian/*.substvars 3 | debian/debhelper-build-stamp 4 | debian/.debhelper 5 | debian/files 6 | -------------------------------------------------------------------------------- /Faces-Credit.txt: -------------------------------------------------------------------------------- 1 | Icons distributed by Tutorial9 http://www.Tutorial9.net/ 2 | Created by Elio Rivero http://www.ilovecolors.com.ar 3 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | mint-common (2.4.6) xia; urgency=medium 2 | 3 | [ Michael Webster ] 4 | * _flatpak.py: Fix error when loading a flatpakref file. 5 | * _flatpak.py: Don't try to pass a NULL branch to construct a FlatpakRemoteRef. 6 | * appstream_pool.py: Check for compressed xml, and return source screenshots if no thumbnails are available. 7 | 8 | -- Clement Lefebvre Fri, 07 Feb 2025 18:45:21 +0000 9 | 10 | mint-common (2.4.5) xia; urgency=medium 11 | 12 | [ Michael Webster ] 13 | * appstream: Check for .desktop suffixed ids also. 14 | * _apt.py: Filter out kernel-related packages. 15 | 16 | -- Clement Lefebvre Sun, 05 Jan 2025 11:13:26 +0000 17 | 18 | mint-common (2.4.4) xia; urgency=medium 19 | 20 | [ Michael Webster ] 21 | * Appstream: Use exact name match for application lookups. 22 | 23 | -- Clement Lefebvre Thu, 19 Dec 2024 16:19:57 +0000 24 | 25 | mint-common (2.4.3) xia; urgency=medium 26 | 27 | [ Michael Webster ] 28 | * appstream_pool.py: Use en_US when loading appstream, if no other locale is set. 29 | 30 | -- Clement Lefebvre Wed, 11 Dec 2024 16:27:38 +0000 31 | 32 | mint-common (2.4.2) xia; urgency=medium 33 | 34 | [ Michael Webster ] 35 | * installer: Don't cache the appstream package at runtime, it can be refreshed and become invalid. 36 | 37 | -- Clement Lefebvre Fri, 06 Dec 2024 10:42:37 +0000 38 | 39 | mint-common (2.4.1) xia; urgency=medium 40 | 41 | [ Michael Webster ] 42 | * Fix Flatpak addon lookup. 43 | * flatpak: More work on addon detection. 44 | 45 | -- Clement Lefebvre Tue, 03 Dec 2024 10:17:41 +0000 46 | 47 | mint-common (2.4.0) xia; urgency=medium 48 | 49 | [ Michael Webster ] 50 | * Use xmlb instead of AppStream for working with flatpak appstream files. 51 | * flatpaks: Don't process installed refs until appstream data has been loaded. 52 | * Fix a warning message. 53 | * Allow pkginfo lookup to accept FlatpakRef string in addition to a simple name. 54 | * Add mint-remove-flatpak. 55 | * Let xmlb load only desired language nodes, instead of relying on language-restricted xpath queries. 56 | * Always check for updated appstream at startup. 57 | * Add keywords to the cache. 58 | 59 | [ fredcw ] 60 | * mint-remove-application: only disallow removal ... (#59) 61 | 62 | [ Soumya Roy ] 63 | * _apt.py : Used setdefault method to initialize sections dictionary (#60) 64 | 65 | [ Clement Lefebvre ] 66 | * l10n: Update POT 67 | 68 | -- Clement Lefebvre Tue, 26 Nov 2024 10:25:48 +0000 69 | 70 | mint-common (2.3.9) wilma; urgency=medium 71 | 72 | [ Michael Webster ] 73 | * flatpak: Use a set() instead of array when gathering locale variants. 74 | 75 | -- Clement Lefebvre Sun, 04 Aug 2024 13:46:53 +0100 76 | 77 | mint-common (2.3.8) wilma; urgency=medium 78 | 79 | [ Michael Webster ] 80 | * flatpaks: backport locale utility function from appstream 1.0+. 81 | * pkginfo: Fall back to html2text if the appstream library doesn't contain AppStream.markup_convert(). 82 | 83 | -- Clement Lefebvre Fri, 26 Jul 2024 17:33:56 +0100 84 | 85 | mint-common (2.3.7) wilma; urgency=medium 86 | 87 | [ Michael Webster ] 88 | * pkgInfo: Filter out apt packages without a candidate, they're broken to mintinstall/mintcommon. 89 | 90 | -- Clement Lefebvre Fri, 19 Jul 2024 14:53:20 +0100 91 | 92 | mint-common (2.3.6) wilma; urgency=medium 93 | 94 | [ Michael Webster ] 95 | * flatpaks: Also check for the verified tag. 96 | * flatpak: Get the developer name for the current locale. 97 | * pkginfo: Fix flatpak description formatting. 98 | * flatpak: Don't add installed runtimes to the in-memory package cache. 99 | * pkginfo: Use AppStream's function for formatting flatpak descriptions. 100 | 101 | -- Clement Lefebvre Wed, 17 Jul 2024 15:59:26 +0100 102 | 103 | mint-common (2.3.5) wilma; urgency=medium 104 | 105 | [ Michael Webster ] 106 | * cache: Don't search for apt packages with the flatpak lookup. 107 | 108 | -- Clement Lefebvre Sat, 06 Jul 2024 00:25:18 +0100 109 | 110 | mint-common (2.3.4) wilma; urgency=medium 111 | 112 | [ Michael Webster ] 113 | * Fix cache lookup for incomplete flatpak ref strings. 114 | * flatpaks: When updating the cache, fetch an updated appstream file before creating a remote Pool. 115 | 116 | -- Clement Lefebvre Fri, 05 Jul 2024 20:59:35 +0100 117 | 118 | mint-common (2.3.3) wilma; urgency=medium 119 | 120 | [ Michael Webster ] 121 | * pkginfo: Initialize cached strings to None 122 | 123 | -- Clement Lefebvre Tue, 18 Jun 2024 18:50:19 +0100 124 | 125 | mint-common (2.3.2) wilma; urgency=medium 126 | 127 | [ Michael Webster ] 128 | * polkit: Remove restrictions for remote/non-active sessions. 129 | * flatpaks: Use AppStream and libxmlb instead of AppStreamGlib. 130 | * Remove any addons when uninstalling a flatpak. 131 | * cache.py: Add a schema version to the package cache. 132 | * Installer: Performance-oriented improvements. 133 | * cache.py: Save existing apt section info when updating flatpaks. 134 | * debian/control: Update dependencies. 135 | * Add .gitignore 136 | * Simplify Xpath query for app verification update. 137 | * _flatpak.py: Set resolve_addons when checking for addons to remove during flatpak uninstall. 138 | * Cache the verified state of Flatpaks. 139 | * _flatpak.py: Remove useless code, small cleanup. 140 | * _flatpak.py: Fix wrong return type checking for addons. 141 | * flatpak: Don't skip loading appstream data even if a repo is marked no-enumerate. 142 | * flatpaks: Query xmlb data for a developer name and cache it. 143 | * flatpak: Don't remove an EOL'd ref from a transaction unless a replacement is supplied. 144 | * flatpaks: Prevent useless warning in _get_addon_refs_for_pkginfo(). 145 | 146 | -- Clement Lefebvre Sun, 16 Jun 2024 11:07:38 +0100 147 | 148 | mint-common (2.3.1) virginia; urgency=medium 149 | 150 | [ Michael Webster ] 151 | * _flatpak.py: Fix end-of-lifed-with-rebase handling. 152 | 153 | -- Clement Lefebvre Mon, 27 Nov 2023 13:52:36 +0000 154 | 155 | mint-common (2.3.0) victoria; urgency=medium 156 | 157 | [ Michael Webster ] 158 | * installer: Add mint-translations to critical packages. 159 | 160 | -- Clement Lefebvre Mon, 26 Jun 2023 11:17:06 +0200 161 | 162 | mint-common (2.2.9) victoria; urgency=medium 163 | 164 | [ Michael Webster ] 165 | * installer: Check if a local flatpak icon exists before trying to use it. 166 | * More rework for flatpak icons. 167 | 168 | -- Clement Lefebvre Wed, 14 Jun 2023 09:21:28 +0200 169 | 170 | mint-common (2.2.8) victoria; urgency=medium 171 | 172 | [ Michael Webster ] 173 | * flatpaks: Skip no-enumerate remotes when loading appstream data. 174 | 175 | [ hduelme ] 176 | * replace equality None check with identity None check (#51) 177 | * prevent NameError in process_full_apt_cache (#54) 178 | * refactor _get_best_save_path (#55) 179 | 180 | [ Michael Webster ] 181 | * apt: Catch broken dependency errors and use a more appropriate task state. 182 | * installer: Add a signal for when appstream is updated. 183 | * installer: Make appstream callback optional. 184 | * pkgInfo.py: Check the theme for APT app icons before looking in legacy locations. 185 | 186 | -- Clement Lefebvre Mon, 05 Jun 2023 13:32:11 +0100 187 | 188 | mint-common (2.2.7) vera; urgency=medium 189 | 190 | [ Michael Webster ] 191 | * flatpaks: Prefer using a locally stored icon over a remote one, and return an icon of the correct size. 192 | * flatpaks: Set the task status to unknown upon any error condition not already handled, so mintinstall knows how to react. 193 | * _flatpak.py: Don't use gdk_threads_enter/leave. 194 | 195 | -- Clement Lefebvre Wed, 11 Jan 2023 11:34:11 +0000 196 | 197 | mint-common (2.2.6) vera; urgency=medium 198 | 199 | [ Michael Webster ] 200 | * Add workflow for actions. 201 | * installer: Fix a couple of runtime errors. 202 | * Fix error when there is a cache loading problem. 203 | * Sanitize output from _apt.py. 204 | 205 | -- Clement Lefebvre Mon, 19 Dec 2022 15:07:08 +0000 206 | 207 | mint-common (2.2.5) vera; urgency=medium 208 | 209 | [ Michael Webster ] 210 | * installer: Clean up logging. 211 | * Fix flatpak end-of-lifed-with-rebase signal. 212 | 213 | -- Clement Lefebvre Sat, 17 Dec 2022 12:08:34 +0000 214 | 215 | mint-common (2.2.4) vera; urgency=medium 216 | 217 | [ Michael Webster ] 218 | * flatpak: Remove any installed addons when removing an application. 219 | * Disable auto-pinning of add-ons. 220 | * flatpak: Add logging for update operations. 221 | * flatpak: Handle a case where a new package must be installed during an update. 222 | * flatpak: Implement FlatpakTransaction:add-remote and finish implementing :end-of-life-with-rebase. 223 | * Fix progress and error handling. 224 | * installer: Remove some useless code. 225 | * cache: Don't destroy existing cache if it's for a specific package type. 226 | * Don't store an empty appstream component, otherwise it will never be checked again. 227 | 228 | -- Clement Lefebvre Thu, 15 Dec 2022 12:53:00 +0000 229 | 230 | mint-common (2.2.3) vera; urgency=medium 231 | 232 | [ Michael Webster ] 233 | * aptdaemon.py: Go back to using aptdaemon's native confirmation dialog. 234 | 235 | -- Clement Lefebvre Sat, 03 Dec 2022 14:43:21 +0000 236 | 237 | mint-common (2.2.2) vera; urgency=medium 238 | 239 | [ Michael Webster ] 240 | * apt: Remove some dead code, fix error handling for critical packages. 241 | 242 | -- Clement Lefebvre Fri, 02 Dec 2022 17:19:26 +0000 243 | 244 | mint-common (2.2.1) vera; urgency=medium 245 | 246 | [ Michael Webster ] 247 | * Allow specifying a remote when searching for a package, provide a method to retrieve RemoteInfo objects, simplify package filtering. 248 | 249 | -- Clement Lefebvre Wed, 30 Nov 2022 14:13:00 +0000 250 | 251 | mint-common (2.2.0) vera; urgency=medium 252 | 253 | [ Michael Webster ] 254 | * installer: Use the correct method for getting the app's homepage. 255 | 256 | [ Clement Lefebvre ] 257 | * mint-remove-application: Run in user mode 258 | 259 | [ Michael Webster ] 260 | * Refactor to get updates working with mintupdate (#45) 261 | 262 | [ hduelme ] 263 | * use double-quoted strings for docstrings (#50) 264 | * replace trailing semicolon (#52) 265 | 266 | [ Clement Lefebvre ] 267 | * l10n: Update POT 268 | 269 | -- Clement Lefebvre Tue, 29 Nov 2022 14:39:57 +0000 270 | 271 | mint-common (2.1.9) vanessa; urgency=medium 272 | 273 | * Adapt to new PPA url 274 | 275 | -- Clement Lefebvre Tue, 20 Sep 2022 14:03:34 +0100 276 | 277 | mint-common (2.1.8) vanessa; urgency=medium 278 | 279 | [ fredcw ] 280 | * Don't allow uninstall of a package if it has dependents (reverse dependencies) (#48) 281 | 282 | [ Michael Webster ] 283 | * Use debhelper-compat. 284 | 285 | [ Clement Lefebvre ] 286 | * l10n: Update POT 287 | 288 | -- Clement Lefebvre Tue, 21 Jun 2022 14:58:20 +0200 289 | 290 | mint-common (2.1.7) una; urgency=medium 291 | 292 | [ Stefan Schmidt ] 293 | * Missing a closing tag, getting parsing error (#49) 294 | 295 | [ Michael Webster ] 296 | * aptdaemon.py: Run the client's cancel callback when authentication fails or is cancelled. 297 | 298 | -- Clement Lefebvre Mon, 07 Mar 2022 18:41:09 +0000 299 | 300 | mint-common (2.1.6) una; urgency=medium 301 | 302 | [ Michael Webster ] 303 | * _flatpak.py: Check the remote appstream timestamp against the actual appstream archive's mtime. 304 | * _apt.py: Filter out snapd, don't allow malformed packages to enter the cache. 305 | 306 | -- Clement Lefebvre Thu, 25 Nov 2021 15:09:27 +0000 307 | 308 | mint-common (2.1.5) uma; urgency=medium 309 | 310 | [ Michael Webster ] 311 | * pkgInfo.py: Prefer application-supplied icons before looking for them in the system theme. 312 | 313 | [ Vincent Vermeulen ] 314 | * Fix for dialogs.py to use translations 315 | 316 | -- Clement Lefebvre Thu, 03 Jun 2021 13:01:08 +0100 317 | 318 | mint-common (2.1.4) ulyssa; urgency=medium 319 | 320 | [ JosephMcc ] 321 | * _flatpak.py: Add missing gi.require_version() 322 | 323 | [ okaestne ] 324 | * dialogs.py: set up gettext only locally by using class-based gettext API 325 | * additionalfiles.py: don't install temporary gettext domains globally 326 | * additionalfiles.py: close opened files 327 | 328 | [ Michael Webster ] 329 | * installer: attempt to filter out transitional packages in apt. 330 | 331 | -- Clement Lefebvre Mon, 30 Nov 2020 14:58:55 +0000 332 | 333 | mint-common (2.1.3) ulyana; urgency=medium 334 | 335 | [ Michael Webster ] 336 | * installer/_apt.py: Send all package names to the aptdaemon for an install, instead of just the selected package. 337 | 338 | -- Clement Lefebvre Sun, 21 Jun 2020 13:56:25 +0100 339 | 340 | mint-common (2.1.2) ulyana; urgency=medium 341 | 342 | [ JosephMcc ] 343 | * pkgInfo.py: Add a couple missing gi.require_version() 344 | 345 | -- Clement Lefebvre Fri, 15 May 2020 10:32:01 +0100 346 | 347 | mint-common (2.1.1) ulyana; urgency=medium 348 | 349 | [ Michael Webster ] 350 | * debian/control: add appstream-glib gir file to depends. 351 | 352 | -- Clement Lefebvre Thu, 14 May 2020 20:33:58 +0100 353 | 354 | mint-common (2.1.0) ulyana; urgency=medium 355 | 356 | [ Michael Webster ] 357 | * (installer) _apt.py: adapt to python-apt 1.9 358 | * cache.py: Catch exception when reading the initial-status.gz file. 359 | * dialogs.py: Increase the install confirmation/additional packages dialog's default size. 360 | * aptdaemon.py: Use the installer's ChangesConfirmDialog instead of AptConfirmDialog. 361 | * installer: Use AppStreamGlib instead of AppStream. 362 | 363 | -- Clement Lefebvre Tue, 21 Apr 2020 15:30:46 +0100 364 | 365 | mint-common (2.0.9) tricia; urgency=medium 366 | 367 | * additionalfiles: Only generate translations for existing MOs 368 | 369 | -- Clement Lefebvre Sat, 16 Nov 2019 12:05:13 +0100 370 | 371 | mint-common (2.0.8) tina; urgency=medium 372 | 373 | [ Michael Webster ] 374 | * _flatpak.py: Create a remote ref directly when installing from a flatpakref file, rather than parsing out a basic ref first. This handles missing (and possibly unnecessary) info from the ref file. 375 | * installer.py: Don't skip no-enumerate remotes when checking for differences between the installer's remote list and the FlatpakInstallation's. 376 | 377 | -- Clement Lefebvre Fri, 25 Oct 2019 10:14:11 +0100 378 | 379 | mint-common (2.0.7) tina; urgency=medium 380 | 381 | [ Michael Webster ] 382 | * installer: Use json instead of pickle for the package cache. (#37) 383 | 384 | -- Clement Lefebvre Fri, 04 Oct 2019 10:03:11 +0100 385 | 386 | mint-common (2.0.6) tina; urgency=medium 387 | 388 | [ Michael Webster ] 389 | * debian/control: remove python dependency 390 | * _flatpak.py: Don't fail an entire job if one package fails to install (#36) 391 | 392 | -- Clement Lefebvre Mon, 23 Sep 2019 10:31:32 +0100 393 | 394 | mint-common (2.0.5) tina; urgency=medium 395 | 396 | [ Michael Webster ] 397 | * dialogs: Set keep-above and skip-taskbar on confirm dialogs when there is no parent window (such as when removing apps from the menu). 398 | * Use appstream api to choose flatpak icons. 399 | * pkgInfo.py: allow appstream 'remote' icons 400 | * pkgInfo.py: Match apt packages against icons ending in -icon from the app-install folder. 401 | * _flatpak.py: Ignore BaseApp packages. 402 | * installer: Remove logging line, replace MintInstall logging references to simply "Installer" 403 | * _flatpak.py: If no apps are installed, don't try to install theme packages during an update (via mintinstall-update-flatpak). 404 | 405 | -- Clement Lefebvre Mon, 19 Aug 2019 18:15:20 +0200 406 | 407 | mint-common (2.0.4) tina; urgency=medium 408 | 409 | * Remove Python 3.6 code 410 | 411 | -- Clement Lefebvre Mon, 05 Aug 2019 13:04:44 +0200 412 | 413 | mint-common (2.0.3) tina; urgency=medium 414 | 415 | * Remove python 3.6 code 416 | 417 | -- Clement Lefebvre Mon, 05 Aug 2019 11:29:52 +0200 418 | 419 | mint-common (2.0.2) tina; urgency=medium 420 | 421 | [ Corbin ] 422 | * prevent flatpak from always installing remote repo & fix typo 423 | 424 | [ Michael Webster ] 425 | * _flatpak.py: Add a bit more logging to flatpakref handling, fix an indentation problem. 426 | * _flatpak.py: Fix condition for ignoring user auth cancel when adding a remote, fix some warnings. 427 | * Add a script to test installer, imports don't work properly anymore when executing installer.py on its own. 428 | * _flatpak.py: Add function to return system-matching theme packages for the mintinstall-update-flatpak script. 429 | 430 | -- Clement Lefebvre Thu, 25 Jul 2019 10:31:46 +0200 431 | 432 | mint-common (2.0.1) tina; urgency=medium 433 | 434 | * Remove version module 435 | * apt_changelog fixes 436 | * mint-remove-application: Sanitize process/subprocess module calls 437 | 438 | -- Clement Lefebvre Sun, 30 Jun 2019 15:20:32 +0200 439 | 440 | mint-common (2.0.0) tessa; urgency=medium 441 | 442 | [ gm10 ] 443 | * Move code into python modules (#30) 444 | * Add apt_changelog module (#31) 445 | 446 | [ Clement Lefebvre ] 447 | * l10n: Fix makepot and update POT 448 | * Remove unused script 449 | * Turn version.py into a module 450 | * Remove module version 451 | * Turn additionalfiles.py into a module 452 | 453 | -- Clement Lefebvre Wed, 27 Feb 2019 11:44:15 +0000 454 | 455 | mint-common (1.3.4) tara; urgency=medium 456 | 457 | [ Michael Webster ] 458 | * Use auth_admin for polkit authentication - this will require an admin user, instead of allowing the current user to perform elevated actions. 459 | 460 | -- Clement Lefebvre Thu, 09 Aug 2018 17:45:15 +0200 461 | 462 | mint-common (1.3.3) tara; urgency=medium 463 | 464 | [ monsta ] 465 | * fix runtime dependencies (#28) 466 | * remove unused scripts (#27) 467 | 468 | [ Clement Lefebvre ] 469 | * Fix flatpak removals 470 | 471 | -- Clement Lefebvre Fri, 08 Jun 2018 12:36:26 +0100 472 | 473 | mint-common (1.3.2) tara; urgency=medium 474 | 475 | * Apt: Force file installation when using install_file() 476 | 477 | -- Clement Lefebvre Thu, 07 Jun 2018 11:52:29 +0100 478 | 479 | mint-common (1.3.1) tara; urgency=medium 480 | 481 | [ Clement Lefebvre ] 482 | * additionalfiles: Add the ability to generate polkit policies 483 | 484 | [ monsta ] 485 | * remove ancient launch_browser_as.py script (#26) 486 | 487 | [ Clement Lefebvre ] 488 | * Add support to install local .deb files 489 | 490 | -- Clement Lefebvre Wed, 06 Jun 2018 11:58:25 +0100 491 | 492 | mint-common (1.3.0) tara; urgency=medium 493 | 494 | [ Clement Lefebvre ] 495 | * Remove progress arguments in synaptic call. 496 | * Remove mint-which-launcher.py 497 | * Provide an APTdaemon interface in Python3 498 | * AptDaemon: Add the ability to remove packages 499 | * AptDaemon: Call finished callback when the user cancel the installation 500 | * AptDaemon: Improve callbacks 501 | * Migrate mint-remove-application to python3/aptdaemon/pkexec 502 | 503 | [ Michael Webster ] 504 | * mint-remove-application.py: let mintinstall's removal script attempt to uninstall flatpak apps. 505 | 506 | [ Clement Lefebvre ] 507 | * l10n: Update files 508 | 509 | -- Clement Lefebvre Mon, 07 May 2018 11:47:25 +0100 510 | 511 | mint-common (1.2.9) sylvia; urgency=medium 512 | 513 | * Focus default buttons when using Gtk.MessageDialog. 514 | * Remove nemo send-by-email action (moved to Nemo) 515 | 516 | -- Clement Lefebvre Tue, 24 Oct 2017 11:01:25 +0100 517 | 518 | mint-common (1.2.8) sonya; urgency=medium 519 | 520 | * Additionalfiles: Support appending to file 521 | * Additionalfiles: Set LANGUAGE rather than LANG, only set comment if not None 522 | * l10n: Generate additional files 523 | 524 | -- Clement Lefebvre Sun, 07 May 2017 13:20:21 +0100 525 | 526 | mint-common (1.2.7) serena; urgency=medium 527 | 528 | [ xenopeek ] 529 | * version.py: improve performance and update to Python 3 (v2) 530 | 531 | -- Clement Lefebvre > Wed, 02 Nov 2016 14:09:38 +0000 532 | 533 | mint-common (1.2.6) sarah; urgency=medium 534 | 535 | * Fixed small bugs in mint-remove-applications 536 | 537 | -- Clement Lefebvre Thu, 16 Jun 2016 18:18:55 +0100 538 | 539 | mint-common (1.2.5) sarah; urgency=medium 540 | 541 | [ Michael Webster ] 542 | * additionalfiles.py: include keyword translations if provided 543 | 544 | [ Clement Lefebvre ] 545 | * Small fixes in usr/lib/linuxmint/common/mint-remove-application.py 546 | 547 | -- Clement Lefebvre Mon, 30 May 2016 12:25:00 +0100 548 | 549 | mint-common (1.2.4) sarah; urgency=medium 550 | 551 | [ Daniel Alley ] 552 | * Removed static configobj library 553 | 554 | -- Clement Lefebvre Tue, 10 May 2016 18:30:26 +0100 555 | 556 | mint-common (1.2.3) sarah; urgency=medium 557 | 558 | * Updated generated files 559 | 560 | -- Clement Lefebvre Thu, 21 Apr 2016 17:04:27 +0100 561 | 562 | mint-common (1.2.2) sarah; urgency=medium 563 | 564 | [ Clement Lefebvre ] 565 | * Removed entrydialog (no longer used by mint-tools) 566 | 567 | [ Daniel Alley ] 568 | * PEP8 formatting 569 | * updated debian/control (was out of date) 570 | * fixed debian/control typo 571 | 572 | -- Clement Lefebvre Fri, 19 Feb 2016 12:19:34 +0000 573 | 574 | mint-common (1.2.1) sarah; urgency=medium 575 | 576 | * Changed shebangs to #!/usr/bin/python2 577 | 578 | -- Clement Lefebvre Mon, 11 Jan 2016 15:59:47 +0000 579 | 580 | mint-common (1.2.0) rosa; urgency=medium 581 | 582 | * Fixed packaging regression in 1.1.9 583 | 584 | -- Clement Lefebvre Sun, 08 Nov 2015 22:16:41 +0000 585 | 586 | mint-common (1.1.9) rosa; urgency=medium 587 | 588 | [ darealshinji ] 589 | * delete temporarily created build files 590 | * update Debian files 591 | * add missing shebang to shell script 592 | * mention license of configobj.py 593 | 594 | [ Clement Lefebvre ] 595 | * Prefer bash 596 | * [darealshinji] delete python bytecode 597 | * Updated desktop files 598 | 599 | -- Clement Lefebvre Fri, 06 Nov 2015 16:54:48 +0000 600 | 601 | mint-common (1.1.8) betsy; urgency=medium 602 | 603 | * Changed the icon for the thunderbird nemo action 604 | 605 | -- Clement Lefebvre Wed, 18 Feb 2015 10:13:07 +0100 606 | 607 | mint-common (1.1.7) betsy; urgency=medium 608 | 609 | * Removed /usr/share/pixmaps/faces/user-generic.png (provided by mdm) 610 | 611 | -- Clement Lefebvre Fri, 30 Jan 2015 09:58:18 +0100 612 | 613 | mint-common (1.1.6) betsy; urgency=medium 614 | 615 | * Updated user pictures in /usr/share/pixmaps/faces 616 | 617 | -- Clement Lefebvre Fri, 30 Jan 2015 09:52:34 +0100 618 | 619 | mint-common (1.1.5) qiana; urgency=medium 620 | 621 | * Support GenericName in additionalfiles.py 622 | 623 | -- Clement Lefebvre Sun, 04 May 2014 13:25:19 +0100 624 | 625 | mint-common (1.1.4) qiana; urgency=medium 626 | 627 | * Improved additionalfiles.py 628 | 629 | -- Clement Lefebvre Sun, 04 May 2014 02:22:41 +0100 630 | 631 | mint-common (1.1.3) qiana; urgency=medium 632 | 633 | * Updated desktop files 634 | * provides additionalfiles.py 635 | 636 | -- Clement Lefebvre Sun, 04 May 2014 01:39:53 +0100 637 | 638 | mint-common (1.1.2) qiana; urgency=medium 639 | 640 | * Correct the syntax for Thunderbird file attachments 641 | * Added mint-remove-application.py and cinnamon-remove-application 642 | * Added dep on synaptic 643 | 644 | -- Clement Lefebvre Mon, 14 Apr 2014 13:07:20 +0100 645 | 646 | mint-common (1.1.1) petra; urgency=low 647 | 648 | * Improved generate_additional_files.py, refreshed translations for email nemo action 649 | 650 | -- Clement Lefebvre Sun, 24 Nov 2013 19:16:27 +0000 651 | 652 | mint-common (1.1.0) olivia; urgency=low 653 | 654 | * Added nemo action for thunderbird 655 | 656 | -- Clement Lefebvre Thu, 22 Aug 2013 14:28:58 +0100 657 | 658 | mint-common (1.0.9) nadia; urgency=low 659 | 660 | * Added a collection of faces icons 661 | 662 | -- Clement Lefebvre Mon, 07 Jan 2013 16:04:21 +0000 663 | 664 | mint-common (1.0.8) maya; urgency=low 665 | 666 | * Added entrydialog 667 | 668 | -- Clement Lefebvre Mon, 30 Jul 2012 14:28:05 +0100 669 | 670 | mint-common (1.0.7) maya; urgency=low 671 | 672 | * Added env_check 673 | 674 | -- Clement Lefebvre Mon, 25 Jun 2012 14:30:28 +0100 675 | 676 | mint-common (1.0.6) lisa; urgency=low 677 | 678 | * Use gksu in KDE if it's installed 679 | 680 | -- Clement Lefebvre Thu, 15 Dec 2011 11:54:24 +0000 681 | 682 | mint-common (1.0.5) helena; urgency=low 683 | 684 | * Added dependency on mint-translations 685 | 686 | -- Clement Lefebvre Tue, 3 Nov 2009 12:23:00 +0000 687 | 688 | mint-common (1.0.4) helena; urgency=low 689 | 690 | * Changed translation framework 691 | 692 | -- Clement Lefebvre Tue, 3 Nov 2009 11:38:00 +0000 693 | 694 | mint-common (1.0.3) helena; urgency=low 695 | 696 | * Added launch_browser_as.py 697 | 698 | -- Clement Lefebvre Fri, 30 Oct 2009 17:43:00 +0000 699 | 700 | mint-common (1.0.2) helena; urgency=low 701 | 702 | * Added a lot of stuff 703 | 704 | -- Clement Lefebvre Wed, 7 Oct 2009 16:41:00 +0000 705 | 706 | mint-common (1.0.1) helena; urgency=low 707 | 708 | * Added Linux Mint logo 709 | 710 | -- Clement Lefebvre Wed, 7 Oct 2009 16:24:00 +0000 711 | 712 | mint-common (1.0.0) helena; urgency=low 713 | 714 | * Initial release 715 | 716 | -- Clement Lefebvre Wed, 7 Oct 2009 14:18:00 +0000 717 | 718 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: mint-common 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Clement Lefebvre 5 | Build-Depends: debhelper-compat (= 12) 6 | Standards-Version: 3.9.5 7 | 8 | Package: mint-common 9 | Architecture: all 10 | Depends: python3, 11 | python3-aptdaemon, 12 | python3-aptdaemon.gtk3widgets, 13 | python3-gi, 14 | python3-html2text, 15 | gir1.2-gtk-3.0, 16 | gir1.2-xapp-1.0, 17 | gir1.2-xmlb-2.0, 18 | mint-translations, 19 | libgtk3-perl 20 | ${misc:Depends}, 21 | Description: Common scripts and resources for Linux Mint 22 | A collection of scripts and resources used by other Linux Mint packages 23 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: mint-common 3 | Upstream-Contact: Clement Lefebvre 4 | 5 | Files: * 6 | Copyright: 2009-2014 Clement Lefebvre 7 | License: GPL-2+ 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | . 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | . 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | . 21 | On Debian GNU/Linux systems, the complete text of the GNU General Public 22 | License version 2 can be found in '/usr/share/common-licenses/GPL-2'. 23 | 24 | Files: usr/lib/linuxmint/common/configobj.py 25 | Copyright: 2005-2008 Michael Foord , Nicola Larosa 26 | License: BSD-3-clause 27 | Redistribution and use in source and binary forms, with or without 28 | modification, are permitted provided that the following conditions are 29 | met: 30 | . 31 | * Redistributions of source code must retain the above copyright 32 | notice, this list of conditions and the following disclaimer. 33 | . 34 | * Redistributions in binary form must reproduce the above 35 | copyright notice, this list of conditions and the following 36 | disclaimer in the documentation and/or other materials provided 37 | with the distribution. 38 | . 39 | * Neither the name of the Linux Mint translation teams nor the names of its 40 | contributors may be used to endorse or promote products derived 41 | from this software without specific prior written permission. 42 | . 43 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 44 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 45 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 46 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 47 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 48 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 49 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 50 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 51 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 52 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 53 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 54 | 55 | -------------------------------------------------------------------------------- /debian/mint-common.install: -------------------------------------------------------------------------------- 1 | usr -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh ${@} 5 | 6 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /installer-interact: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import subprocess 3 | import signal 4 | import readline 5 | import code 6 | 7 | from gi.repository import GLib 8 | from mintcommon.installer import installer, cache, _flatpak, _apt 9 | 10 | def interact(): 11 | variables = globals().copy() 12 | variables.update(locals()) 13 | shell = code.InteractiveConsole(variables) 14 | shell.interact() 15 | 16 | if __name__ == "__main__": 17 | signal.signal(signal.SIGINT, signal.SIG_DFL) 18 | 19 | print(""" 20 | 21 | i is an Installer instance, already initialized 22 | i.cache is the cache for it. 23 | _flatpak, _apt for packaging specific stuff 24 | 25 | help() works 26 | 27 | """) 28 | 29 | i = installer.Installer() 30 | i.init(ready_callback=interact) 31 | 32 | ml = GLib.MainLoop.new(None, True) 33 | ml.run() 34 | -------------------------------------------------------------------------------- /makepot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | xgettext --language=Python --keyword=_ --output=mint-common.pot usr/bin/* usr/lib/python3/dist-packages/mintcommon/*.py usr/lib/python3/dist-packages/mintcommon/installer/*.py usr/lib/linuxmint/common/* 4 | 5 | 6 | -------------------------------------------------------------------------------- /mint-common.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-11-26 10:25+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: usr/bin/cinnamon-remove-application:33 usr/bin/mint-remove-application:33 21 | msgid "" 22 | "This menu item is not associated to any package. Do you want to remove it " 23 | "from the menu anyway?" 24 | msgstr "" 25 | 26 | #: usr/bin/cinnamon-remove-application:78 usr/bin/mint-remove-application:78 27 | msgid "The following packages will be removed:" 28 | msgstr "" 29 | 30 | #: usr/bin/cinnamon-remove-application:85 usr/bin/mint-remove-application:85 31 | msgid "Packages to be removed" 32 | msgstr "" 33 | 34 | #: usr/bin/cinnamon-remove-application:146 usr/bin/mint-remove-application:146 35 | #, python-format 36 | msgid "Cannot remove package %s as it is required by a system package." 37 | msgstr "" 38 | 39 | #: usr/bin/cinnamon-remove-application:152 usr/bin/mint-remove-application:152 40 | #, python-format 41 | msgid "Package %s is a dependency of the following packages:" 42 | msgstr "" 43 | 44 | #: usr/bin/mint-remove-flatpak:59 45 | msgid "Removing" 46 | msgstr "" 47 | 48 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:39 49 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:110 50 | msgid "Install" 51 | msgstr "" 52 | 53 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:40 54 | msgid "Reinstall" 55 | msgstr "" 56 | 57 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:41 58 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:120 59 | msgid "Remove" 60 | msgstr "" 61 | 62 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:42 63 | msgid "Purge" 64 | msgstr "" 65 | 66 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:43 67 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:140 68 | msgid "Upgrade" 69 | msgstr "" 70 | 71 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:44 72 | msgid "Downgrade" 73 | msgstr "" 74 | 75 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:45 76 | msgid "Skip upgrade" 77 | msgstr "" 78 | 79 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:61 80 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:145 81 | msgid "Please take a look at the list of changes below." 82 | msgstr "" 83 | 84 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:68 85 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:155 86 | msgid "Additional software will be installed" 87 | msgstr "" 88 | 89 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:70 90 | msgid "Additional software will be re-installed" 91 | msgstr "" 92 | 93 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:72 94 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:157 95 | msgid "Additional software will be removed" 96 | msgstr "" 97 | 98 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:74 99 | msgid "Additional software will be purged" 100 | msgstr "" 101 | 102 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:76 103 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:159 104 | msgid "Additional software will be upgraded" 105 | msgstr "" 106 | 107 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:78 108 | msgid "Additional software will be downgraded" 109 | msgstr "" 110 | 111 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:80 112 | msgid "Updates will be skipped" 113 | msgstr "" 114 | 115 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:88 116 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:168 117 | msgid "Additional changes are required" 118 | msgstr "" 119 | 120 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:93 121 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:174 122 | #, python-format 123 | msgid "%s will be downloaded in total." 124 | msgstr "" 125 | 126 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:97 127 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:178 128 | #, python-format 129 | msgid "%s of disk space will be freed." 130 | msgstr "" 131 | 132 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:101 133 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:182 134 | #, python-format 135 | msgid "%s more disk space will be used." 136 | msgstr "" 137 | 138 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:106 139 | msgid "Flatpaks" 140 | msgstr "" 141 | 142 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:251 143 | msgid "Flatpak" 144 | msgstr "" 145 | 146 | #: usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py:324 147 | msgid "An error occurred" 148 | msgstr "" 149 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sudo rm -rf /usr/lib/linuxmint/common 4 | sudo rm -rf /usr/lib/python3/dist-packages/mintcommon 5 | sudo cp -R usr / 6 | -------------------------------------------------------------------------------- /usr/bin/apt-changelog: -------------------------------------------------------------------------------- 1 | /usr/lib/python3/dist-packages/mintcommon/apt_changelog.py -------------------------------------------------------------------------------- /usr/bin/cinnamon-remove-application: -------------------------------------------------------------------------------- 1 | mint-remove-application -------------------------------------------------------------------------------- /usr/bin/mint-remove-application: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import gettext 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | import gi 9 | gi.require_version('Gtk', '3.0') 10 | from gi.repository import Gtk 11 | 12 | import mintcommon.aptdaemon 13 | 14 | # i18n 15 | gettext.install("mint-common", "/usr/share/linuxmint/locale") 16 | 17 | class MintRemoveWindow: 18 | 19 | def __init__(self, desktopFile): 20 | 21 | #find deb package 22 | self.desktopFile = desktopFile 23 | process = subprocess.run(["dpkg", "-S", self.desktopFile], stdout=subprocess.PIPE) 24 | output = process.stdout.decode("utf-8") 25 | package = output[:output.find(":")].split(",")[0] 26 | 27 | if process.returncode != 0: #deb package not found, try remove flatpack 28 | if not self.try_remove_flatpak(desktopFile): 29 | warnDlg = Gtk.MessageDialog(parent=None, 30 | flags=0, 31 | message_type=Gtk.MessageType.WARNING, 32 | buttons=Gtk.ButtonsType.YES_NO, 33 | text=_("This menu item is not associated to any package. Do you want to remove it from the menu anyway?")) 34 | warnDlg.get_widget_for_response(Gtk.ResponseType.YES).grab_focus() 35 | warnDlg.vbox.set_spacing(12) 36 | warnDlg.set_border_width(12) 37 | response = warnDlg.run() 38 | if response == Gtk.ResponseType.YES: 39 | try: 40 | os.remove(self.desktopFile) 41 | except OSError as error: 42 | print("Could not remove desktop file in user mode.") 43 | subprocess.call(["pkexec", "/usr/lib/linuxmint/common/mint-remove-application.py", self.desktopFile]) 44 | warnDlg.destroy() 45 | sys.exit(0) 46 | 47 | #get package + dependents (reverse dependencies) 48 | rdependenciesLines = subprocess.getoutput("apt-get -s -q remove " + package + " | grep Remv") 49 | rdependenciesLines = rdependenciesLines.split("\n") 50 | rdependencies = [] 51 | for line in rdependenciesLines: 52 | rdependencies.append(line.split()[1]) 53 | 54 | dontRemoves = ["cinnamon", "mint-meta-cinnamon", "mint-meta-core", "mint-meta-mate", "mint-meta-xfce"] 55 | if any(packageName in dontRemoves for packageName in rdependencies): 56 | self.no_remove_dialog(package, rdependencies) 57 | else: 58 | self.remove_dialog(package, rdependencies) 59 | 60 | def try_remove_flatpak(self, desktopFile): 61 | if not "flatpak" in desktopFile: 62 | return False 63 | 64 | if not os.path.exists('/usr/bin/mint-remove-flatpak'): 65 | return False 66 | 67 | flatpak_remover = subprocess.Popen(['/usr/bin/mint-remove-flatpak', desktopFile]) 68 | retcode = flatpak_remover.wait() 69 | 70 | return retcode == 0 71 | 72 | def remove_dialog(self, package, rdependencies): 73 | #create dialogue 74 | warnDlg = Gtk.MessageDialog(parent=None, 75 | flags=0, 76 | message_type=Gtk.MessageType.WARNING, 77 | buttons=Gtk.ButtonsType.OK_CANCEL, 78 | text=_("The following packages will be removed:")) 79 | warnDlg.set_keep_above(True) 80 | 81 | warnDlg.get_widget_for_response(Gtk.ResponseType.OK).grab_focus() 82 | warnDlg.vbox.set_spacing(12) 83 | 84 | treeview = Gtk.TreeView() 85 | column1 = Gtk.TreeViewColumn(_("Packages to be removed")) 86 | renderer = Gtk.CellRendererText() 87 | column1.pack_start(renderer, False) 88 | column1.add_attribute(renderer, "text", 0) 89 | treeview.append_column(column1) 90 | 91 | packages_to_remove = rdependencies + self.get_autoremovable_dependencies(package) 92 | model = Gtk.ListStore(str) 93 | for item in packages_to_remove: 94 | model.append([item]) 95 | 96 | treeview.set_model(model) 97 | treeview.show() 98 | 99 | scrolledwindow = Gtk.ScrolledWindow() 100 | scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_OUT) 101 | scrolledwindow.set_size_request(150, 150) 102 | scrolledwindow.add(treeview) 103 | scrolledwindow.show() 104 | 105 | warnDlg.get_content_area().add(scrolledwindow) 106 | warnDlg.set_border_width(12) 107 | 108 | self.apt = mintcommon.aptdaemon.APT(warnDlg) 109 | 110 | response = warnDlg.run() 111 | if response == Gtk.ResponseType.OK: 112 | self.apt.set_finished_callback(self.on_finished) 113 | self.apt.remove_packages(packages_to_remove) 114 | elif response == Gtk.ResponseType.CANCEL: 115 | sys.exit(0) 116 | 117 | warnDlg.destroy() 118 | 119 | def get_autoremovable_dependencies(self, package): 120 | #Find autoremovable packages before removal of package 121 | output = subprocess.getoutput("apt-get -s -q autoremove | grep Remv") 122 | unreq_before = [] 123 | if len(output) > 1: 124 | output = output.split("\n") 125 | for line in output: 126 | unreq_before.append(line.split()[1]) 127 | 128 | #Find autoremovable packages after removal of package 129 | output = subprocess.getoutput("LC_ALL=C apt-get -s remove " + package) 130 | unreq_after = [] 131 | begin = output.find("installed and are no longer required:") 132 | if begin > 0: 133 | output = output[begin + 37:output.find("Use '")] 134 | unreq_after = output.split() 135 | 136 | #find autoremovable packages due to removal of package 137 | additional_unreq = [item for item in unreq_after if item not in unreq_before] 138 | 139 | return additional_unreq 140 | 141 | def no_remove_dialog(self, package, rdependencies): 142 | warnDlg = Gtk.MessageDialog(parent=None, 143 | flags=0, 144 | message_type=Gtk.MessageType.ERROR, 145 | buttons=Gtk.ButtonsType.CLOSE, 146 | text=_("Cannot remove package %s as it is required by a system package.") % package) 147 | warnDlg.set_keep_above(True) 148 | warnDlg.get_widget_for_response(Gtk.ResponseType.CLOSE).grab_focus() 149 | warnDlg.vbox.set_spacing(12) 150 | 151 | treeview = Gtk.TreeView() 152 | column1 = Gtk.TreeViewColumn(_("Package %s is a dependency of the following packages:") % package) 153 | renderer = Gtk.CellRendererText() 154 | column1.pack_start(renderer, False) 155 | column1.add_attribute(renderer, "text", 0) 156 | treeview.append_column(column1) 157 | 158 | model = Gtk.ListStore(str) 159 | for rdependency in rdependencies: 160 | rdependency = rdependency.replace("Remv ", "") 161 | if package != rdependency.split()[0]: 162 | model.append([rdependency]) 163 | 164 | treeview.set_model(model) 165 | treeview.show() 166 | 167 | scrolledwindow = Gtk.ScrolledWindow() 168 | scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_OUT) 169 | scrolledwindow.set_size_request(150, 150) 170 | scrolledwindow.add(treeview) 171 | scrolledwindow.show() 172 | 173 | warnDlg.get_content_area().add(scrolledwindow) 174 | warnDlg.set_border_width(12) 175 | 176 | response = warnDlg.run() 177 | sys.exit(0) 178 | 179 | def on_finished(self, transaction=None, exit_state=None): 180 | sys.exit(0) 181 | 182 | if __name__ == "__main__": 183 | 184 | # Exit if the given path does not exist 185 | if len(sys.argv) < 2 or not os.path.exists(sys.argv[1]): 186 | print("No argument or file not found") 187 | sys.exit(1) 188 | 189 | path = sys.argv[1] 190 | dpath = f"{path}.desktop" 191 | if not os.path.exists(path): 192 | if os.path.exists(dpath): 193 | path = dpath 194 | else: 195 | print("Path not found", path) 196 | sys.exit(1) 197 | 198 | mainwin = MintRemoveWindow(path) 199 | Gtk.main() 200 | -------------------------------------------------------------------------------- /usr/bin/mint-remove-flatpak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import gi 4 | gi.require_version('Gtk', '3.0') 5 | from gi.repository import Gtk, GLib, Gdk 6 | 7 | import sys 8 | import os 9 | import gettext 10 | import subprocess 11 | 12 | from pathlib import Path 13 | from mintcommon.installer import installer 14 | from mintcommon.installer import dialogs 15 | from mintcommon.installer.misc import check_ml 16 | 17 | # i18n 18 | gettext.install("mintinstall", "/usr/share/linuxmint/locale") 19 | 20 | class AppUninstaller: 21 | def __init__(self, desktopFile): 22 | self.desktopFile = desktopFile 23 | 24 | self.error = None 25 | self.installer = installer.Installer().init(self.on_installer_ready) 26 | self.progress_window = None 27 | self.progress_bar = None 28 | self.pkg_name = None 29 | 30 | def on_installer_ready(self): 31 | pkg_name = self.get_fp_name() 32 | 33 | if pkg_name is None: 34 | print("Package for '%s' not found" % self.desktopFile) 35 | self.on_finished(None, 1) 36 | 37 | self.pkginfo = self.installer.find_pkginfo(pkg_name) 38 | 39 | if self.pkginfo and self.installer.pkginfo_is_installed(self.pkginfo): 40 | self.installer.select_pkginfo(self.pkginfo, 41 | self.on_installer_info_ready, None, 42 | self.on_uninstall_complete, self.on_uninstall_progress, use_mainloop=True) 43 | else: 44 | print("Package '%s' is not installed" % pkg_name) 45 | self.on_uninstall_complete(None) 46 | 47 | def on_installer_info_ready(self, task): 48 | self.task = task 49 | if self.installer.confirm_task(task): 50 | self.installer.execute_task(task) 51 | else: 52 | print("cancel task") 53 | self.installer.cancel_task(task) 54 | 55 | def on_uninstall_progress(self, pkginfo, progress, estimating, status_text=None): 56 | if self.progress_window is None: 57 | self.progress_window = Gtk.Dialog() 58 | self.progress_window.set_default_size(400, -1) 59 | self.progress_window.set_title(_("Removing")) 60 | self.progress_window.connect("delete-event", self.dialog_delete_event) 61 | 62 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, valign=Gtk.Align.CENTER) 63 | self.progress_window.get_content_area().pack_start(box, True, True, 0) 64 | 65 | self.pkg_name = Gtk.Label(max_width_chars=45, wrap=True) 66 | box.pack_start(self.pkg_name, True, False, 6) 67 | spinner = Gtk.Spinner(active=True) 68 | spinner.set_size_request(36, 36) 69 | box.pack_start(spinner, True, False, 0) 70 | box.show_all() 71 | self.pkg_name.set_label(pkginfo.get_display_name()) 72 | 73 | self.progress_window.run() 74 | 75 | def dialog_delete_event(self, widget, event): 76 | self.installer.cancel_task(self.task) 77 | 78 | def on_installer_info_error(self, task): 79 | pass 80 | 81 | def get_fp_name(self): 82 | path = Path(self.desktopFile) 83 | 84 | if "flatpak" not in path.parts: 85 | return None 86 | 87 | return path.stem 88 | 89 | def on_uninstall_complete(self, task): 90 | if task.error_message: 91 | print("Could not remove %s: %s" % (task.pkginfo.name, task.error_message)) 92 | 93 | if self.progress_window is not None: 94 | # let the window be visible long enough to know what it's doing (uninstalls are fast) 95 | Gdk.threads_add_timeout_seconds(GLib.PRIORITY_DEFAULT, 1, self.destroy_window, None) 96 | 97 | Gtk.main_quit() 98 | 99 | def destroy_window(self, data=None): 100 | self.progress_window.destroy() 101 | return False 102 | 103 | if __name__ == "__main__": 104 | 105 | # Exit if the given path does not exist 106 | if len(sys.argv) < 2 or not os.path.exists(sys.argv[1]) or not sys.argv[1].endswith(".desktop"): 107 | print("mint-remove-flatpak: Single argument required, the full path of a desktop file.") 108 | sys.exit(1) 109 | 110 | mainwin = AppUninstaller(sys.argv[1]) 111 | Gtk.main() 112 | 113 | exit(1 if mainwin.error else 0) 114 | -------------------------------------------------------------------------------- /usr/lib/linuxmint/common/mint-remove-application.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | 8 | # Exit if the given path does not exist 9 | if len(sys.argv) < 2 or not os.path.exists(sys.argv[1]): 10 | print("No argument or file not found") 11 | sys.exit(1) 12 | 13 | path = sys.argv[1] 14 | dpath = f"{path}.desktop" 15 | if not os.path.exists(path): 16 | if os.path.exists(dpath): 17 | path = dpath 18 | else: 19 | print("Path not found", path) 20 | sys.exit(1) 21 | 22 | os.remove(path) 23 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | """Collection of classes shared by Mint packages.""" 3 | 4 | __all__ = ["aptdaemon", "installer", "apt_changelog", "additionalfiles"] 5 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/additionalfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import gettext 5 | 6 | def strip_split_and_recombine(comma_separated): 7 | word_list = comma_separated.split(",") 8 | out = "" 9 | for item in word_list: 10 | out += item.strip() 11 | out+=";" 12 | 13 | return out 14 | 15 | def generate(domain, path, filename, prefix, name, comment, suffix, genericName=None, keywords=None, append=False): 16 | if append: 17 | desktopFile = open(filename, "a") 18 | else: 19 | desktopFile = open(filename, "w") 20 | 21 | desktopFile.writelines(prefix) 22 | 23 | desktopFile.writelines("Name=%s\n" % name) 24 | for directory in sorted(os.listdir(path)): 25 | mo_file = os.path.join(path, directory, "LC_MESSAGES", "%s.mo" % domain) 26 | if os.path.exists(mo_file): 27 | try: 28 | language = gettext.translation(domain, path, languages=[directory]) 29 | L_ = language.gettext 30 | if (L_(name) != name): 31 | desktopFile.writelines("Name[%s]=%s\n" % (directory, L_(name))) 32 | except: 33 | pass 34 | 35 | if comment is not None: 36 | desktopFile.writelines("Comment=%s\n" % comment) 37 | for directory in sorted(os.listdir(path)): 38 | mo_file = os.path.join(path, directory, "LC_MESSAGES", "%s.mo" % domain) 39 | if os.path.exists(mo_file): 40 | try: 41 | language = gettext.translation(domain, path, languages=[directory]) 42 | L_ = language.gettext 43 | if (L_(comment) != comment): 44 | desktopFile.writelines("Comment[%s]=%s\n" % (directory, L_(comment))) 45 | except: 46 | pass 47 | 48 | if keywords is not None: 49 | formatted = strip_split_and_recombine(keywords) 50 | desktopFile.writelines("Keywords=%s\n" % formatted) 51 | for directory in sorted(os.listdir(path)): 52 | mo_file = os.path.join(path, directory, "LC_MESSAGES", "%s.mo" % domain) 53 | if os.path.exists(mo_file): 54 | try: 55 | language = gettext.translation(domain, path, languages=[directory]) 56 | L_ = language.gettext 57 | if (L_(keywords) != keywords): 58 | translated = strip_split_and_recombine(L_(keywords)) 59 | desktopFile.writelines("Keywords[%s]=%s\n" % (directory, translated)) 60 | except: 61 | pass 62 | 63 | if genericName is not None: 64 | desktopFile.writelines("GenericName=%s\n" % genericName) 65 | for directory in sorted(os.listdir(path)): 66 | mo_file = os.path.join(path, directory, "LC_MESSAGES", "%s.mo" % domain) 67 | if os.path.exists(mo_file): 68 | try: 69 | language = gettext.translation(domain, path, languages=[directory]) 70 | L_ = language.gettext 71 | if (L_(genericName) != genericName): 72 | desktopFile.writelines("GenericName[%s]=%s\n" % (directory, L_(genericName))) 73 | except: 74 | pass 75 | 76 | desktopFile.writelines(suffix) 77 | desktopFile.close() 78 | 79 | def generate_polkit_policy(domain, path, filename, prefix, message, suffix, append=False): 80 | if append: 81 | policyFile = open(filename, "a") 82 | else: 83 | policyFile = open(filename, "w") 84 | 85 | policyFile.writelines(prefix) 86 | 87 | policyFile.writelines("%s\n" % message) 88 | for directory in sorted(os.listdir(path)): 89 | mo_file = os.path.join(path, directory, "LC_MESSAGES", "%s.mo" % domain) 90 | if os.path.exists(mo_file): 91 | try: 92 | language = gettext.translation(domain, path, languages=[directory]) 93 | L_ = language.gettext 94 | if (L_(message) != message): 95 | policyFile.writelines("%s\n" % (directory, L_(message))) 96 | except: 97 | pass 98 | 99 | policyFile.writelines(suffix) 100 | policyFile.close() 101 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/apt_changelog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | apt_changelog.py 4 | 5 | Replacement for apt changelog, can be used both standalone and as a module. 6 | 7 | In addition to apt changelog sources, it directly supports also Linux Mint 8 | and Launchpad repositories and will try to retrieve a changelog for packages 9 | from all other debian package sources as well. Most importantly, changelogs 10 | for installed packages are retrieved locally to avoid unnecessary network 11 | activity. 12 | 13 | Unlike apt it does not support wildcards for the package name. You get to 14 | check one specific changelog at a time only. I see no value in multi-lookups. 15 | 16 | Copyright (c) 2018 gm10 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. 35 | """ 36 | 37 | import fnmatch 38 | import gzip 39 | import lzma 40 | import os 41 | import sys 42 | import tarfile 43 | import tempfile 44 | 45 | import requests 46 | from apt.cache import Cache 47 | from apt.debfile import DebPackage 48 | 49 | import apt_pkg 50 | 51 | 52 | class _Package(): 53 | def __init__(self,name:str, fullname:str, architecture:str, version:str, 54 | source_name:str, source_version:str, uri:str, filename:str, origin:str, 55 | component:str, downloadable:bool, is_installed:bool, dependencies:list): 56 | self.name = name 57 | self.fullname = fullname 58 | self.architecture = architecture 59 | self.version = version 60 | self.source_name = source_name 61 | self.source_version = source_version 62 | self.source_version_raw = None 63 | self.uri = uri 64 | self.filename = filename 65 | self.origin = origin 66 | self.component = component 67 | self.downloadable = downloadable 68 | self.is_installed = is_installed 69 | self.installed_files = list() 70 | self.dependencies = dependencies 71 | 72 | class _AptChangelog(): 73 | 74 | def __init__(self, interactive:bool=False): 75 | self.interactive = interactive 76 | 77 | # constants 78 | # apt uses MB rather than MiB, so let's stay consistent 79 | self.MB = 1000 ** 2 80 | # downloads larger than this require confirmation or fail 81 | self.max_download_size_default = 1.5 * self.MB 82 | self.max_download_size = self.max_download_size_default 83 | max_download_size_msg_template = "\ 84 | To retrieve the full changelog, %s MB have to be downloaded.\n%s\ 85 | \n\ 86 | Proceed with the download?" 87 | self.max_download_size_msg_lc = max_download_size_msg_template % ("%.1f", 88 | "Otherwise we will try to retrieve just the last change.\n") 89 | self.max_download_size_msg = max_download_size_msg_template % ("%.1f","") 90 | self.max_download_size_msg_unknown = max_download_size_msg_template % ("an unknown amount of", "") 91 | 92 | self.apt_cache = None 93 | self.apt_cache_date = None 94 | self.candidate = None 95 | 96 | # get apt's configuration 97 | apt_pkg.init_config() 98 | if apt_pkg.config.exists("Acquire::Changelogs::URI::Origin"): 99 | self.apt_origins = apt_pkg.config.subtree("Acquire::Changelogs::URI::Origin") 100 | else: 101 | self.apt_origins = None 102 | if apt_pkg.config.exists("Dir::Cache::pkgcache"): 103 | self.apt_cache_path = apt_pkg.config.find_dir("Dir::Cache") 104 | self.pkgcache = apt_pkg.config.find_file("Dir::Cache::pkgcache") 105 | else: 106 | self.apt_cache = "invalid" 107 | if (self.apt_cache or 108 | not os.path.isdir(self.apt_cache_path) or 109 | not os.path.isfile(self.pkgcache) 110 | ): 111 | print("E: Invalid APT configuration found, try to run `apt update` first", 112 | file=sys.stderr) 113 | self.close(99) 114 | 115 | def get_cache_date(self): 116 | if os.path.isfile(self.pkgcache): 117 | return os.path.getmtime(self.pkgcache) 118 | return None 119 | 120 | def refresh_cache(self): 121 | cache_date = self.get_cache_date() 122 | 123 | if not self.apt_cache: 124 | self.apt_cache = Cache() 125 | self.apt_cache_date = cache_date 126 | elif cache_date != self.apt_cache_date: 127 | self.apt_cache.open(None) 128 | self.apt_cache_date = cache_date 129 | 130 | def drop_cache(self): 131 | if self.candidate: 132 | self.candidate = None 133 | self.apt_cache = None 134 | 135 | def get_changelog(self, pkg_name:str, no_local:bool=False): 136 | self.refresh_cache() 137 | self.candidate = self.parse_package_metadata(pkg_name) 138 | 139 | # parse the package's origin 140 | if not self.candidate.downloadable: 141 | origin = "local_package" 142 | elif self.candidate.origin == "linuxmint": 143 | origin = "linuxmint" 144 | elif self.candidate.origin.startswith("LP-PPA-"): 145 | origin = "LP-PPA" 146 | elif self.apt_origins and self.candidate.origin in self.apt_origins.list(): 147 | origin = "APT" 148 | else: 149 | origin = "unsupported" 150 | 151 | # Check for changelog of installed package first 152 | has_local_changelog = False 153 | uri = None 154 | if not no_local and self.candidate.is_installed: 155 | if _DEBUG: print("Package is installed...") 156 | uri = self.get_changelog_from_filelist( 157 | self.candidate.installed_files, local=True) 158 | # Ubuntu kernel workarounds 159 | if self.candidate.origin == "Ubuntu": 160 | if self.candidate.source_name == "linux-signed": 161 | uri = uri.replace("linux-image","linux-modules") 162 | if self.candidate.source_name == "linux-meta": 163 | uri = None 164 | if uri and not os.path.isfile(uri): 165 | uri = None 166 | 167 | # Do nothing if local changelog exists 168 | if uri: 169 | has_local_changelog = True 170 | # all origins that APT supports 171 | elif origin == 'APT': 172 | uri = self.get_apt_changelog_uri( 173 | self.apt_origins.get(self.candidate.origin)) 174 | r = self.check_url(uri) 175 | if not r: 176 | self.exit_on_fail(2) 177 | # Linux Mint repo 178 | elif origin == 'linuxmint': 179 | # Mint repos don't have .debian.tar.xz files, only full packages, so 180 | # check the package cache first 181 | base_uri, _ = os.path.split(self.candidate.uri) 182 | r, uri = self.get_changelog_uri(base_uri) 183 | if not r: 184 | # fall back to last change info for the source package 185 | # Mint's naming scheme seems to be using amd64 unless source 186 | # is i386 only, we always check amd64 first 187 | base_uri = "http://packages.linuxmint.com/dev/%s_%s_%s.changes" 188 | uri = base_uri % (self.candidate.source_name, 189 | self.candidate.source_version, "amd64") 190 | r = self.check_url(uri, False) 191 | if not r: 192 | uri = base_uri % (self.candidate.source_name, 193 | self.candidate.source_version, "i386") 194 | r = self.check_url(uri, False) 195 | if not r: 196 | self.exit_on_fail(3) 197 | 198 | # Launchpad PPA 199 | elif origin == 'LP-PPA': 200 | ppa_url, ppa_owner, ppa_name, _ = \ 201 | self.candidate.uri.split("://")[1].split("/", 3) 202 | base_uri = "https://ppa.launchpadcontent.net/%s/%s/ubuntu/pool/main/{self.source_prefix()}/%s" % (ppa_owner, ppa_name, self.candidate.source_name) 203 | r, uri = self.get_changelog_uri(base_uri) 204 | if not r: 205 | # fall back to last change info only 206 | uri = "https://launchpad.net/~%s/+archive/ubuntu/%s/+files/%s_%s_source.changes" % (ppa_owner, ppa_name, self.candidate.source_name, self.candidate.source_version) 207 | r = self.check_url(uri, False) 208 | if not r: 209 | self.exit_on_fail(4) 210 | # Not supported origin 211 | elif origin == 'unsupported': 212 | if _DEBUG: print("Unsupported Package") 213 | base_uri, _ = os.path.split(self.candidate.uri) 214 | r, uri = self.get_changelog_uri(base_uri) 215 | if not r: 216 | self.exit_on_fail(5) 217 | # Locally installed package without local changelog or remote 218 | # source, hope it's cached and contains a changelog 219 | elif origin == 'local_package': 220 | uri = self.apt_cache_path + self.candidate.filename 221 | if not os.path.isfile(uri): 222 | self.exit_on_fail(6) 223 | 224 | # Changelog downloading, extracting and processing: 225 | changelog = "" 226 | # local changelog 227 | if has_local_changelog and not no_local: 228 | if _DEBUG: print("Using local changelog:",uri) 229 | try: 230 | filename = os.path.basename(uri) 231 | # determine file type by name/extension 232 | # as per debian policy 4.4 the encoding must be UTF-8 233 | # as per policy 12.7 the name must be changelog.Debian.gz or 234 | # changelog.gz (deprecated) 235 | if filename.lower().endswith('.gz'): 236 | changelog = gzip.open(uri,'r').read().decode('utf-8') 237 | elif filename.lower().endswith('.xz'): 238 | # just in case / future proofing 239 | changelog = lzma.open(uri,'r').read().decode('utf-8') 240 | elif filename.lower() == 'changelog': 241 | changelog = open(uri, 'r').read().encode().decode('utf-8') 242 | else: 243 | raise ValueError('Unknown changelog format') 244 | except Exception as e: 245 | _generic_exception_handler(e) 246 | self.exit_on_fail(1) 247 | # APT-format changelog, download directly 248 | # - unfortunately this is slow since the servers support no compression 249 | elif origin == "APT": 250 | if _DEBUG: print("Downloading: %s (%.2f MB)" % (uri, r.length / self.MB)) 251 | changelog = r.text 252 | r.close() 253 | # last change changelog, download directly 254 | elif uri.endswith('.changes'): 255 | if _DEBUG: print("Downloading: %s (%.2f MB)" % (uri, r.length / self.MB)) 256 | changes = r.text.split("Changes:")[1].split("Checksums")[0].split("\n") 257 | r.close() 258 | for change in changes: 259 | change = change.strip() 260 | if change: 261 | if change == ".": 262 | change = "" 263 | changelog += change + "\n" 264 | # compressed binary source, download and extract changelog 265 | else: 266 | source_is_cache = uri.startswith(self.apt_cache_path) 267 | if _DEBUG: print("Using cached package:" if source_is_cache else 268 | "Downloading: %s (%.2f MB)" % (uri, r.length / self.MB)) 269 | try: 270 | if not source_is_cache: 271 | # download stream to temporary file 272 | tmpFile = tempfile.NamedTemporaryFile(prefix="apt-changelog-") 273 | if self.interactive and r.length: 274 | # download chunks with progress indicator 275 | recv_length = 0 276 | blocks = 60 277 | for data in r.iter_content(chunk_size=16384): 278 | recv_length += len(data) 279 | tmpFile.write(data) 280 | recv_pct = recv_length / r.length 281 | recv_blocks = int(blocks * recv_pct) 282 | print("\r[%(progress)s%(spacer)s] %(percentage).1f%%" % 283 | { 284 | "progress": "=" * recv_blocks, 285 | "spacer": " " * (blocks - recv_blocks), 286 | "percentage": recv_pct * 100 287 | }, end="", flush=True) 288 | # clear progress bar when done 289 | print("\r" + " " * (blocks + 10), end="\r", flush=True) 290 | else: 291 | # no content-length or non-interactive, download in one go 292 | # up to the configured max_download_size, ask only when 293 | # exceeded 294 | r.raw.decode_content = True 295 | size = 0 296 | size_exceeded = False 297 | while True: 298 | buf = r.raw.read(16*1024) 299 | if not size_exceeded: 300 | size += len(buf) 301 | if size > self.max_download_size: 302 | if not self.user_confirm(self.max_download_size_msg_unknown): 303 | r.close() 304 | tmpFile.close() 305 | return "" 306 | else: 307 | size_exceeded = True 308 | if not buf: 309 | break 310 | tmpFile.write(buf) 311 | r.close() 312 | tmpFile.seek(0) 313 | if uri.endswith(".deb"): 314 | # process .deb file 315 | if source_is_cache: 316 | f = uri 317 | else: 318 | f = tmpFile.name 319 | # We could copy the downloaded .deb files to the apt 320 | # cache here but then we'd need to run the script elevated: 321 | # shutil.copy(f, self.apt_cache_path + os.path.basename(uri)) 322 | deb = DebPackage(f) 323 | changelog_file = self.get_changelog_from_filelist(deb.filelist) 324 | if changelog_file: 325 | changelog = deb.data_content(changelog_file) 326 | if changelog.startswith('Automatically decompressed:'): 327 | changelog = changelog[29:] 328 | else: 329 | raise ValueError('Malformed Debian package') 330 | elif uri.endswith(".diff.gz"): 331 | # Ubuntu partner repo has .diff.gz files, 332 | # we can extract a changelog from that 333 | data = gzip.open(tmpFile.name, "r").read().decode('utf-8') 334 | additions = data.split("+++") 335 | for addition in additions: 336 | lines = addition.split("\n") 337 | if "/debian/changelog" in lines[0]: 338 | for line in lines[2:]: 339 | if line.startswith("+"): 340 | changelog += "%s\n" % line[1:] 341 | else: 342 | break 343 | if not changelog: 344 | raise ValueError('No changelog in .diff.gz') 345 | else: 346 | # process .tar.xz file 347 | with tarfile.open(fileobj=tmpFile, mode="r:xz") as tar: 348 | changelog_file = self.get_changelog_from_filelist( 349 | [s.name for s in tar.getmembers() if s.type in (b"0", b"2")]) 350 | if changelog_file: 351 | changelog = tar.extractfile(changelog_file).read().decode() 352 | else: 353 | raise ValueError('No changelog in source package') 354 | except Exception as e: 355 | _generic_exception_handler(e) 356 | self.exit_on_fail(520) 357 | if 'tmpFile' in vars(): 358 | try: 359 | tmpFile.close() 360 | except Exception as e: 361 | _generic_exception_handler(e) 362 | 363 | # ALL DONE 364 | return changelog 365 | 366 | def parse_package_metadata(self, pkg_name:str): 367 | """ Creates the self.candidate object based on package name=version/release 368 | 369 | Wildcard matching is only used for version and release, and only the 370 | first match is processed. 371 | """ 372 | # parse =version declaration 373 | if "=" in pkg_name: 374 | (pkg_name, pkg_version) = pkg_name.split("=", 1) 375 | pkg_release = None 376 | # parse /release declaration (only if no version specified) 377 | elif "/" in pkg_name: 378 | (pkg_name, pkg_release) = pkg_name.split("/", 1) 379 | pkg_version = None 380 | else: 381 | pkg_version = None 382 | pkg_release = None 383 | 384 | # check if pkg_name exists 385 | # unlike apt no pattern matching, a single exact match only 386 | if pkg_name in self.apt_cache: 387 | pkg = self.apt_cache[pkg_name] 388 | else: 389 | print("E: Unable to locate package %s" % pkg_name, file=sys.stderr) 390 | self.close(13) 391 | 392 | # get package data 393 | _candidate = None 394 | candidate = None 395 | if pkg_release or pkg_version: 396 | match_found = False 397 | for _pkg in pkg.versions: 398 | if pkg_version: 399 | if fnmatch.fnmatch(_pkg.version, pkg_version): 400 | match_found = True 401 | else: 402 | for _origin in _pkg.origins: 403 | if fnmatch.fnmatch(_origin.archive, pkg_release): 404 | match_found = True 405 | if match_found: 406 | _candidate = _pkg 407 | break 408 | if not match_found: 409 | if pkg_release: 410 | print('E: Release "%s" is unavailable for "%s"' % (pkg_release, pkg.name), 411 | file=sys.stderr) 412 | else: 413 | print('E: Version "%s" is unavailable for "%s"' % (pkg_version, pkg.name), 414 | file=sys.stderr) 415 | self.close(14) 416 | else: 417 | _candidate = pkg.candidate 418 | candidate = _Package( 419 | version = _candidate.version, 420 | name = _candidate.package.name, 421 | fullname = None, 422 | architecture = pkg.architecture, 423 | source_name = _candidate.source_name, 424 | source_version = _candidate.source_version, 425 | uri = _candidate.uri, 426 | filename = os.path.basename(_candidate.filename), 427 | origin = _candidate.origins[0].origin, 428 | component = _candidate.origins[0].component, 429 | downloadable = _candidate.downloadable, 430 | is_installed = _candidate.is_installed, 431 | dependencies = _candidate.dependencies 432 | ) 433 | if candidate.is_installed: 434 | candidate.installed_files = pkg.installed_files 435 | candidate.source_version_raw = candidate.source_version 436 | if ":" in candidate.source_version: 437 | candidate.source_version = candidate.source_version.split(":", 1)[1] 438 | return candidate 439 | 440 | def check_url(self, url:str, check_size:bool=True, stream:bool=True, 441 | msg:str=None): 442 | """ True if url can be downloaded and fits size requirements """ 443 | if _DEBUG: print("Checking:", url) 444 | try: 445 | _r = requests.get(url, stream=stream, timeout=5) 446 | except Exception as e: 447 | _generic_exception_handler(e) 448 | else: 449 | if _r: 450 | if not _r.encoding: 451 | _r.encoding = "utf-8" 452 | length = _r.headers.get("Content-Length") 453 | if length: 454 | _r.length = int(length) 455 | else: 456 | _r.length = 0 457 | if (not check_size or not 458 | (check_size and _r.length > self.max_download_size and not 459 | self.user_confirm( 460 | (self.max_download_size_msg_lc if not msg else msg) % 461 | (_r.length / self.MB)) 462 | )): 463 | return _r 464 | if '_r' in vars(): 465 | _r.close() 466 | return False 467 | 468 | @staticmethod 469 | def close(err:int=0): 470 | """ Exit """ 471 | sys.exit(err) 472 | 473 | def exit_on_fail(self, err:int=404): 474 | """ Prints error message and calls self.close() """ 475 | try: 476 | details = "Changelog unavailable for %s=%s" % (self.candidate.source_name, self.candidate.source_version_raw) 477 | except AttributeError: 478 | details = "" 479 | print("E: Failed to fetch changelog. %s" % details, file=sys.stderr) 480 | self.close(err) 481 | 482 | @staticmethod 483 | def strtobool (val): 484 | val = val.lower() 485 | if val in ('y', 'yes'): 486 | return True 487 | elif val in ('n', 'no'): 488 | return False 489 | else: 490 | raise ValueError("Invalid response value %s" % val) 491 | 492 | def user_confirm(self, q:str): 493 | """ returns bool (always False in non-interactive mode) """ 494 | if not self.interactive: 495 | if _DEBUG: print("Maximum size exceeded, skipping in non-interactive mode") 496 | return False 497 | print("%s [y/n] " % q, end="") 498 | while True: 499 | try: 500 | response = self.strtobool(input()) 501 | print("") 502 | return response 503 | except ValueError: 504 | print("Invalid response. Try again [y/n]: ", end="") 505 | except KeyboardInterrupt: 506 | pass 507 | 508 | def get_deb_or_tar(self, uri_tar:str=None): 509 | """ Returns request and URI of the preferred source 510 | 511 | The choice is made based on availability and size. If .deb is smaller 512 | than comparison_trigger_size, or if check_tar is False, then .deb is 513 | always selected. 514 | """ 515 | comparison_trigger_size = 50000 516 | r_deb = self.check_url(self.candidate.uri, False) 517 | if r_deb: 518 | if uri_tar and r_deb.length > comparison_trigger_size: 519 | # try for .tar.xz 520 | r_tar = self.check_url(uri_tar, False) 521 | # validate and compare sizes 522 | if r_tar and r_tar.length < r_deb.length: 523 | _r = r_tar 524 | r_deb.close() 525 | else: 526 | _r = r_deb 527 | if r_tar: 528 | r_tar.close() 529 | else: 530 | _r = r_deb 531 | if (not _r.length > self.max_download_size or 532 | self.user_confirm(self.max_download_size_msg_lc % 533 | (_r.length / self.MB)) 534 | ): 535 | return (_r, _r.url) 536 | return (False, "") 537 | 538 | def get_changelog_from_filelist(self, filelist:list, local:bool=False): 539 | """ Returns hopefully the correct "changelog" or an empty string. 540 | 541 | We should not need to be searching because the debian policy says it 542 | must be at debian/changelog for source packages but not all seem to 543 | adhere to the policy: 544 | https://www.debian.org/doc/debian-policy/ch-source.html#debian-changelog-debian-changelog 545 | 546 | """ 547 | files = [s for s in filelist if "changelog" in s.lower()] 548 | if local: 549 | testpath = "/usr/share/doc/%s/changelog" % self.candidate.name 550 | for item in files: 551 | if item.lower().startswith(testpath): 552 | return item 553 | else: 554 | testpath = "debian/changelog" 555 | if testpath in files: 556 | return testpath 557 | testpath = "recipe/debian/changelog" 558 | if testpath in files: 559 | return testpath 560 | testpath = "usr/share/doc/%s/changelog" % self.candidate.name 561 | for item in files: 562 | if item.lower().startswith(testpath): 563 | return item 564 | # no hits in the standard locations, let's try our luck in 565 | # random locations at the risk of getting the wrong file 566 | for item in files: 567 | if os.path.basename(item).lower().startswith("changelog"): 568 | return item 569 | return None 570 | 571 | def get_apt_changelog_uri(self, uri_template:str): 572 | """ Returns URI based on provided apt changelog URI template. 573 | 574 | Emulates apt's std::string pkgAcqChangelog::URI 575 | The template must contain the @CHANGEPATH@ variable, which will 576 | be expanded to 577 | COMPONENT/SRC/SRCNAME/SRCNAME_SRCVER 578 | Component is omitted for releases without one (= flat-style 579 | repositories). 580 | """ 581 | source_version = self.candidate.source_version 582 | 583 | def get_kernel_version_from_meta_package(pkg): 584 | for dependency in pkg.dependencies: 585 | if not dependency.target_versions or not dependency.rawtype == "Depends": 586 | if _DEBUG: print("W: Kernel dependency not found:", dependency) 587 | return None 588 | deppkg = dependency.target_versions[0] 589 | if deppkg.source_name in ("linux", "linux-signed"): 590 | return deppkg.source_version 591 | if deppkg.source_name.startswith("linux-meta"): 592 | _pkg = self.parse_package_metadata(str(deppkg)) 593 | return get_kernel_version_from_meta_package(_pkg) 594 | return None 595 | 596 | # Ubuntu kernel meta package workaround 597 | if self.candidate.origin == "Ubuntu" and \ 598 | self.candidate.source_name.startswith("linux-meta"): 599 | _source_version = get_kernel_version_from_meta_package(self.candidate) 600 | if _source_version: 601 | source_version = _source_version 602 | self.candidate.source_name = "linux" 603 | 604 | # Ubuntu signed kernel workaround 605 | if self.candidate.origin == "Ubuntu" and \ 606 | self.candidate.source_name == "linux-signed": 607 | self.candidate.source_name = "linux" 608 | 609 | # XXX: Debian does not seem to reliably keep changelogs for previous 610 | # (kernel) versions, so should we always look for the latest 611 | # version instead on Debian? apt does not do this but the 612 | # packages.debian.org website shows the latest version in the 613 | # selected archive 614 | 615 | # strip epoch 616 | if ":" in source_version: 617 | source_version = source_version.split(":", 1)[1] 618 | 619 | # the path is: COMPONENT/SRC/SRCNAME/SRCNAME_SRCVER, e.g. 620 | # main/a/apt/apt_1.1 or contrib/liba/libapt/libapt_2.0 621 | return uri_template.replace('@CHANGEPATH@', 622 | "%(component)s%(source_prefix)s/%(source_name)s/%(source_name)s_%(source_version)s" % 623 | { 624 | "component": self.candidate.component + "/" if \ 625 | self.candidate.component and \ 626 | self.candidate.component != "" else "", 627 | "source_prefix": self.source_prefix(), 628 | "source_name": self.candidate.source_name, 629 | "source_version": source_version 630 | }) 631 | 632 | def source_prefix(self, source_name:str=None): 633 | """ Return prefix used for build repository URL """ 634 | if not source_name: 635 | source_name = self.candidate.source_name 636 | return source_name[0] if not source_name.startswith("lib") else \ 637 | source_name[:4] 638 | 639 | def parse_dsc(self, url:str): 640 | """ Returns filename or None """ 641 | _r = self.check_url(url, False, False) 642 | if _r: 643 | target = "" 644 | lines = _r.text.split("Files:", 1)[1].split(":", 1)[0].split("-----BEGIN", 1)[0].split("\n") 645 | target = [s.strip() for s in lines if s.strip().lower().endswith('.debian.tar.xz')] 646 | if not target: 647 | target = [s.strip() for s in lines if s.strip().lower().endswith('.diff.gz')] 648 | if not target: 649 | target = [s.strip() for s in lines if s.strip().lower().endswith('.tar.xz')] 650 | # don't even test for .tar.gz, it will be too big compared to the .deb 651 | # if not target: 652 | # target = [s.strip() for s in lines if s.strip().lower().endswith('.tar.gz')] 653 | if target: 654 | return target[0].split()[-1] 655 | elif _DEBUG: print(".dsc parse error for", url) 656 | return None 657 | 658 | def get_changelog_uri(self, base_uri:str): 659 | """ Tries to find a changelog in files listed in .dsc, locally cached 660 | packages as well as the remote .deb file 661 | 662 | Returns r and uri 663 | """ 664 | uri = None 665 | # XXX: For APT sources we could just read the apt_pkg.SourceRecords() 666 | # directly, if available, which it is not for most users, so 667 | # probably not worth it 668 | target_filename = self.parse_dsc("%s/%s_%s.dsc" % (base_uri, self.candidate.source_name, self.candidate.source_version)) 669 | # get .debian.tar.xz or .diff.gz as a priority as the smallest options 670 | if (base_uri and target_filename and ( 671 | target_filename.lower().endswith('.debian.tar.xz') or 672 | target_filename.lower().endswith('.diff.gz') 673 | )): 674 | uri = "%s/%s" % (base_uri, target_filename) 675 | target_filename = None 676 | r = self.check_url(uri, msg = self.max_download_size_msg) 677 | else: 678 | r = None 679 | if not r: 680 | # fall back to cached local package 681 | uri = self.apt_cache_path + self.candidate.filename 682 | if not os.path.isfile(uri): 683 | # cache miss, download the full source package or the .deb, 684 | # depending on size and availability 685 | if target_filename: 686 | uri_tar = "%s/%s" % (base_uri, target_filename) 687 | else: 688 | uri_tar = None 689 | r, uri = self.get_deb_or_tar(uri_tar) 690 | return (r, uri) 691 | 692 | def _generic_exception_handler(e): 693 | if _DEBUG: 694 | import traceback 695 | print("%s: %s\n" % (e.__class__.__name__, traceback.format_exc()), file=sys.stderr) 696 | 697 | def drop_cache(): 698 | """ Drop the apt cache to free up memory. """ 699 | # For some reason this does not free about 11M the first time it is run, and 700 | # additional 21M (32M total) the second time it runs (if the cache had been 701 | # opened again in the meantime). All consecutive runs keep those 32M total 702 | # without additional loss. 703 | # This only affects the freeing of memory, the maximum memory usage is stable. 704 | # It's unclear whether this is a python-apt or a python issue. 705 | 706 | if apt_changelog: 707 | apt_changelog.drop_cache() 708 | 709 | def get_changelog(pkg_name:str, interactive:bool=False, output:bool=False, 710 | paged_output:bool=False, no_local:bool=False, max_download_size:int=0): 711 | """ Returns changelog for given package name, if any, and if within 712 | size-restrictions 713 | """ 714 | changelog = None 715 | try: 716 | if not apt_changelog: 717 | __init__(interactive) 718 | if int(max_download_size) > 0: 719 | apt_changelog.max_download_size = int(max_download_size) 720 | else: 721 | apt_changelog.max_download_size = apt_changelog.max_download_size_default 722 | changelog = apt_changelog.get_changelog(pkg_name, no_local) 723 | except SystemExit: 724 | if interactive: 725 | raise 726 | except KeyboardInterrupt: 727 | sys.exit(130) 728 | else: 729 | if output: 730 | if not changelog: 731 | # empty changelog 732 | apt_changelog.exit_on_fail(7) 733 | if paged_output: 734 | try: 735 | from pydoc import pager 736 | pager(changelog) 737 | except Exception as e: 738 | _generic_exception_handler(e) 739 | paged_output = False 740 | else: 741 | print(changelog) 742 | return changelog 743 | 744 | def print(*args, **kwargs): 745 | try: 746 | return __builtins__.print(*args, **kwargs) 747 | except: 748 | pass 749 | 750 | def set_debug(value:bool): 751 | global _DEBUG 752 | _DEBUG = bool(value) 753 | 754 | def __init__(interactive:bool=False): 755 | """ Instantiate _AptChangelog to global apt_changelog """ 756 | global apt_changelog 757 | apt_changelog = _AptChangelog(interactive) 758 | 759 | _DEBUG = False 760 | apt_changelog = None 761 | 762 | if __name__ == "__main__": 763 | if "--debug" in sys.argv: 764 | set_debug(True) 765 | sys.argv.remove("--debug") 766 | if "--no-local" in sys.argv: 767 | sys.argv.remove("--no-local") 768 | _no_local=True 769 | else: 770 | _no_local=False 771 | if len(sys.argv) != 2: 772 | print("""\ 773 | Usage: apt changelog [options] 774 | 775 | Tries to retrieve the changelog of a package and display it through a pager. 776 | By default it displays the changelog for the version that is installed. 777 | However, you can specify the same options as for the install command. 778 | 779 | Options: 780 | --no-local 781 | Always retrieve changelogs remotely, where possible. This can be 782 | useful when the locally installed changelog has been truncated. 783 | 784 | Changelog lookup may fail for some packages if source repositories 785 | are not enabled""") 786 | sys.exit(1) 787 | else: 788 | isatty = sys.stdin.isatty() and sys.stdout.isatty() 789 | get_changelog(sys.argv[1], interactive=isatty, output=True, 790 | paged_output=isatty, no_local=_no_local) 791 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/aptdaemon.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | 3 | import gi 4 | gi.require_version('Gtk', '3.0') 5 | gi.require_version('GdkX11', '3.0') # Needed to get xid 6 | gi.require_version('XApp', '1.0') 7 | from gi.repository import Gtk, XApp 8 | 9 | import aptdaemon.client 10 | import aptdaemon.enums 11 | import aptdaemon.errors 12 | from aptdaemon.gtk3widgets import AptErrorDialog, AptProgressDialog, AptConfirmDialog 13 | 14 | class APT(object): 15 | 16 | def __init__(self, parent_window=None): 17 | self.parent_window = parent_window 18 | self.progress_callback = None 19 | self.finished_callback = None 20 | self.error_callback = None 21 | self.cancelled_callback = None 22 | 23 | def set_progress_callback(self, progress_callback): 24 | self.progress_callback = progress_callback 25 | 26 | def set_finished_callback(self, finished_callback): 27 | self.finished_callback = finished_callback 28 | 29 | def set_error_callback(self, error_callback): 30 | self.error_callback = error_callback 31 | 32 | def set_cancelled_callback(self, cancelled_callback): 33 | self.cancelled_callback = cancelled_callback 34 | 35 | def update_cache(self): 36 | aptdaemon_client = aptdaemon.client.AptClient() 37 | update_transaction = aptdaemon_client.update_cache() 38 | self._run_transaction(update_transaction) 39 | 40 | def install_file(self, path): 41 | aptdaemon_client = aptdaemon.client.AptClient() 42 | aptdaemon_client.install_file(path, force=True, wait=False, reply_handler=self._simulate_trans, error_handler=self._on_error) 43 | 44 | def install_packages(self, packages): 45 | aptdaemon_client = aptdaemon.client.AptClient() 46 | aptdaemon_client.install_packages(packages, reply_handler=self._simulate_trans, error_handler=self._on_error) 47 | 48 | def remove_packages(self, packages): 49 | aptdaemon_client = aptdaemon.client.AptClient() 50 | aptdaemon_client.remove_packages(packages, reply_handler=self._simulate_trans, error_handler=self._on_error) 51 | 52 | def _run_transaction(self, transaction): 53 | if self.progress_callback is None: 54 | dia = AptProgressDialog(transaction, parent=self.parent_window) 55 | dia.run(close_on_finished=True, show_error=True, reply_handler=lambda: True, error_handler=self._on_error) 56 | transaction.connect("finished", self._on_finish) 57 | else: 58 | AptDaemonTransaction(transaction, self.progress_callback, self.finished_callback, self.error_callback, self.parent_window) 59 | 60 | def _simulate_trans(self, trans): 61 | trans.simulate(reply_handler=lambda: self._confirm_deps(trans), error_handler=self._on_error) 62 | 63 | def _confirm_deps(self, trans): 64 | try: 65 | if [pkgs for pkgs in trans.dependencies if pkgs]: 66 | dia = AptConfirmDialog(trans, parent=self.parent_window) 67 | res = dia.run() 68 | dia.hide() 69 | if res != Gtk.ResponseType.OK: 70 | if self.cancelled_callback is not None: 71 | self.cancelled_callback() 72 | return 73 | self._run_transaction(trans) 74 | except Exception as e: 75 | print(e) 76 | 77 | def _on_error(self, error): 78 | if isinstance(error, aptdaemon.errors.NotAuthorizedError): 79 | if self.cancelled_callback is not None: 80 | self.cancelled_callback() 81 | return 82 | elif not isinstance(error, aptdaemon.errors.TransactionFailed): 83 | # Catch internal errors of the client 84 | error = aptdaemon.errors.TransactionFailed(aptdaemon.enums.ERROR_UNKNOWN, str(error)) 85 | dia = AptErrorDialog(error) 86 | dia.run() 87 | dia.hide() 88 | 89 | def _on_finish(self, transaction, exit_state): 90 | if self.finished_callback is not None: 91 | self.finished_callback(transaction, exit_state) 92 | 93 | class AptDaemonTransaction(): 94 | 95 | def __init__(self, transaction, progress_callback, finished_callback, error_callback, parent_window): 96 | self.progress_callback = progress_callback 97 | self.finished_callback = finished_callback 98 | self.error_callback = error_callback 99 | self.transaction = transaction 100 | self.parent_window = parent_window 101 | transaction.set_debconf_frontend("gnome") 102 | transaction.connect("progress-changed", self.on_transaction_progress) 103 | # transaction.connect("cancellable-changed", self.on_driver_changes_cancellable_changed) 104 | transaction.connect("finished", self.on_transaction_finish) 105 | transaction.connect("error", self.on_transaction_error) 106 | transaction.run() 107 | 108 | def on_transaction_progress(self, transaction, progress): 109 | if self.progress_callback is not None: 110 | self.progress_callback(progress) 111 | if self.parent_window is not None: 112 | XApp.set_window_progress(self.parent_window, progress) 113 | 114 | def on_transaction_error(self, transaction, error_code, error_details): 115 | if self.error_callback is not None: 116 | self.error_callback(error_code, error_details) 117 | if self.parent_window is not None: 118 | XApp.set_window_progress(self.parent_window, 0) 119 | 120 | def on_transaction_finish(self, transaction, exit_state): 121 | if (exit_state == aptdaemon.enums.EXIT_SUCCESS): 122 | if self.finished_callback is not None: 123 | self.finished_callback(transaction, exit_state) 124 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | 3 | """Collection of classes shared by Mint packages.""" 4 | 5 | __all__ = ["cache", "installer", "_apt", "_flatpak", "dialogs", "misc"] -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/_apt.py: -------------------------------------------------------------------------------- 1 | import time 2 | import threading 3 | import apt 4 | 5 | import gi 6 | gi.require_version('Gtk', '3.0') 7 | gi.require_version('Gdk', '3.0') 8 | gi.require_version("PackageKitGlib", "1.0") 9 | 10 | from gi.repository import Gtk, GLib 11 | from gi.repository import PackageKitGlib as packagekit 12 | 13 | from aptdaemon.gtk3widgets import AptProgressDialog 14 | 15 | from .pkgInfo import AptPkgInfo 16 | from .dialogs import ChangesConfirmDialog 17 | from .misc import check_ml, warn, debug 18 | from . import dialogs 19 | 20 | # List extra packages that aren't necessarily marked in their control files, but 21 | # we know better... 22 | CRITICAL_PACKAGES = ["mint-common", "mint-translations", "mint-meta-core", "mintdesktop", "python3", "perl"] 23 | 24 | def capitalize(string): 25 | if len(string) > 1: 26 | return (string[0].upper() + string[1:]) 27 | 28 | return (string) 29 | 30 | _apt_cache = None 31 | _apt_cache_lock = threading.Lock() 32 | _as_pool = None 33 | 34 | def get_apt_cache(full=False): 35 | global _apt_cache 36 | 37 | if full or (not _apt_cache): 38 | with _apt_cache_lock: 39 | _apt_cache = apt.Cache() 40 | 41 | return _apt_cache 42 | 43 | def add_prefix(name): 44 | return "apt:%s" % (name) 45 | 46 | def get_real_error(code): 47 | n = int(code) 48 | if n > 255: 49 | return packagekit.ErrorEnum(n - 255) 50 | else: 51 | return packagekit.ClientError(n) 52 | 53 | def make_pkg_hash(apt_pkg): 54 | if not isinstance(apt_pkg, apt.Package): 55 | raise TypeError("apt.make_pkg_hash_make must receive apt.Package, not %s" % type(apt_pkg)) 56 | 57 | return add_prefix(apt_pkg.name) 58 | 59 | def process_full_apt_cache(cache): 60 | apt_time = time.time() 61 | apt_cache = get_apt_cache() 62 | 63 | sections = {} 64 | 65 | keys = apt_cache.keys() 66 | 67 | for key in keys: 68 | name = apt_cache[key].name 69 | pkg = apt_cache[key] 70 | 71 | if name.startswith("lib") and not name.startswith(("libreoffice", "librecad", "libk3b7", "libimage-exiftool-perl")): 72 | continue 73 | if name.endswith(":i386") and name != "steam:i386": 74 | continue 75 | if name.endswith("-dev"): 76 | continue 77 | if name.endswith("-dbg"): 78 | continue 79 | if name.endswith("-doc"): 80 | continue 81 | if name.endswith("-common"): 82 | continue 83 | if name.endswith("-data"): 84 | continue 85 | if "-locale-" in name: 86 | continue 87 | if "-l10n-" in name: 88 | continue 89 | if name.endswith("-dbgsym"): 90 | continue 91 | if name.endswith("l10n"): 92 | continue 93 | if name.endswith("-perl"): 94 | continue 95 | if name == "snapd": 96 | continue 97 | if name == "pepperflashplugin-nonfree": # formerly marked broken, it's now a dummy and has no dependents (and only exists in Mint 20). 98 | continue 99 | if pkg.candidate is None: 100 | continue 101 | # kernel, universe/kernel, multiverse/kernel, restricted/kernel 102 | if pkg.candidate.section.endswith("kernel"): 103 | continue 104 | if name.startswith(("linux-headers-", "linux-tools-")): 105 | continue 106 | if ":" in name and name.split(":")[0] in keys: 107 | continue 108 | try: 109 | if "transitional" in pkg.candidate.summary.lower(): 110 | continue 111 | except Exception as e: 112 | warn("Problem parsing package (maybe it's virtual): %s: %s" % (name, e)) 113 | continue 114 | # pass 115 | 116 | pkg_hash = make_pkg_hash(pkg) 117 | 118 | section_string = pkg.candidate.section 119 | 120 | if "/" in section_string: 121 | section = section_string.split("/")[1] 122 | else: 123 | section = section_string 124 | 125 | sections.setdefault(section, []).append(pkg_hash) 126 | 127 | cache[pkg_hash] = AptPkgInfo(pkg_hash, pkg) 128 | 129 | debug('Installer: Processing APT packages for cache took %0.3f ms' % ((time.time() - apt_time) * 1000.0)) 130 | 131 | return cache, sections 132 | 133 | def search_for_pkginfo_apt_pkg(pkginfo): 134 | name = pkginfo.name 135 | 136 | apt_cache = get_apt_cache() 137 | 138 | try: 139 | return apt_cache[name] 140 | except: 141 | return None 142 | 143 | def pkginfo_is_installed(pkginfo): 144 | global _apt_cache_lock 145 | apt_cache = get_apt_cache() 146 | 147 | with _apt_cache_lock: 148 | try: 149 | return apt_cache[pkginfo.name].installed is not None 150 | except: 151 | return False 152 | 153 | def sync_cache_installed_states(): 154 | get_apt_cache(full=True) 155 | 156 | def select_packages(task): 157 | task.transaction = MetaTransaction(task) 158 | 159 | debug("Installer: Calculating changes required for APT package: %s" % task.pkginfo.name) 160 | 161 | class MetaTransaction(packagekit.Task): 162 | def __init__(self, task): 163 | packagekit.Task.__init__(self) 164 | 165 | self.task = task 166 | self.simulated_download_size = 0 167 | 168 | thread = threading.Thread(target=self._calculate_apt_changes) 169 | thread.start() 170 | 171 | def _calculate_apt_changes(self): 172 | global _apt_cache_lock 173 | apt_cache = get_apt_cache() 174 | with _apt_cache_lock: 175 | apt_cache.clear() 176 | apt_pkg = apt_cache[self.task.pkginfo.name] 177 | results = None 178 | 179 | pkg_id = packagekit.Package.id_build(apt_pkg.shortname, "", apt_pkg.architecture(), "") 180 | 181 | self.set_simulate(True) 182 | 183 | try: 184 | if self.task.type == "remove": 185 | results = self.remove_packages_sync( 186 | [pkg_id], 187 | True, True, # allow_deps, autoremove 188 | self.task.cancellable, # cancellable 189 | self.on_transaction_progress, 190 | None # progress data 191 | ) 192 | elif self.task.type == "install": 193 | results = self.install_packages_sync( 194 | [pkg_id], 195 | self.task.cancellable, # cancellable 196 | self.on_transaction_progress, 197 | None # progress data 198 | ) 199 | elif self.task.type == "update": 200 | debug("todo update") 201 | except GLib.Error as e: 202 | self.on_transaction_error(e) 203 | 204 | self.on_transaction_finished(results) 205 | 206 | def on_transaction_error(self, error): 207 | # PkErrorEnums are sent from the backend mainly 208 | # PkClientErrors are related to interaction with a task/client - accept/deny, etc... 209 | if error.code == packagekit.ClientError.DECLINED_SIMULATION: 210 | # canceled via additional-changes dialog 211 | return 212 | 213 | # it thinks it's a PkClientError but it's really PkErrorEnum 214 | # the GError code is set to 0xFF + code 215 | real_code = error.code 216 | if error.code >= 0xFF: 217 | real_code = error.code - 0xFF 218 | 219 | if real_code == packagekit.ErrorEnum.NOT_AUTHORIZED: 220 | # Silently ignore auth failures or cancellation. 221 | return 222 | 223 | if self.task.cancellable.is_cancelled(): 224 | # user navigated away before simulation was complete, etc... 225 | return 226 | 227 | if real_code == packagekit.ErrorEnum.CANNOT_REMOVE_SYSTEM_PACKAGE or self.task.pkginfo.name in CRITICAL_PACKAGES: 228 | self.task.info_ready_status = self.task.STATUS_FORBIDDEN 229 | 230 | if self.task.info_ready_status == self.task.STATUS_NONE: 231 | if real_code == packagekit.ErrorEnum.DEP_RESOLUTION_FAILED: 232 | self.task.info_ready_status = self.task.STATUS_BROKEN 233 | else: 234 | self.task.info_ready_status = self.task.STATUS_UNKNOWN 235 | 236 | self.task.handle_error(error, info_stage = self.get_simulate()) 237 | 238 | def on_transaction_finished(self, results): 239 | # == operation was successful 240 | if results: 241 | exit_code = results.get_exit_code() 242 | pkerror = results.get_error_code() 243 | if pkerror: 244 | warn("Finished code: ", pkerror.get_code(), pkerror.get_details()) 245 | debug("Exit code:", exit_code) 246 | 247 | if self.task.error_message: 248 | self.task.call_error_cleanup_callback() 249 | else: 250 | self.task.call_finished_cleanup_callback() 251 | 252 | def on_transaction_progress(self, progress, ptype, data=None): 253 | if progress.get_status() == packagekit.StatusEnum.UNKNOWN: 254 | return 255 | 256 | if self.get_simulate(): 257 | if ptype == packagekit.ProgressType.DOWNLOAD_SIZE_REMAINING: 258 | new_size = progress.get_download_size_remaining() 259 | if new_size > self.simulated_download_size: 260 | self.simulated_download_size = new_size 261 | # print("current:", progress.get_package_id(), progress.get_status()) 262 | return 263 | 264 | if ptype == packagekit.ProgressType.PERCENTAGE: 265 | if self.task.client_progress_cb: 266 | GLib.idle_add(self.task.client_progress_cb, 267 | self.task.pkginfo, 268 | progress.get_percentage(), 269 | False, 270 | priority=GLib.PRIORITY_DEFAULT) 271 | 272 | def do_simulate_question(self, request, results): 273 | if self.task.cancellable.is_cancelled(): 274 | self.user_declined() 275 | return; 276 | 277 | self.task.pkit_request_id = request 278 | sack = results.get_package_sack() 279 | 280 | install_dbginfo = [] 281 | remove_dbginfo = [] 282 | update_dbginfo = [] 283 | added_size = 0 284 | freed_size = 0 285 | 286 | global _apt_cache_lock 287 | apt_cache = get_apt_cache() 288 | 289 | with _apt_cache_lock: 290 | for pkg in sack.get_array(): 291 | info = pkg.get_info() 292 | 293 | def calc_space(pkg, is_update=False): 294 | apt_pkg = apt_cache["%s:%s" % (pkg.get_name(), pkg.get_arch())] 295 | 296 | candidate = apt_pkg.candidate 297 | 298 | if is_update: 299 | for version in apt_pkg.versions: 300 | if version.is_installed: 301 | return candidate.installed_size - version.installed_size 302 | 303 | return candidate.installed_size 304 | 305 | if info == packagekit.InfoEnum.INSTALLING: 306 | self.task.to_install.append(pkg) 307 | added_size += calc_space(pkg) 308 | install_dbginfo.append("%s:%s (%s)" % (pkg.get_name(), pkg.get_arch(), pkg.get_version())) 309 | elif info == packagekit.InfoEnum.UPDATING: 310 | self.task.to_update.append(pkg) 311 | added_size += calc_space(pkg, is_update=True) 312 | update_dbginfo.append("%s:%s (%s)" % (pkg.get_name(), pkg.get_arch(), pkg.get_version())) 313 | elif info == packagekit.InfoEnum.REMOVING: 314 | self.task.to_remove.append(pkg) 315 | freed_size += calc_space(pkg) 316 | remove_dbginfo.append("%s:%s (%s)" % (pkg.get_name(), pkg.get_arch(), pkg.get_version())) 317 | 318 | debug("For install:", install_dbginfo) 319 | debug("For removal:", remove_dbginfo) 320 | debug("For upgrade:", update_dbginfo) 321 | 322 | self.task.download_size = self.simulated_download_size 323 | 324 | space = added_size - freed_size 325 | 326 | if space < 0: 327 | self.task.freed_size = space * -1 328 | self.task.install_size = 0 329 | else: 330 | self.task.freed_size = 0 331 | self.task.install_size = space 332 | 333 | for pkg in self.task.to_remove: 334 | apt_pkg_name = apt_cache["%s:%s" % (pkg.get_name(), pkg.get_arch())] 335 | 336 | if self._is_critical_package(apt_cache[apt_pkg_name]): 337 | warn("Installer: apt - cannot remove critical package: %s" % apt_pkg_name) 338 | self.task.info_ready_status = self.task.STATUS_FORBIDDEN 339 | 340 | if self.task.info_ready_status not in (self.task.STATUS_FORBIDDEN, self.task.STATUS_BROKEN): 341 | self.task.info_ready_status = self.task.STATUS_OK 342 | self.task.confirm = self._confirm_transaction 343 | self.task.cancel = self._cancel_transaction 344 | self.task.execute = self._execute_transaction 345 | 346 | self.task.call_info_ready_callback() 347 | 348 | def _is_critical_package(self, pkg): 349 | try: 350 | if pkg.versions[0].priority == "required" or pkg.name in CRITICAL_PACKAGES: 351 | return True 352 | 353 | return False 354 | except Exception: 355 | return False 356 | 357 | def _confirm_transaction(self): 358 | if len(self.task.to_install) > 1 or len(self.task.to_remove) > 1 or len(self.task.to_update) > 0: 359 | dia = ChangesConfirmDialog(self, self.task, parent=self.task.parent_window) 360 | res = dia.run() 361 | dia.hide() 362 | dia.destroy() 363 | 364 | return res == Gtk.ResponseType.OK 365 | else: 366 | return True 367 | 368 | def _cancel_transaction(self): 369 | self.task.cancellable.cancel() 370 | 371 | if self.task.pkit_request_id > 0: 372 | self.user_declined(self.task.pkit_request_id) 373 | self.task.pkit_request_id = 0 374 | 375 | def _execute_transaction(self): 376 | self.set_simulate(False) 377 | 378 | if self.task.cancellable.is_cancelled(): 379 | return 380 | 381 | if self.task.client_progress_cb is not None: 382 | self.task.has_window = True 383 | 384 | if self.task.has_window: 385 | self.user_accepted(self.task.pkit_request_id) 386 | else: 387 | progress_window = AptProgressDialog(self) 388 | progress_window.run(show_error=False, error_handler=self._on_error) 389 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/appstream_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import locale 4 | import os 5 | 6 | import gi 7 | gi.require_version('Xmlb', '2.0') 8 | from gi.repository import GLib, Xmlb 9 | 10 | from .misc import debug_query, debug, warn, print_timing 11 | 12 | KIND_APP = 0 13 | KIND_RUNTIME = 1 14 | 15 | # From appstream-1.0.2 (as-utils.c) 16 | def locale_to_bcp47(locale): 17 | has_variant = False 18 | if locale == None: 19 | return None 20 | 21 | ret = locale 22 | if "@" in ret: 23 | ret, variant = ret.split("@") 24 | if variant == "cyrillic": 25 | ret += "-Cyrl" 26 | elif variant == "devanagari": 27 | ret += "-Deva" 28 | elif variant == "latin": 29 | ret += "-Latn" 30 | elif variant == "shaw": 31 | ret += "-Shaw" 32 | elif variant != "euro": 33 | ret += "-" + variant 34 | 35 | return ret 36 | 37 | class Icon(): 38 | def __init__(self, icon_node): 39 | pass 40 | 41 | class Image(): 42 | __slots__ = ( 43 | "width", 44 | "height", 45 | "scale", 46 | "url", 47 | "is_source" 48 | ) 49 | 50 | def __init__(self, img_node): 51 | 52 | try: 53 | self.height = int(img_node.get_attr("height")) 54 | self.width = int(img_node.get_attr("width")) 55 | except TypeError: 56 | # probably a source image, we don't care about size for them. 57 | self.height = 0 58 | self.width = 0 59 | # optional 60 | try: 61 | self.scale = int(img_node.get_attr("scale")) 62 | except: 63 | self.scale = 1 64 | self.is_source = img_node.get_attr("type") == "source" 65 | 66 | self.url = img_node.get_text() 67 | 68 | class Screenshot(): 69 | __slots__ = ( 70 | "caption", 71 | "images", 72 | "source_image", 73 | "ss_node" 74 | ) 75 | 76 | def __init__(self, ss_node, caption): 77 | self.caption = caption 78 | self.images = {} 79 | self.source_image = None 80 | self.ss_node = ss_node 81 | 82 | images = ss_node.query("image", 0) 83 | for image in images: 84 | img = Image(image) 85 | if img.is_source: 86 | self.source_image = img 87 | continue 88 | key = self.make_key(img.width, img.scale) 89 | self.images[key] = img 90 | 91 | def make_key(self, width, scale): 92 | return f"{width}x{scale}" 93 | 94 | def get_image(self, width, height, scale=1): 95 | key = self.make_key(width, scale) 96 | try: 97 | return self.images[key] 98 | except KeyError: 99 | return self._get_closest_image(width, height, scale) 100 | 101 | def _get_closest_image(self, width, height, scale): 102 | closest = None 103 | closest_diff = 999999 104 | 105 | for key, img in self.images.items(): 106 | w_diff = abs(img.width - width) 107 | 108 | if w_diff < closest_diff: 109 | closest = img 110 | closest_diff = w_diff 111 | 112 | return closest or self.source_image 113 | 114 | def get_source_image(self): 115 | return self.source_image 116 | 117 | class Package(): 118 | __slots__ = ( 119 | "name", 120 | "remote_name", 121 | "remote", 122 | "appstream_dir", 123 | "xbnode", 124 | "kind", 125 | "verified", 126 | "bundle_id", 127 | "keywords" 128 | ) 129 | 130 | def __init__(self, name, remote, xbnode): 131 | self.name = name 132 | self.remote = remote 133 | self.remote_name = remote.get_name() 134 | self.appstream_dir = remote.get_appstream_dir() 135 | self.xbnode = xbnode 136 | self.kind = self.xbnode.get_attr("type") 137 | self.verified = None 138 | self.bundle_id = None 139 | self.keywords = [] 140 | 141 | def get_name(self): 142 | return self.name 143 | 144 | def query_for_node(self, node, xpath): 145 | result = None 146 | 147 | try: 148 | result = node.query_first(xpath) 149 | except GLib.Error as e: 150 | debug_query(f"Could not make query: {xpath} - {e.message}") 151 | 152 | return result 153 | 154 | def query_string(self, node, xpath): 155 | str_node = self.query_for_node(node, xpath) 156 | 157 | return str_node.get_text() if str_node is not None else None 158 | 159 | def get_display_name(self): 160 | name = self.query_string(self.xbnode, "name") 161 | if name is None: 162 | name = self.name 163 | 164 | return name 165 | 166 | def get_summary(self): 167 | return self.query_string(self.xbnode, "summary") 168 | 169 | def get_description(self): 170 | desc_node = self.query_for_node(self.xbnode, "description") 171 | 172 | if desc_node is not None: 173 | try: 174 | return desc_node.export( 175 | Xmlb.NodeExportFlags.FORMAT_MULTILINE | 176 | Xmlb.NodeExportFlags.FORMAT_INDENT | 177 | Xmlb.NodeExportFlags.ONLY_CHILDREN) 178 | except GLib.Error as e: 179 | pass 180 | 181 | return None 182 | 183 | def get_url(self, urlkind=None): 184 | return self.query_string(self.xbnode, f"url[@type='{urlkind}']") 185 | 186 | def get_homepage_url(self): 187 | return self.get_url("homepage") 188 | 189 | def get_help_url(self): 190 | return self.get_url("help") 191 | 192 | def get_version(self): 193 | releases_node = self.query_for_node(self.xbnode, "releases") 194 | if releases_node is None: 195 | return None 196 | 197 | releases = releases_node.query("release", 0) 198 | newest_timestamp = 0 199 | newest_version = None 200 | 201 | for release in releases: 202 | timestamp = int(release.get_attr("timestamp")) 203 | if timestamp > newest_timestamp: 204 | newest_timestamp = timestamp 205 | newest_version = release.get_attr("version") 206 | 207 | return newest_version 208 | 209 | def get_bundle_id(self): 210 | bundle_id = None 211 | 212 | if self.bundle_id is None: 213 | bundle_id = self.query_string(self.xbnode, "bundle[@type='flatpak']") 214 | # GNOME apps tend to have bundle info under 215 | if bundle_id is None: 216 | self.bundle_id = self.query_string(self.xbnode, "custom/bundle[@type='flatpak']") 217 | 218 | if bundle_id is not None: 219 | self.bundle_id = bundle_id 220 | 221 | return self.bundle_id 222 | 223 | def get_verified(self): 224 | if self.verified is None: 225 | try: 226 | self.verified = self.xbnode.query_first( 227 | "custom/value[(@key='flathub::verification::verified') and (text()='true')]" 228 | ) is not None 229 | except: 230 | try: 231 | self.verified = self.xbnode.query_first( 232 | "metadata/value[(@key='flathub::verification::verified') and (text()='true')]" 233 | ) is not None 234 | except: 235 | self.verified = False 236 | 237 | return self.verified 238 | 239 | def get_developer(self): 240 | # "developer" is recent, replacing "developer_name". Currently both are allowed, though older 241 | # libappstream doesn't support it, causing us to only see the developer name in mintinstall if they're 242 | # still using "developer_name". If all else fails, project_group may have something. 243 | dev_name = None 244 | 245 | try: 246 | developer_node = self.xbnode.query_first("developer") 247 | dev_name = self.query_string(developer_node, "name") 248 | except: 249 | pass 250 | 251 | if dev_name is None: 252 | try: 253 | dev_name = self.query_string(self.xbnode, "developer_name") 254 | except: 255 | pass 256 | 257 | if dev_name is None: 258 | try: 259 | dev_name = self.query_string(self.xbnode, "project_group") 260 | except: 261 | pass 262 | 263 | return dev_name 264 | 265 | def get_keywords(self): 266 | if len(self.keywords) == 0: 267 | kw_node = self.query_for_node(self.xbnode, "keywords") 268 | 269 | if kw_node is None: 270 | return [] 271 | 272 | keywords = kw_node.query("keyword", 0) 273 | for keyword in keywords: 274 | self.keywords.append(keyword.get_text()) 275 | 276 | return self.keywords 277 | 278 | def get_screenshots(self): 279 | ss_node = self.query_for_node(self.xbnode, "screenshots") 280 | 281 | if ss_node is None: 282 | return [] 283 | 284 | screenshots = ss_node.query("screenshot", 0) 285 | ret = [] 286 | 287 | for screenshot_node in screenshots: 288 | caption = self.query_string(screenshot_node, "caption") 289 | ret.append(Screenshot(screenshot_node, caption)) 290 | 291 | return ret 292 | 293 | def get_addons(self): 294 | root_node = self.query_for_node(self.xbnode, "..") 295 | addon_nodes = [] 296 | 297 | try: 298 | addon_nodes = root_node.query( 299 | f"component[@type='addon']/extends[starts-with(text(),'{self.name}')]/..", 0 300 | ) 301 | except GLib.Error as e: 302 | debug_query(f"Could not query for addons or there are none: {self.name} - {e.message}") 303 | return [] 304 | 305 | addons = [] 306 | for addon_node in addon_nodes: 307 | name = self.query_string(addon_node, "id") 308 | addon_pkg = Package(name, self.remote, addon_node) 309 | addons.append(addon_pkg) 310 | 311 | return addons 312 | 313 | def get_launchables(self): 314 | launchables = None 315 | 316 | try: 317 | l_nodes = self.xbnode.query( 318 | "launchable[@type='desktop-id']", 0 319 | ) 320 | 321 | launchables = [] 322 | 323 | for node in l_nodes: 324 | launchables.append(node.get_text()) 325 | except GLib.Error as e: 326 | debug_query(f"Could not query for launchables or there are none: {self.name} - {e.message}") 327 | 328 | return launchables 329 | 330 | def get_icon(self, size=64): 331 | icon_to_use = None 332 | remote_icon = None 333 | local_exists_icon = None 334 | theme_icon = None 335 | try: 336 | icons = self.xbnode.query( 337 | f"icon", 0 338 | ) 339 | except GLib.Error as e: 340 | debug_query(f"No icon size {size} found or unable to query: {self.name} - {e.message}") 341 | return None 342 | 343 | def get_height(i): 344 | height = i.get_attr("height") 345 | if height is not None: 346 | return int(height) 347 | return 999 348 | icons = sorted(icons, key=get_height, reverse=False) 349 | for icon in icons: 350 | test_height = icon.get_attr("height") 351 | if test_height is None: 352 | test_height = 64 353 | 354 | kind = icon.get_attr("type") 355 | # Some icons of the same size will have both cached and remote entries. Prefer the cached one, 356 | # but keep track of the remote 357 | if kind == "remote": 358 | remote_icon = icon.get_text() 359 | elif kind in ("cached", "local"): 360 | text = icon.get_text() 361 | if text.startswith("/"): 362 | if os.path.exists(text): 363 | local_exists_icon = text 364 | else: 365 | icon_path = f"{self.appstream_dir.get_path()}/icons/{test_height}x{test_height}/{text}" 366 | if os.path.exists(icon_path): 367 | theme_icon = icon_path 368 | elif kind == "stock": 369 | theme_icon = icon.get_text() 370 | 371 | icon_to_use = theme_icon or local_exists_icon or remote_icon 372 | if icon_to_use: 373 | if test_height and int(test_height) >= size: 374 | return icon_to_use 375 | 376 | # All else fails, try using the package's name (which icon names should match for flatpaks). 377 | # You may end up with a third-party icon, but it's better than none. 378 | return icon_to_use or self.name 379 | 380 | class Pool(): 381 | def __init__(self, remote): 382 | self.remote = remote 383 | self.appstream_dir = self.remote.get_appstream_dir() 384 | 385 | self.as_pool = None 386 | self.pkg_hash_to_as_pkg_dict = {} 387 | self.xmlb_silo = None 388 | 389 | self.locale_variants = [] 390 | tmp = set() 391 | # There really needs to be some enforced consistency in appstream. 392 | # Need to account for hyphenated vs underscored, and all-lower vs 393 | # uppercase region codes. 394 | debug("Reported languages: %s" % str(GLib.get_language_names())) 395 | for name in GLib.get_language_names(): 396 | if "." in name: 397 | continue 398 | if name == "C": 399 | continue 400 | 401 | tmp.add(name) 402 | tmp.add(name.replace("_", "-")) 403 | tmp.add(name.replace("_", "-").lower()) 404 | tmp.add(name.replace("-", "_")) 405 | tmp.add(name.replace("-", "_").lower()) 406 | 407 | # Live session has only C.UTF-8, assume en_US. 408 | if len(tmp) == 0: 409 | tmp = ['en-us', 'en', 'en_US', 'en_us', 'en-US'] 410 | 411 | self.locale_variants = [locale_to_bcp47(v) for v in tmp] 412 | 413 | debug("Appstream languages: %s" % str(self.locale_variants)) 414 | self._load_xmlb_silo() 415 | 416 | def lookup_appstream_package(self, pkginfo): 417 | debug_query("Lookup appstream package for %s" % pkginfo.refid) 418 | if self.xmlb_silo is None: 419 | return None 420 | 421 | package = None 422 | 423 | try: 424 | package = self.pkg_hash_to_as_pkg_dict[pkginfo.pkg_hash] 425 | debug_query("Found existing appstream package") 426 | return package 427 | except KeyError: 428 | base_node = None 429 | kind = pkginfo.kind 430 | 431 | try: 432 | if kind == KIND_APP: 433 | try: 434 | base_node = self.xmlb_silo.query_first( 435 | f"components/component/id[text()='{pkginfo.name}']/.." 436 | ) 437 | except GLib.Error as e: 438 | base_node = self.xmlb_silo.query_first( 439 | f"components/component/id[text()='{pkginfo.name}.desktop']/.." 440 | ) 441 | else: 442 | base_nodes = self.xmlb_silo.query( 443 | f"components/component/id[starts-with(text(),'{pkginfo.name}')]/..", 444 | 0 445 | ) 446 | if base_nodes is not None: 447 | for node in base_nodes: 448 | bundle_id = node.query_first("bundle[@type='flatpak']") 449 | if bundle_id.get_text() == pkginfo.refid: 450 | base_node = node 451 | break 452 | except GLib.Error as e: 453 | debug_query("Could not find appstream package") 454 | 455 | if base_node is not None: 456 | debug_query("Found matching appstream package: %s" % pkginfo.refid) 457 | package = Package(pkginfo.name, self.remote, base_node) 458 | 459 | if package is not None: 460 | self.pkg_hash_to_as_pkg_dict[pkginfo.pkg_hash] = package 461 | 462 | return package 463 | 464 | @print_timing 465 | def _load_xmlb_silo(self): 466 | xml_file = self.appstream_dir.get_child("appstream.xml") 467 | if not xml_file.query_exists(None): 468 | xml_file = self.appstream_dir.get_child("appstream.xml.gz") 469 | 470 | source = Xmlb.BuilderSource() 471 | try: 472 | ret = source.load_file(xml_file, Xmlb.BuilderSourceFlags.NONE, None) 473 | builder = Xmlb.Builder() 474 | for locale in self.locale_variants: 475 | builder.add_locale(locale) 476 | builder.import_source(source) 477 | self.xmlb_silo = builder.compile( 478 | Xmlb.BuilderCompileFlags.SINGLE_LANG | Xmlb.BuilderCompileFlags.SINGLE_ROOT, 479 | None 480 | ) 481 | except GLib.Error as e: 482 | warn("Could not mmap appstream xml file for remote '%s': %s" % (self.remote.get_name(), e.message)) 483 | self.xmlb_silo = None 484 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/cache.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | from pathlib import Path 4 | import json 5 | import threading 6 | 7 | from gi.repository import GLib, GObject 8 | 9 | from . import _apt 10 | from . import _flatpak 11 | from ._flatpak import FlatpakRemoteInfo 12 | from .pkgInfo import FlatpakPkgInfo, AptPkgInfo 13 | from .misc import print_timing, debug, warn 14 | from typing import Optional 15 | 16 | SYS_CACHE_PATH = "/var/cache/mintinstall/pkginfo.json" 17 | USER_CACHE_PATH = os.path.join(GLib.get_user_cache_dir(), "mintinstall", "pkginfo.json") 18 | 19 | MAX_AGE = 7 * (60 * 60 * 24) # days 20 | 21 | CACHE_SCHEMA_VERSION = 3 22 | 23 | class CacheLoadingError(Exception): 24 | """Thrown when there was an issue loading the pickled package set""" 25 | 26 | class JsonObject(object): 27 | def __init__(self, pkginfo_cache, section_lists, flatpak_remote_infos): 28 | super(JsonObject, self).__init__() 29 | 30 | self.schema_version = CACHE_SCHEMA_VERSION 31 | self.pkginfo_cache = pkginfo_cache 32 | self.section_lists = section_lists 33 | self.flatpak_remote_infos = flatpak_remote_infos 34 | 35 | @classmethod 36 | def from_json(cls, json_data: dict): 37 | schema_version = json_data.get("schema_version", 0) 38 | if schema_version != CACHE_SCHEMA_VERSION: 39 | warn("PkgCache schema version doesn't match, regenerating cache") 40 | return None 41 | 42 | pkgcache_dict = {} 43 | for key in json_data["pkginfo_cache"].keys(): 44 | pkginfo_data = json_data["pkginfo_cache"][key] 45 | 46 | if pkginfo_data["pkg_hash"].startswith("a"): 47 | pkgcache_dict[key] = AptPkgInfo.from_json(pkginfo_data) 48 | else: 49 | pkgcache_dict[key] = FlatpakPkgInfo.from_json(pkginfo_data) 50 | 51 | remotes_dict = {} 52 | for key in json_data["flatpak_remote_infos"].keys(): 53 | remote_data = json_data["flatpak_remote_infos"][key] 54 | remotes_dict[key] = FlatpakRemoteInfo.from_json(remote_data) 55 | 56 | return cls(pkgcache_dict, 57 | json_data["section_lists"], 58 | remotes_dict) 59 | 60 | def to_json(self): 61 | return self.__dict__ 62 | 63 | class PkgCache(object): 64 | STATUS_EMPTY = 0 65 | STATUS_OK = 1 66 | 67 | @print_timing 68 | def __init__(self, pkg_type, cache_path=None, have_flatpak=True): 69 | super(PkgCache, self).__init__() 70 | 71 | self.status = self.STATUS_EMPTY 72 | self.cache_content = pkg_type 73 | 74 | if cache_path is not None: 75 | self.custom_cache_path = Path(cache_path) 76 | else: 77 | self.custom_cache_path = None 78 | 79 | self.have_flatpak = have_flatpak and pkg_type in ("f", None) 80 | 81 | self._items = {} 82 | self._item_lock = threading.Lock() 83 | 84 | try: 85 | cache, sections, flatpak_remote_infos = self._load_cache() 86 | except CacheLoadingError: 87 | cache = {} 88 | sections = {} 89 | flatpak_remote_infos = {} 90 | 91 | if len(cache) > 0: 92 | self.status = self.STATUS_OK 93 | else: 94 | self.status = self.STATUS_EMPTY 95 | 96 | self._items = cache 97 | self.sections = sections 98 | self.flatpak_remote_infos = flatpak_remote_infos 99 | 100 | def keys(self): 101 | with self._item_lock: 102 | return self._items.keys() 103 | 104 | def values(self): 105 | with self._item_lock: 106 | return self._items.values() 107 | 108 | def __getitem__(self, key): 109 | with self._item_lock: 110 | return self._items[key] 111 | 112 | def __setitem__(self, key, value): 113 | with self._item_lock: 114 | self._items[key] = value 115 | 116 | def __delitem__(self, key): 117 | with self._item_lock: 118 | del self._items[key] 119 | 120 | def __contains__(self, pkg_hash): 121 | with self._item_lock: 122 | return pkg_hash in self._items 123 | 124 | def __len__(self): 125 | with self._item_lock: 126 | return len(self._items) 127 | 128 | def __iter__(self): 129 | with self._item_lock: 130 | for pkg_hash in self._items: 131 | yield self[pkg_hash] 132 | return 133 | 134 | def _generate_cache(self): 135 | cache = {} 136 | sections = {} 137 | flatpak_remote_infos = {} 138 | 139 | # If there's no cache, always generate both package types. 140 | if self.have_flatpak and (self.cache_content in ("f", None) or self.status == self.STATUS_EMPTY): 141 | cache, flatpak_remote_infos = _flatpak.process_full_flatpak_installation(cache) 142 | 143 | if self.cache_content in ("a", None) or self.status == self.STATUS_EMPTY: 144 | cache, sections = _apt.process_full_apt_cache(cache) 145 | 146 | return cache, sections, flatpak_remote_infos 147 | 148 | def _get_best_load_path(self): 149 | # If a custom path is set, always regenerate the cache. 150 | if self.custom_cache_path is not None: 151 | return None 152 | 153 | try: 154 | sys_mtime = os.path.getmtime(SYS_CACHE_PATH) 155 | 156 | if ((time.time() - MAX_AGE) > sys_mtime) or not os.access(SYS_CACHE_PATH, os.R_OK): 157 | debug("Installer: System pkgcache too old or not accessible, skipping") 158 | sys_mtime = 0 159 | except OSError: 160 | sys_mtime = 0 161 | 162 | try: 163 | user_mtime = os.path.getmtime(USER_CACHE_PATH) 164 | 165 | if (time.time() - MAX_AGE) > user_mtime: 166 | debug("Installer: User pkgcache too old, skipping") 167 | user_mtime = 0 168 | except OSError: 169 | user_mtime = 0 170 | 171 | # If neither exist, return None, and a new cache will be generated 172 | if sys_mtime == 0 and user_mtime == 0: 173 | return None 174 | 175 | most_recent = None 176 | 177 | # Select the most recent 178 | if sys_mtime > user_mtime: 179 | most_recent = SYS_CACHE_PATH 180 | debug("Installer: System pkgcache is most recent, using it.") 181 | else: 182 | most_recent = USER_CACHE_PATH 183 | debug("Installer: User pkgcache is most recent, using it.") 184 | 185 | return Path(most_recent) 186 | 187 | @print_timing 188 | def _load_cache(self): 189 | """ 190 | The cache pickle file can be in either a system or user location, 191 | depending on how the cache was generated. If it exists in both places, take the 192 | most recent one. If it's more than MAX_AGE, generate a new one anyhow. 193 | """ 194 | 195 | cache = None 196 | sections = None 197 | flatpak_remote_infos = None 198 | 199 | path = self._get_best_load_path() 200 | 201 | if path is None: 202 | raise CacheLoadingError 203 | try: 204 | with path.open(mode='r', encoding="utf8") as f: 205 | json_obj = JsonObject.from_json(json.load(f)) 206 | cache = json_obj.pkginfo_cache 207 | sections = json_obj.section_lists 208 | flatpak_remote_infos = json_obj.flatpak_remote_infos 209 | except Exception as e: 210 | warn("Installer: Error loading pkginfo cache:", str(e)) 211 | cache = None 212 | 213 | if cache is None: 214 | raise CacheLoadingError 215 | 216 | return cache, sections, flatpak_remote_infos 217 | 218 | def _get_best_save_path(self) -> Optional[Path]: 219 | if self.custom_cache_path is not None: 220 | return self.custom_cache_path 221 | 222 | # Prefer the system location, as all users can access it 223 | try: 224 | path = Path(SYS_CACHE_PATH) 225 | path.parent.mkdir(parents=True, exist_ok=True) 226 | path.touch(exist_ok=True) 227 | return path 228 | except PermissionError: 229 | try: 230 | path = Path(USER_CACHE_PATH) 231 | path.parent.mkdir(parents=True, exist_ok=True) 232 | return path 233 | except Exception: 234 | return None 235 | 236 | def _save_cache(self, to_be_json): 237 | path = self._get_best_save_path() 238 | 239 | FlatpakPkgInfo.__module__ = "installer.pkgInfo" 240 | AptPkgInfo.__module__ = "installer.pkgInfo" 241 | FlatpakRemoteInfo.__module__ = "installer._flatpak" 242 | 243 | try: 244 | with path.open(mode='w', encoding="utf8") as f: 245 | json.dump(to_be_json, f, default=lambda o: o.to_json(), indent=4) 246 | except Exception as e: 247 | warn("Installer: Could not save cache:", str(e)) 248 | 249 | def _new_cache_common(self): 250 | debug("Installer: Generating new pkgcache") 251 | cache, sections, flatpak_remote_infos = self._generate_cache() 252 | 253 | # If we're refreshing only a specific package type, don't destroy existing 254 | # items of the other type (otherwise if the cache is refreshed by mintupdate's 255 | # flatpak updater mintinstall will end up starting without any apt package info 256 | # and look broken). 257 | with self._item_lock: 258 | if self.cache_content == "f": 259 | for key in [key for key in self._items.keys() if key.startswith("a")]: 260 | cache[key] = self._items[key] 261 | sections = self.sections 262 | elif self.cache_content == "a": 263 | for key in [key for key in self._items.keys() if key.startswith("f")]: 264 | cache[key] = self._items[key] 265 | 266 | if len(cache) > 0: 267 | self._save_cache(JsonObject(cache, sections, flatpak_remote_infos)) 268 | 269 | with self._item_lock: 270 | self._items = cache 271 | self.sections = sections 272 | self.flatpak_remote_infos = flatpak_remote_infos 273 | 274 | if len(cache) == 0: 275 | self.status = self.STATUS_EMPTY 276 | else: 277 | self.status = self.STATUS_OK 278 | 279 | def _generate_cache_thread(self, callback=None): 280 | self._new_cache_common() 281 | 282 | if callback is not None: 283 | GObject.idle_add(callback) 284 | 285 | def get_subset_of_type(self, pkg_type): 286 | with self._item_lock: 287 | return {k: v for k, v in self._items.items() if k.startswith(pkg_type)} 288 | 289 | def force_new_cache_async(self, idle_callback=None): 290 | thread = threading.Thread(target=self._generate_cache_thread, 291 | kwargs={"callback" : idle_callback}) 292 | thread.start() 293 | 294 | def force_new_cache(self): 295 | self._new_cache_common() 296 | 297 | def find_pkginfo(self, string, pkg_type=None, remote=None): 298 | if pkg_type == "a" and not string.startswith("apt:"): 299 | string = "apt:" + string 300 | try: 301 | return self[string] 302 | except KeyError: 303 | if string[0:4] == "apt:": 304 | return None 305 | if self.have_flatpak: 306 | pkginfo = _flatpak.find_pkginfo(self, string, remote) 307 | if pkginfo is not None: 308 | return pkginfo 309 | 310 | return None 311 | 312 | def _get_manually_installed_debs(self): 313 | """ 314 | Generate list of manually installed Debian package. 315 | Requires a package list provided by the installer. 316 | Currently knows only Ubiquity's /var/log/installer/initial-status.gz 317 | """ 318 | installer_log = "/var/log/installer/initial-status.gz" 319 | if not os.path.isfile(installer_log): 320 | return None 321 | import gzip 322 | try: 323 | installer_log = gzip.open(installer_log, "r").read().decode('utf-8').splitlines() 324 | except Exception as e: 325 | # There are a number of different exceptions here, but there's only one response 326 | warn("Could not get initial installed packages list (check /var/log/installer/initial-status.gz): %s" % str(e)) 327 | return None 328 | initial_status = [x[9:] for x in installer_log if x.startswith("Package: ")] 329 | if not initial_status: 330 | return None 331 | from . import _apt 332 | pkgcache = [x[4:] for x in self.get_subset_of_type("a")] 333 | current_status = ["apt:%s" % pkg for pkg in _apt.get_apt_cache() if 334 | (pkg.installed and 335 | not pkg.is_auto_installed and 336 | pkg.shortname not in initial_status and 337 | pkg.shortname in pkgcache)] 338 | return current_status 339 | 340 | def get_manually_installed_packages(self): 341 | """ Get list of all manually installed packages (apt and flatpak) """ 342 | installed_packages = None 343 | installed_packages_apt = self._get_manually_installed_debs() 344 | if installed_packages_apt: 345 | installed_packages = installed_packages_apt 346 | installed_packages += [x for x in self.get_subset_of_type("f")] 347 | return installed_packages 348 | 349 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/dialogs.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('XApp', '1.0') 3 | from gi.repository import GLib, Gtk, GObject, Gdk, XApp, Pango 4 | 5 | import gettext 6 | APP = 'mint-common' 7 | LOCALE_DIR = "/usr/share/linuxmint/locale" 8 | t = gettext.translation(APP, LOCALE_DIR, fallback=True) 9 | _ = t.gettext 10 | 11 | from aptdaemon.gtk3widgets import AptConfirmDialog 12 | 13 | ######################### Subclass Apt's dialog to keep consistency 14 | 15 | class ChangesConfirmDialog(AptConfirmDialog): 16 | 17 | """Dialog to confirm the changes that would be required by a 18 | transaction. 19 | """ 20 | 21 | def __init__(self, transaction, task=None, parent=None): 22 | super(ChangesConfirmDialog, self).__init__(transaction, cache=None, parent=parent) 23 | self.parent_window = parent 24 | self.set_size_request(500, 350) 25 | self.task = task 26 | 27 | def _show_changes(self): 28 | """Show a message and the dependencies in the dialog.""" 29 | self.treestore.clear() 30 | if not self.parent_window: 31 | self.set_skip_taskbar_hint(True) 32 | self.set_keep_above(True) 33 | 34 | # Run parent method for apt 35 | if not self.task or (self.task.pkginfo and self.task.pkginfo.pkg_hash.startswith("a")): 36 | """Show a message and the dependencies in the dialog.""" 37 | self.treestore.clear() 38 | for pkg_list, msg, min_packages in ( 39 | [self.task.to_install, _("Install"), 1 if self.task.type == self.task.INSTALL_TASK else 0], 40 | [self.task.to_reinstall, _("Reinstall"), 0], 41 | [self.task.to_remove, _("Remove"), 1 if self.task.type == self.task.UNINSTALL_TASK else 0], 42 | [self.task.to_purge, _("Purge"), 0], 43 | [self.task.to_update, _("Upgrade"), 0], 44 | [self.task.to_downgrade, _("Downgrade"), 0], 45 | [self.task.to_skip_upgrade, _("Skip upgrade"), 0] 46 | ): 47 | 48 | if len(pkg_list) > min_packages: 49 | piter = self.treestore.append(None, ["%s" % msg]) 50 | 51 | for pkg in pkg_list: 52 | if pkg_list == self.task.to_install and pkg.get_name() == self.task.name: 53 | continue 54 | 55 | self.treestore.append(piter, [pkg.get_name()]) 56 | # If there is only one type of changes (e.g. only installs) expand the 57 | # tree 58 | # FIXME: adapt the title and message accordingly 59 | # FIXME: Should we have different modes? Only show dependencies, only 60 | # initial packages or both? 61 | msg = _("Please take a look at the list of changes below.") 62 | if len(self.treestore) == 1: 63 | filtered_store = self.treestore.filter_new(Gtk.TreePath.new_first()) 64 | self.treeview.expand_all() 65 | self.treeview.set_model(filtered_store) 66 | self.treeview.set_show_expanders(False) 67 | if len(self.task.to_install) > 1: 68 | title = _("Additional software will be installed") 69 | elif len(self.task.to_reinstall) > 0: 70 | title = _("Additional software will be re-installed") 71 | elif len(self.task.to_remove) > 0: 72 | title = _("Additional software will be removed") 73 | elif len(self.task.to_purge) > 0: 74 | title = _("Additional software will be purged") 75 | elif len(self.task.to_update) > 0: 76 | title = _("Additional software will be upgraded") 77 | elif len(self.task.to_downgrade) > 0: 78 | title = _("Additional software will be downgraded") 79 | elif len(self.task.to_skip_upgrade) > 0: 80 | title = _("Updates will be skipped") 81 | if len(filtered_store) < 6: 82 | self.set_resizable(False) 83 | self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, 84 | Gtk.PolicyType.NEVER) 85 | else: 86 | self.treeview.set_size_request(350, 200) 87 | else: 88 | title = _("Additional changes are required") 89 | self.treeview.set_size_request(350, 200) 90 | self.treeview.collapse_all() 91 | if self.task.download_size > 0: 92 | msg += "\n" 93 | msg += (_("%s will be downloaded in total.") % 94 | GLib.format_size(self.task.download_size)) 95 | if self.task.freed_size > 0: 96 | msg += "\n" 97 | msg += (_("%s of disk space will be freed.") % 98 | GLib.format_size(self.task.freed_size)) 99 | elif self.task.install_size > 0: 100 | msg += "\n" 101 | msg += (_("%s more disk space will be used.") % 102 | GLib.format_size(self.task.install_size)) 103 | self.label.set_markup("%s\n\n%s" % (title, msg)) 104 | else: 105 | # flatpak 106 | self.set_title(_("Flatpaks")) 107 | 108 | min_packages = 1 if self.task.type == self.task.INSTALL_TASK else 0 109 | if len(self.task.to_install) > min_packages: 110 | piter = self.treestore.append(None, ["%s" % _("Install")]) 111 | 112 | for ref in self.task.to_install: 113 | if self.task.pkginfo and self.task.pkginfo.refid == ref.format_ref(): 114 | continue 115 | 116 | self.treestore.append(piter, [ref.get_name()]) 117 | 118 | min_packages = 1 if self.task.type == self.task.UNINSTALL_TASK else 0 119 | if len(self.task.to_remove) > min_packages: 120 | piter = self.treestore.append(None, ["%s" % _("Remove")]) 121 | 122 | for ref in self.task.to_remove: 123 | if self.task.pkginfo and self.task.pkginfo.refid == ref.format_ref(): 124 | continue 125 | 126 | self.treestore.append(piter, [ref.get_name()]) 127 | 128 | if len(self.task.to_update) > 0: 129 | # If this is an update task (like from mintupdate) we may have selected updates explicitly, and there may be 130 | # updates we *didn't* select but are required for an update we did. We only want to add those updates that 131 | # are pulled in the second case, since the updates we did select do not need to be displayed again (this is 132 | # following apt behavior, where we only list dependencies here and unexpected changes). 133 | header_added = False 134 | for ref in self.task.to_update: 135 | if self.task.type == self.task.UPDATE_TASK: 136 | if len(self.task.initial_refs_to_update) == 0 or ref.format_ref() in self.task.initial_refs_to_update: 137 | continue 138 | 139 | if not header_added: 140 | piter = self.treestore.append(None, ["%s" % _("Upgrade")]) 141 | header_added = True 142 | 143 | self.treestore.append(piter, [ref.get_name()]) 144 | 145 | msg = _("Please take a look at the list of changes below.") 146 | 147 | if len(self.treestore) == 1: 148 | filtered_store = self.treestore.filter_new( 149 | Gtk.TreePath.new_first()) 150 | self.treeview.expand_all() 151 | self.treeview.set_model(filtered_store) 152 | self.treeview.set_show_expanders(False) 153 | 154 | if len(self.task.to_install) > 1: 155 | title = _("Additional software will be installed") 156 | elif len(self.task.to_remove) > 0: 157 | title = _("Additional software will be removed") 158 | elif len(self.task.to_update) > 0: 159 | title = _("Additional software will be upgraded") 160 | 161 | if len(filtered_store) < 6: 162 | self.set_resizable(False) 163 | self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, 164 | Gtk.PolicyType.NEVER) 165 | else: 166 | self.treeview.set_size_request(350, 200) 167 | else: 168 | title = _("Additional changes are required") 169 | self.treeview.set_size_request(350, 200) 170 | self.treeview.collapse_all() 171 | 172 | if self.task.download_size > 0: 173 | msg += "\n" 174 | msg += (_("%s will be downloaded in total.") % 175 | GLib.format_size(self.task.download_size)) 176 | if self.task.freed_size > 0: 177 | msg += "\n" 178 | msg += (_("%s of disk space will be freed.") % 179 | GLib.format_size(self.task.freed_size)) 180 | elif self.task.install_size > 0: 181 | msg += "\n" 182 | msg += (_("%s more disk space will be used.") % 183 | GLib.format_size(self.task.install_size)) 184 | self.label.set_markup("%s\n\n%s" % (title, msg)) 185 | 186 | def map_package(self, pkg): 187 | """Map a package to a different object type, e.g. applications 188 | and return a list of those. 189 | 190 | By default return the package itself inside a list. 191 | 192 | Override this method if you don't want to store package names 193 | in the treeview. 194 | """ 195 | return [pkg] 196 | 197 | def render_package_desc(self, column, cell, model, iter, data): 198 | value = model.get_value(iter, 0) 199 | 200 | cell.set_property("markup", value) 201 | 202 | 203 | class FlatpakProgressWindow(Gtk.Dialog): 204 | """ 205 | Progress dialog for standalone flatpak installs, removals, updates. 206 | Intended to be used when not working as part of a parent app (like mintinstall) 207 | """ 208 | 209 | def __init__(self, task, parent=None): 210 | Gtk.Dialog.__init__(self, parent=parent) 211 | self.set_default_size(400, 140) 212 | self.task = task 213 | self.finished = False 214 | 215 | # Progress goes directly to this window 216 | task.client_progress_cb = self.window_client_progress_cb 217 | 218 | # finished callbacks route thru the installer 219 | # but we want to see them in this window also. 220 | self.final_finished_cb = task.client_finished_cb 221 | task.client_finished_cb = self.window_client_finished_cb 222 | self.pulse_timer = 0 223 | 224 | self.real_progress_text = None 225 | 226 | # Setup the dialog 227 | self.set_border_width(6) 228 | self.set_resizable(False) 229 | self.get_content_area().set_spacing(6) 230 | # Setup the cancel button 231 | self.button = Gtk.Button.new_from_stock(Gtk.STOCK_CANCEL) 232 | self.button.set_use_stock(True) 233 | self.get_action_area().pack_start(self.button, False, False, 0) 234 | self.button.connect("clicked", self.on_button_clicked) 235 | self.button.show() 236 | 237 | # labels and progressbar 238 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 239 | vbox.set_spacing(12) 240 | vbox.set_border_width(10) 241 | 242 | self.label = Gtk.Label(max_width_chars=45) 243 | vbox.pack_start(self.label, False, False, 0) 244 | self.label.set_halign(Gtk.Align.START) 245 | self.label.set_line_wrap(True) 246 | 247 | self.progress = Gtk.ProgressBar() 248 | vbox.pack_end(self.progress, False, True, 0) 249 | self.get_content_area().pack_start(vbox, True, True, 0) 250 | 251 | self.set_title(_("Flatpak")) 252 | XApp.set_window_icon_name(self, "system-software-installer") 253 | 254 | vbox.show_all() 255 | self.realize() 256 | 257 | self.progress.set_size_request(350, -1) 258 | functions = Gdk.WMFunction.MOVE | Gdk.WMFunction.RESIZE 259 | try: 260 | self.get_window().set_functions(functions) 261 | except TypeError: 262 | # workaround for older and broken GTK typelibs 263 | self.get_window().set_functions(Gdk.WMFunction(functions)) 264 | 265 | # catch ESC and behave as if cancel was clicked 266 | self.connect("delete-event", self._on_dialog_delete_event) 267 | 268 | def start_progress_pulse(self): 269 | if self.pulse_timer > 0: 270 | return 271 | 272 | self.progress.pulse() 273 | self.pulse_timer = GObject.timeout_add(1050, self.progress_pulse_tick) 274 | 275 | def progress_pulse_tick(self): 276 | self.progress.pulse() 277 | 278 | return GLib.SOURCE_CONTINUE 279 | 280 | def stop_progress_pulse(self): 281 | if self.pulse_timer > 0: 282 | GObject.source_remove(self.pulse_timer) 283 | self.pulse_timer = 0 284 | 285 | def _on_dialog_delete_event(self, dialog, event): 286 | self.button.clicked() 287 | return True 288 | 289 | def window_client_progress_cb(self, pkginfo, progress, estimating, status_text): 290 | if estimating: 291 | self.start_progress_pulse() 292 | else: 293 | self.stop_progress_pulse() 294 | 295 | self.progress.set_fraction(progress / 100.0) 296 | XApp.set_window_progress(self, progress) 297 | 298 | self.label.set_text(status_text) 299 | 300 | def window_client_finished_cb(self, task): 301 | self.finished = True 302 | 303 | self.destroy() 304 | self.final_finished_cb(task) 305 | 306 | def on_button_clicked(self, button): 307 | if not self.finished: 308 | self.task.cancel() 309 | 310 | def show_error(message, parent_window=None): 311 | Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, _show_error_mainloop, message, parent_window) 312 | 313 | def _show_error_mainloop(message, parent_window): 314 | dialog = Gtk.MessageDialog(None, 315 | Gtk.DialogFlags.DESTROY_WITH_PARENT, 316 | Gtk.MessageType.ERROR, 317 | Gtk.ButtonsType.OK, 318 | "") 319 | if parent_window is not None: 320 | dialog.set_transient_for(parent_window) 321 | dialog.set_modal(True) 322 | dialog.set_title(GLib.get_application_name()) 323 | 324 | text = _("An error occurred") 325 | dialog.set_markup("%s" % text) 326 | 327 | scroller = Gtk.ScrolledWindow(min_content_height = 75, max_content_height=400, min_content_width=400, propagate_natural_height=True) 328 | dialog.get_message_area().pack_start(scroller, False, False, 8) 329 | 330 | message_label = Gtk.Label(message, lines=20, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, selectable=True) 331 | message_label.set_max_width_chars(60) 332 | message_label.show() 333 | scroller.add(message_label) 334 | 335 | dialog.show_all() 336 | dialog.run() 337 | dialog.destroy() 338 | 339 | return GLib.SOURCE_REMOVE 340 | 341 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/installer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import threading 3 | import time 4 | import tempfile 5 | 6 | import gi 7 | gi.require_version('Gtk', '3.0') 8 | from gi.repository import GLib, GObject, Gio, Gtk 9 | 10 | from . import cache, _flatpak, _apt, dialogs 11 | from .misc import print_timing, check_ml, debug, warn 12 | 13 | PKG_TYPE_ALL = None 14 | PKG_TYPE_APT = "a" 15 | PKG_TYPE_FLATPAK = "f" 16 | 17 | Gtk.IconTheme.get_default().append_search_path("/usr/share/linuxmint/icons") 18 | 19 | class InstallerTask: 20 | # task types 21 | INSTALL_TASK = "install" 22 | UNINSTALL_TASK = "remove" 23 | UPDATE_TASK = "update" 24 | 25 | # Set after a package selection, reflects whether task can proceed or not 26 | STATUS_NONE = "none" 27 | STATUS_OK = "ok" 28 | STATUS_BROKEN = "broken" 29 | STATUS_FORBIDDEN = "forbidden" 30 | STATUS_UNKNOWN = "unknown" 31 | 32 | # Used by standalone progress window to update labels appropriately 33 | PROGRESS_STATE_INIT = "init" 34 | PROGRESS_STATE_INSTALLING = "installing" 35 | PROGRESS_STATE_UPDATING = "updating" 36 | PROGRESS_STATE_REMOVING = "removing" 37 | PROGRESS_STATE_FINISHED = "finished" 38 | PROGRESS_STATE_FAILED = "failed" 39 | 40 | def __init__(self, pkginfo, installer, 41 | client_info_ready_callback, client_info_error_callback, 42 | client_installer_finished_cb, client_installer_progress_cb, 43 | installer_cleanup_cb, installer_error_cleanup_cb, is_addon_task=False, use_mainloop=False, parent_window=None): 44 | self.type = InstallerTask.INSTALL_TASK 45 | 46 | self.use_mainloop = use_mainloop 47 | self.parent_window = parent_window 48 | 49 | # pkginfo will be None for an update task 50 | self.pkginfo = pkginfo 51 | self.is_addon_task = is_addon_task 52 | 53 | # AsApp if available 54 | self.as_pkg = None 55 | 56 | self.name = None 57 | 58 | # Lists the refs we selected for an update, vs those added as dependencies (this is for our confirm dialog to 59 | # make layout decisions) 60 | self.initial_refs_to_update = [] 61 | 62 | # FlatpakRef : pre-transaction-version or commit 63 | # For activity logging so we can record pre and post-version. 64 | self.ref_prior_versions_dict = {} 65 | self.transaction_log = [] 66 | 67 | # Set by .select_pkginfo(), the re-entry point after a task is fully 68 | # calculated, and the UI should be updated with detailed info about 69 | # the pending operation (disk use, etc..) 70 | self.info_ready_callback = client_info_ready_callback 71 | self.info_error_callback = client_info_error_callback 72 | # To be checked by the info_ready_callback, to allow the UI to reflect 73 | # the ability to proceed with a task, or report that something is not right. 74 | self.info_ready_status = self.STATUS_NONE 75 | 76 | # Set by the backend, the functions to call to actually confirm and perform the 77 | # task (will be none on STATUS_BROKEN or _FORBIDDEN) 78 | self.confirm = lambda: True 79 | self.cancel = lambda: True 80 | self.execute = None 81 | 82 | # Passed to _flatpak operations to respond to the Cancel button in the 83 | # standalone progress window. eventually it may be used elsewhere. 84 | self.cancellable = Gio.Cancellable() 85 | 86 | # Callbacks that will be used at various points during a task being operated on. 87 | # The .client_* callbacks are arguments of Installer.execute_task(). The 88 | # client_finished_cb is required. If the progress callback is missing, a standalone 89 | # progress window will be provided. 90 | self.client_progress_cb = client_installer_progress_cb 91 | self.client_finished_cb = client_installer_finished_cb 92 | 93 | # These are internally used - called as the 'real' error and finished callback, 94 | # to do some cleanup like removing the task and reloading the apt cache before 95 | # finally calling task.client_finished_cb 96 | self.error_cleanup_cb = installer_error_cleanup_cb 97 | self.finished_cleanup_cb = installer_cleanup_cb 98 | 99 | self.has_window = False 100 | # Updated throughout a flatpak operation - for now it's used for updating the 101 | # standalone flatpak progress window 102 | self.progress_state = self.PROGRESS_STATE_INIT 103 | # Same - allows the flatpak window to update the current package being installed/removed 104 | self.current_package_name = None 105 | # The error message displayed in a popup if a flatpak operation fails. 106 | self.error_message = None 107 | 108 | self.transaction = None 109 | self.pkit_request_id = 0 110 | 111 | # The command that can be used to launch the current target package, if it's installed 112 | self.exec_string = None 113 | 114 | # List of additional packages to install, remove or update, based on the selected 115 | # pkginfo. Depending on the backend, they will consist of PkPackages or stringified 116 | # flatpak refs (the result of ref.format_ref()). 117 | self.to_install = [] 118 | self.to_reinstall = [] # unused 119 | self.to_remove = [] 120 | self.to_purge = [] # unused 121 | self.to_update = [] 122 | self.to_downgrade = [] # unused 123 | self.to_skip_upgrade = [] # unused 124 | 125 | # Size info for display, calculated by the backend during .select_pkginfo() 126 | self.download_size = 0 127 | self.install_size = 0 128 | self.freed_size = 0 129 | 130 | # Static info filled in for display 131 | if pkginfo: 132 | self.name = pkginfo.name 133 | 134 | if pkginfo.pkg_hash.startswith("a"): 135 | self.arch = "" 136 | self.branch = "" 137 | self.remote = "" 138 | else: 139 | self.arch = pkginfo.arch 140 | self.remote = pkginfo.remote 141 | self.branch = pkginfo.branch 142 | 143 | def set_version(self, installer): 144 | if self.type == InstallerTask.INSTALL_TASK: 145 | # install packages, show pending version 146 | self.version = installer.get_version(self.pkginfo) 147 | else: 148 | # Remove packages, show current version 149 | self.version = installer.get_installed_version(self.pkginfo) 150 | 151 | def get_transaction_log(self): 152 | return self.transaction_log 153 | 154 | def call_info_ready_callback(self): 155 | if self.info_ready_callback is None: 156 | return 157 | 158 | if self.use_mainloop: 159 | GLib.idle_add(self.info_ready_callback, self, priority=GLib.PRIORITY_DEFAULT) 160 | else: 161 | self.info_ready_callback(self) 162 | 163 | def handle_error(self, error, info_stage=False): 164 | try: 165 | self.error_message = error.message 166 | except: 167 | self.error_message = str(error) 168 | 169 | if info_stage: 170 | if self.info_error_callback is None: 171 | dialogs.show_error(self.error_message, self.parent_window) 172 | return 173 | 174 | if self.use_mainloop: 175 | GLib.idle_add(self.info_error_callback, self, priority=GLib.PRIORITY_DEFAULT) 176 | else: 177 | self.info_error_callback(self) 178 | else: 179 | dialogs.show_error(self.error_message, self.parent_window) 180 | 181 | def call_finished_cleanup_callback(self): 182 | if not self.finished_cleanup_cb: 183 | return 184 | 185 | if self.use_mainloop: 186 | GLib.idle_add(self.finished_cleanup_cb, self, priority=GLib.PRIORITY_DEFAULT) 187 | else: 188 | self.finished_cleanup_cb(self) 189 | 190 | def call_error_cleanup_callback(self): 191 | if not self.error_cleanup_cb: 192 | return 193 | 194 | if self.use_mainloop: 195 | GLib.idle_add(self.error_cleanup_cb, self, priority=GLib.PRIORITY_DEFAULT) 196 | else: 197 | self.error_cleanup_cb(self) 198 | 199 | class Installer(GObject.Object): 200 | __gsignals__ = { 201 | 'appstream-changed': (GObject.SignalFlags.RUN_LAST, None, ()), 202 | } 203 | def __init__(self, pkg_type=PKG_TYPE_ALL, temp=False): 204 | GObject.Object.__init__(self) 205 | 206 | self.tasks = {} 207 | self.pkg_type = pkg_type 208 | 209 | if temp: 210 | f = tempfile.NamedTemporaryFile(prefix="mint-common-installer-tmp") 211 | self.cache_path = f.name 212 | else: 213 | self.cache_path = None 214 | 215 | self.remotes_changed = False 216 | self.inited = False 217 | 218 | self.have_flatpak = False 219 | self.have_flatpak = self._get_flatpak_status() 220 | 221 | self.cache = {} 222 | self._init_cb = None 223 | 224 | self.startup_timer = time.time() 225 | 226 | def _get_flatpak_status(self): 227 | try: 228 | gi.require_version('Flatpak', '1.0') 229 | from gi.repository import Flatpak 230 | 231 | return True 232 | except: 233 | warn("No flatpak support, install flatpak and gir1.2-flatpak-1.0 and restart mintinstall to enable it.") 234 | 235 | return False 236 | 237 | def init_sync(self): 238 | """ 239 | Loads the cache synchronously. Returns True if all went ok, and returns False if there 240 | is no cache (or it's too old.) You should then call init() with a callback so the cache 241 | can be regenerated. 242 | """ 243 | 244 | if self.pkg_type == PKG_TYPE_FLATPAK and not self.have_flatpak: 245 | debug("Not syncing for flatpaks only, as there is currently no support") 246 | return True 247 | 248 | self.settings = Gio.Settings(schema_id="com.linuxmint.install") 249 | 250 | if self._fp_remotes_have_changed(): 251 | self.remotes_changed = True 252 | 253 | self.backend_table = {} 254 | 255 | self.cache = cache.PkgCache(self.pkg_type, self.cache_path, self.have_flatpak) 256 | 257 | if self.cache.status == self.cache.STATUS_OK and not self.remotes_changed: 258 | self.inited = True 259 | 260 | self.initialize_appstream() 261 | 262 | return True 263 | 264 | return False 265 | 266 | def init(self, ready_callback=None): 267 | """ 268 | Loads the cache asynchronously. If there is no cache (or it's too old,) it causes 269 | one to be generated and saved. The ready_callback is called on idle once this is finished. 270 | """ 271 | self.backend_table = {} 272 | 273 | self.cache = cache.PkgCache(self.pkg_type, self.cache_path, self.have_flatpak) 274 | 275 | self._init_cb = ready_callback 276 | 277 | if self.cache.status == self.cache.STATUS_OK and not self.remotes_changed: 278 | self.inited = True 279 | 280 | GObject.idle_add(self._idle_cache_load_done) 281 | else: 282 | if self.remotes_changed: 283 | debug("Installer: Flatpak remotes have changed, forcing a new cache.") 284 | 285 | self.cache.force_new_cache_async(self._idle_cache_load_done) 286 | 287 | return self 288 | 289 | def force_new_cache(self, ready_callback=None): 290 | """ 291 | Forces the cache to regenerate, calling read_callback when complete 292 | """ 293 | self.cache.force_new_cache_async(ready_callback) 294 | 295 | def force_new_cache_sync(self): 296 | """ 297 | Forces the cache to regenerate synchronously 298 | """ 299 | self.cache.force_new_cache() 300 | 301 | def _idle_cache_load_done(self): 302 | self.inited = True 303 | 304 | if self.remotes_changed: 305 | self._store_remotes() 306 | self.remotes_changed = False 307 | 308 | self.initialize_appstream() 309 | 310 | debug('Full installer startup took %0.3f ms' % ((time.time() - self.startup_timer) * 1000.0)) 311 | 312 | if self._init_cb: 313 | self._init_cb() 314 | 315 | @print_timing 316 | def _fp_remotes_have_changed(self): 317 | """ 318 | We check here for changed remotes. We care if names, urls, and disabled status changed. 319 | The 'noenumerate' property won't change, and is usually marked on standalone (-source) ref 320 | installs. We don't want to generate a new cache for those - their app can be accessed via 321 | installed apps, plus if you uninstall the app, the remote gets auto-removed. 322 | """ 323 | changed = False 324 | real_remote_count = 0 325 | 326 | saved_remotes = self.settings.get_strv("flatpak-remotes") 327 | fp_remotes = self.list_flatpak_remotes() 328 | 329 | for remote_info in fp_remotes: 330 | real_remote_count += 1 331 | 332 | item = "%s::%s::%s" % (remote_info.name, remote_info.url, str(remote_info.disabled)) 333 | 334 | if item not in saved_remotes: 335 | changed = True 336 | break 337 | 338 | if not changed: 339 | if len(saved_remotes) != real_remote_count: 340 | changed = True 341 | 342 | debug("Remotes have changed:", changed) 343 | 344 | return changed 345 | 346 | @print_timing 347 | def _store_remotes(self): 348 | new_remotes = [] 349 | 350 | fp_remotes = self.list_flatpak_remotes() 351 | 352 | for remote_info in fp_remotes: 353 | item = "%s::%s::%s" % (remote_info.name, remote_info.url, str(remote_info.disabled)) 354 | 355 | new_remotes.append(item) 356 | 357 | self.settings.set_strv("flatpak-remotes", new_remotes) 358 | 359 | def select_pkginfo(self, pkginfo, 360 | client_info_ready_callback, client_info_error_callback, 361 | client_installer_finished_cb, client_installer_progress_cb, 362 | use_mainloop=False, parent_window=None): 363 | """ 364 | Initiates calculations for installing or removing a particular package 365 | (depending upon whether or not the selected package is installed. Creates 366 | an InstallerTask instance and populates it with info relevant for display 367 | and for execution later. When this is completed, ready_callback is called, 368 | with the newly-created task as its argument. Note: At that point, this is 369 | the *only* reference to the task object. It can be safely discarded. If 370 | the task is to be run, Installer.execute_task() is called, passing this task 371 | object, along with callback functions. The task object is then added to a 372 | queue (and is tracked in self.tasks from there on out.) 373 | """ 374 | if pkginfo.pkg_hash in self.tasks.keys(): 375 | task = self.tasks[pkginfo.pkg_hash] 376 | 377 | GObject.idle_add(task.info_ready_callback, task) 378 | return task.cancellable 379 | 380 | task = InstallerTask(pkginfo, self, 381 | client_info_ready_callback, client_info_error_callback, 382 | client_installer_finished_cb, client_installer_progress_cb, 383 | self._task_finished, self._task_error, 384 | use_mainloop=use_mainloop, parent_window=parent_window) 385 | 386 | if self.pkginfo_is_installed(pkginfo): 387 | # It's not installed, so assume we're installing 388 | task.type = InstallerTask.UNINSTALL_TASK 389 | else: 390 | task.type = InstallerTask.INSTALL_TASK 391 | 392 | task.set_version(self) 393 | task.as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 394 | 395 | if pkginfo.pkg_hash.startswith("a"): 396 | _apt.select_packages(task) 397 | else: 398 | _flatpak.select_packages(task) 399 | 400 | return task.cancellable 401 | 402 | def select_flatpak_updates(self, refs, 403 | client_info_ready_callback, client_info_error_callback, 404 | client_installer_finished_cb, client_installer_progress_cb, 405 | use_mainloop=False): 406 | """ 407 | Creates an InstallerTask populated with all flatpak packages that can be 408 | updated. If refs is empty, selects all possible updates, otherwise, only attempts 409 | to update refs. 410 | """ 411 | task = InstallerTask(None, self, 412 | client_info_ready_callback, client_info_error_callback, 413 | client_installer_finished_cb, client_installer_progress_cb, 414 | self._task_finished, self._task_error, 415 | use_mainloop=use_mainloop) 416 | 417 | task.type = InstallerTask.UPDATE_TASK 418 | task.initial_refs_to_update = refs if refs else [] 419 | 420 | _flatpak.select_updates(task) 421 | 422 | def list_updated_flatpak_pkginfos(self): 423 | """ 424 | Returns a list of flatpak pkginfos that can be updated. Unlike 425 | prepare_flatpak_update, this is for the convenience of displaying information 426 | to the user. 427 | """ 428 | return _flatpak.list_updated_pkginfos(self.cache) 429 | 430 | def find_pkginfo(self, name, pkg_type=PKG_TYPE_ALL, remote=None): 431 | """ 432 | Attempts to find and return a PkgInfo object, given a package name. If 433 | pkg_type is None, looks in first apt, then flatpaks. 434 | """ 435 | return self.cache.find_pkginfo(name, pkg_type, remote) 436 | 437 | def get_pkginfo_from_ref_file(self, file, ready_callback): 438 | """ 439 | Accepts a GFile to a .flatpakref on a local path. If the flatpak's remote 440 | has not been previously added to the system installation, this also adds 441 | it and downloads Appstream info as well, before calling ready_callback with 442 | the created (or existing) PkgInfo as an argument. 443 | """ 444 | if self.have_flatpak: 445 | _flatpak.get_pkginfo_from_file(self.cache, file, ready_callback) 446 | 447 | def add_remote_from_repo_file(self, file, ready_callback): 448 | """ 449 | Accepts a GFile to a .flatpakrepo on a local path. Adds the remote if it 450 | doesn't exist already, fetches any appstream data, and then calls 451 | ready_callback 452 | """ 453 | 454 | if self.have_flatpak: 455 | _flatpak.add_remote_from_repo_file(self.cache, file, ready_callback) 456 | else: 457 | ready_callback(None, "no-flatpak-support") 458 | 459 | def list_flatpak_remotes(self): 460 | """ 461 | Returns a list of FlatpakRemoteInfos. The remote_name can be used to match 462 | with PkgInfo.remote and the title is for display. 463 | """ 464 | if self.have_flatpak: 465 | return _flatpak.list_remotes() 466 | else: 467 | return [] 468 | 469 | def get_remote_info_for_name(self, remote_name): 470 | if self.have_flatpak: 471 | for remote in _flatpak.list_remotes(): 472 | if remote.name == remote_name: 473 | return remote 474 | 475 | return [] 476 | 477 | def pkginfo_is_installed(self, pkginfo): 478 | """ 479 | Returns whether or not a given package is currently installed. This uses 480 | the AptCache or the FlatpakInstallation to check. 481 | """ 482 | if self.inited: 483 | if pkginfo.pkg_hash.startswith("a"): 484 | return _apt.pkginfo_is_installed(pkginfo) 485 | elif self.have_flatpak and pkginfo.pkg_hash.startswith("f"): 486 | return _flatpak.pkginfo_is_installed(pkginfo) 487 | 488 | return False 489 | 490 | @print_timing 491 | def generate_uncached_pkginfos(self): 492 | """ 493 | Flatpaks installed from .flatpakref files may not actually be in the saved 494 | pkginfo cache, specifically, if they're added from no-enumerate-marked remotes. 495 | This gets run at startup to collect and generate their info. 496 | """ 497 | if self.have_flatpak: 498 | _flatpak.generate_uncached_pkginfos(self.cache) 499 | 500 | @print_timing 501 | def initialize_appstream(self): 502 | """ 503 | Loads and caches the xmlb pools so they can be used to provide 504 | display info for packages. 505 | """ 506 | if self.have_flatpak: 507 | _flatpak.initialize_appstream(cb=self.on_appstream_loaded) 508 | 509 | # Open the apt cache while we're in a thread. 510 | _apt.get_apt_cache() 511 | 512 | def on_appstream_loaded(self): 513 | self.generate_uncached_pkginfos() 514 | self.emit("appstream-changed") 515 | 516 | def get_appstream_pkg_for_pkginfo(self, pkginfo): 517 | backend_component = None 518 | 519 | if pkginfo.pkg_hash.startswith("a"): 520 | backend_component = _apt.search_for_pkginfo_apt_pkg(pkginfo) 521 | if backend_component is not None: 522 | self.backend_table[pkginfo] = backend_component 523 | else: 524 | backend_component = _flatpak.search_for_pkginfo_appstream_package(pkginfo) 525 | 526 | return backend_component 527 | 528 | def get_flatpak_launchables(self, pkginfo): 529 | """ 530 | Return the launchables associated with the AsApp for this pkginfo. 531 | """ 532 | 533 | if pkginfo.pkg_hash.startswith("a"): 534 | debug("launch_flatpak: pkginfo is not a flatpak") 535 | 536 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 537 | 538 | if as_pkg is None: 539 | return None 540 | 541 | return as_pkg.get_launchables() 542 | 543 | def get_flatpak_root_path(self): 544 | """ 545 | Return the root path for the flatpak installation (generally /var/lib/flatpak for system 546 | and ~/.local/share/flatpak for user. 547 | """ 548 | 549 | return _flatpak.get_fp_sys().get_path().get_path() 550 | 551 | def get_addons(self, pkginfo): 552 | """ 553 | Returns an array of app ids of names of available addons 554 | """ 555 | if pkginfo.pkg_hash.startswith("a"): 556 | return None 557 | 558 | addons = _flatpak._get_addons_for_pkginfo(pkginfo) 559 | 560 | if len(addons) == 0: 561 | return None 562 | 563 | return addons 564 | 565 | def get_description(self, pkginfo, for_search=False): 566 | """ 567 | Returns the description of the package. If for_search is True, 568 | this is the raw, unformatted string in the case of apt. 569 | """ 570 | if for_search and pkginfo.pkg_hash.startswith("a"): 571 | try: 572 | return _apt._apt_cache[pkginfo.name].candidate.description 573 | except Exception: 574 | pass 575 | 576 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 577 | return pkginfo.get_description(as_pkg) 578 | 579 | def get_screenshots(self, pkginfo): 580 | """ 581 | Returns a list of screenshot urls 582 | """ 583 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 584 | 585 | return pkginfo.get_screenshots(as_pkg) 586 | 587 | def get_version(self, pkginfo): 588 | """ 589 | Returns the current version string, if available 590 | """ 591 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 592 | 593 | return pkginfo.get_version(as_pkg) 594 | 595 | def get_developer(self, pkginfo): 596 | """ 597 | Returns the current version string, if available 598 | """ 599 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 600 | 601 | return pkginfo.get_developer(as_pkg) 602 | 603 | def get_installed_version(self, pkginfo): 604 | """ 605 | Returns the currently deployed version of a flatpak. 606 | """ 607 | if pkginfo.pkg_hash.startswith("a"): 608 | # apt packages we don't really need to make a distinction. 609 | return self.get_version(pkginfo) 610 | else: 611 | # flatpak packages, the appstream as_pkg shows the latest version provided in the xml, 612 | # not the actual installed version. 613 | return _flatpak._get_deployed_version(pkginfo) 614 | 615 | def get_homepage_url(self, pkginfo): 616 | """ 617 | Returns the home page url for a package. If there is 618 | no url for the package, in the case of flatpak, the remote's url 619 | is displayed instead 620 | """ 621 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 622 | 623 | return pkginfo.get_homepage_url(as_pkg) 624 | 625 | def get_help_url(self, pkginfo): 626 | """ 627 | Returns the help url for a package. If there is 628 | no url for the package, returns an empty string. Apt always returns 629 | an empty string. 630 | """ 631 | as_pkg = self.get_appstream_pkg_for_pkginfo(pkginfo) 632 | 633 | return pkginfo.get_help_url(as_pkg) 634 | 635 | def is_busy(self): 636 | return len(self.tasks.keys()) > 0 637 | 638 | def get_task_count(self): 639 | return len(self.tasks.keys()) 640 | 641 | def get_active_pkginfos(self): 642 | pkginfos = [] 643 | 644 | for pkg_hash in self.tasks.keys(): 645 | pkginfos.append(self.tasks[pkg_hash].pkginfo) 646 | 647 | return pkginfos 648 | 649 | def task_running(self, task): 650 | """ 651 | Returns whether a given task is currently executing. 652 | """ 653 | return task.pkginfo.pkg_hash in self.tasks.keys() 654 | 655 | def confirm_task(self, task): 656 | return task.confirm() 657 | 658 | def cancel_task(self, task): 659 | task.cancel() 660 | 661 | def execute_task(self, task): 662 | """ 663 | Executes a given task. The client_finished_cb is required always, to notify 664 | when the task completes. The progress and error callbacks are optional. If 665 | they're left out, a standalone progress window is created to allow the user to 666 | see the task's progress (and cancel it if desired.) 667 | """ 668 | 669 | if task.pkginfo is not None: 670 | key = task.pkginfo.pkg_hash 671 | else: 672 | key = "updates" 673 | 674 | self.tasks[key] = task 675 | 676 | debug("Starting task for package %s, type '%s'" % (key, task.type)) 677 | 678 | task.execute() 679 | 680 | def _task_finished(self, task): 681 | if not task.pkginfo: 682 | try: 683 | del self.tasks["updates"] 684 | debug("Done with update task (success)") 685 | except: 686 | pass 687 | else: 688 | key = task.pkginfo.pkg_hash 689 | 690 | if key: 691 | try: 692 | del self.tasks[key] 693 | debug("Done with task (success)", key) 694 | except: 695 | pass 696 | 697 | self._post_task_update(task) 698 | 699 | def _task_error(self, task): 700 | if not task.pkginfo: 701 | try: 702 | del self.tasks["updates"] 703 | debug("Done with update task (failure)") 704 | except: 705 | pass 706 | else: 707 | key = task.pkginfo.pkg_hash 708 | 709 | if key: 710 | try: 711 | del self.tasks[key] 712 | debug("Done with task (failure)", key) 713 | except: 714 | pass 715 | 716 | self._post_task_update(task) 717 | 718 | def _post_task_update(self, task): 719 | if task.pkginfo and task.pkginfo.pkg_hash.startswith("a"): 720 | thread = threading.Thread(target=self._apt_post_task_update_thread, args=(task,)) 721 | thread.start() 722 | else: 723 | self._run_client_callback(task) 724 | 725 | def _apt_post_task_update_thread(self, task): 726 | _apt.sync_cache_installed_states() 727 | 728 | # This needs to be called after reloading the apt cache, otherwise our installed 729 | # apps don't update correctly 730 | self._run_client_callback(task) 731 | 732 | def _run_client_callback(self, task): 733 | if task.client_finished_cb: 734 | GObject.idle_add(task.client_finished_cb, task, priority=GLib.PRIORITY_DEFAULT) 735 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import time 5 | import inspect 6 | import threading 7 | import sys 8 | import html2text 9 | import html2text.config 10 | 11 | DEBUG_MODE = os.getenv("DEBUG", False) 12 | DEBUG_QUERIES = os.getenv("DEBUG_QUERIES", False) 13 | 14 | class dash_match_dummy(): 15 | def sub(self, a, b): 16 | return b 17 | 18 | # html2text wants to escape content (not markdown) dashes. 19 | html2text.config.RE_MD_DASH_MATCHER = dash_match_dummy() 20 | html_converter = html2text.HTML2Text() 21 | # Asterisks are lame - appstream's converter used bullets. 22 | html_converter.ul_item_mark = "•" 23 | html_converter.wrap_list_items = True 24 | html_converter.ignore_emphasis = True 25 | html_converter.pad_tables = True 26 | 27 | # Used as a decorator to time functions 28 | def print_timing(func): 29 | if not DEBUG_MODE: 30 | return func 31 | else: 32 | def wrapper(*arg): 33 | t1 = time.time() 34 | res = func(*arg) 35 | t2 = time.time() 36 | print('mint-common (DEBUG): %s took %0.3f ms' % (func.__qualname__, (t2 - t1) * 1000.0), flush=True, file=sys.stderr) 37 | return res 38 | return wrapper 39 | 40 | def check_ml(): 41 | if not DEBUG_MODE: 42 | return 43 | 44 | if threading.current_thread().name is not None: 45 | tid = threading.current_thread().name 46 | else: 47 | tid = str(threading.get_ident()) 48 | fid = inspect.stack()[1][3] 49 | on_ml = threading.current_thread() == threading.main_thread() 50 | print("%s in thread: %s" % (fid, tid), flush=True, file=sys.stderr) 51 | 52 | def debug(*args): 53 | if not DEBUG_MODE: 54 | return 55 | sanitized = [str(arg) for arg in args if arg is not None] 56 | argstr = " ".join(sanitized) 57 | print("mint-common (DEBUG): %s" % argstr, file=sys.stderr, flush=True) 58 | 59 | def debug_query(*args): 60 | if not DEBUG_QUERIES: 61 | return 62 | sanitized = [str(arg) for arg in args if arg is not None] 63 | argstr = " ".join(sanitized) 64 | print("mint-common (DEBUG): %s" % argstr, file=sys.stderr, flush=True) 65 | 66 | def warn(*args): 67 | sanitized = [str(arg) for arg in args if arg is not None] 68 | argstr = " ".join(sanitized) 69 | print("mint-common (WARN): %s" % argstr, file=sys.stderr, flush=True) 70 | 71 | def xml_markup_convert_to_text(markup): 72 | if markup is None: 73 | return "" 74 | try: 75 | return html_converter.handle(markup) 76 | except Exception as e: 77 | warn("Could not convert description to text: %s" % str(e)) 78 | return markup 79 | -------------------------------------------------------------------------------- /usr/lib/python3/dist-packages/mintcommon/installer/pkgInfo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info.major < 3: 3 | raise "python3 required" 4 | import os 5 | 6 | import gi 7 | gi.require_version("Gtk", "3.0") 8 | from gi.repository import Gtk 9 | 10 | from .misc import warn, xml_markup_convert_to_text 11 | 12 | # this should hopefully be supplied by remote info someday. 13 | FLATHUB_MEDIA_BASE_URL = "https://dl.flathub.org/media/" 14 | 15 | def capitalize(string): 16 | if string and len(string) > 1: 17 | return (string[0].upper() + string[1:]) 18 | else: 19 | return (string) 20 | 21 | class PkgInfo: 22 | __slots__ = ( 23 | "name", 24 | "pkg_hash", 25 | "refid", 26 | "remote", 27 | "kind", 28 | "arch", 29 | "branch", 30 | "commit", 31 | "remote_url", 32 | "display_name", 33 | "summary", 34 | "description", 35 | "version", 36 | "icon", 37 | "screenshots", 38 | "homepage_url", 39 | "help_url", 40 | "categories", 41 | "installed", 42 | "verified", 43 | "developer", 44 | "keywords" 45 | ) 46 | 47 | def __init__(self, pkg_hash=None): 48 | # Saved stuff 49 | self.pkg_hash = None 50 | if pkg_hash: 51 | self.pkg_hash = pkg_hash 52 | 53 | self.name = None 54 | # some flatpak-specific things 55 | self.refid = "" 56 | self.remote = "" 57 | self.kind = 0 58 | self.arch = "" 59 | self.branch = "" 60 | self.commit = "" 61 | self.remote_url = "" 62 | 63 | # Display info fetched by methods always 64 | self.display_name = None 65 | self.summary = None 66 | self.description = None 67 | self.developer = None 68 | self.version = None 69 | self.icon = {} 70 | self.screenshots = [] 71 | self.homepage_url = None 72 | self.help_url = None 73 | self.keywords = None 74 | 75 | # Runtime categories 76 | self.categories = [] 77 | 78 | class AptPkgInfo(PkgInfo): 79 | def __init__(self, pkg_hash=None, apt_pkg=None): 80 | super(AptPkgInfo, self).__init__(pkg_hash) 81 | 82 | # This is cheap.. but keeps from having an additional fp/apt check every time we check it. 83 | self.verified = True 84 | 85 | if apt_pkg: 86 | self.name = apt_pkg.name 87 | self.display_name = self.get_display_name(apt_pkg) 88 | self.summary = self.get_summary(apt_pkg) 89 | self.get_icon(apt_pkg, 48) 90 | self.get_icon(apt_pkg, 64) 91 | 92 | @classmethod 93 | def from_json(cls, json_data:dict): 94 | inst = cls() 95 | inst.pkg_hash = json_data["pkg_hash"] 96 | inst.name = json_data["name"] 97 | inst.display_name = json_data["display_name"] 98 | inst.summary = json_data["summary"] 99 | 100 | try: 101 | cached = json_data["icon"] 102 | 103 | while True: 104 | size, icon = cached.popitem() 105 | inst.icon[int(size)] = icon 106 | except Exception as e: 107 | pass 108 | 109 | return inst 110 | 111 | def to_json(self): 112 | trimmed_dict = { 113 | key: getattr(self, key, None) 114 | for key in ("pkg_hash", 115 | "name", 116 | "display_name", 117 | "summary", 118 | "icon") 119 | } 120 | 121 | return trimmed_dict 122 | 123 | def get_display_name(self, apt_pkg=None): 124 | # fastest 125 | if self.display_name: 126 | return self.display_name 127 | 128 | if apt_pkg: 129 | self.display_name = apt_pkg.name.capitalize() 130 | 131 | if not self.display_name: 132 | self.display_name = self.name.capitalize() 133 | 134 | self.display_name = self.display_name.replace(":i386", "") 135 | 136 | return self.display_name 137 | 138 | def get_summary(self, apt_pkg=None): 139 | # fastest 140 | if self.summary: 141 | return self.summary 142 | 143 | if apt_pkg and apt_pkg.candidate: 144 | candidate = apt_pkg.candidate 145 | 146 | summary = "" 147 | if candidate.summary is not None: 148 | summary = candidate.summary 149 | 150 | self.summary = capitalize(summary) 151 | 152 | if self.summary is None: 153 | self.summary = "" 154 | 155 | return self.summary 156 | 157 | def get_description(self, apt_pkg=None): 158 | # fastest 159 | if self.description: 160 | return self.description 161 | 162 | if apt_pkg and apt_pkg.candidate: 163 | candidate = apt_pkg.candidate 164 | 165 | description = "" 166 | if candidate.description is not None: 167 | description = candidate.description 168 | description = description.replace("

", "").replace("

", "\n") 169 | for tags in ["
    ", "
", "
  • ", "
  • "]: 170 | description = description.replace(tags, "") 171 | 172 | self.description = capitalize(description) 173 | 174 | if self.description is None: 175 | self.description = "" 176 | 177 | return self.description 178 | 179 | def get_keywords(self): 180 | return "" 181 | 182 | def get_icon(self, apt_pkg=None, size=64): 183 | try: 184 | return self.icon[size] 185 | except: 186 | pass 187 | 188 | theme = Gtk.IconTheme.get_default() 189 | 190 | for name in [self.name, self.name.split(":")[0], self.name.split("-")[0], self.name.split(".")[-1].lower()]: 191 | if theme.has_icon(name): 192 | self.icon[size] = name 193 | return self.icon[size] 194 | 195 | # Look in app-install-data and pixmaps 196 | for extension in ['svg', 'png', 'xpm']: 197 | for suffix in ['', '-icon']: 198 | icon_path = "/usr/share/app-install/icons/%s%s.%s" % (self.name, suffix, extension) 199 | if os.path.exists(icon_path): 200 | self.icon[size] = icon_path 201 | return self.icon[size] 202 | 203 | icon_path = "/usr/share/pixmaps/%s.%s" % (self.name, extension) 204 | if os.path.exists(icon_path): 205 | self.icon[size] = icon_path 206 | return self.icon[size] 207 | 208 | return None 209 | 210 | def get_screenshots(self, apt_pkg=None): 211 | return [] # handled in mintinstall for now 212 | 213 | def get_version(self, apt_pkg=None): 214 | if self.version: 215 | return self.version 216 | 217 | if apt_pkg: 218 | if apt_pkg.is_installed: 219 | self.version = apt_pkg.installed.version 220 | else: 221 | if apt_pkg.candidate is not None: 222 | self.version = apt_pkg.candidate.version 223 | 224 | if self.version is None: 225 | self.version = "" 226 | 227 | return self.version 228 | 229 | def get_homepage_url(self, apt_pkg=None): 230 | if self.homepage_url: 231 | return self.homepage_url 232 | 233 | if apt_pkg: 234 | if apt_pkg.is_installed: 235 | self.homepage_url = apt_pkg.installed.homepage 236 | else: 237 | if apt_pkg.candidate is not None: 238 | self.homepage_url = apt_pkg.candidate.homepage 239 | 240 | if self.homepage_url is None: 241 | self.homepage_url = "" 242 | 243 | return self.homepage_url 244 | 245 | def get_help_url(self, apt_pkg=None): 246 | # We can only get the homepage from apt 247 | return "" 248 | 249 | class FlatpakPkgInfo(PkgInfo): 250 | def __init__(self, pkg_hash=None, remote=None, ref=None, remote_url=None, installed=False): 251 | super(FlatpakPkgInfo, self).__init__(pkg_hash) 252 | 253 | if not pkg_hash: 254 | return 255 | 256 | self.name = ref.get_name() # org.foo.Bar 257 | self.remote = remote # "flathub" 258 | self.remote_url = remote_url 259 | 260 | self.installed = installed 261 | 262 | self.refid = ref.format_ref() # app/org.foo.Bar/x86_64/stable 263 | self.kind = ref.get_kind() # Will be app for now 264 | self.arch = ref.get_arch() 265 | self.branch = ref.get_branch() 266 | self.commit = ref.get_commit() 267 | self.verified = False 268 | 269 | @classmethod 270 | def from_json(cls, json_data:dict): 271 | inst = cls() 272 | inst.pkg_hash = json_data["pkg_hash"] 273 | inst.name = json_data["name"] 274 | inst.refid = json_data["refid"] 275 | inst.remote = json_data["remote"] 276 | inst.kind = json_data["kind"] 277 | inst.arch = json_data["arch"] 278 | inst.branch = json_data["branch"] 279 | inst.commit = json_data["commit"] 280 | inst.remote_url = json_data["remote_url"] 281 | inst.verified = json_data["verified"] 282 | inst.display_name = json_data["display_name"] 283 | inst.summary = json_data["summary"] 284 | inst.icon = json_data["icon"] 285 | inst.keywords = json_data["keywords"] 286 | return inst 287 | 288 | def to_json(self): 289 | trimmed_dict = { 290 | key: getattr(self, key, None) 291 | for key in ( 292 | "pkg_hash", 293 | "name", 294 | "refid", 295 | "remote", 296 | "kind", 297 | "arch", 298 | "branch", 299 | "commit", 300 | "remote_url", 301 | "verified", 302 | "display_name", 303 | "summary", 304 | "icon", 305 | "keywords" 306 | ) 307 | } 308 | 309 | return trimmed_dict 310 | 311 | def add_cached_appstream_data(self, as_pkg): 312 | if as_pkg: 313 | self.display_name = as_pkg.get_display_name() 314 | 315 | summary = as_pkg.get_summary() 316 | if summary is None: 317 | summary = "" 318 | 319 | self.summary = summary 320 | self.icon["48"] = as_pkg.get_icon(48) 321 | self.verified = as_pkg.get_verified() 322 | 323 | try: 324 | self.keywords = ",".join(as_pkg.get_keywords()) 325 | except TypeError: 326 | self.keywords = "" 327 | else: 328 | self.display_name = self.name 329 | self.summary = "" 330 | self.icon = {} 331 | self.verified = False 332 | self.keywords = "" 333 | 334 | def get_display_name(self): 335 | return self.display_name 336 | 337 | def get_summary(self): 338 | return self.summary 339 | 340 | def get_description(self, as_pkg=None): 341 | if self.description: 342 | return self.description 343 | 344 | if as_pkg: 345 | description = as_pkg.get_description() 346 | if description is not None: 347 | self.description = xml_markup_convert_to_text(description) 348 | 349 | if self.description is None: 350 | return "" 351 | 352 | return self.description 353 | 354 | def get_keywords(self): 355 | return self.keywords 356 | 357 | def get_icon(self, size=64, as_pkg=None): 358 | try: 359 | return self.icon[str(size)] 360 | except KeyError: 361 | pass 362 | 363 | if as_pkg: 364 | icon = as_pkg.get_icon(size) 365 | if icon: 366 | self.icon[str(size)] = icon 367 | return icon 368 | 369 | return None 370 | 371 | def get_screenshots(self, as_pkg=None): 372 | if len(self.screenshots) > 0: 373 | return self.screenshots 374 | 375 | if as_pkg: 376 | self.screenshots = as_pkg.get_screenshots() 377 | 378 | return self.screenshots 379 | 380 | def get_version(self, as_pkg=None): 381 | if self.version: 382 | return self.version 383 | 384 | if as_pkg: 385 | version = as_pkg.get_version() 386 | if version: 387 | self.version = version 388 | 389 | if self.version is None: 390 | return "" 391 | 392 | return self.version 393 | 394 | def get_developer(self, as_pkg=None): 395 | if self.developer: 396 | return self.developer 397 | 398 | if as_pkg: 399 | self.developer = as_pkg.get_developer() 400 | 401 | if self.developer is None: 402 | return "" 403 | 404 | return self.developer 405 | 406 | def get_homepage_url(self, as_pkg=None): 407 | if self.homepage_url: 408 | return self.homepage_url 409 | 410 | if as_pkg: 411 | url = as_pkg.get_homepage_url() 412 | 413 | if url is not None: 414 | self.homepage_url = url 415 | 416 | if self.homepage_url is None: 417 | return "" 418 | 419 | return self.homepage_url 420 | 421 | def get_help_url(self, as_pkg=None): 422 | if self.help_url: 423 | return self.help_url 424 | 425 | if as_pkg: 426 | url = as_pkg.get_help_url() 427 | 428 | if url is not None: 429 | self.help_url = url 430 | 431 | if self.help_url is None: 432 | return "" 433 | 434 | return self.help_url 435 | 436 | -------------------------------------------------------------------------------- /usr/share/linuxmint/icons/flatpak-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 48 | 50 | 51 | 53 | image/svg+xml 54 | 56 | 58 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 74 | 75 | 76 | 81 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /usr/share/linuxmint/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/linuxmint/logo.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/0_cars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/0_cars.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/0_chess.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/0_chess.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/0_coffee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/0_coffee.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/0_guitar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/0_guitar.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/2_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/2_10.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/2_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/2_11.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/2_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/2_12.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/2_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/2_13.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/3_lightning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/3_lightning.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/3_mountain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/3_mountain.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/3_sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/3_sky.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/3_sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/3_sunset.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/4_cinnamon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/4_cinnamon.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/4_flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/4_flower.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/4_leaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/4_leaf.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/4_sunflower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/4_sunflower.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/5_fish.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/5_fish.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/5_kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/5_kitten.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/5_penguin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/5_penguin.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/5_puppy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/5_puppy.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/6_astronaut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/6_astronaut.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/6_butterfly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/6_butterfly.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/6_flake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/6_flake.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/6_grapes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/6_grapes.jpg -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_bat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_bat.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_dog.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_elephant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_elephant.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_fox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_fox.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_lion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_lion.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_panda.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_penguin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_penguin.png -------------------------------------------------------------------------------- /usr/share/pixmaps/faces/7_tucan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxmint/mintcommon/cc7779f46bfa132bb4516b73a3e8f1f825265442/usr/share/pixmaps/faces/7_tucan.png -------------------------------------------------------------------------------- /usr/share/polkit-1/actions/com.linuxmint.mintcommon.policy: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Linux Mint 8 | https://linuxmint.com/ 9 | 10 | 11 | system-software-package-manager 12 | 13 | auth_admin_keep 14 | auth_admin_keep 15 | auth_admin_keep 16 | 17 | /usr/lib/linuxmint/common/mint-remove-application.py 18 | true 19 | 20 | 21 | --------------------------------------------------------------------------------