1#!/usr/bin/python3 -B 2# Copyright 2023 The Bazel Authors. All rights reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""A small script to update bazel files within the repo. 17 18We are not running this with 'bazel run' to keep the dependencies minimal 19""" 20 21# NOTE @aignas 2023-01-09: We should only depend on core Python 3 packages. 22import argparse 23import difflib 24import json 25import os 26import pathlib 27import sys 28import textwrap 29from collections import defaultdict 30from dataclasses import dataclass 31from typing import Any 32from urllib import request 33 34from tools.private.update_deps.args import path_from_runfiles 35from tools.private.update_deps.update_file import update_file 36 37# This should be kept in sync with //python:versions.bzl 38_supported_platforms = { 39 # Windows is unsupported right now 40 # "win_amd64": "x86_64-pc-windows-msvc", 41 "manylinux2014_x86_64": "x86_64-unknown-linux-gnu", 42 "manylinux2014_aarch64": "aarch64-unknown-linux-gnu", 43 "macosx_11_0_arm64": "aarch64-apple-darwin", 44 "macosx_10_9_x86_64": "x86_64-apple-darwin", 45} 46 47 48@dataclass 49class Dep: 50 name: str 51 platform: str 52 python: str 53 url: str 54 sha256: str 55 56 @property 57 def repo_name(self): 58 return f"pypi__{self.name}_{self.python}_{self.platform}" 59 60 def __repr__(self): 61 return "\n".join( 62 [ 63 "(", 64 f' "{self.url}",', 65 f' "{self.sha256}",', 66 ")", 67 ] 68 ) 69 70 71@dataclass 72class Deps: 73 deps: list[Dep] 74 75 def __repr__(self): 76 deps = defaultdict(dict) 77 for d in self.deps: 78 deps[d.python][d.platform] = d 79 80 parts = [] 81 for python, contents in deps.items(): 82 inner = textwrap.indent( 83 "\n".join([f'"{platform}": {d},' for platform, d in contents.items()]), 84 prefix=" ", 85 ) 86 parts.append('"{}": {{\n{}\n}},'.format(python, inner)) 87 return "{{\n{}\n}}".format(textwrap.indent("\n".join(parts), prefix=" ")) 88 89 90def _get_platforms(filename: str, name: str, version: str, python_version: str): 91 return filename[ 92 len(f"{name}-{version}-{python_version}-{python_version}-") : -len(".whl") 93 ].split(".") 94 95 96def _map( 97 name: str, 98 filename: str, 99 python_version: str, 100 url: str, 101 digests: list, 102 platform: str, 103 **kwargs: Any, 104): 105 if platform not in _supported_platforms: 106 return None 107 108 return Dep( 109 name=name, 110 platform=_supported_platforms[platform], 111 python=python_version, 112 url=url, 113 sha256=digests["sha256"], 114 ) 115 116 117def _parse_args() -> argparse.Namespace: 118 parser = argparse.ArgumentParser(__doc__) 119 parser.add_argument( 120 "--name", 121 default="coverage", 122 type=str, 123 help="The name of the package", 124 ) 125 parser.add_argument( 126 "version", 127 type=str, 128 help="The version of the package to download", 129 ) 130 parser.add_argument( 131 "--py", 132 nargs="+", 133 type=str, 134 default=["cp38", "cp39", "cp310", "cp311", "cp312"], 135 help="Supported python versions", 136 ) 137 parser.add_argument( 138 "--dry-run", 139 action="store_true", 140 help="Whether to write to files", 141 ) 142 parser.add_argument( 143 "--update-file", 144 type=path_from_runfiles, 145 default=os.environ.get("UPDATE_FILE"), 146 help="The path for the file to be updated, defaults to the value taken from UPDATE_FILE", 147 ) 148 return parser.parse_args() 149 150 151def main(): 152 args = _parse_args() 153 154 api_url = f"https://pypi.org/pypi/{args.name}/{args.version}/json" 155 req = request.Request(api_url) 156 with request.urlopen(req) as response: 157 data = json.loads(response.read().decode("utf-8")) 158 159 urls = [] 160 for u in data["urls"]: 161 if u["yanked"]: 162 continue 163 164 if not u["filename"].endswith(".whl"): 165 continue 166 167 if u["python_version"] not in args.py: 168 continue 169 170 if f'_{u["python_version"]}m_' in u["filename"]: 171 continue 172 173 platforms = _get_platforms( 174 u["filename"], 175 args.name, 176 args.version, 177 u["python_version"], 178 ) 179 180 result = [_map(name=args.name, platform=p, **u) for p in platforms] 181 urls.extend(filter(None, result)) 182 183 urls.sort(key=lambda x: f"{x.python}_{x.platform}") 184 185 # Update the coverage_deps, which are used to register deps 186 update_file( 187 path=args.update_file, 188 snippet=f"_coverage_deps = {repr(Deps(urls))}\n", 189 start_marker="# START: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps <version>'", 190 end_marker="# END: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps <version>'", 191 dry_run=args.dry_run, 192 ) 193 194 return 195 196 197if __name__ == "__main__": 198 main() 199