generateMojang.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. import copy
  2. import hashlib
  3. import os
  4. from collections import defaultdict, namedtuple
  5. from operator import attrgetter
  6. from pprint import pprint
  7. from packaging import version as pversion
  8. from typing import Optional, List
  9. from meta.common import ensure_component_dir, polymc_path, upstream_path, static_path
  10. from meta.common.mojang import VERSION_MANIFEST_FILE, MINECRAFT_COMPONENT, LWJGL3_COMPONENT, LWJGL_COMPONENT, \
  11. STATIC_OVERRIDES_FILE, VERSIONS_DIR, LIBRARY_PATCHES_FILE
  12. from meta.model import MetaVersion, Library, GradleSpecifier, MojangLibraryDownloads, MojangArtifact, Dependency, \
  13. MetaPackage, MojangRules
  14. from meta.model.mojang import MojangIndexWrap, MojangIndex, MojangVersion, LegacyOverrideIndex, LibraryPatches
  15. APPLY_SPLIT_NATIVES_WORKAROUND = True
  16. PMC_DIR = polymc_path()
  17. UPSTREAM_DIR = upstream_path()
  18. STATIC_DIR = static_path()
  19. ensure_component_dir(MINECRAFT_COMPONENT)
  20. ensure_component_dir(LWJGL_COMPONENT)
  21. ensure_component_dir(LWJGL3_COMPONENT)
  22. def map_log4j_artifact(version):
  23. x = pversion.parse(version)
  24. if x <= pversion.parse("2.0"):
  25. return "2.0-beta9-fixed", "https://polymc.github.io/files/maven/%s"
  26. if x <= pversion.parse("2.17.1"):
  27. return "2.17.1", "https://repo1.maven.org/maven2/%s" # This is the only version that's patched (as of 2022/02/19)
  28. return None, None
  29. LOG4J_HASHES = {
  30. "2.0-beta9-fixed": {
  31. "log4j-api": {
  32. "sha1": "b61eaf2e64d8b0277e188262a8b771bbfa1502b3",
  33. "size": 107347
  34. },
  35. "log4j-core": {
  36. "sha1": "677991ea2d7426f76309a73739cecf609679492c",
  37. "size": 677588
  38. }
  39. },
  40. "2.17.1": {
  41. "log4j-api": {
  42. "sha1": "d771af8e336e372fb5399c99edabe0919aeaf5b2",
  43. "size": 301872
  44. },
  45. "log4j-core": {
  46. "sha1": "779f60f3844dadc3ef597976fcb1e5127b1f343d",
  47. "size": 1790452
  48. },
  49. "log4j-slf4j18-impl": {
  50. "sha1": "ca499d751f4ddd8afb016ef698c30be0da1d09f7",
  51. "size": 21268
  52. }
  53. }
  54. }
  55. # We want versions that contain natives for all platforms. If there are multiple, pick the latest one
  56. # LWJGL versions we want
  57. PASS_VARIANTS = [
  58. "6f32ef730d05562ede7db0b845b72ea16dd239d5", # 3.3.3 (2024-05-29 12:04:43+00:00)
  59. "765b4ab443051d286bdbb1c19cd7dc86b0792dce", # 3.3.2 (2024-01-17 13:19:20+00:00)
  60. "54c4fb1d6a96ac3007c947bf310c8bcf94a862be", # 3.3.1 (2023-04-20 11:55:19+00:00) split natives, with WoA natives
  61. "ea4973ebc9eadf059f30f0958c89f330898bff51", # 3.2.2 (2019-07-04 14:41:05+00:00) will be patched, missing tinyfd
  62. "235fc413bc4c76b269c207f7bca6464f1e1f1d80", # 3.2.1 (2019-02-13 16:12:08+00:00)
  63. "deb1a436d806413207350735a00e04b54d113916", # 3.1.6 (2018-10-18 14:46:12+00:00)
  64. "3e47f0f742fb759401754769fa59c508fd8fda75", # 3.1.2 (2018-06-21 12:57:11+00:00)
  65. "a3f254df5a63a0a1635755733022029e8cfae1b3", # 2.9.4-nightly-20150209 (2016-12-20 14:05:34+00:00)
  66. "879be09c0bd0d4bafc2ea4ea3d2ab8607a0d976c", # 2.9.3 (2015-01-30 11:58:24+00:00)
  67. "8d4951d00253dfaa36a0faf1c8be541431861c30", # 2.9.1 (2014-05-22 14:44:33+00:00)
  68. "cf58c9f92fed06cb041a7244c6b4b667e6d544cc", # 2.9.1-nightly-20131120 (2013-12-06 13:55:34+00:00)
  69. "27dcadcba29a1a7127880ca1a77efa9ece866f24", # 2.9.0 (2013-09-06 12:31:58+00:00)
  70. ]
  71. # LWJGL versions we def. don't want!
  72. BAD_VARIANTS = [
  73. "73974b3af2afeb5b272ffbadcd7963014387c84f", # 3.3.3 (2024-05-15 12:00:35+00:00)
  74. "8a9b08f11271eb4de3b50e5d069949500b2c7bc1", # 3.3.3 (2024-04-16 11:57:30+00:00)
  75. "79bde9e46e9ad9accebda11e8293ed08d80dbdc3", # 3.3.2 (2023-09-05 12:06:20+00:00)
  76. "8836c419f90f69a278b97d945a34af165c24ff60", # 3.3.1 (2022-05-18 13:51:54+00:00) split natives, with workaround, replaced by 23w26a
  77. "3c624b94c06dbc4abae08fe6156d74abe4a2cca5", # 3.3.1 (2022-05-04 14:41:35+00:00) we already have a nice 3.3.1
  78. "e1106ca765798218323b7a6d7528050260ea9d88", # 3.3.1 (2022-05-04 14:41:35+00:00) doesn't use split natives
  79. "90b3d9ca01058286c033b6b7ae7f6dc370a04015", # 3.2.2 (2022-03-31 14:53:25+00:00) only linux, windows
  80. "d986df9598fa2bcf4a5baab5edf044548e66d011", # 3.2.2 (2021-12-10 03:36:38+00:00) only linux, windows
  81. "4b73fccb9e5264c2068bdbc26f9651429abbf21a", # 3.2.2 (2021-08-25 14:41:57+00:00) only linux, windows
  82. "090cec3577ecfe438b890b2a9410ea07aa725e16", # 3.2.2 (2021-04-07 14:04:09+00:00) only linux, windows
  83. "ab463e9ebc6a36abf22f2aa27b219dd372ff5069", # 3.2.2 (2019-08-13 07:33:42+00:00) only linux, windows
  84. "51d8ff5a7efc949b4ad2088930e151d6b88ba616", # 3.2.2 (2019-07-19 09:25:47+00:00) only linux, windows
  85. "854649a5bd1455b89117593ae82ff90c8132cacf", # 3.2.1 (2019-04-18 11:05:19+00:00) only osx, windows
  86. "89fcb489261b05f622e8052fe0b588b0cfe49c24", # 3.1.6 (2019-04-18 11:05:19+00:00) only linux
  87. "f04052162b50fa1433f67e1a90bc79466c4ab776", # 2.9.0 (2013-10-21 16:34:47+00:00) only linux, windows
  88. "6442fc475f501fbd0fc4244fd1c38c02d9ebaf7e", # 2.9.0 (2011-03-30 22:00:00+00:00) fine but newer variant available
  89. ]
  90. def add_or_get_bucket(buckets, rules: Optional[MojangRules]) -> MetaVersion:
  91. rule_hash = None
  92. if rules:
  93. rule_hash = hash(rules.json())
  94. if rule_hash in buckets:
  95. bucket = buckets[rule_hash]
  96. else:
  97. bucket = MetaVersion(name="LWJGL", version="undetermined", uid=LWJGL_COMPONENT)
  98. bucket.type = "release"
  99. buckets[rule_hash] = bucket
  100. return bucket
  101. def hash_lwjgl_version(lwjgl: MetaVersion):
  102. lwjgl_copy = copy.deepcopy(lwjgl)
  103. lwjgl_copy.release_time = None
  104. return hashlib.sha1(lwjgl_copy.json().encode("utf-8", "strict")).hexdigest()
  105. def sort_libs_by_name(library):
  106. return library.name
  107. LWJGLEntry = namedtuple('LWJGLEntry', ('version', 'sha1'))
  108. lwjglVersionVariants = defaultdict(list)
  109. def add_lwjgl_version(variants, lwjgl):
  110. lwjgl_copy = copy.deepcopy(lwjgl)
  111. libraries = list(lwjgl_copy.libraries)
  112. libraries.sort(key=sort_libs_by_name)
  113. lwjgl_copy.libraries = libraries
  114. version = lwjgl_copy.version
  115. current_hash = hash_lwjgl_version(lwjgl_copy)
  116. found = False
  117. for variant in variants[version]:
  118. existing_hash = variant.sha1
  119. if current_hash == existing_hash:
  120. found = True
  121. break
  122. if not found:
  123. print("!!! New variant for LWJGL version %s" % version)
  124. variants[version].append(LWJGLEntry(version=lwjgl_copy, sha1=current_hash))
  125. def remove_paths_from_lib(lib):
  126. if lib.downloads.artifact:
  127. lib.downloads.artifact.path = None
  128. if lib.downloads.classifiers:
  129. for key, value in lib.downloads.classifiers.items():
  130. value.path = None
  131. def adapt_new_style_arguments(arguments):
  132. foo = []
  133. # we ignore the jvm arguments entirely.
  134. # grab the strings, log the complex stuff
  135. for arg in arguments.game:
  136. if isinstance(arg, str):
  137. if arg == '--clientId':
  138. continue
  139. if arg == '${clientid}':
  140. continue
  141. if arg == '--xuid':
  142. continue
  143. if arg == '${auth_xuid}':
  144. continue
  145. foo.append(arg)
  146. else:
  147. print("!!! Unrecognized structure in Minecraft game arguments:")
  148. pprint(arg)
  149. return ' '.join(foo)
  150. def is_macos_only(rules: Optional[MojangRules]):
  151. allows_osx = False
  152. allows_all = False
  153. # print("Considering", specifier, "rules", rules)
  154. if rules:
  155. for rule in rules:
  156. if rule.action == "allow" and rule.os and rule.os.name == "osx":
  157. allows_osx = True
  158. if rule.action == "allow" and not rule.os:
  159. allows_all = True
  160. if allows_osx and not allows_all:
  161. return True
  162. return False
  163. def patch_library(lib: Library, patches: LibraryPatches) -> List[Library]:
  164. to_patch = [lib]
  165. new_libraries = []
  166. while to_patch:
  167. target = to_patch.pop(0)
  168. for patch in patches:
  169. if patch.applies(target):
  170. if patch.override:
  171. target.merge(patch.override)
  172. if patch.additionalLibraries:
  173. additional_copy = copy.deepcopy(patch.additionalLibraries)
  174. new_libraries += list(dict.fromkeys(additional_copy))
  175. if patch.patchAdditionalLibraries:
  176. to_patch += additional_copy
  177. return new_libraries
  178. def process_single_variant(lwjgl_variant: MetaVersion, patches: LibraryPatches):
  179. lwjgl_version = lwjgl_variant.version
  180. v = copy.deepcopy(lwjgl_variant)
  181. new_libraries = []
  182. for lib in v.libraries:
  183. new_libraries += patch_library(lib, patches)
  184. v.libraries += list(dict.fromkeys(new_libraries))
  185. if lwjgl_version[0] == '2':
  186. filename = os.path.join(PMC_DIR, LWJGL_COMPONENT, f"{lwjgl_version}.json")
  187. v.name = 'LWJGL 2'
  188. v.uid = LWJGL_COMPONENT
  189. v.conflicts = [Dependency(uid=LWJGL3_COMPONENT)]
  190. elif lwjgl_version[0] == '3':
  191. filename = os.path.join(PMC_DIR, LWJGL3_COMPONENT, f"{lwjgl_version}.json")
  192. v.name = 'LWJGL 3'
  193. v.uid = LWJGL3_COMPONENT
  194. v.conflicts = [Dependency(uid=LWJGL_COMPONENT)]
  195. # remove jutils and jinput from LWJGL 3
  196. # this is a dependency that Mojang kept in, but doesn't belong there anymore
  197. filtered_libraries = list(filter(lambda l: l.name.artifact not in ["jutils", "jinput"], v.libraries))
  198. v.libraries = filtered_libraries
  199. else:
  200. raise Exception("LWJGL version not recognized: %s" % v.version)
  201. v.volatile = True
  202. v.order = -1
  203. good = True
  204. for lib in v.libraries:
  205. # skip libraries without natives or that we patched
  206. if not lib.natives or lib in new_libraries:
  207. continue
  208. checked_dict = {'linux', 'windows', 'osx'}
  209. if not checked_dict.issubset(lib.natives.keys()):
  210. print("Missing system classifier!", v.version, lib.name, lib.natives.keys())
  211. good = False
  212. break
  213. if lib.downloads:
  214. for entry in checked_dict:
  215. baked_entry = lib.natives[entry]
  216. if baked_entry not in lib.downloads.classifiers:
  217. print("Missing download for classifier!", v.version, lib.name, baked_entry,
  218. lib.downloads.classifiers.keys())
  219. good = False
  220. break
  221. if good:
  222. v.write(filename)
  223. else:
  224. print("Skipped LWJGL", v.version)
  225. def lib_is_split_native(lib: Library) -> bool:
  226. if lib.name.classifier and lib.name.classifier.startswith("natives-"):
  227. return True
  228. return False
  229. def version_has_split_natives(v: MojangVersion) -> bool:
  230. for lib in v.libraries:
  231. if lib_is_split_native(lib):
  232. return True
  233. return False
  234. def main():
  235. # get the local version list
  236. override_index = LegacyOverrideIndex.parse_file(os.path.join(STATIC_DIR, STATIC_OVERRIDES_FILE))
  237. library_patches = LibraryPatches.parse_file(os.path.join(STATIC_DIR, LIBRARY_PATCHES_FILE))
  238. found_any_lwjgl3 = False
  239. for filename in os.listdir(os.path.join(UPSTREAM_DIR, VERSIONS_DIR)):
  240. input_file = os.path.join(UPSTREAM_DIR, VERSIONS_DIR, filename)
  241. if not input_file.endswith(".json"):
  242. # skip non JSON files
  243. continue
  244. print("Processing", filename)
  245. mojang_version = MojangVersion.parse_file(input_file)
  246. v = mojang_version.to_meta_version("Minecraft", MINECRAFT_COMPONENT, mojang_version.id)
  247. libs_minecraft = []
  248. new_libs_minecraft = []
  249. is_lwjgl_3 = False
  250. has_split_natives = version_has_split_natives(v)
  251. buckets = {}
  252. for lib in v.libraries:
  253. specifier = lib.name
  254. # generic fixes
  255. remove_paths_from_lib(lib)
  256. if APPLY_SPLIT_NATIVES_WORKAROUND and lib_is_split_native(lib):
  257. # merge classifier into artifact name to workaround bug in launcher
  258. specifier.artifact += f"-{specifier.classifier}"
  259. specifier.classifier = None
  260. if specifier.is_lwjgl():
  261. if has_split_natives: # implies lwjgl3
  262. bucket = add_or_get_bucket(buckets, None)
  263. is_lwjgl_3 = True
  264. found_any_lwjgl3 = True
  265. bucket.version = specifier.version
  266. if not bucket.libraries:
  267. bucket.libraries = []
  268. bucket.libraries.append(lib)
  269. bucket.release_time = v.release_time
  270. else:
  271. rules = None
  272. if lib.rules:
  273. rules = lib.rules
  274. lib.rules = None
  275. if is_macos_only(rules):
  276. print("Candidate library ", specifier, " is only for macOS and is therefore ignored.")
  277. continue
  278. bucket = add_or_get_bucket(buckets, rules)
  279. if specifier.group == "org.lwjgl.lwjgl" and specifier.artifact == "lwjgl":
  280. bucket.version = specifier.version
  281. if specifier.group == "org.lwjgl" and specifier.artifact == "lwjgl":
  282. is_lwjgl_3 = True
  283. found_any_lwjgl3 = True
  284. bucket.version = specifier.version
  285. if not bucket.libraries:
  286. bucket.libraries = []
  287. bucket.libraries.append(lib)
  288. bucket.release_time = v.release_time
  289. # FIXME: workaround for insane log4j nonsense from December 2021. Probably needs adjustment.
  290. elif lib.name.is_log4j():
  291. version_override, maven_override = map_log4j_artifact(lib.name.version)
  292. if version_override and maven_override:
  293. if version_override not in LOG4J_HASHES:
  294. raise Exception("ERROR: unhandled log4j version (overriden) %s!" % version_override)
  295. if lib.name.artifact not in LOG4J_HASHES[version_override]:
  296. raise Exception("ERROR: unhandled log4j artifact %s!" % lib.name.artifact)
  297. replacement_name = GradleSpecifier("org.apache.logging.log4j", lib.name.artifact, version_override)
  298. artifact = MojangArtifact(
  299. url=maven_override % (replacement_name.path()),
  300. sha1=LOG4J_HASHES[version_override][lib.name.artifact]["sha1"],
  301. size=LOG4J_HASHES[version_override][lib.name.artifact]["size"]
  302. )
  303. libs_minecraft.append(Library(
  304. name=replacement_name,
  305. downloads=MojangLibraryDownloads(artifact=artifact)
  306. ))
  307. else:
  308. libs_minecraft.append(lib)
  309. else:
  310. new_libs_minecraft += patch_library(lib, library_patches)
  311. libs_minecraft.append(lib)
  312. if len(buckets) == 1:
  313. for key in buckets:
  314. lwjgl = buckets[key]
  315. lwjgl.libraries = sorted(lwjgl.libraries, key=attrgetter("name"))
  316. add_lwjgl_version(lwjglVersionVariants, lwjgl)
  317. print("Found only candidate LWJGL", lwjgl.version, key)
  318. else:
  319. # multiple buckets for LWJGL. [None] is common to all, other keys are for different sets of rules
  320. for key in buckets:
  321. if key is None:
  322. continue
  323. lwjgl = buckets[key]
  324. if None in buckets:
  325. lwjgl.libraries = sorted(lwjgl.libraries + buckets[None].libraries, key=attrgetter("name"))
  326. else:
  327. lwjgl.libraries = sorted(lwjgl.libraries, key=attrgetter('name'))
  328. add_lwjgl_version(lwjglVersionVariants, lwjgl)
  329. print("Found candidate LWJGL", lwjgl.version, key)
  330. # remove the common bucket...
  331. if None in buckets:
  332. del buckets[None]
  333. v.libraries = libs_minecraft + list(dict.fromkeys(new_libs_minecraft))
  334. if is_lwjgl_3:
  335. lwjgl_dependency = Dependency(uid=LWJGL3_COMPONENT)
  336. else:
  337. lwjgl_dependency = Dependency(uid=LWJGL_COMPONENT)
  338. if len(buckets) == 1:
  339. suggested_version = next(iter(buckets.values())).version
  340. if is_lwjgl_3:
  341. lwjgl_dependency.suggests = suggested_version
  342. else:
  343. lwjgl_dependency.suggests = '2.9.4-nightly-20150209'
  344. else:
  345. bad_versions = {'3.1.6', '3.2.1'}
  346. our_versions = set()
  347. for lwjgl in iter(buckets.values()):
  348. our_versions = our_versions.union({lwjgl.version})
  349. if our_versions == bad_versions:
  350. print("Found broken 3.1.6/3.2.1 combo, forcing LWJGL to 3.2.1")
  351. suggested_version = '3.2.1'
  352. lwjgl_dependency.suggests = suggested_version
  353. else:
  354. raise Exception("ERROR: cannot determine single suggested LWJGL version in %s" % mojang_version.id)
  355. # if it uses LWJGL 3, add the trait that enables starting on first thread on macOS
  356. if is_lwjgl_3:
  357. if not v.additional_traits:
  358. v.additional_traits = []
  359. v.additional_traits.append("FirstThreadOnMacOS")
  360. v.requires = [lwjgl_dependency]
  361. v.order = -2
  362. # process 1.13 arguments into previous version
  363. if not mojang_version.minecraft_arguments and mojang_version.arguments:
  364. v.minecraft_arguments = adapt_new_style_arguments(mojang_version.arguments)
  365. out_filename = os.path.join(PMC_DIR, MINECRAFT_COMPONENT, f"{v.version}.json")
  366. if v.version in override_index.versions:
  367. override = override_index.versions[v.version]
  368. override.apply_onto_meta_version(v)
  369. v.write(out_filename)
  370. for lwjglVersionVariant in lwjglVersionVariants:
  371. decided_variant = None
  372. passed_variants = 0
  373. unknown_variants = 0
  374. print("%d variant(s) for LWJGL %s:" % (len(lwjglVersionVariants[lwjglVersionVariant]), lwjglVersionVariant))
  375. for variant in lwjglVersionVariants[lwjglVersionVariant]:
  376. if variant.sha1 in BAD_VARIANTS:
  377. print("Variant %s ignored because it's marked as bad." % variant.sha1)
  378. continue
  379. if variant.sha1 in PASS_VARIANTS:
  380. print("Variant %s accepted." % variant.sha1)
  381. decided_variant = variant
  382. passed_variants += 1
  383. continue
  384. # print natives classifiers to decide which variant to use
  385. n = [x.natives.keys() for x in variant.version.libraries if x.natives is not None]
  386. print(n)
  387. print(f" \"{variant.sha1}\", # {lwjglVersionVariant} ({variant.version.release_time})")
  388. unknown_variants += 1
  389. print("")
  390. if decided_variant and passed_variants == 1 and unknown_variants == 0:
  391. process_single_variant(decided_variant.version, library_patches)
  392. else:
  393. raise Exception("No variant decided for version %s out of %d possible ones and %d unknown ones." % (
  394. lwjglVersionVariant, passed_variants, unknown_variants))
  395. lwjgl_package = MetaPackage(uid=LWJGL_COMPONENT, name='LWJGL 2')
  396. lwjgl_package.recommended = ['2.9.4-nightly-20150209']
  397. lwjgl_package.write(os.path.join(PMC_DIR, LWJGL_COMPONENT, "package.json"))
  398. if found_any_lwjgl3:
  399. lwjgl_package = MetaPackage(uid=LWJGL3_COMPONENT, name='LWJGL 3')
  400. lwjgl_package.recommended = ['3.1.2']
  401. lwjgl_package.write(os.path.join(PMC_DIR, LWJGL3_COMPONENT, "package.json"))
  402. mojang_index = MojangIndexWrap(MojangIndex.parse_file(os.path.join(UPSTREAM_DIR, VERSION_MANIFEST_FILE)))
  403. minecraft_package = MetaPackage(uid=MINECRAFT_COMPONENT, name='Minecraft')
  404. minecraft_package.recommended = [mojang_index.latest.release]
  405. minecraft_package.write(os.path.join(PMC_DIR, MINECRAFT_COMPONENT, "package.json"))
  406. if __name__ == '__main__':
  407. main()