Last active
July 9, 2021 05:00
-
-
Save awhatson/8666de81fccc9dcf470f3c87e1b8560e to your computer and use it in GitHub Desktop.
Helper script for downgrading MSYS2
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
#!/usr/bin/env python3 | |
from argparse import ArgumentParser | |
from datetime import datetime, timedelta | |
from pathlib import Path | |
import json | |
import re | |
import subprocess | |
import sys | |
import tempfile | |
import urllib.request | |
PACKAGE_CACHE_FILE = Path(tempfile.gettempdir()).joinpath('msys2_downgrade.json') | |
PACKAGE_LIST_URLS = [ | |
'http://repo.msys2.org/msys/x86_64', | |
'http://repo.msys2.org/mingw/i686', | |
'http://repo.msys2.org/mingw/x86_64', | |
] | |
LIST_START_REGEX = re.compile('^.*<pre>') | |
LIST_END_REGEX = re.compile('^.*</pre>') | |
LIST_ENTRY_REGEX = re.compile(r''' | |
^ \s* <a\ href=" (?P<name> [^"]+ ) ">[^<]+</a> | |
\s+ (?P<date> \S+ ) | |
\s+ (?P<time> \S+ ) | |
\s+ (?P<size> \S+ ) \s* $''', re.VERBOSE) | |
PACKAGE_FILE_REGEX = re.compile(r''' | |
^ (?P<package> .+? ) | |
- (?P<version> [^-]+ - \d+ ) | |
- (?P<arch> i686 | x86_64 | any ) \.pkg\.tar\.xz $''', re.VERBOSE) | |
def warn(*args, **kwargs): | |
print(*args, **kwargs, file=sys.stderr) | |
def query_installed_packages(): | |
installed = {} | |
output = subprocess.check_output(['pacman', '-Q']) | |
for line in output.decode().splitlines(): | |
package, version = line.split(maxsplit=1) | |
installed[package] = version | |
return installed | |
def fetch_available_versions(cache, url): | |
entries = [] | |
cached = cache.get(url) | |
if cached: | |
timestamp = datetime.fromisoformat(cached['timestamp']) | |
if timestamp > datetime.now() - timedelta(hours=12): | |
warn('info: using cached data for %s' % url) | |
entries = cached['entries'] | |
if not entries: | |
warn('info: fetching data from %s' % url) | |
with urllib.request.urlopen(url) as fh: | |
in_list = False | |
for line in fh: | |
line = line.decode() | |
if not in_list: | |
if LIST_START_REGEX.match(line): | |
in_list = True | |
continue | |
if LIST_END_REGEX.match(line): | |
break | |
match = LIST_ENTRY_REGEX.match(line) | |
if not match: | |
warn('error: failed to parse line: %r' % line) | |
continue | |
filename = match['name'] | |
if not filename.endswith('.pkg.tar.xz'): | |
continue | |
timestamp = '%s %s' % (match['date'], match['time']) | |
timestamp = datetime.strptime(timestamp, '%d-%b-%Y %H:%M') | |
timestamp = timestamp.isoformat() | |
entries.append([filename, timestamp]) | |
entries.sort(key=lambda entry: entry[1]) | |
cache[url] = { | |
'timestamp': datetime.now().isoformat(), | |
'entries': entries, | |
} | |
available = {} | |
for filename, timestamp in entries: | |
match = PACKAGE_FILE_REGEX.match(filename) | |
if not match: | |
warn('error: failed to parse name: %r' % filename) | |
continue | |
package = match['package'] | |
version = match['version'] | |
download = '%s/%s' % (url, filename) | |
timestamp = datetime.fromisoformat(timestamp) | |
available.setdefault(package, []).append({ | |
'version': version, | |
'download': download, | |
'timestamp': timestamp, | |
}) | |
return available | |
def get_package_info(): | |
cache = {} | |
if PACKAGE_CACHE_FILE.exists(): | |
warn('info: loading cache file %s' % PACKAGE_CACHE_FILE) | |
with PACKAGE_CACHE_FILE.open('r') as fh: | |
cache = json.load(fh) | |
packages = {} | |
installed = query_installed_packages() | |
for package, version in installed.items(): | |
packages[package] = {'installed': version, 'available': []} | |
for url in PACKAGE_LIST_URLS: | |
available = fetch_available_versions(cache, url) | |
for package, info in packages.items(): | |
versions = available.get(package) | |
if versions: | |
info['available'] = versions | |
with PACKAGE_CACHE_FILE.open('w') as fh: | |
json.dump(cache, fh) | |
return packages | |
def print_package_info(package, info): | |
print('===', package) | |
print(' installed:', info['installed']) | |
if not info['available']: | |
print(' available: None') | |
return | |
first = True | |
for available in info['available']: | |
label = ' available:' if first else \ | |
' ' | |
first = False | |
print('%s %s (%s)' % (label, available['version'], available['timestamp'])) | |
def show_package(package): | |
packages = get_package_info() | |
info = packages.get(package) | |
if not info: | |
warn('error: unknown package %r' % package) | |
return | |
print_package_info(package, info) | |
def search_packages(search): | |
packages = get_package_info() | |
matches = {} | |
for package, info in packages.items(): | |
if search in package: | |
matches[package] = info | |
for package, info in sorted(matches.items()): | |
print_package_info(package, info) | |
def print_package_list(packages, save_urls): | |
urls = [] | |
for package, latest in sorted(packages.items()): | |
urls.append(latest['download']) | |
print('%s %s (%s)' % (package, latest['version'], latest['timestamp'])) | |
if save_urls: | |
with save_urls.open('w', newline='\n') as fh: | |
for url in urls: | |
fh.write('%s\n' % url) | |
def list_upgraded_since(timestamp, save_urls): | |
timestamp = datetime.fromisoformat(timestamp) | |
packages = get_package_info() | |
matches = {} | |
for package, info in packages.items(): | |
if not info['available']: | |
warn('warning: no versions available for %r' % package) | |
continue | |
latest = info['available'][-1] | |
if latest['timestamp'] >= timestamp: | |
matches[package] = latest | |
print_package_list(matches, save_urls) | |
def list_downgrade_to(timestamp, save_urls): | |
timestamp = datetime.fromisoformat(timestamp) | |
packages = get_package_info() | |
matches = {} | |
for package, info in packages.items(): | |
if not info['available']: | |
warn('warning: no versions available for %r' % package) | |
continue | |
latest = None | |
for available in reversed(info['available']): | |
if available['timestamp'] < timestamp: | |
latest = available | |
break | |
if not latest: | |
warn('warning: no suitable version for %r' % package) | |
continue | |
if info['installed'] != latest['version']: | |
matches[package] = latest | |
print_package_list(matches, save_urls) | |
def main(): | |
parser = ArgumentParser(description='Helpers for downgrading MSYS2 packages') | |
parser.add_argument('--show', metavar='PACKAGE', help='print info for the specified package') | |
parser.add_argument('--search', metavar='PACKAGE', help='print info for matching packages') | |
parser.add_argument('--upgraded-since', metavar='DATE', help='list packages upgraded since date') | |
parser.add_argument('--downgrade-to', metavar='DATE', help='list packages needed for downgrade to date') | |
parser.add_argument('--save-urls', metavar='FILE', type=Path, help='save URLs for upgrade/downgrade to file') | |
args = parser.parse_args() | |
if args.show: | |
return show_package(args.show) | |
if args.search: | |
return search_packages(args.search) | |
if args.upgraded_since: | |
return list_upgraded_since(args.upgraded_since, args.save_urls) | |
if args.downgrade_to: | |
return list_downgrade_to(args.downgrade_to, args.save_urls) | |
parser.print_usage() | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
hi @awhatson
I recently needed to "downgrade" (today). And the script no longer works.
Today 10/23 I updated and everything quit working so I needed to downgrade to 2020-10-22 or earlier. When I run the script, I get "warning: no versions available for ''" even though I know there should be something.
The --search-url always returns "available: None" when I put in a package I know updated today like 'xhash'.
I think there might be a minor bug in the script (due to python updates) but I do not know where it might be because I absolutely have never worked with python. Any insight you have would be very worthwhile.
Best Regards,
Dave