Created
May 5, 2023 12:56
-
-
Save OscarL/92d79e4c82ae5690a655bceba214a9d3 to your computer and use it in GitHub Desktop.
Proof of Concept for `pkgman search --not-required`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!python3 | |
""" | |
Proof of concept for `pkgman search --not-required`. | |
The idea is *very* simple. | |
- Get the list of installed packages. | |
- From that, builds a list of all unique requirements. | |
- Print the list of packages that do not provide any of those requirements. | |
This PoC uses assumes a few things, and takes a couple of shortcuts: | |
- Assumes installed packages are the ones directly under /system/packages/ and ~/config/packages | |
(no "activated_packages" or state_* dirs get considered). | |
- Depends on `ls *.hpkg`, and `package list -i <package_name>`. | |
- Version info in requirements/provides is ignored. | |
""" | |
from dataclasses import dataclass | |
from pprint import pprint | |
import hashlib | |
import os | |
import pickle | |
import subprocess | |
import sys | |
HELP_TEXT = """%s {-h,-v} | |
Prints a list of packages that are not dependencies of any other installed package. | |
-h Prints this helps and exits. | |
-v Be more verbose. | |
Note: The first run, or ones after you (un)install packages, will be slow (it takes aprox | |
40 second to process 354 packages on my PC). | |
Subsequent runs take half a second. | |
""" % os.path.basename(__file__) | |
@dataclass | |
class Package: | |
name: str | |
filename: str | |
provides: list | |
requires: list | |
def get_installed_package_files(): | |
sys = subprocess.check_output('ls -1 /boot/system/packages/*.hpkg', shell=True, text=True).splitlines() | |
home = subprocess.check_output('ls -1 /boot/home/config/packages/*.hpkg', shell=True, text=True).splitlines() | |
return sys + home | |
def get_package_info(filename): | |
def drop_version(str): | |
for c in ['>', '<', '=']: | |
if c in str: | |
str = str.split(c)[0] | |
return str | |
lines = subprocess.check_output(['package', 'list', '-i', filename], text=True).splitlines() | |
lines = [l.strip() for l in lines] | |
provides = [] | |
requires = [] | |
name = None | |
for l in lines: | |
if l.startswith('name: '): | |
columns = [c.strip() for c in l.split(' ') if c.strip() != ''] | |
name = columns[1] | |
elif l.startswith('provides: ') or l.startswith('requires: '): | |
columns = [c.strip() for c in l.split(' ') if c.strip() != ''] | |
if l.startswith('provides: '): | |
provides.append(drop_version(columns[1])) | |
elif l.startswith('requires: '): | |
requires.append(drop_version(columns[1])) | |
return Package(name, filename, provides, requires) | |
def update_packages_info(package_files): | |
packages_info = dict() | |
for p in package_files: | |
package_info = get_package_info(p) | |
packages_info[package_info.name] = package_info | |
return packages_info | |
def get_reqs(packages): | |
requirements = set() | |
for p in packages: | |
for req in packages[p].requires: | |
requirements.add(req) | |
# print('Debug: len(requirements) = %s' % len(requirements)) | |
# pprint(requirements) | |
required = set() | |
for p in packages: | |
for prov in packages[p].provides: | |
if prov in requirements: | |
required.add(p) | |
break | |
return required, set(packages) - required | |
def main(verbose): | |
package_files = get_installed_package_files() | |
packages_info = dict() | |
hash = hashlib.sha256() | |
hash.update(''.join(package_files).encode()) | |
info_hash = hash.digest() | |
PACKAGE_INFO_CACHE_FILE="pinfo.pickle" | |
refresh_cache = False | |
try: | |
with open(PACKAGE_INFO_CACHE_FILE, 'rb') as f: | |
info_hash_cached, packages_info = pickle.load(f) | |
if info_hash_cached != info_hash: | |
print('Outdated cache... Will refresh info (slow).', file=sys.stderr) | |
refresh_cache = True | |
except: | |
print('Problems reading the cache file. Will refresh info (slow).', file=sys.stderr) | |
refresh_cache = True | |
if refresh_cache: | |
packages_info = update_packages_info(package_files) | |
with open(PACKAGE_INFO_CACHE_FILE, 'wb') as f: | |
pickle.dump([info_hash, packages_info], f, pickle.HIGHEST_PROTOCOL) | |
# print(packages_info['vim']) | |
pi = dict(packages_info) | |
required, not_required = get_reqs(pi) | |
num_nr = len(not_required) | |
if num_nr: | |
if verbose: | |
print('Packages not required by other installed packages:') | |
for n in sorted(not_required): | |
print(n) | |
if verbose: | |
print('Number of independent packages: %d (of a total of %d).' % (num_nr, len(required) + num_nr)) | |
def help(exit): | |
print(HELP_TEXT) | |
if exit: | |
sys.exit(0) | |
if __name__ == '__main__': | |
if sys.argv[-1].startswith('-h'): | |
help(exit=True) | |
verbose=False | |
if sys.argv[-1].startswith('-v'): | |
verbose=True | |
main(verbose) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment