1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
#!/usr/bin/env python3
'Adds the latest miniforge and mambaforge releases.'
from pathlib import Path
import logging
import os
import string
import requests
logger = logging.getLogger(__name__)
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO'))
MINIFORGE_REPO = 'conda-forge/miniforge'
DISTRIBUTIONS = ['miniforge']
DISTRIBUTIONS_PRE25 = ['miniforge', 'mambaforge']
SKIPPED_RELEASES = [
'4.13.0-0', #has no Mambaforge. We already generated scripts for Miniforge
'22.11.1-0', #MacOS packages are broken (have broken dep tarballs, downloading them fails with 403)
'22.11.1-1', #MacOS packages are broken (have broken dep tarballs, downloading them fails with 403)
'22.11.1-2', #MacOS packages are broken (have broken dep tarballs, downloading them fails with 403)
'25.3.0-0', #marked as prerelease, no Linux version
]
install_script_fmt = """
case "$(anaconda_architecture 2>/dev/null || true)" in
{install_lines}
* )
{{ echo
colorize 1 "ERROR"
echo ": The binary distribution of {flavor} is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
}} >&2
exit 1
;;
esac
""".lstrip()
install_line_fmt = """
"{os}-{arch}" )
install_script "{filename}" "{url}#{sha}" "miniconda" verify_py{py_version}
;;
""".strip()
here = Path(__file__).resolve()
out_dir: Path = here.parent.parent / "share" / "python-build"
def download_sha(url):
logger.info('Downloading SHA file %(url)s', locals())
tup = tuple(reversed(requests.get(url).text.replace('./', '').rstrip().split()))
logger.debug('Got %(tup)s', locals())
return tup
def create_spec(filename, sha, url):
flavor_with_suffix, version, subversion, os, arch = filename.replace('.sh', '').split('-')
suffix = flavor_with_suffix[-1]
if suffix in string.digits:
flavor = flavor_with_suffix[:-1]
else:
flavor = flavor_with_suffix
spec = {
'filename': filename,
'sha': sha,
'url': url,
'py_version': py_version(version),
'flavor': flavor,
'os': os,
'arch': arch,
'installer_filename': f'{flavor_with_suffix.lower()}-{version}-{subversion}',
}
logger.debug('Created spec %(spec)s', locals())
return spec
def version_tuple(version):
return tuple(int(part) for part in version.split('-')[0].split("."))
def py_version(version):
"""Suffix for `verify_pyXXX` to call in the generated build script"""
version_tuple_ = version_tuple(version)
# current version: mentioned under https://github.com/conda-forge/miniforge?tab=readme-ov-file#requirements-and-installers
# transition points:
# https://github.com/conda-forge/miniforge/blame/main/Miniforge3/construct.yaml
# look for "- python <version>" in non-pypy branch and which tag the commit is first in
if version_tuple_ >= (24,5):
# yes, they jumped from 3.10 directly to 3.12
# https://github.com/conda-forge/miniforge/commit/bddad0baf22b37cfe079e47fd1680fdfb2183590
return "312"
if version_tuple_ >= (4,14):
return "310"
raise ValueError("Bundled Python version unknown for release `%s'"%version)
def supported(filename):
return ('pypy' not in filename) and ('Windows' not in filename)
def add_version(release, distributions):
tag_name = release['tag_name']
download_urls = { f['name']: f['browser_download_url'] for f in release['assets'] }
# can assume that sha files are named similar to release files so can also check supported(on their names)
shas = dict([download_sha(url) for (name, url) in download_urls.items() if name.endswith('.sha256') and supported(os.path.basename(name)) and tag_name in name])
specs = [create_spec(filename, sha, download_urls[filename]) for (filename, sha) in shas.items() if supported(filename)]
for distribution in distributions:
distribution_specs = [spec for spec in specs if distribution in spec['flavor'].lower()]
count = len(distribution_specs)
if count > 0:
output_file = out_dir / distribution_specs[0]['installer_filename']
logger.info('Writing %(count)d specs for %(distribution)s to %(output_file)s', locals())
script_str = install_script_fmt.format(
install_lines="\n".join([install_line_fmt.format_map(s) for s in distribution_specs]),
flavor=distribution_specs[0]['flavor'],
)
with open(output_file, 'w') as f:
f.write(script_str)
else:
logger.info('Did not find specs for %(distribution)s', locals())
for release in requests.get(f'https://api.github.com/repos/{MINIFORGE_REPO}/releases').json():
version = release['tag_name']
if version in SKIPPED_RELEASES:
continue
logger.info('Looking for %(version)s in %(out_dir)s', locals())
# mambaforge is retired https://github.com/conda-forge/miniforge/releases/tag/24.11.2-0
if version_tuple(version) >= (24,11,2):
distributions = DISTRIBUTIONS
else:
distributions = DISTRIBUTIONS_PRE25
if any(not list(out_dir.glob(f'{distribution}*-{version}')) for distribution in distributions):
logger.info('Downloading %(version)s', locals())
add_version(release, distributions)
|