|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Generate comprehensive flatpak-sources.json by scanning Gradle cache. |
| 4 | +
|
| 5 | +FAST approach: |
| 6 | +1. Scan ~/.gradle/caches/modules-2/files-2.1/ for all cached artifacts |
| 7 | +2. Compute SHA512 from local files (no network needed) |
| 8 | +3. Determine Maven repo URL based on group name heuristics |
| 9 | +4. For POMs not in local cache, download from repos (tries multiple) |
| 10 | +5. Skip files with non-standard names (AAR/klib with internal cache names) |
| 11 | +6. Output flatpak-sources.json |
| 12 | +
|
| 13 | +Files that Gradle stores under internal names (e.g., "animation.aar" instead of |
| 14 | +"animation-android-1.10.0.aar") are SKIPPED — they are Android/iOS platform |
| 15 | +artifacts that are not needed for the Desktop/Flatpak build. |
| 16 | +""" |
| 17 | + |
| 18 | +import hashlib |
| 19 | +import json |
| 20 | +import os |
| 21 | +import sys |
| 22 | +import urllib.request |
| 23 | +import ssl |
| 24 | +from pathlib import Path |
| 25 | +from collections import defaultdict |
| 26 | + |
| 27 | +GRADLE_CACHE = Path(os.path.expanduser("~")) / ".gradle" / "caches" / "modules-2" / "files-2.1" |
| 28 | +OUTPUT_DIR = Path(__file__).parent |
| 29 | +SSL_CTX = ssl.create_default_context() |
| 30 | + |
| 31 | +# All repos |
| 32 | +ALL_REPOS = [ |
| 33 | + "https://repo1.maven.org/maven2", |
| 34 | + "https://dl.google.com/dl/android/maven2", |
| 35 | + "https://maven.pkg.jetbrains.space/public/p/compose/dev", |
| 36 | + "https://plugins.gradle.org/m2", |
| 37 | +] |
| 38 | + |
| 39 | + |
| 40 | +def get_repos_for_group(group): |
| 41 | + """Get ordered list of repos to try for a given group.""" |
| 42 | + g = group.lower() |
| 43 | + if any(g.startswith(p) for p in ["androidx.", "com.android.", "com.google.android.", |
| 44 | + "com.google.firebase", "com.google.gms", "com.google.testing."]): |
| 45 | + return ["https://dl.google.com/dl/android/maven2", "https://repo1.maven.org/maven2", |
| 46 | + "https://maven.pkg.jetbrains.space/public/p/compose/dev"] |
| 47 | + if g.startswith("org.jetbrains.compose"): |
| 48 | + return ["https://maven.pkg.jetbrains.space/public/p/compose/dev", |
| 49 | + "https://repo1.maven.org/maven2", "https://plugins.gradle.org/m2"] |
| 50 | + if g.startswith("org.gradle.") or g.startswith("gradle.plugin."): |
| 51 | + return ["https://plugins.gradle.org/m2", "https://repo1.maven.org/maven2"] |
| 52 | + return ["https://repo1.maven.org/maven2", "https://dl.google.com/dl/android/maven2", |
| 53 | + "https://maven.pkg.jetbrains.space/public/p/compose/dev", "https://plugins.gradle.org/m2"] |
| 54 | + |
| 55 | + |
| 56 | +def sha512_file(filepath): |
| 57 | + h = hashlib.sha512() |
| 58 | + with open(filepath, "rb") as f: |
| 59 | + for chunk in iter(lambda: f.read(65536), b""): |
| 60 | + h.update(chunk) |
| 61 | + return h.hexdigest() |
| 62 | + |
| 63 | + |
| 64 | +def download_and_hash(url): |
| 65 | + """Download file, return SHA512 or None.""" |
| 66 | + try: |
| 67 | + req = urllib.request.Request(url) |
| 68 | + req.add_header("User-Agent", "flatpak-gen/1.0") |
| 69 | + with urllib.request.urlopen(req, timeout=20, context=SSL_CTX) as resp: |
| 70 | + if resp.status == 200: |
| 71 | + data = resp.read() |
| 72 | + if len(data) < 500 and b'<html' in data[:100].lower(): |
| 73 | + return None |
| 74 | + return hashlib.sha512(data).hexdigest() |
| 75 | + except: |
| 76 | + pass |
| 77 | + return None |
| 78 | + |
| 79 | + |
| 80 | +def is_standard_filename(artifact, version, filename): |
| 81 | + """Check if filename follows Maven naming convention (artifact-version.ext).""" |
| 82 | + base = f"{artifact}-{version}" |
| 83 | + # Standard: artifact-version.ext or artifact-version-classifier.ext |
| 84 | + if filename.startswith(base): |
| 85 | + return True |
| 86 | + return False |
| 87 | + |
| 88 | + |
| 89 | +def scan_gradle_cache(): |
| 90 | + artifacts = defaultdict(dict) |
| 91 | + if not GRADLE_CACHE.exists(): |
| 92 | + print(f"ERROR: Gradle cache not found at {GRADLE_CACHE}", file=sys.stderr) |
| 93 | + sys.exit(1) |
| 94 | + for group_dir in GRADLE_CACHE.iterdir(): |
| 95 | + if not group_dir.is_dir(): continue |
| 96 | + for artifact_dir in group_dir.iterdir(): |
| 97 | + if not artifact_dir.is_dir(): continue |
| 98 | + for version_dir in artifact_dir.iterdir(): |
| 99 | + if not version_dir.is_dir(): continue |
| 100 | + for hash_dir in version_dir.iterdir(): |
| 101 | + if not hash_dir.is_dir(): continue |
| 102 | + for f in hash_dir.iterdir(): |
| 103 | + if f.is_file(): |
| 104 | + artifacts[(group_dir.name, artifact_dir.name, version_dir.name)][f.name] = str(f) |
| 105 | + return artifacts |
| 106 | + |
| 107 | + |
| 108 | +def main(): |
| 109 | + print("=" * 60) |
| 110 | + print("Flatpak Sources Generator (fast, skip non-standard names)") |
| 111 | + print("=" * 60) |
| 112 | + |
| 113 | + print("\nScanning Gradle cache...") |
| 114 | + artifacts = scan_gradle_cache() |
| 115 | + print(f"Found {len(artifacts)} unique artifacts") |
| 116 | + |
| 117 | + all_entries = [] |
| 118 | + seen = set() |
| 119 | + stats = {"local": 0, "downloaded": 0, "skipped": 0, "failed_pom": 0} |
| 120 | + total = len(artifacts) |
| 121 | + |
| 122 | + for idx, ((group, artifact, version), files) in enumerate(sorted(artifacts.items())): |
| 123 | + if (idx + 1) % 200 == 0: |
| 124 | + print(f" [{idx+1}/{total}] {stats}") |
| 125 | + |
| 126 | + group_path = group.replace(".", "/") |
| 127 | + base_name = f"{artifact}-{version}" |
| 128 | + repos = get_repos_for_group(group) |
| 129 | + |
| 130 | + needed = set() |
| 131 | + has_jar_or_aar = False |
| 132 | + |
| 133 | + for fname in files: |
| 134 | + if fname.endswith("-sources.jar") or fname.endswith("-javadoc.jar"): |
| 135 | + continue |
| 136 | + if not fname.endswith((".jar", ".pom", ".module", ".klib", ".aar")): |
| 137 | + continue |
| 138 | + # SKIP files with non-standard names (Gradle internal cache names) |
| 139 | + if not is_standard_filename(artifact, version, fname): |
| 140 | + stats["skipped"] += 1 |
| 141 | + continue |
| 142 | + needed.add(fname) |
| 143 | + if fname.endswith((".jar", ".aar")): |
| 144 | + has_jar_or_aar = True |
| 145 | + |
| 146 | + # Ensure POM if we have JAR/AAR |
| 147 | + pom_name = f"{base_name}.pom" |
| 148 | + if has_jar_or_aar and pom_name not in needed: |
| 149 | + needed.add(pom_name) |
| 150 | + |
| 151 | + for fname in sorted(needed): |
| 152 | + key = f"{group_path}/{artifact}/{version}/{fname}" |
| 153 | + if key in seen: continue |
| 154 | + seen.add(key) |
| 155 | + |
| 156 | + local_path = files.get(fname) |
| 157 | + if local_path and os.path.exists(local_path): |
| 158 | + sha = sha512_file(local_path) |
| 159 | + url = f"{repos[0]}/{group_path}/{artifact}/{version}/{fname}" |
| 160 | + all_entries.append({ |
| 161 | + "type": "file", "url": url, "sha512": sha, |
| 162 | + "dest": f"offline-repository/{group_path}/{artifact}/{version}", |
| 163 | + "dest-filename": fname |
| 164 | + }) |
| 165 | + stats["local"] += 1 |
| 166 | + else: |
| 167 | + # Download (missing POMs mostly) |
| 168 | + found = False |
| 169 | + for repo in repos: |
| 170 | + url = f"{repo}/{group_path}/{artifact}/{version}/{fname}" |
| 171 | + sha = download_and_hash(url) |
| 172 | + if sha: |
| 173 | + all_entries.append({ |
| 174 | + "type": "file", "url": url, "sha512": sha, |
| 175 | + "dest": f"offline-repository/{group_path}/{artifact}/{version}", |
| 176 | + "dest-filename": fname |
| 177 | + }) |
| 178 | + stats["downloaded"] += 1 |
| 179 | + found = True |
| 180 | + break |
| 181 | + if not found: |
| 182 | + stats["failed_pom"] += 1 |
| 183 | + |
| 184 | + # Plugin markers |
| 185 | + print("\nAdding plugin marker POMs...") |
| 186 | + mc = add_plugin_markers(artifacts, all_entries, seen) |
| 187 | + |
| 188 | + all_entries.sort(key=lambda e: (e["dest"], e["dest-filename"])) |
| 189 | + |
| 190 | + output = OUTPUT_DIR / "flatpak-sources.json" |
| 191 | + with open(output, "w") as f: |
| 192 | + json.dump(all_entries, f, indent=4) |
| 193 | + |
| 194 | + print(f"\n{'='*60}") |
| 195 | + print(f" Local: {stats['local']}") |
| 196 | + print(f" Downloaded: {stats['downloaded']}") |
| 197 | + print(f" Markers: {mc}") |
| 198 | + print(f" Skipped: {stats['skipped']} (non-standard filenames)") |
| 199 | + print(f" Failed: {stats['failed_pom']}") |
| 200 | + print(f" TOTAL: {len(all_entries)}") |
| 201 | + print(f"\nWritten to {output}") |
| 202 | + |
| 203 | + |
| 204 | +def add_plugin_markers(artifacts, entries, seen): |
| 205 | + count = 0 |
| 206 | + pv = {} |
| 207 | + for (g, a, v), _ in artifacts.items(): |
| 208 | + if g == "org.jetbrains.kotlin" and a == "kotlin-gradle-plugin": pv["kotlin"] = v |
| 209 | + if g == "org.jetbrains.compose" and a == "compose-gradle-plugin": pv["compose"] = v |
| 210 | + if g == "com.google.devtools.ksp" and a == "symbol-processing-gradle-plugin": pv["ksp"] = v |
| 211 | + if g == "com.android.tools.build" and a == "gradle": pv["agp"] = v |
| 212 | + if g == "org.jlleitschuh.gradle" and a == "ktlint-gradle": pv["ktlint"] = v |
| 213 | + if g == "com.codingfeline.buildkonfig" and a == "buildkonfig-gradle-plugin": pv["buildkonfig"] = v |
| 214 | + if g == "androidx.room" and a == "room-gradle-plugin": pv["room"] = v |
| 215 | + if g == "org.gradle.kotlin" and a == "gradle-kotlin-dsl-plugins": pv["kotlin-dsl"] = v |
| 216 | + |
| 217 | + print(f" Versions: {pv}") |
| 218 | + |
| 219 | + markers = [] |
| 220 | + if "kotlin" in pv: |
| 221 | + for pid in ["org.jetbrains.kotlin.multiplatform", "org.jetbrains.kotlin.plugin.serialization", |
| 222 | + "org.jetbrains.kotlin.plugin.compose", "org.jetbrains.kotlin.jvm", "org.jetbrains.kotlin.android"]: |
| 223 | + markers.append((pid, pv["kotlin"])) |
| 224 | + if "compose" in pv: |
| 225 | + markers.append(("org.jetbrains.compose", pv["compose"])) |
| 226 | + if "ksp" in pv: |
| 227 | + markers.append(("com.google.devtools.ksp", pv["ksp"])) |
| 228 | + if "agp" in pv: |
| 229 | + for pid in ["com.android.application", "com.android.library", "com.android.kotlin.multiplatform.library"]: |
| 230 | + markers.append((pid, pv["agp"])) |
| 231 | + if "ktlint" in pv: |
| 232 | + markers.append(("org.jlleitschuh.gradle.ktlint", pv["ktlint"])) |
| 233 | + if "buildkonfig" in pv: |
| 234 | + markers.append(("com.codingfeline.buildkonfig", pv["buildkonfig"])) |
| 235 | + if "room" in pv: |
| 236 | + markers.append(("androidx.room", pv["room"])) |
| 237 | + if "kotlin-dsl" in pv: |
| 238 | + markers.append(("org.gradle.kotlin.kotlin-dsl", pv["kotlin-dsl"])) |
| 239 | + markers.append(("io.github.jwharm.flatpak-gradle-generator", "1.7.0")) |
| 240 | + |
| 241 | + repos = ["https://plugins.gradle.org/m2", "https://dl.google.com/dl/android/maven2", |
| 242 | + "https://maven.pkg.jetbrains.space/public/p/compose/dev", "https://repo1.maven.org/maven2"] |
| 243 | + |
| 244 | + for pid, ver in markers: |
| 245 | + gp = pid.replace(".", "/") |
| 246 | + art = f"{pid}.gradle.plugin" |
| 247 | + fn = f"{art}-{ver}.pom" |
| 248 | + key = f"{gp}/{art}/{ver}/{fn}" |
| 249 | + if key in seen: continue |
| 250 | + seen.add(key) |
| 251 | + for repo in repos: |
| 252 | + url = f"{repo}/{gp}/{art}/{ver}/{fn}" |
| 253 | + sha = download_and_hash(url) |
| 254 | + if sha: |
| 255 | + entries.append({"type": "file", "url": url, "sha512": sha, |
| 256 | + "dest": f"offline-repository/{gp}/{art}/{ver}", "dest-filename": fn}) |
| 257 | + count += 1 |
| 258 | + print(f" + {pid}:{ver}") |
| 259 | + break |
| 260 | + return count |
| 261 | + |
| 262 | + |
| 263 | +if __name__ == "__main__": |
| 264 | + main() |
0 commit comments