Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Helper script for downgrading MSYS2
#!/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')
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 > - 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
if LIST_END_REGEX.match(line):
match = LIST_ENTRY_REGEX.match(line)
if not match:
warn('error: failed to parse line: %r' % line)
filename = match['name']
if not filename.endswith('.pkg.tar.xz'):
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] = {
'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)
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 = {}
warn('info: loading cache file %s' % PACKAGE_CACHE_FILE)
with'r') as fh:
cache = json.load(fh)
packages = {}
installed = query_installed_packages()
for package, version in installed.items():
packages[package] = {'installed': version, 'available': []}
available = fetch_available_versions(cache, url)
for package, info in packages.items():
versions = available.get(package)
if versions:
info['available'] = versions
with'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')
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)
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()):
print('%s %s (%s)' % (package, latest['version'], latest['timestamp']))
if save_urls:
with'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)
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)
latest = None
for available in reversed(info['available']):
if available['timestamp'] < timestamp:
latest = available
if not latest:
warn('warning: no suitable version for %r' % package)
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()
return show_package(
return search_packages(
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.