UNPKG

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