|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Download clang-format or clang-tidy wheel for the current platform. |
| 4 | +
|
| 5 | +This script automatically detects your platform and downloads the appropriate |
| 6 | +wheel from the GitHub releases of cpp-linter/clang-tools-wheel. |
| 7 | +""" |
| 8 | + |
| 9 | +import argparse |
| 10 | +import platform |
| 11 | +import subprocess |
| 12 | +import sys |
| 13 | +import urllib.request |
| 14 | +import json |
| 15 | +from pathlib import Path |
| 16 | + |
| 17 | + |
| 18 | +def get_platform_tag(): |
| 19 | + """Detect the current platform and return the appropriate wheel tag.""" |
| 20 | + system = platform.system().lower() |
| 21 | + machine = platform.machine().lower() |
| 22 | + |
| 23 | + # Normalize machine architecture names |
| 24 | + arch_map = { |
| 25 | + "x86_64": "x86_64", |
| 26 | + "amd64": "x86_64", |
| 27 | + "aarch64": "aarch64", |
| 28 | + "arm64": "arm64", |
| 29 | + "armv7l": "armv7l", |
| 30 | + "i386": "i686", |
| 31 | + "i686": "i686", |
| 32 | + "ppc64le": "ppc64le", |
| 33 | + "s390x": "s390x", |
| 34 | + } |
| 35 | + |
| 36 | + arch = arch_map.get(machine, machine) |
| 37 | + |
| 38 | + if system == "darwin": # macOS |
| 39 | + if arch in ["arm64", "aarch64"]: |
| 40 | + return ["macosx_11_0_arm64"] |
| 41 | + else: |
| 42 | + return ["macosx_10_9_x86_64"] |
| 43 | + |
| 44 | + elif system == "linux": |
| 45 | + # Try to detect libc type |
| 46 | + try: |
| 47 | + # Check if we're on musl |
| 48 | + result = subprocess.run( |
| 49 | + ["ldd", "--version"], capture_output=True, text=True, check=False |
| 50 | + ) |
| 51 | + if "musl" in result.stderr.lower(): |
| 52 | + libc = "musllinux" |
| 53 | + else: |
| 54 | + libc = "manylinux" |
| 55 | + except (FileNotFoundError, subprocess.SubprocessError): |
| 56 | + # Default to manylinux if ldd is not available |
| 57 | + libc = "manylinux" |
| 58 | + |
| 59 | + if libc == "manylinux": |
| 60 | + if arch == "x86_64": |
| 61 | + # Try multiple manylinux versions in order of preference |
| 62 | + return [ |
| 63 | + "manylinux_2_27_x86_64.manylinux_2_28_x86_64", |
| 64 | + "manylinux_2_17_x86_64.manylinux2014_x86_64", |
| 65 | + ] |
| 66 | + elif arch == "aarch64": |
| 67 | + return [ |
| 68 | + "manylinux_2_26_aarch64.manylinux_2_28_aarch64", |
| 69 | + "manylinux_2_17_aarch64.manylinux2014_aarch64", |
| 70 | + ] |
| 71 | + elif arch == "i686": |
| 72 | + return [ |
| 73 | + "manylinux_2_26_i686.manylinux_2_28_i686", |
| 74 | + "manylinux_2_17_i686.manylinux2014_i686", |
| 75 | + ] |
| 76 | + elif arch == "ppc64le": |
| 77 | + return ["manylinux_2_26_ppc64le.manylinux_2_28_ppc64le"] |
| 78 | + elif arch == "s390x": |
| 79 | + return ["manylinux_2_26_s390x.manylinux_2_28_s390x"] |
| 80 | + elif arch == "armv7l": |
| 81 | + return ["manylinux_2_31_armv7l"] |
| 82 | + else: # musllinux |
| 83 | + return [f"musllinux_1_2_{arch}"] |
| 84 | + |
| 85 | + elif system == "windows": |
| 86 | + if arch == "amd64" or arch == "x86_64": |
| 87 | + return ["win_amd64"] |
| 88 | + elif arch == "arm64": |
| 89 | + return ["win_arm64"] |
| 90 | + else: |
| 91 | + return ["win32"] |
| 92 | + |
| 93 | + raise ValueError(f"Unsupported platform: {system} {machine}") |
| 94 | + |
| 95 | + |
| 96 | +def get_release_info(repo="cpp-linter/clang-tools-wheel", version=None): |
| 97 | + """Get information about a specific release or latest release from GitHub API.""" |
| 98 | + if version: |
| 99 | + # Remove 'v' prefix if present for the API call |
| 100 | + clean_version = version.lstrip("v") |
| 101 | + url = f"https://api.github.com/repos/{repo}/releases/tags/v{clean_version}" |
| 102 | + else: |
| 103 | + url = f"https://api.github.com/repos/{repo}/releases/latest" |
| 104 | + |
| 105 | + try: |
| 106 | + with urllib.request.urlopen(url) as response: |
| 107 | + return json.loads(response.read().decode()) |
| 108 | + except Exception as e: |
| 109 | + if version: |
| 110 | + raise RuntimeError( |
| 111 | + f"Failed to fetch release info for version {version}: {e}" |
| 112 | + ) |
| 113 | + else: |
| 114 | + raise RuntimeError(f"Failed to fetch latest release info: {e}") |
| 115 | + |
| 116 | + |
| 117 | +def get_latest_release_info(repo="cpp-linter/clang-tools-wheel"): |
| 118 | + """Get information about the latest release from GitHub API.""" |
| 119 | + return get_release_info(repo) |
| 120 | + |
| 121 | + |
| 122 | +def find_wheel_asset(assets, tool, platform_tags, version=None): |
| 123 | + """Find the appropriate wheel asset for the given tool and platform.""" |
| 124 | + # Try both naming conventions: clang-format and clang_format |
| 125 | + tool_underscore = tool.replace("-", "_") |
| 126 | + |
| 127 | + wheel_pattern_hyphen = f"{tool}-" |
| 128 | + wheel_pattern_underscore = f"{tool_underscore}-" |
| 129 | + |
| 130 | + if version: |
| 131 | + wheel_pattern_hyphen += f"{version}-" |
| 132 | + wheel_pattern_underscore += f"{version}-" |
| 133 | + |
| 134 | + wheel_pattern_hyphen += "py2.py3-none-" |
| 135 | + wheel_pattern_underscore += "py2.py3-none-" |
| 136 | + |
| 137 | + # Try each platform tag |
| 138 | + for platform_tag in platform_tags: |
| 139 | + for asset in assets: |
| 140 | + name = asset["name"] |
| 141 | + if ( |
| 142 | + ( |
| 143 | + name.startswith(wheel_pattern_hyphen) |
| 144 | + or name.startswith(wheel_pattern_underscore) |
| 145 | + ) |
| 146 | + and name.endswith(".whl") |
| 147 | + and platform_tag in name |
| 148 | + ): |
| 149 | + return asset |
| 150 | + |
| 151 | + return None |
| 152 | + |
| 153 | + |
| 154 | +def download_file(url, filename): |
| 155 | + """Download a file from URL to the specified filename.""" |
| 156 | + print(f"Downloading {filename}...") |
| 157 | + try: |
| 158 | + urllib.request.urlretrieve(url, filename) |
| 159 | + print(f"Successfully downloaded {filename}") |
| 160 | + return True |
| 161 | + except Exception as e: |
| 162 | + print(f"Failed to download {filename}: {e}") |
| 163 | + return False |
| 164 | + |
| 165 | + |
| 166 | +def main(): |
| 167 | + parser = argparse.ArgumentParser( |
| 168 | + description="Download clang-format or clang-tidy wheel for current platform", |
| 169 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 170 | + epilog=""" |
| 171 | +Examples: |
| 172 | + %(prog)s clang-format # Download latest clang-format |
| 173 | + %(prog)s clang-tidy # Download latest clang-tidy |
| 174 | + %(prog)s clang-format --version 20.1.8 # Download specific version |
| 175 | + %(prog)s clang-format --output ./wheels # Download to specific directory |
| 176 | + """, |
| 177 | + ) |
| 178 | + |
| 179 | + parser.add_argument( |
| 180 | + "tool", |
| 181 | + choices=["clang-format", "clang-tidy"], |
| 182 | + help="Tool to download (clang-format or clang-tidy)", |
| 183 | + ) |
| 184 | + parser.add_argument( |
| 185 | + "--version", "-v", help="Specific version to download (default: latest)" |
| 186 | + ) |
| 187 | + parser.add_argument( |
| 188 | + "--output", |
| 189 | + "-o", |
| 190 | + default=".", |
| 191 | + help="Output directory (default: current directory)", |
| 192 | + ) |
| 193 | + parser.add_argument( |
| 194 | + "--platform", help="Override platform detection (advanced usage)" |
| 195 | + ) |
| 196 | + parser.add_argument( |
| 197 | + "--list-platforms", |
| 198 | + action="store_true", |
| 199 | + help="List all available platforms for the latest release", |
| 200 | + ) |
| 201 | + |
| 202 | + args = parser.parse_args() |
| 203 | + |
| 204 | + try: |
| 205 | + # Get release information |
| 206 | + if args.version: |
| 207 | + print(f"Fetching release information for version {args.version}...") |
| 208 | + else: |
| 209 | + print("Fetching latest release information...") |
| 210 | + |
| 211 | + release_info = get_release_info(version=args.version) |
| 212 | + |
| 213 | + if args.list_platforms: |
| 214 | + print( |
| 215 | + f"Available platforms for {args.tool} in release {release_info['tag_name']}:" |
| 216 | + ) |
| 217 | + # Try both naming conventions: clang-format and clang_format |
| 218 | + tool_underscore = args.tool.replace("-", "_") |
| 219 | + tool_assets = [ |
| 220 | + a |
| 221 | + for a in release_info["assets"] |
| 222 | + if ( |
| 223 | + ( |
| 224 | + a["name"].startswith(f"{args.tool}-") |
| 225 | + or a["name"].startswith(f"{tool_underscore}-") |
| 226 | + ) |
| 227 | + and a["name"].endswith(".whl") |
| 228 | + ) |
| 229 | + ] |
| 230 | + platforms = set() |
| 231 | + for asset in tool_assets: |
| 232 | + name = asset["name"] |
| 233 | + # Extract platform part after py2.py3-none- |
| 234 | + # First remove .whl extension, then split by '-' |
| 235 | + name_without_ext = name.replace(".whl", "") |
| 236 | + parts = name_without_ext.split("-") |
| 237 | + if len(parts) >= 5: |
| 238 | + # Platform part starts after 'py2.py3-none' |
| 239 | + platform_part = "-".join(parts[4:]) |
| 240 | + platforms.add(platform_part) |
| 241 | + |
| 242 | + for platform_tag in sorted(platforms): |
| 243 | + print(f" {platform_tag}") |
| 244 | + return |
| 245 | + |
| 246 | + # Detect platform |
| 247 | + if args.platform: |
| 248 | + platform_tags = [args.platform] |
| 249 | + else: |
| 250 | + try: |
| 251 | + platform_tags = get_platform_tag() |
| 252 | + print(f"Detected platform: {platform_tags[0]}") |
| 253 | + except ValueError as e: |
| 254 | + print(f"Error: {e}") |
| 255 | + print( |
| 256 | + "Use --platform to specify manually, or --list-platforms to see available options" |
| 257 | + ) |
| 258 | + return 1 |
| 259 | + |
| 260 | + # Find the wheel |
| 261 | + # Extract version from release tag for pattern matching |
| 262 | + release_version = release_info["tag_name"].lstrip("v") |
| 263 | + wheel_asset = find_wheel_asset( |
| 264 | + release_info["assets"], args.tool, platform_tags, release_version |
| 265 | + ) |
| 266 | + |
| 267 | + if not wheel_asset: |
| 268 | + print(f"No wheel found for {args.tool} on platform {platform_tags[0]}") |
| 269 | + if args.version: |
| 270 | + print(f"Requested version: {args.version}") |
| 271 | + print(f"Available release: {release_info['tag_name']}") |
| 272 | + print("Use --list-platforms to see all available platforms") |
| 273 | + return 1 |
| 274 | + |
| 275 | + # Create output directory if it doesn't exist |
| 276 | + output_dir = Path(args.output) |
| 277 | + output_dir.mkdir(parents=True, exist_ok=True) |
| 278 | + |
| 279 | + # Download the wheel |
| 280 | + filename = output_dir / wheel_asset["name"] |
| 281 | + if download_file(wheel_asset["browser_download_url"], filename): |
| 282 | + print("\nWheel downloaded successfully!") |
| 283 | + print(f"File: {filename}") |
| 284 | + print(f"Size: {wheel_asset['size'] / (1024 * 1024):.1f} MB") |
| 285 | + print(f"\nTo install: pip install {filename}") |
| 286 | + else: |
| 287 | + return 1 |
| 288 | + |
| 289 | + except Exception as e: |
| 290 | + print(f"Error: {e}") |
| 291 | + return 1 |
| 292 | + |
| 293 | + |
| 294 | +if __name__ == "__main__": |
| 295 | + sys.exit(main()) |
0 commit comments