UNPKG

24.7 kBPlain TextView Raw
1import { Installer } from "@xmcl/installer";
2import { downloadFileWork, got } from "@xmcl/net";
3import Task from "@xmcl/task";
4import Unzip from "@xmcl/unzip";
5import { JavaExecutor, MinecraftFolder, MinecraftLocation, vfs } from "@xmcl/util";
6import { Version } from "@xmcl/version";
7import * as path from "path";
8import { Readable } from "stream";
9
10async function findMainClass(lib: string) {
11 const zip = await Unzip.open(lib, { lazyEntries: true });
12 const [manifest] = await zip.filterEntries(["META-INF/MANIFEST.MF"]);
13 let mainClass: string | undefined;
14 if (manifest) {
15 const content = await zip.readEntry(manifest).then((b) => b.toString());
16 const mainClassPair = content.split("\n").map((l) => l.split(": ")).filter((arr) => arr[0] === "Main-Class")[0];
17 if (mainClassPair) {
18 mainClass = mainClassPair[1].trim();
19 }
20 }
21 zip.close();
22 return mainClass;
23}
24
25/**
26 * The forge installer Module to install forge to the game
27 */
28export namespace ForgeInstaller {
29 /**
30 * The forge version metadata to download a forge
31 */
32 export interface VersionMeta {
33 /**
34 * The installer info
35 */
36 installer: {
37 md5: string;
38 sha1: string;
39 /**
40 * The url path to concat with forge maven
41 */
42 path: string;
43 };
44 universal: {
45 md5: string;
46 sha1: string;
47 /**
48 * The url path to concat with forge maven
49 */
50 path: string;
51 };
52 /**
53 * The minecraft version
54 */
55 mcversion: string;
56 /**
57 * The forge version (without minecraft version)
58 */
59 version: string;
60 }
61
62 export const DEFAULT_FORGE_MAVEN = "http://files.minecraftforge.net";
63
64 export interface InstallProfile {
65 spec: number;
66 profile: string;
67 version: string;
68 json: string;
69 path: string;
70 minecraft: string;
71 data: {
72 [key: string]: {
73 client: string,
74 server: string,
75 },
76 };
77 processors: Array<{
78 jar: string,
79 classpath: string[],
80 args: string[],
81 outputs?: {
82 [key: string]: string,
83 },
84 }>;
85 libraries: Version.NormalLibrary[];
86 }
87
88 /**
89 * The forge diagnosis report. It may have some intersection with `Version.Diagnosis`.
90 */
91 export interface Diagnosis {
92 /**
93 * When this flag is true, please reinstall totally
94 */
95 badInstall: boolean;
96 /**
97 * When only this is not empty
98 */
99 badProcessedFiles: Array<InstallProfile["processors"][number]>;
100 badVersionJson: boolean;
101 /**
102 * When this is not empty, please use `postProcessInstallProfile`
103 */
104 missingInstallDependencies: Version.NormalLibrary[];
105
106 missingBinpatch: boolean;
107
108 /**
109 * Alt for badProcessedFiles
110 */
111 missingSrgJar: boolean;
112 /**
113 * Alt for badProcessedFiles
114 */
115 missingMinecraftExtraJar: boolean;
116 /**
117 * Alt for badProcessedFiles
118 */
119 missingForgePatchesJar: boolean;
120 }
121
122 /**
123 * Diagnose for specific forge version. Majorly for the current installer forge. (mcversion >= 1.13)
124 *
125 * Don't use this with the version less than 1.13
126 * @param versionOrProfile If the version string present, it will try to find the installer profile under version folder. Otherwise it will use presented installer profile to diagnose
127 * @param minecraft The minecraft location.
128 */
129 export async function diagnoseForgeVersion(versionOrProfile: string | InstallProfile, minecraft: MinecraftLocation): Promise<Diagnosis> {
130 const version = typeof versionOrProfile === "string" ? versionOrProfile : versionOrProfile.version;
131 const mc = MinecraftFolder.from(minecraft);
132 const verRoot = mc.getVersionRoot(version);
133 const versionJsonPath = mc.getVersionJson(version);
134
135 const diag: Diagnosis = {
136 badProcessedFiles: [],
137 missingInstallDependencies: [],
138 badVersionJson: false,
139 missingBinpatch: false,
140 badInstall: false,
141 missingSrgJar: false,
142 missingMinecraftExtraJar: false,
143 missingForgePatchesJar: false,
144 };
145
146 let prof: InstallProfile | undefined;
147 if (typeof versionOrProfile === "string") {
148 const installProfPath = path.join(verRoot, "install_profile.json");
149 if (await vfs.exists(installProfPath)) {
150 prof = JSON.parse(await vfs.readFile(installProfPath).then((b) => b.toString()));
151 }
152 } else {
153 prof = versionOrProfile;
154 }
155 if (prof) {
156 const processedProfile = postProcessInstallProfile(mc, prof);
157 for (const proc of processedProfile.processors) {
158 if (proc.outputs) {
159 let bad = false;
160 for (const file in proc.outputs) {
161 if (! await vfs.validate(file, { algorithm: "sha1", hash: proc.outputs[file].replace(/\'/g, "") })) {
162 bad = true;
163 break;
164 }
165 }
166 if (bad) {
167 diag.badProcessedFiles.push(proc);
168 }
169 }
170 }
171 // if we have to process file, we have to check if the forge deps are ready
172 if (diag.badProcessedFiles.length !== 0) {
173 const libValidMask = await Promise.all(processedProfile.libraries.map(async (lib) => {
174 const artifact = lib.downloads.artifact;
175 const libPath = mc.getLibraryByPath(artifact.path);
176 if (await vfs.exists(libPath)) {
177 return artifact.sha1 ? vfs.validate(libPath) : true;
178 }
179 return false;
180 }));
181 const missingLibraries = processedProfile.libraries.filter((_, i) => !libValidMask[i]);
182 diag.missingInstallDependencies.push(...missingLibraries);
183
184 const validClient = await vfs.stat(processedProfile.data.BINPATCH.client).then((s) => s.size !== 0).catch((_) => false);
185 if (!validClient) {
186 diag.missingBinpatch = true;
187 diag.badInstall = true;
188 }
189 }
190 }
191 if (await vfs.exists(versionJsonPath)) {
192 const versionJSON: Version = JSON.parse(await vfs.readFile(versionJsonPath).then((b) => b.toString()));
193 if (versionJSON.arguments && versionJSON.arguments.game) {
194 const args = versionJSON.arguments.game;
195 const forgeVersion = args.indexOf("--fml.forgeVersion") + 1;
196 const mcVersion = args.indexOf("--fml.mcVersion") + 1;
197 const mcpVersion = args.indexOf("--fml.mcpVersion") + 1;
198 if (!forgeVersion || !mcVersion || !mcpVersion) {
199 diag.badVersionJson = true;
200 diag.badInstall = true;
201 } else {
202 const srgPath = mc.getLibraryByPath(`net/minecraft/client/${mcVersion}-${mcpVersion}/client-${mcVersion}-${mcpVersion}-srg.jar`);
203 const extraPath = mc.getLibraryByPath(`net/minecraft/client/${mcVersion}/client-${mcVersion}-extra.jar`);
204 const forgePatchPath = mc.getLibraryByPath(`net/minecraftforge/forge/${mcVersion}-${forgeVersion}/forge-${mcVersion}-${forgeVersion}-client.jar`);
205 diag.missingSrgJar = await vfs.missing(srgPath);
206 diag.missingMinecraftExtraJar = await vfs.missing(extraPath);
207 diag.missingForgePatchesJar = await vfs.missing(forgePatchPath);
208 }
209 } else {
210 diag.badVersionJson = true;
211 diag.badInstall = true;
212 }
213 } else {
214 diag.badVersionJson = true;
215 diag.badInstall = true;
216 }
217
218 return diag;
219 }
220
221 /**
222 * Post processing function for new forge installer (mcversion >= 1.13). You can use this with `ForgeInstaller.diagnose`.
223 *
224 * @param mc The minecraft location
225 * @param proc The processor
226 * @param java The java executor
227 */
228 export async function postProcess(mc: MinecraftFolder, proc: InstallProfile["processors"][number], java: JavaExecutor) {
229 const jarRealPath = mc.getLibraryByPath(Version.getLibraryInfo(proc.jar).path);
230 const mainClass = await findMainClass(jarRealPath);
231 if (!mainClass) { throw new Error(`Cannot find main class for processor ${proc.jar}.`); }
232 const cp = [...proc.classpath, proc.jar].map(Version.getLibraryInfo).map((p) => mc.getLibraryByPath(p.path)).join(path.delimiter);
233 const cmd = ["-cp", cp, mainClass, ...proc.args];
234 await java(cmd);
235 let failed = false;
236 if (proc.outputs) {
237 for (const file in proc.outputs) {
238 if (! await vfs.validate(file, { algorithm: "sha1", hash: proc.outputs[file].replace(/\'/g, "") })) {
239 console.error(`Fail to process ${proc.jar} @ ${file} since its validation failed.`);
240 failed = true;
241 }
242 }
243 }
244 if (failed) {
245 console.error(`Java arguments: ${JSON.stringify(cmd)}`);
246 throw new Error(`Fail to process post processing since its validation failed.`);
247 }
248 }
249
250 function postProcessInstallProfile(mc: MinecraftFolder, installProfile: InstallProfile) {
251 function processValue(v: string) {
252 if (v.match(/^\[.+\]$/g)) {
253 const targetId = v.substring(1, v.length - 1);
254 return mc.getLibraryByPath(Version.getLibraryInfo(targetId).path);
255 }
256 return v;
257 }
258 function processMapping(data: InstallProfile["data"], m: string) {
259 m = processValue(m);
260 if (m.match(/^{.+}$/g)) {
261 const key = m.substring(1, m.length - 1);
262 m = data[key].client;
263 }
264 return m;
265 }
266 const profile: InstallProfile = JSON.parse(JSON.stringify(installProfile));
267 profile.data.MINECRAFT_JAR = {
268 client: mc.getVersionJar(profile.minecraft),
269 server: "",
270 };
271 for (const key in profile.data) {
272 const value = profile.data[key];
273 value.client = processValue(value.client);
274 value.server = processValue(value.server);
275
276 if (key === "BINPATCH") {
277 const verRoot = mc.getVersionRoot(profile.version);
278 value.client = path.join(verRoot, value.client);
279 value.server = path.join(verRoot, value.server);
280 }
281 }
282 for (const proc of profile.processors) {
283 proc.args = proc.args.map((a) => processMapping(profile.data, a));
284 if (proc.outputs) {
285 const replacedOutput: InstallProfile["processors"][0]["outputs"] = {};
286 for (const key in proc.outputs) {
287 replacedOutput[processMapping(profile.data, key)] = processMapping(profile.data, proc.outputs[key]);
288 }
289 proc.outputs = replacedOutput;
290 }
291 }
292
293 return profile;
294 }
295
296 /**
297 * Install for forge installer step 2 and 3.
298 * @param version The version string or installer profile
299 * @param minecraft The minecraft location
300 */
301 export function installByInstallerPartialTask(version: string | InstallProfile, minecraft: MinecraftLocation, option: {
302 java?: JavaExecutor,
303 } & Installer.LibraryOption = {}) {
304 return Task.create("installForge", async (context) => {
305 const mc = MinecraftFolder.from(minecraft);
306 let prof: InstallProfile;
307 let ver: Version;
308 if (typeof version === "string") {
309 const versionRoot = mc.getVersionRoot(version);
310 prof = await vfs.readFile(path.join(versionRoot, "install_profile.json")).then((b) => b.toString()).then(JSON.parse);
311 } else {
312 prof = version;
313 }
314 ver = await vfs.readFile(mc.getVersionJson(prof.version)).then((b) => b.toString()).then(JSON.parse);
315 await installByInstallerPartialWork(mc, prof, ver, option.java || JavaExecutor.createSimple("java"), option)(context);
316 });
317 }
318
319 /**
320 * Install for forge installer step 2 and 3.
321 * @param version The version string or installer profile
322 * @param minecraft The minecraft location
323 */
324 export async function installByInstallerPartial(version: string | InstallProfile, minecraft: MinecraftLocation, option: {
325 java?: JavaExecutor,
326 } & Installer.LibraryOption = {}) {
327 return installByInstallerPartialTask(version, minecraft, option).execute();
328 }
329
330 function installByInstallerPartialWork(mc: MinecraftFolder, profile: InstallProfile, versionJson: Version, java: JavaExecutor, installLibOption: Installer.LibraryOption) {
331 return async (context: Task.Context) => {
332 profile = postProcessInstallProfile(mc, profile);
333
334 const parsedLibs = Version.resolveLibraries([...profile.libraries, ...versionJson.libraries]);
335 await context.execute("downloadLibraries", Installer.installLibrariesDirectTask(parsedLibs, mc, installLibOption).work);
336
337 await context.execute("postProcessing", async (ctx) => {
338 ctx.update(0, profile.processors.length);
339 let i = 0;
340 const errs: Error[] = [];
341 for (const proc of profile.processors) {
342 try {
343 await postProcess(mc, proc, java);
344 } catch (e) {
345 errs.push(e);
346 }
347 ctx.update(i += 1, profile.processors.length);
348 }
349 i += 1;
350 ctx.update(i, profile.processors.length);
351
352 if (errs.length !== 0) {
353 throw new Error(`Fail to post processing`);
354 }
355 });
356 };
357 }
358
359 function installByInstallerTask(version: VersionMeta, minecraft: MinecraftLocation, maven: string, installLibOption: Installer.LibraryOption, java: JavaExecutor, tempDir?: string, clearTempDir: boolean = true) {
360 return async (context: Task.Context) => {
361 const mc = typeof minecraft === "string" ? new MinecraftFolder(minecraft) : minecraft;
362 const forgeVersion = `${version.mcversion}-${version.version}`;
363 let versionId: string;
364
365 const installerURLFallback = `${maven}/maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-installer.jar`;
366 const installerURL = `${maven}${version.installer.path}`;
367
368 function downloadInstallerTask(installer: string, dest: string) {
369 return async (ctx: Task.Context) => {
370 let inStream;
371 if (await vfs.validate(dest, { algorithm: "md5", hash: version.installer.md5 }, { algorithm: "sha1", hash: version.installer.sha1 })) {
372 inStream = vfs.createReadStream(dest);
373 }
374 if (!inStream) {
375 inStream = got.stream(installer, {
376 method: "GET",
377 headers: { connection: "keep-alive" },
378 }).on("error", () => { })
379 .on("downloadProgress", (progress) => { ctx.update(progress.transferred, progress.total as number); });
380
381 inStream.pipe(vfs.createWriteStream(dest));
382 }
383
384 return inStream.pipe(Unzip.createParseStream({ lazyEntries: true }))
385 .wait();
386 };
387 }
388
389 const temp = tempDir || await vfs.mkdtemp(mc.root + path.sep);
390 await vfs.ensureDir(temp);
391 const installJar = path.join(temp, "forge-installer.jar");
392 let profile!: InstallProfile;
393 let versionJson!: Version;
394
395 async function processVersion(zip: Unzip.ZipFile, installProfileEntry: Unzip.Entry, versionEntry: Unzip.Entry, clientDataEntry: Unzip.Entry) {
396 profile = await zip.readEntry(installProfileEntry).then((b) => b.toString()).then(JSON.parse);
397 versionJson = await zip.readEntry(versionEntry).then((b) => b.toString()).then(JSON.parse);
398 versionId = versionJson.id;
399
400 const rootPath = mc.getVersionRoot(versionJson.id);
401 const jsonPath = path.join(rootPath, `${versionJson.id}.json`);
402 const installJsonPath = path.join(rootPath, `install_profile.json`);
403 const clientDataPath = path.join(rootPath, profile.data.BINPATCH.client);
404
405 await vfs.ensureFile(jsonPath);
406 await vfs.writeFile(installJsonPath, JSON.stringify(profile));
407 await vfs.writeFile(jsonPath, JSON.stringify(versionJson));
408
409 await vfs.ensureFile(clientDataPath);
410 const stream = await zip.openEntry(clientDataEntry);
411 await vfs.waitStream(stream.pipe(vfs.createWriteStream(clientDataPath)));
412 }
413 async function processExtractLibrary(s: Readable, p: string) {
414 const file = mc.getLibraryByPath(p);
415 await vfs.ensureFile(file);
416 await vfs.waitStream(s.pipe(vfs.createWriteStream(file)));
417 }
418 try {
419 let zip: Unzip.LazyZipFile;
420 try {
421 zip = await context.execute("downloadInstaller", downloadInstallerTask(installerURL, installJar));
422 } catch {
423 zip = await context.execute("downloadInstaller", downloadInstallerTask(installerURLFallback, installJar));
424 }
425
426 const [forgeEntry, forgeUniversalEntry, clientDataEntry, installProfileEntry, versionEntry] = await zip.filterEntries([
427 `maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}.jar`,
428 `maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-universal.jar`,
429 "data/client.lzma",
430 "install_profile.json",
431 "version.json"]);
432
433 if (!forgeEntry) {
434 throw new Error("Missing forge jar entry");
435 }
436 if (!forgeUniversalEntry) {
437 throw new Error("Missing forge universal entry");
438 }
439 if (!installProfileEntry) {
440 throw new Error("Missing install profile");
441 }
442 if (!versionEntry) {
443 throw new Error("Missing version entry");
444 }
445
446 await processExtractLibrary(await zip.openEntry(forgeEntry), forgeEntry.fileName);
447 await processExtractLibrary(await zip.openEntry(forgeUniversalEntry), forgeUniversalEntry.fileName);
448 await processVersion(zip, installProfileEntry, versionEntry, clientDataEntry);
449
450 await installByInstallerPartialWork(mc, profile, versionJson, java, installLibOption)(context);
451
452 return versionId!;
453 } catch (e) {
454 if (clearTempDir) {
455 await vfs.remove(temp);
456 }
457 throw e;
458 }
459 };
460 }
461
462 function installByUniversalTask(version: VersionMeta, minecraft: MinecraftLocation, maven: string, checkDependecies: boolean) {
463 return async (context: Task.Context) => {
464 const mc = typeof minecraft === "string" ? new MinecraftFolder(minecraft) : minecraft;
465 const forgeVersion = `${version.mcversion}-${version.version}`;
466 const jarPath = mc.getLibraryByPath(`net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}.jar`);
467 let fullVersion: string;
468 let realJarPath: string;
469
470 const universalURLFallback = `${maven}/maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-universal.jar`;
471 const universalURL = `${maven}${version.universal.path}`;
472
473 await context.execute("installForgeJar", async () => {
474 if (await vfs.exists(jarPath)) {
475 const valid = await vfs.validate(jarPath, { algorithm: "md5", hash: version.universal.md5 }, { algorithm: "sha1", hash: version.universal.sha1 });
476 if (valid) {
477 return;
478 }
479 }
480
481 try {
482 await context.execute("downloadJar", downloadFileWork({ url: universalURL, destination: jarPath }));
483 } catch {
484 await context.execute("redownloadJar", downloadFileWork({ url: universalURLFallback, destination: jarPath }));
485 }
486 });
487
488 await context.execute("installForgeJson", async () => {
489 const zip = await Unzip.open(jarPath, { lazyEntries: true });
490 const [versionEntry] = await zip.filterEntries(["version.json"]);
491
492 if (versionEntry) {
493 const buf = await zip.readEntry(versionEntry);
494 const raw = JSON.parse(buf.toString());
495 const id = raw.id;
496 fullVersion = id;
497 const rootPath = mc.getVersionRoot(fullVersion);
498 realJarPath = mc.getLibraryByPath(Version.getLibraryInfo(raw.libraries.find((l: any) => l.name.startsWith("net.minecraftforge:forge"))).path);
499
500 await vfs.ensureDir(rootPath);
501 await vfs.writeFile(path.join(rootPath, `${id}.json`), buf);
502 } else {
503 throw new Error(`Cannot install forge json for ${version.version} since the version json is missing!`);
504 }
505 });
506
507 if (realJarPath! !== jarPath) {
508 await vfs.ensureFile(realJarPath!);
509 await vfs.copyFile(jarPath, realJarPath!);
510 await vfs.unlink(jarPath);
511 }
512
513 if (checkDependecies) {
514 const resolvedVersion = await Version.parse(minecraft, fullVersion!);
515 context.execute("installDependencies", Installer.installDependenciesTask(resolvedVersion).work);
516 }
517
518 return fullVersion!;
519 };
520 }
521
522 /**
523 * Install forge to target location.
524 * Installation task for forge with mcversion >= 1.13 requires java installed on your pc.
525 * @param version The forge version meta
526 */
527 export function install(version: VersionMeta, minecraft: MinecraftLocation, option?: {
528 maven?: string,
529 forceCheckDependencies?: boolean,
530 java?: JavaExecutor,
531 tempDir?: string,
532 clearTempDirAfterInstall?: boolean,
533 }) {
534 return installTask(version, minecraft, option).execute();
535 }
536
537 /**
538 * Install forge to target location.
539 * Installation task for forge with mcversion >= 1.13 requires java installed on your pc.
540 * @param version The forge version meta
541 */
542 export function installTask(version: VersionMeta, minecraft: MinecraftLocation, option: {
543 maven?: string,
544 forceInstallDependencies?: boolean,
545 java?: JavaExecutor,
546 tempDir?: string,
547 clearTempDirAfterInstall?: boolean,
548 } & Installer.LibraryOption = {}): Task<string> {
549 let byInstaller = true;
550 try {
551 const minorVersion = Number.parseInt(version.mcversion.split(".")[1], 10);
552 byInstaller = minorVersion >= 13;
553 } catch { }
554 const work = byInstaller ? installByInstallerTask(version, minecraft, option.maven || DEFAULT_FORGE_MAVEN, option, option.java || JavaExecutor.createSimple("java"), option.tempDir, option.clearTempDirAfterInstall)
555 : installByUniversalTask(version, minecraft, option.maven || DEFAULT_FORGE_MAVEN, option.forceInstallDependencies === true);
556 return Task.create("installForge", work);
557 }
558}
559
560export * from "./forgeweb";
561
562export default ForgeInstaller;