@xmcl/installer
Version:
The installers of Minecraft/Forge/Fabric/Liteloader/Quilt
1,283 lines (1,271 loc) • 73.4 kB
JavaScript
// fabric.ts
import { MinecraftFolder } from "@xmcl/core";
import { writeFile } from "fs/promises";
import { request } from "undici";
// utils.ts
import { spawn } from "child_process";
import { access, mkdir, stat } from "fs/promises";
import { dirname } from "path";
import { checksum } from "@xmcl/core";
function missing(target) {
return access(target).then(() => false, () => true);
}
async function ensureDir(target) {
try {
await mkdir(target);
} catch (err) {
const e = err;
if (await stat(target).then((s) => s.isDirectory()).catch(() => false)) {
return;
}
if (e.code === "EEXIST") {
return;
}
if (e.code === "ENOENT") {
if (dirname(target) === target) {
throw e;
}
try {
await ensureDir(dirname(target));
await mkdir(target);
} catch {
if (await stat(target).then((s) => s.isDirectory()).catch((e2) => false)) {
return;
}
throw e;
}
return;
}
throw e;
}
}
function ensureFile(target) {
return ensureDir(dirname(target));
}
function normalizeArray(arr = []) {
return arr instanceof Array ? arr : [arr];
}
function spawnProcess(spawnJavaOptions, args, options) {
const process2 = ((spawnJavaOptions == null ? void 0 : spawnJavaOptions.spawn) ?? spawn)(spawnJavaOptions.java ?? "java", args, options);
return waitProcess(process2);
}
function waitProcess(process2) {
return new Promise((resolve2, reject) => {
var _a, _b, _c, _d;
const errorMsg = [];
process2.on("error", (err) => {
reject(err);
});
process2.on("close", (code) => {
if (code !== 0) {
reject(errorMsg.join(""));
} else {
resolve2();
}
});
process2.on("exit", (code) => {
if (code !== 0) {
reject(errorMsg.join(""));
} else {
resolve2();
}
});
(_a = process2.stdout) == null ? void 0 : _a.setEncoding("utf-8");
(_b = process2.stdout) == null ? void 0 : _b.on("data", (buf) => {
});
(_c = process2.stderr) == null ? void 0 : _c.setEncoding("utf-8");
(_d = process2.stderr) == null ? void 0 : _d.on("data", (buf) => {
errorMsg.push(buf.toString());
});
});
}
function joinUrl(a, b) {
if (a.endsWith("/") && b.startsWith("/")) {
return a + b.substring(1);
}
if (!a.endsWith("/") && !b.startsWith("/")) {
return a + "/" + b;
}
return a + b;
}
function errorToString(e) {
if (e instanceof Error) {
return e.stack ? e.stack : e.message;
}
return e.toString();
}
// fabric.ts
var YARN_MAVEN_URL = "https://maven.fabricmc.net/net/fabricmc/yarn/maven-metadata.xml";
var LOADER_MAVEN_URL = "https://maven.fabricmc.net/net/fabricmc/fabric-loader/maven-metadata.xml";
async function getFabricArtifacts(options) {
const response = await request("https://meta.fabricmc.net/v2/versions", { throwOnError: true, dispatcher: options == null ? void 0 : options.dispatcher });
const body = response.body.json();
return body;
}
async function getYarnArtifactList(options) {
const response = await request("https://meta.fabricmc.net/v2/versions/yarn", { throwOnError: true, dispatcher: options == null ? void 0 : options.dispatcher });
const body = response.body.json();
return body;
}
async function getYarnArtifactListFor(minecraft, options) {
const response = await request("https://meta.fabricmc.net/v2/versions/yarn/" + minecraft, { throwOnError: true, dispatcher: options == null ? void 0 : options.dispatcher });
const body = response.body.json();
return body;
}
async function getLoaderArtifactList(options) {
const response = await request("https://meta.fabricmc.net/v2/versions/loader", { throwOnError: true, dispatcher: options == null ? void 0 : options.dispatcher });
const body = response.body.json();
return body;
}
async function getLoaderArtifactListFor(minecraft, options) {
const response = await request("https://meta.fabricmc.net/v2/versions/loader/" + minecraft, { throwOnError: true, dispatcher: options == null ? void 0 : options.dispatcher });
const body = response.body.json();
return body;
}
async function getFabricLoaderArtifact(minecraft, loader, options) {
const response = await request("https://meta.fabricmc.net/v2/versions/loader/" + minecraft + "/" + loader, { throwOnError: true, dispatcher: options == null ? void 0 : options.dispatcher });
const body = response.body.json();
return body;
}
async function installFabric(loader, minecraft, options = {}) {
const folder = MinecraftFolder.from(minecraft);
let yarn;
const side = options.side ?? "client";
let id = options.versionId;
let mcversion;
if (options.yarnVersion) {
const yarnVersion = options.yarnVersion;
if (typeof yarnVersion === "string") {
yarn = yarnVersion;
mcversion = yarn.split("+")[0];
} else {
yarn = yarnVersion.version;
mcversion = yarnVersion.gameVersion || yarn.split("+")[0];
}
} else {
mcversion = loader.intermediary.version;
}
if (!id) {
id = mcversion;
if (yarn) {
id += `-fabric${yarn}-loader${loader.loader.version}`;
} else {
id += `-fabric${loader.loader.version}`;
}
}
const libraries = [
{ name: loader.loader.maven, url: "https://maven.fabricmc.net/" },
{ name: loader.intermediary.maven, url: "https://maven.fabricmc.net/" },
...options.yarnVersion ? [{ name: `net.fabricmc:yarn:${yarn}`, url: "https://maven.fabricmc.net/" }] : [],
...loader.launcherMeta.libraries.common,
...loader.launcherMeta.libraries[side]
];
const mainClass = loader.launcherMeta.mainClass[side];
const inheritsFrom = options.inheritsFrom || mcversion;
const jsonFile = folder.getVersionJson(id);
await ensureFile(jsonFile);
await writeFile(jsonFile, JSON.stringify({
id,
inheritsFrom,
mainClass,
libraries,
arguments: {
game: [],
jvm: []
},
releaseTime: (/* @__PURE__ */ new Date()).toJSON(),
time: (/* @__PURE__ */ new Date()).toJSON()
}));
return id;
}
// liteloader.ts
import { MinecraftFolder as MinecraftFolder2 } from "@xmcl/core";
import { task } from "@xmcl/task";
import { readFile, writeFile as writeFile2 } from "fs/promises";
import { join } from "path";
import { request as request2 } from "undici";
var DEFAULT_VERSION_MANIFEST = "http://dl.liteloader.com/versions/versions.json";
function processLibraries(lib) {
if (Object.keys(lib).length === 1 && lib.name) {
if (lib.name.startsWith("org.ow2.asm")) {
lib.url = "https://files.minecraftforge.net/maven/";
}
}
return lib;
}
var LiteloaderVersionList;
((LiteloaderVersionList2) => {
function parse(content) {
const result = JSON.parse(content);
const metalist = { meta: result.meta, versions: {} };
for (const mcversion in result.versions) {
const versions = metalist.versions[mcversion] = {};
const snapshots = result.versions[mcversion].snapshots;
const artifacts = result.versions[mcversion].artefacts;
const url = result.versions[mcversion].repo.url;
if (snapshots) {
const { stream, file, version, md5, timestamp, tweakClass, libraries } = snapshots["com.mumfrey:liteloader"].latest;
const type = stream === "RELEASE" ? "RELEASE" : "SNAPSHOT";
versions.snapshot = {
url,
type,
file,
version,
md5,
timestamp,
mcversion,
tweakClass,
libraries: libraries.map(processLibraries)
};
}
if (artifacts) {
const { stream, file, version, md5, timestamp, tweakClass, libraries } = artifacts["com.mumfrey:liteloader"].latest;
const type = stream === "RELEASE" ? "RELEASE" : "SNAPSHOT";
versions.release = {
url,
type,
file,
version,
md5,
timestamp,
mcversion,
tweakClass,
libraries: libraries.map(processLibraries)
};
}
}
return metalist;
}
LiteloaderVersionList2.parse = parse;
})(LiteloaderVersionList || (LiteloaderVersionList = {}));
var snapshotRoot = "http://dl.liteloader.com/versions/";
var releaseRoot = "http://repo.mumfrey.com/content/repositories/liteloader/";
var MissingVersionJsonError = class extends Error {
constructor(version, path) {
super();
this.version = version;
this.path = path;
this.name = "MissingVersionJson";
}
};
async function getLiteloaderVersionList(options = {}) {
const response = await request2(DEFAULT_VERSION_MANIFEST, { dispatcher: options.dispatcher, throwOnError: true });
const body = await response.body.text();
return LiteloaderVersionList.parse(body);
}
function installLiteloader(versionMeta, location, options) {
return installLiteloaderTask(versionMeta, location, options).startAndWait();
}
function buildVersionInfo(versionMeta, mountedJSON) {
const id = `${mountedJSON.id}-Liteloader${versionMeta.mcversion}-${versionMeta.version}`;
const time = new Date(Number.parseInt(versionMeta.timestamp, 10) * 1e3).toISOString();
const releaseTime = time;
const type = versionMeta.type;
const libraries = [
{
name: `com.mumfrey:liteloader:${versionMeta.version}`,
url: type === "SNAPSHOT" ? snapshotRoot : releaseRoot
},
...versionMeta.libraries.map(processLibraries)
];
const mainClass = "net.minecraft.launchwrapper.Launch";
const inheritsFrom = mountedJSON.id;
const jar = mountedJSON.jar || mountedJSON.id;
const info = {
id,
time,
releaseTime,
type,
libraries,
mainClass,
inheritsFrom,
jar
};
if (mountedJSON.arguments) {
info.arguments = {
game: ["--tweakClass", versionMeta.tweakClass],
jvm: []
};
} else {
info.minecraftArguments = `--tweakClass ${versionMeta.tweakClass} ` + mountedJSON.minecraftArguments;
}
return info;
}
function installLiteloaderTask(versionMeta, location, options = {}) {
return task("installLiteloader", async function installLiteloader2() {
const mc = MinecraftFolder2.from(location);
const mountVersion = options.inheritsFrom || versionMeta.mcversion;
const mountedJSON = await this.yield(task("resolveVersionJson", async function resolveVersionJson() {
if (await missing(mc.getVersionJson(mountVersion))) {
throw new MissingVersionJsonError(mountVersion, mc.getVersionJson(mountVersion));
}
return readFile(mc.getVersionJson(mountVersion)).then((b) => b.toString()).then(JSON.parse);
}));
const versionInf = await this.yield(task("generateLiteloaderJson", async function generateLiteloaderJson() {
const inf = buildVersionInfo(versionMeta, mountedJSON);
inf.id = options.versionId || inf.id;
inf.inheritsFrom = options.inheritsFrom || inf.inheritsFrom;
const versionPath = mc.getVersionRoot(inf.id);
await ensureDir(versionPath);
await writeFile2(join(versionPath, inf.id + ".json"), JSON.stringify(inf, void 0, 4));
return inf;
}));
return versionInf.id;
});
}
// forge.ts
import { LibraryInfo as LibraryInfo2, MinecraftFolder as MinecraftFolder5, Version as VersionJson3 } from "@xmcl/core";
import { parse as parseForge } from "@xmcl/forge-site-parser";
import { task as task4 } from "@xmcl/task";
import { filterEntries, open as open3, openEntryReadStream, readEntry as readEntry2 } from "@xmcl/unzip";
import { createWriteStream } from "fs";
import { writeFile as writeFile4 } from "fs/promises";
import { dirname as dirname3, join as join3 } from "path";
import { pipeline } from "stream/promises";
import { request as request4 } from "undici";
// downloadTask.ts
import { download, DownloadAbortError } from "@xmcl/file-transfer";
import { AbortableTask } from "@xmcl/task";
var DownloadTask = class extends AbortableTask {
constructor(options) {
super();
this.options = options;
this._from = options.url instanceof Array ? options.url[0] : options.url;
this._to = options.destination;
}
abort = () => {
};
onProgress(url, chunkSize, progress, total) {
this._progress = progress;
this._total = total;
this._from = url.toString();
this.update(chunkSize);
}
process() {
const listeners = [];
const aborted = () => this.isCancelled || this.isPaused;
const signal = {
get aborted() {
return aborted();
},
addEventListener(event, listener) {
if (event !== "abort") {
return this;
}
listeners.push(listener);
return this;
},
removeEventListener(event, listener) {
return this;
}
};
this.abort = () => {
listeners.forEach((l) => l());
};
return download({
...this.options,
progressController: this,
abortSignal: signal
});
}
isAbortedError(e) {
if (e instanceof DownloadAbortError) {
return true;
}
return false;
}
};
// minecraft.ts
import { MinecraftFolder as MinecraftFolder3, Version as VersionJson } from "@xmcl/core";
import { ChecksumNotMatchError, JsonValidator } from "@xmcl/file-transfer";
import { task as task2 } from "@xmcl/task";
import { readFile as readFile2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
import { join as join2 } from "path";
import { request as request3 } from "undici";
// zipValdiator.ts
import { ValidationError } from "@xmcl/file-transfer";
import { open } from "@xmcl/unzip";
var ZipValidator = class {
async validate(destination, url) {
try {
const file = await open(destination, { autoClose: false, lazyEntries: true });
file.close();
} catch (e) {
throw new ValidationError("InvalidZipError", e.message);
}
}
};
// minecraft.ts
var DEFAULT_VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
var DEFAULT_RESOURCE_ROOT_URL = "https://resources.download.minecraft.net";
async function getVersionList(options = {}) {
const response = await request3(DEFAULT_VERSION_MANIFEST_URL, { dispatcher: options.dispatcher, throwOnError: true });
return await response.body.json();
}
function resolveDownloadUrls(original, version, option) {
const result = [];
if (typeof option === "function") {
result.unshift(...normalizeArray(option(version)));
} else {
result.unshift(...normalizeArray(option));
}
if (result.indexOf(original) === -1) {
result.push(original);
}
return result;
}
async function install(versionMeta, minecraft, option = {}) {
return installTask(versionMeta, minecraft, option).startAndWait();
}
function installVersion(versionMeta, minecraft, options = {}) {
return installVersionTask(versionMeta, minecraft, options).startAndWait();
}
function installDependencies(version, options) {
return installDependenciesTask(version, options).startAndWait();
}
function installAssets(version, options = {}) {
return installAssetsTask(version, options).startAndWait();
}
function installLibraries(version, options = {}) {
return installLibrariesTask(version, options).startAndWait();
}
async function installResolvedLibraries(libraries, minecraft, option) {
await installLibrariesTask({ libraries, minecraftDirectory: typeof minecraft === "string" ? minecraft : minecraft.root }, option).startAndWait();
}
function installTask(versionMeta, minecraft, options = {}) {
return task2("install", async function() {
const version = await this.yield(installVersionTask(versionMeta, minecraft, options));
if (options.side !== "server") {
await this.yield(installDependenciesTask(version, options));
}
return version;
});
}
function installVersionTask(versionMeta, minecraft, options = {}) {
return task2("version", async function() {
await this.yield(new InstallJsonTask(versionMeta, minecraft, options));
const version = await VersionJson.parse(minecraft, versionMeta.id);
if (version.downloads[options.side ?? "client"]) {
await this.yield(new InstallJarTask(version, minecraft, options));
}
return version;
}, versionMeta);
}
function installDependenciesTask(version, options = {}) {
return task2("dependencies", async function() {
await Promise.all([
this.yield(installAssetsTask(version, options)),
this.yield(installLibrariesTask(version, options))
]);
return version;
});
}
function installAssetsTask(version, options = {}) {
return task2("assets", async function() {
var _a, _b;
const folder = MinecraftFolder3.from(version.minecraftDirectory);
if ((_b = (_a = version.logging) == null ? void 0 : _a.client) == null ? void 0 : _b.file) {
const file = version.logging.client.file;
await this.yield(new DownloadTask({
url: file.url,
validator: {
algorithm: "sha1",
hash: file.sha1
},
destination: folder.getLogConfig(file.id),
agent: options.agent,
headers: options.headers
}).setName("asset", { name: file.id, hash: file.sha1, size: file.size }));
}
const jsonPath = folder.getPath("assets", "indexes", version.assets + ".json");
if (version.assetIndex) {
await this.yield(new InstallAssetIndexTask(version, options));
}
await ensureDir(folder.getPath("assets", "objects"));
const getAssetIndexFallback = async () => {
var _a2;
const urls = resolveDownloadUrls(version.assetIndex.url, version, options.assetsIndexUrl);
for (const url of urls) {
try {
const response = await request3(url, { dispatcher: (_a2 = options.agent) == null ? void 0 : _a2.dispatcher });
const json = await response.body.json();
await writeFile3(jsonPath, JSON.stringify(json));
return json;
} catch {
}
}
};
let objectArray;
try {
const { objects } = JSON.parse(await readFile2(jsonPath).then((b) => b.toString()));
objectArray = Object.keys(objects).map((k) => ({ name: k, ...objects[k] }));
} catch (e) {
if (e instanceof SyntaxError) {
throw e;
}
const { objects } = await getAssetIndexFallback();
objectArray = Object.keys(objects).map((k) => ({ name: k, ...objects[k] }));
}
await this.all(objectArray.map((o) => new InstallAssetTask(o, folder, options)), {
throwErrorImmediately: options.throwErrorImmediately ?? false,
getErrorMessage: (errs) => `Errors during install Minecraft ${version.id}'s assets at ${version.minecraftDirectory}: ${errs.map(errorToString).join("\n")}`
});
return version;
});
}
function installLibrariesTask(version, options = {}) {
return task2("libraries", async function() {
const folder = MinecraftFolder3.from(version.minecraftDirectory);
await this.all(version.libraries.map((lib) => new InstallLibraryTask(lib, folder, options)), {
throwErrorImmediately: options.throwErrorImmediately ?? false,
getErrorMessage: (errs) => `Errors during install libraries at ${version.minecraftDirectory}: ${errs.map(errorToString).join("\n")}`
});
});
}
function installResolvedLibrariesTask(libraries, minecraft, option) {
return installLibrariesTask({ libraries, minecraftDirectory: typeof minecraft === "string" ? minecraft : minecraft.root }, option);
}
function installResolvedAssetsTask(assets, folder, options = {}) {
return task2("assets", async function() {
await ensureDir(folder.getPath("assets", "objects"));
await this.all(assets.map((o) => new InstallAssetTask(o, folder, options)), {
throwErrorImmediately: false,
getErrorMessage: (errs) => `Errors during install assets at ${folder.root}:
${errs.map(errorToString).join("\n")}`
});
});
}
var InstallJsonTask = class extends DownloadTask {
constructor(version, minecraft, options) {
var _a;
const folder = MinecraftFolder3.from(minecraft);
const destination = folder.getVersionJson(version.id);
const expectSha1 = version.url.split("/")[5];
const urls = resolveDownloadUrls(version.url, version, options.json);
super({
url: urls,
headers: options.headers,
agent: options.agent,
validator: expectSha1 ? ((_a = options.checksumValidatorResolver) == null ? void 0 : _a.call(options, { algorithm: "sha1", hash: expectSha1 })) || { algorithm: "sha1", hash: expectSha1 } : new JsonValidator(),
destination,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this.name = "json";
this.param = version;
}
};
var InstallJarTask = class extends DownloadTask {
constructor(version, minecraft, options) {
var _a;
const folder = MinecraftFolder3.from(minecraft);
const type = options.side ?? "client";
const destination = join2(
folder.getVersionRoot(version.id),
type === "client" ? version.id + ".jar" : version.id + "-" + type + ".jar"
);
const download2 = version.downloads[type];
if (!download2) {
throw new Error(`Cannot find downloadable jar in ${type}`);
}
const urls = resolveDownloadUrls(download2.url, version, options[type]);
const expectSha1 = download2.sha1;
super({
url: urls,
validator: ((_a = options.checksumValidatorResolver) == null ? void 0 : _a.call(options, { algorithm: "sha1", hash: expectSha1 })) || { algorithm: "sha1", hash: expectSha1 },
destination,
headers: options.headers,
agent: options.agent,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this.name = "jar";
this.param = version;
}
};
var InstallAssetIndexTask = class extends DownloadTask {
constructor(version, options = {}) {
var _a;
const folder = MinecraftFolder3.from(version.minecraftDirectory);
const jsonPath = folder.getPath("assets", "indexes", version.assets + ".json");
const expectSha1 = version.assetIndex.sha1;
super({
url: resolveDownloadUrls(version.assetIndex.url, version, options.assetsIndexUrl),
destination: jsonPath,
validator: ((_a = options.checksumValidatorResolver) == null ? void 0 : _a.call(options, { algorithm: "sha1", hash: expectSha1 })) || { algorithm: "sha1", hash: expectSha1 },
headers: options.headers,
agent: options.agent,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this.name = "assetIndex";
this.param = version;
}
};
var InstallLibraryTask = class extends DownloadTask {
constructor(lib, folder, options) {
var _a;
const libraryPath = lib.download.path;
const destination = join2(folder.libraries, libraryPath);
const urls = resolveLibraryDownloadUrls(lib, options);
const expectSha1 = lib.download.sha1;
super({
url: urls,
validator: lib.download.sha1 === "" ? new ZipValidator() : ((_a = options.checksumValidatorResolver) == null ? void 0 : _a.call(options, { algorithm: "sha1", hash: expectSha1 })) || { algorithm: "sha1", hash: expectSha1 },
destination,
headers: options.headers,
agent: options.agent,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this.name = "library";
this.param = lib;
}
};
var InstallAssetTask = class extends DownloadTask {
constructor(asset, folder, options) {
var _a;
const assetsHosts = normalizeArray(options.assetsHost || []);
if (assetsHosts.indexOf(DEFAULT_RESOURCE_ROOT_URL) === -1) {
assetsHosts.push(DEFAULT_RESOURCE_ROOT_URL);
}
const { hash, size, name } = asset;
const head = hash.substring(0, 2);
const dir = folder.getPath("assets", "objects", head);
const file = join2(dir, hash);
const urls = assetsHosts.map((h) => `${h}/${head}/${hash}`);
super({
url: urls,
destination: file,
validator: options.prevalidSizeOnly ? {
async validate(destination, url) {
const fstat = await stat2(destination).catch(() => ({ size: -1 }));
if (fstat.size !== size) {
throw new ChecksumNotMatchError("size", size.toString(), fstat.size.toString(), destination, url);
}
}
} : ((_a = options.checksumValidatorResolver) == null ? void 0 : _a.call(options, { algorithm: "sha1", hash })) || { algorithm: "sha1", hash },
headers: options.headers,
agent: options.agent,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this._total = size;
this.name = "asset";
this.param = asset;
}
};
var DEFAULT_MAVENS = ["https://repo1.maven.org/maven2/"];
function resolveLibraryDownloadUrls(library, libraryOptions) {
var _a;
const libraryHosts = ((_a = libraryOptions.libraryHost) == null ? void 0 : _a.call(libraryOptions, library)) ?? [];
const url = new URL(library.download.url);
return [.../* @__PURE__ */ new Set([
// user defined alternative host to download
...normalizeArray(libraryHosts),
...normalizeArray(libraryOptions.mavenHost).map((m) => joinUrl(m, url.pathname)),
library.download.url,
...normalizeArray(libraryOptions.mavenHost).map((m) => joinUrl(m, library.download.path)),
...DEFAULT_MAVENS.map((m) => joinUrl(m, library.download.path))
])];
}
// profile.ts
import { LibraryInfo, MinecraftFolder as MinecraftFolder4, Version as VersionJson2 } from "@xmcl/core";
import { AbortableTask as AbortableTask2, CancelledError, task as task3 } from "@xmcl/task";
import { open as open2, readEntry, walkEntriesGenerator } from "@xmcl/unzip";
import { spawn as spawn2 } from "child_process";
import { readFile as readFile3 } from "fs/promises";
import { delimiter, dirname as dirname2 } from "path";
function resolveProcessors(side, installProfile, minecraft) {
function normalizePath(val) {
if (val && val.match(/^\[.+\]$/g)) {
const name = val.substring(1, val.length - 1);
return minecraft.getLibraryByPath(LibraryInfo.resolve(name).path);
}
return val;
}
function normalizeVariable(val) {
if (val && val.match(/^{.+}$/g)) {
const key = val.substring(1, val.length - 1);
return variables[key][side];
}
return val;
}
const variables = {
SIDE: {
client: "client",
server: "server"
},
MINECRAFT_JAR: {
client: minecraft.getVersionJar(installProfile.minecraft),
server: minecraft.getVersionJar(installProfile.minecraft, "server")
}
};
if (installProfile.data) {
for (const key in installProfile.data) {
const { client, server } = installProfile.data[key];
variables[key] = {
client: normalizePath(client),
server: normalizePath(server)
};
}
}
if (variables.INSTALLER) {
variables.ROOT = {
client: dirname2(variables.INSTALLER.client),
server: dirname2(variables.INSTALLER.server)
};
}
const resolveOutputs = (proc, args) => {
const original = proc.outputs ? Object.entries(proc.outputs).map(([k, v]) => ({ [normalizeVariable(k)]: normalizeVariable(v) })).reduce((a, b) => Object.assign(a, b), {}) : {};
for (const [key, val] of Object.entries(original)) {
original[key] = val.replace(/'/g, "");
}
const outputIndex = args.indexOf("--output") === -1 ? args.indexOf("--out-jar") : args.indexOf("--output");
const outputFile = outputIndex !== -1 ? args[outputIndex + 1] : void 0;
if (outputFile && !original[outputFile]) {
original[outputFile] = "";
}
return original;
};
const processors = (installProfile.processors || []).map((proc) => {
const args = proc.args.map(normalizePath).map(normalizeVariable);
return {
...proc,
args,
outputs: resolveOutputs(proc, args)
};
}).filter((proc) => proc.sides ? proc.sides.indexOf(side) !== -1 : true);
return processors;
}
function postProcess(processors, minecraft, javaOptions) {
return new PostProcessingTask(processors, minecraft, javaOptions).startAndWait();
}
function installByProfile(installProfile, minecraft, options = {}) {
return installByProfileTask(installProfile, minecraft, options).startAndWait();
}
function installByProfileTask(installProfile, minecraft, options = {}) {
return task3("installByProfile", async function() {
const minecraftFolder = MinecraftFolder4.from(minecraft);
const processor = resolveProcessors(options.side || "client", installProfile, minecraftFolder);
const versionJson = await readFile3(minecraftFolder.getVersionJson(installProfile.version)).then((b) => b.toString()).then(JSON.parse);
const installRequiredLibs = VersionJson2.resolveLibraries(installProfile.libraries);
await this.all(installRequiredLibs.map((lib) => new InstallLibraryTask(lib, minecraftFolder, options)), {
throwErrorImmediately: options.throwErrorImmediately ?? false,
getErrorMessage: (errs) => `Errors during install libraries at ${minecraftFolder.root}: ${errs.map(errorToString).join("\n")}`
});
await this.yield(new PostProcessingTask(processor, minecraftFolder, options));
const libraries = VersionJson2.resolveLibraries(versionJson.libraries);
await this.all(libraries.map((lib) => new InstallLibraryTask(lib, minecraftFolder, options)), {
throwErrorImmediately: options.throwErrorImmediately ?? false,
getErrorMessage: (errs) => `Errors during install libraries at ${minecraftFolder.root}: ${errs.map(errorToString).join("\n")}`
});
});
}
var PostProcessBadJarError = class extends Error {
constructor(jarPath, causeBy) {
super(`Fail to post process bad jar: ${jarPath}`);
this.jarPath = jarPath;
this.causeBy = causeBy;
}
name = "PostProcessBadJarError";
};
var PostProcessNoMainClassError = class extends Error {
constructor(jarPath) {
super(`Fail to post process bad jar without main class: ${jarPath}`);
this.jarPath = jarPath;
}
name = "PostProcessNoMainClassError";
};
var PostProcessFailedError = class extends Error {
constructor(jarPath, commands, message) {
super(message);
this.jarPath = jarPath;
this.commands = commands;
}
name = "PostProcessFailedError";
};
var PAUSEED = Symbol("PAUSED");
var PostProcessingTask = class extends AbortableTask2 {
constructor(processors, minecraft, java) {
super();
this.processors = processors;
this.minecraft = minecraft;
this.java = java;
this.param = processors;
this._total = processors.length;
}
name = "postProcessing";
pointer = 0;
_abort = () => {
};
async findMainClass(lib) {
var _a;
let zip;
let mainClass;
try {
zip = await open2(lib, { lazyEntries: true });
for await (const entry of walkEntriesGenerator(zip)) {
if (entry.fileName === "META-INF/MANIFEST.MF") {
const content = await readEntry(zip, entry).then((b) => b.toString());
mainClass = (_a = content.split("\n").map((l) => l.split(": ")).find((arr) => arr[0] === "Main-Class")) == null ? void 0 : _a[1].trim();
break;
}
}
} catch (e) {
throw new PostProcessBadJarError(lib, e);
} finally {
zip == null ? void 0 : zip.close();
}
if (!mainClass) {
throw new PostProcessNoMainClassError(lib);
}
return mainClass;
}
async isInvalid(outputs) {
for (const [file, expect] of Object.entries(outputs)) {
const sha1 = await checksum(file, "sha1").catch((e) => "");
if (!sha1)
return true;
if (!expect)
return false;
const expected = expect.replace(/'/g, "");
if (expected !== sha1) {
return true;
}
}
return false;
}
async postProcess(mc, proc, javaOptions) {
const jarRealPath = mc.getLibraryByPath(LibraryInfo.resolve(proc.jar).path);
const mainClass = await this.findMainClass(jarRealPath);
this._to = proc.jar;
const cp = [...proc.classpath, proc.jar].map(LibraryInfo.resolve).map((p) => mc.getLibraryByPath(p.path)).join(delimiter);
const cmd = ["-cp", cp, mainClass, ...proc.args];
try {
await new Promise((resolve2, reject) => {
const process2 = ((javaOptions == null ? void 0 : javaOptions.spawn) ?? spawn2)(javaOptions.java ?? "java", cmd);
waitProcess(process2).then(resolve2, reject);
this._abort = () => {
reject(PAUSEED);
process2.kill(1);
};
});
} catch (e) {
if (typeof e === "string") {
throw new PostProcessFailedError(proc.jar, [javaOptions.java ?? "java", ...cmd], e);
}
throw e;
}
if (proc.outputs && await this.isInvalid(proc.outputs)) {
throw new PostProcessFailedError(proc.jar, [javaOptions.java ?? "java", ...cmd], "Validate the output of process failed!");
}
}
async process() {
for (; this.pointer < this.processors.length; this.pointer++) {
const proc = this.processors[this.pointer];
if (this.isCancelled) {
throw new CancelledError();
}
if (this.isPaused) {
throw PAUSEED;
}
if (!proc.outputs || await this.isInvalid(proc.outputs)) {
await this.postProcess(this.minecraft, proc, this.java);
}
if (this.isCancelled) {
throw new CancelledError();
}
if (this.isPaused) {
throw PAUSEED;
}
this._progress = this.pointer;
this.update(1);
}
}
async abort(isCancelled) {
this._abort();
}
isAbortedError(e) {
return e === PAUSEED;
}
};
// forge.ts
var DEFAULT_FORGE_MAVEN = "http://files.minecraftforge.net/maven";
var DownloadForgeInstallerTask = class extends DownloadTask {
installJarPath;
constructor(forgeVersion, installer, minecraft, options) {
var _a;
const path = installer ? installer.path : `net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-installer.jar`;
let url;
if (installer) {
try {
const parsedUrl = new URL(path);
url = parsedUrl.toString();
} catch (e) {
const forgeMavenPath = path.replace("/maven", "").replace("maven", "");
url = joinUrl(DEFAULT_FORGE_MAVEN, forgeMavenPath);
}
} else {
const forgeMavenPath = path.replace("/maven", "").replace("maven", "");
url = joinUrl(DEFAULT_FORGE_MAVEN, forgeMavenPath);
}
const library = VersionJson3.resolveLibrary({
name: `net.minecraftforge:forge:${forgeVersion}:installer`,
downloads: {
artifact: {
url,
path: `net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-installer.jar`,
size: -1,
sha1: (installer == null ? void 0 : installer.sha1) || ""
}
}
});
const mavenHost = options.mavenHost ? normalizeArray(options.mavenHost) : [];
if (mavenHost.indexOf(DEFAULT_FORGE_MAVEN) === -1) {
mavenHost.push(DEFAULT_FORGE_MAVEN);
}
const urls = resolveLibraryDownloadUrls(library, { ...options, mavenHost });
const installJarPath = minecraft.getLibraryByPath(library.path);
super({
url: urls,
destination: installJarPath,
validator: (installer == null ? void 0 : installer.sha1) ? ((_a = options.checksumValidatorResolver) == null ? void 0 : _a.call(options, { algorithm: "sha1", hash: installer == null ? void 0 : installer.sha1 })) || { algorithm: "sha1", hash: installer == null ? void 0 : installer.sha1 } : new ZipValidator(),
agent: options.agent,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this.installJarPath = installJarPath;
this.name = "downloadInstaller";
this.param = { version: forgeVersion };
}
};
function getLibraryPathWithoutMaven(mc, name) {
return mc.getLibraryByPath(name.substring(name.indexOf("/") + 1));
}
function extractEntryTo(zip, e, dest) {
return openEntryReadStream(zip, e).then((stream) => pipeline(stream, createWriteStream(dest)));
}
async function installLegacyForgeFromZip(zip, entries, profile, mc, jarFilePath, options) {
const versionJson = profile.versionInfo;
if (!versionJson) {
throw new Error(`Malform legacy installer json ${profile.version}`);
}
versionJson.id = options.versionId || versionJson.id;
versionJson.inheritsFrom = options.inheritsFrom || versionJson.inheritsFrom;
const rootPath = mc.getVersionRoot(versionJson.id);
const versionJsonPath = join3(rootPath, `${versionJson.id}.json`);
await ensureFile(versionJsonPath);
const forgeLib = versionJson.libraries.find((l) => l.name.startsWith("net.minecraftforge:forge") || l.name.startsWith("net.minecraftforge:minecraftforge"));
if (!forgeLib) {
throw new BadForgeInstallerJarError(jarFilePath);
}
const library = LibraryInfo2.resolve(forgeLib);
const jarPath = mc.getLibraryByPath(library.path);
await ensureFile(jarPath);
await Promise.all([
writeFile4(versionJsonPath, JSON.stringify(versionJson, void 0, 4)),
extractEntryTo(zip, entries.legacyUniversalJar, jarPath)
]);
return versionJson.id;
}
async function unpackForgeInstaller(zip, entries, forgeVersion, profile, mc, jarPath, options) {
const versionJson = await readEntry2(zip, entries.versionJson).then((b) => b.toString()).then(JSON.parse);
versionJson.id = options.versionId || versionJson.id;
versionJson.inheritsFrom = options.inheritsFrom || versionJson.inheritsFrom;
const rootPath = mc.getVersionRoot(versionJson.id);
const versionJsonPath = join3(rootPath, `${versionJson.id}.json`);
const installJsonPath = join3(rootPath, "install_profile.json");
const dataRoot = dirname3(jarPath);
const unpackData = (entry) => {
promises.push(extractEntryTo(zip, entry, join3(dataRoot, entry.fileName.substring("data/".length))));
};
await ensureFile(versionJsonPath);
const promises = [];
if (entries.forgeUniversalJar) {
promises.push(extractEntryTo(zip, entries.forgeUniversalJar, getLibraryPathWithoutMaven(mc, entries.forgeUniversalJar.fileName)));
}
if (!profile.data) {
profile.data = {};
}
const installerMaven = `net.minecraftforge:forge:${forgeVersion}:installer`;
profile.data.INSTALLER = {
client: `[${installerMaven}]`,
server: `[${installerMaven}]`
};
if (entries.serverLzma) {
const serverMaven = `net.minecraftforge:forge:${forgeVersion}:serverdata@lzma`;
profile.data.BINPATCH.server = `[${serverMaven}]`;
const serverBinPath = mc.getLibraryByPath(LibraryInfo2.resolve(serverMaven).path);
await ensureFile(serverBinPath);
promises.push(extractEntryTo(zip, entries.serverLzma, serverBinPath));
}
if (entries.clientLzma) {
const clientMaven = `net.minecraftforge:forge:${forgeVersion}:clientdata@lzma`;
profile.data.BINPATCH.client = `[${clientMaven}]`;
const clientBinPath = mc.getLibraryByPath(LibraryInfo2.resolve(clientMaven).path);
await ensureFile(clientBinPath);
promises.push(extractEntryTo(zip, entries.clientLzma, clientBinPath));
}
if (entries.forgeJar) {
promises.push(extractEntryTo(zip, entries.forgeJar, getLibraryPathWithoutMaven(mc, entries.forgeJar.fileName)));
}
if (entries.runBat) {
unpackData(entries.runBat);
}
if (entries.runSh) {
unpackData(entries.runSh);
}
if (entries.winArgs) {
unpackData(entries.winArgs);
}
if (entries.unixArgs) {
unpackData(entries.unixArgs);
}
if (entries.userJvmArgs) {
unpackData(entries.userJvmArgs);
}
promises.push(
writeFile4(installJsonPath, JSON.stringify(profile)),
writeFile4(versionJsonPath, JSON.stringify(versionJson))
);
await Promise.all(promises);
return versionJson.id;
}
function isLegacyForgeInstallerEntries(entries) {
return !!entries.legacyUniversalJar && !!entries.installProfileJson;
}
function isForgeInstallerEntries(entries) {
return !!entries.installProfileJson && !!entries.versionJson;
}
async function walkForgeInstallerEntries(zip, forgeVersion) {
const [forgeJar, forgeUniversalJar, clientLzma, serverLzma, installProfileJson, versionJson, legacyUniversalJar, runSh, runBat, unixArgs, userJvmArgs, winArgs] = await filterEntries(zip, [
`maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}.jar`,
`maven/net/minecraftforge/forge/${forgeVersion}/forge-${forgeVersion}-universal.jar`,
"data/client.lzma",
"data/server.lzma",
"install_profile.json",
"version.json",
(e) => e.fileName === `forge-${forgeVersion}-universal.jar` || e.fileName.startsWith("forge-") && e.fileName.endsWith("-universal.jar") || e.fileName.startsWith("minecraftforge-universal-"),
// legacy installer format
"data/run.sh",
"data/run.bat",
"data/unix_args.txt",
"data/user_jvm_args.txt",
"data/win_args.txt"
]);
return {
forgeJar,
forgeUniversalJar,
clientLzma,
serverLzma,
installProfileJson,
versionJson,
legacyUniversalJar,
runSh,
runBat,
unixArgs,
userJvmArgs,
winArgs
};
}
var BadForgeInstallerJarError = class extends Error {
constructor(jarPath, entry) {
super(entry ? `Missing entry ${entry} in forge installer jar: ${jarPath}` : `Bad forge installer: ${jarPath}`);
this.jarPath = jarPath;
this.entry = entry;
}
name = "BadForgeInstallerJarError";
};
function installByInstallerTask(version, minecraft, options) {
return task4("installForge", async function() {
function getForgeArtifactVersion() {
const [_, minor] = version.mcversion.split(".");
const minorVersion = Number.parseInt(minor);
if (minorVersion >= 7 && minorVersion <= 8) {
return `${version.mcversion}-${version.version}-${version.mcversion}`;
}
if (version.version.startsWith(version.mcversion)) {
return version.version;
}
return `${version.mcversion}-${version.version}`;
}
const forgeVersion = getForgeArtifactVersion();
const mc = MinecraftFolder5.from(minecraft);
const jarPath = await this.yield(new DownloadForgeInstallerTask(forgeVersion, version.installer, mc, options).map(function() {
return this.installJarPath;
}));
const zip = await open3(jarPath, { lazyEntries: true, autoClose: false });
const entries = await walkForgeInstallerEntries(zip, forgeVersion);
if (!entries.installProfileJson) {
throw new BadForgeInstallerJarError(jarPath, "install_profile.json");
}
const profile = await readEntry2(zip, entries.installProfileJson).then((b) => b.toString()).then(JSON.parse);
if (isForgeInstallerEntries(entries)) {
const versionId = await unpackForgeInstaller(zip, entries, forgeVersion, profile, mc, jarPath, options);
await this.concat(installByProfileTask(profile, minecraft, options));
return versionId;
} else if (isLegacyForgeInstallerEntries(entries)) {
return installLegacyForgeFromZip(zip, entries, profile, mc, jarPath, options);
} else {
throw new BadForgeInstallerJarError(jarPath);
}
});
}
function installForge(version, minecraft, options) {
return installForgeTask(version, minecraft, options).startAndWait();
}
function installForgeTask(version, minecraft, options = {}) {
return installByInstallerTask(version, minecraft, options);
}
async function getForgeVersionList(options = {}) {
const mcversion = options.minecraft || "";
const url = mcversion === "" ? "http://files.minecraftforge.net/maven/net/minecraftforge/forge/index.html" : `http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_${mcversion}.html`;
const response = await request4(url, {
dispatcher: options.dispatcher,
maxRedirections: 3
});
const body = parseForge(await response.body.text());
return body;
}
// neoForged.ts
import { MinecraftFolder as MinecraftFolder6, Version as VersionJson4 } from "@xmcl/core";
import { open as open4, readEntry as readEntry3 } from "@xmcl/unzip";
import { task as task5 } from "@xmcl/task";
var DownloadNeoForgedInstallerTask = class extends DownloadTask {
installJarPath;
constructor(project, version, minecraft, options) {
const url = `https://maven.neoforged.net/releases/net/neoforged/${project}/${version}/${project}-${version}-installer.jar`;
const library = VersionJson4.resolveLibrary({
name: `net.neoforged:${project}:${version}:installer`,
downloads: {
artifact: {
url,
path: `net/neoforged/${project}/${version}/${project}-${version}-installer.jar`,
size: -1,
sha1: ""
}
}
});
const mavenHost = options.mavenHost ? normalizeArray(options.mavenHost) : [];
const urls = resolveLibraryDownloadUrls(library, { ...options, mavenHost });
const installJarPath = minecraft.getLibraryByPath(library.path);
super({
url: urls,
destination: installJarPath,
validator: new ZipValidator(),
agent: options.agent,
skipPrevalidate: options.skipPrevalidate,
skipRevalidate: options.skipRevalidate
});
this.installJarPath = installJarPath;
this.name = "downloadInstaller";
this.param = { version };
}
};
async function installNeoForged(project, version, minecraft, options) {
return installNeoForgedTask(project, version, minecraft, options).startAndWait();
}
function installNeoForgedTask(project, version, minecraft, options) {
return task5("installForge", async function() {
const [_, forgeVersion] = version.split("-");
const mc = MinecraftFolder6.from(minecraft);
const jarPath = await this.yield(new DownloadNeoForgedInstallerTask(project, version, mc, options).map(function() {
return this.installJarPath;
}));
const zip = await open4(jarPath, { lazyEntries: true, autoClose: false });
const entries = await walkForgeInstallerEntries(zip, forgeVersion);
if (!entries.installProfileJson) {
throw new BadForgeInstallerJarError(jarPath, "install_profile.json");
}
const profile = await readEntry3(zip, entries.installProfileJson).then((b) => b.toString()).then(JSON.parse);
if (isForgeInstallerEntries(entries)) {
const versionId = await unpackForgeInstaller(zip, entries, forgeVersion, profile, mc, jarPath, options);
await this.concat(installByProfileTask(profile, minecraft, options));
return versionId;
} else {
throw new BadForgeInstallerJarError(jarPath);
}
});
}
// optifine.ts
import { ClassReader, ClassVisitor, Opcodes } from "@xmcl/asm";
import { MinecraftFolder as MinecraftFolder7 } from "@xmcl/core";
import { task as task6 } from "@xmcl/task";
import { getEntriesRecord, open as open5, readAllEntries, readEntry as readEntry4 } from "@xmcl/unzip";
import { writeFile as writeFile5 } from "fs/promises";
function generateOptifineVersion(editionRelease, minecraftVersion, launchWrapperVersion, options = {}) {
const id = options.versionId ?? `${minecraftVersion}-Optifine_${editionRelease}`;
const inheritsFrom = options.inheritsFrom ?? minecraftVersion;
const mainClass = "net.minecraft.launchwrapper.Launch";
const libraries = [{ name: `optifine:Optifine:${minecraftVersion}_${editionRelease}` }];
if (launchWrapperVersion) {
libraries.unshift({ name: `optifine:launchwrapper-of:${launchWrapperVersion}` });
} else {
libraries.unshift({ name: "net.minecraft:launchwrapper:1.12" });
}
return {
id,
inheritsFrom,
arguments: {
game: ["--tweakClass", options.useForgeTweaker ? "optifine.OptiFineForgeTweaker" : "optifine.OptiFineTweaker"],
jvm: []
},
releaseTime: (/* @__PURE__ */ new Date()).toJSON(),
time: (/* @__PURE__ */ new Date()).toJSON(),
type: "release",
libraries,
mainClass,
minimumLauncherVersion: 21
};
}
function installOptifine(installer, minecraft, options) {
return installOptifineTask(installer, minecraft, options).startAndWait();
}
var BadOptifineJarError = class extends Error {
constructor(optifine, entry) {
super(`Missing entry ${entry} in optifine installer: ${optifine}`);
this.optifine = optifine;
this.entry = entry;
}
error = "BadOptifineJarError";
};
function installOptifineTask(installer, minecraft, options = {}) {
return task6("installOptifine", async function() {
const mc = MinecraftFolder7.from(minecraft);
const zip = await open5(installer);
const entries = await readAllEntries(zip);
const record = getEntriesRecord(entries);
const entry = record["net/optifine/Config.class"] ?? record["Config.class"] ?? record["notch/net/optifine/Config.class"];
if (!entry) {
throw new BadOptifineJarError(installer, "net/optifine/Config.class");
}
const launchWrapperVersionEntry = record["launchwrapper-of.txt"];
const launchWrapperVersion = launchWrapperVersionEntry ? await readEntry4(zip, launchWrapperVersionEntry).then((b) => b.toString()) : void 0;
const buf = await readEntry4(zip, entry);
const reader = new ClassReader(buf);
class OptifineVisitor extends ClassVisitor {
fields = {};
visitField(access2, name, desc, signature, value) {
this.fields[name] = value;
return null;
}
}
const visitor = new OptifineVisitor(Opcodes.ASM5);
reader.accept(visitor);
const mcversion = visitor.fields.MC_VERSION;
const edition = visitor.fields.OF_EDITION;
const release = visitor.fields.OF_RELEASE;
const editionRelease = edition + "_" + release;
const versionJSON = generateOptifineVersion(editionRelease, mcversion, launchWrapperVersion, options);
const versionJSONPath = mc.getVersionJson(versionJSON.id);
await this.yield(task6("json", async () => {
await ensureFile(versionJSONPath);
await writeFile5(versionJSONPath, JSON.stringify(versionJSON, null, 4));
}));
const launchWrapperEntry = record[`launchwrapper-of-${launchWrapperVersion}.jar`];
if (launchWrapperEntry) {
await this.yield(task6("library", async () => {
const wrapperDest = mc.getLibraryByPath(`optifine/launchwrapper-of/${launchWrapperVersion}/launchwrapper-of-${launchWrapperVersion}.jar`);
await ensureFile(wrapperDest);
await writeFile5(wrapperDest, await readEntry4(zip, launchWrap