First pass at Claritas implementation.

The resolution logic was reworked so that Claritas only needs to be invoked once per supported type, ie only once for ForgeMod and LiteMod resolutions per server. The resolver now uses identifies module candidates and collects them. Claritas is invoked and the resulting metadata is stored. The module resolution then proceeds with all of this data available.
Toggleable module logic was also reworked to first accumulate all candidates and then process. This required the resolution function to optionally take a preprocess and postprocess callback to perform the necessary cleanup and transformations.
The minor rework was necessary because spawning child process is expensive, and we should only do it as often as we must to keep the application performant.
Claritas resolution also supports exceptions defined by the structure class. This is to facilitate handling of special cases (ex. Optifine).
This commit is contained in:
Daniel Scalzi
2020-07-14 23:12:59 -04:00
parent 7620971c99
commit bda96eb24e
26 changed files with 447 additions and 203 deletions

View File

@@ -0,0 +1,6 @@
export enum LibraryType {
FORGE = 'FORGE',
LITELOADER = 'LITELOADER'
}

View File

@@ -0,0 +1,26 @@
export interface ClaritasModuleMetadata {
/**
* Present on ForgeMods
*/
id?: string
/**
* Always Present
*/
group: string
/**
* Possibly present on ForgeMods 1.12-
*/
version?: string
/**
* Possibly present on ForgeMods 1.12-
*/
name?: string
}
export interface ClaritasResult {
[jarPath: string]: ClaritasModuleMetadata | undefined
}

View File

@@ -1,9 +1,12 @@
import { mkdirs } from 'fs-extra'
import { join, resolve } from 'path'
import { FileStructure } from './FileStructure'
import { Logger } from 'winston'
import { LoggerUtil } from '../../util/LoggerUtil'
export abstract class BaseFileStructure implements FileStructure {
protected logger: Logger
protected containerDirectory: string
constructor(
@@ -13,10 +16,13 @@ export abstract class BaseFileStructure implements FileStructure {
) {
this.relativeRoot = join(relativeRoot, structRoot)
this.containerDirectory = resolve(absoluteRoot, structRoot)
this.logger = LoggerUtil.getLogger(this.getLoggerName())
}
public async init(): Promise<void> {
mkdirs(this.containerDirectory)
}
public abstract getLoggerName(): string
}

View File

@@ -5,15 +5,21 @@ import { resolve as resolveURL } from 'url'
import { ModuleStructure } from './module.struct'
import { readdir, stat } from 'fs-extra'
import { join, resolve, sep } from 'path'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
export class MiscFileStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'files', baseUrl, Type.File)
super(absoluteRoot, relativeRoot, 'files', baseUrl, minecraftVersion, Type.File)
}
public getLoggerName(): string {
return 'MiscFileStructure'
}
public async getSpecModel(): Promise<Module[]> {

View File

@@ -5,6 +5,8 @@ import { resolve } from 'url'
import { VersionSegmented } from '../../../../util/VersionSegmented'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
import { ToggleableModuleStructure } from './toggleablemodule.struct'
import { LibraryType } from '../../../claritas/ClaritasLibraryType'
import { ClaritasException } from './module.struct'
export abstract class BaseForgeModStructure extends ToggleableModuleStructure implements VersionSegmented {
@@ -13,9 +15,10 @@ export abstract class BaseForgeModStructure extends ToggleableModuleStructure im
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, Type.ForgeMod)
super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, minecraftVersion, Type.ForgeMod)
}
public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean
@@ -29,4 +32,21 @@ export abstract class BaseForgeModStructure extends ToggleableModuleStructure im
return null
}
protected getClaritasExceptions(): ClaritasException[] {
return [{
exceptionName: 'optifine',
proxyMetadata: {
group: 'net.optifine'
}
}]
}
protected getClaritasType(): LibraryType {
return LibraryType.FORGE
}
protected discernResult(claritasValue: string | undefined, crudeInference: string): string {
return (claritasValue == null || claritasValue == '') ? crudeInference : claritasValue
}
}

View File

@@ -5,12 +5,9 @@ import { VersionUtil } from '../../../../../util/versionutil'
import { ModsToml } from '../../../../forge/modstoml'
import { BaseForgeModStructure } from '../forgemod.struct'
import { MinecraftVersion } from '../../../../../util/MinecraftVersion'
import { LoggerUtil } from '../../../../../util/LoggerUtil'
export class ForgeModStructure113 extends BaseForgeModStructure {
private static readonly logger = LoggerUtil.getLogger('ForgeModStructure (1.13)')
public static readonly IMPLEMENTATION_VERSION_REGEX = /^Implementation-Version: (.+)[\r\n]/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -23,18 +20,23 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, baseUrl)
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion)
}
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return ForgeModStructure113.isForVersion(version, libraryVersion)
}
public getLoggerName(): string {
return 'ForgeModStructure (1.13)'
}
protected async getModuleId(name: string, path: string): Promise<string> {
const fmData = await this.getForgeModMetadata(name, path)
return this.generateMavenIdentifier(fmData.mods[0].modId, fmData.mods[0].version)
return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.mods[0].modId, fmData.mods[0].version)
}
protected async getModuleName(name: string, path: string): Promise<string> {
return capitalize((await this.getForgeModMetadata(name, path)).mods[0].displayName)
@@ -52,7 +54,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
zip.on('error', err => reject(err))
zip.on('ready', () => {
try {
const res = this.processZip(zip, name)
const res = this.processZip(zip, name, path)
zip.close()
resolve(res)
return
@@ -71,7 +73,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
})
}
private processZip(zip: StreamZip, name: string): ModsToml {
private processZip(zip: StreamZip, name: string, path: string): ModsToml {
// Optifine is a tweak that can be loaded as a forge mod. It does not
// appear to contain a mcmod.info class. This a special case we will
@@ -116,12 +118,21 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
const parsed = toml.parse(raw.toString()) as ModsToml
this.forgeModMetadata[name] = parsed
} catch (err) {
ForgeModStructure113.logger.error(`ForgeMod ${name} contains an invalid mods.toml file.`)
this.logger.error(`ForgeMod ${name} contains an invalid mods.toml file.`)
}
} else {
ForgeModStructure113.logger.error(`ForgeMod ${name} does not contain mods.toml file.`)
this.logger.error(`ForgeMod ${name} does not contain mods.toml file.`)
}
const cRes = this.claritasResult?.[path]
if(cRes == null) {
this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`)
this.logger.error('Is this mod malformated or does Claritas need an update?')
}
const claritasId = cRes?.id
const crudeInference = this.attemptCrudeInference(name)
if(this.forgeModMetadata[name] != null) {
@@ -130,7 +141,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
for(const entry of x.mods) {
if(entry.modId === this.EXAMPLE_MOD_ID) {
entry.modId = crudeInference.name.toLowerCase()
entry.modId = this.discernResult(claritasId, crudeInference.name.toLowerCase())
entry.displayName = crudeInference.name
}
@@ -139,16 +150,16 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
try {
const manifest = zip.entryDataSync('META-INF/MANIFEST.MF')
const keys = manifest.toString().split('\n')
ForgeModStructure113.logger.debug(keys)
this.logger.debug(keys)
for (const key of keys) {
const match = ForgeModStructure113.IMPLEMENTATION_VERSION_REGEX.exec(key)
if (match != null) {
version = match[1]
}
}
ForgeModStructure113.logger.debug(`ForgeMod ${name} contains a version wildcard, inferring ${version}`)
this.logger.debug(`ForgeMod ${name} contains a version wildcard, inferring ${version}`)
} catch {
ForgeModStructure113.logger.debug(`ForgeMod ${name} contains a version wildcard yet no MANIFEST.MF.. Defaulting to ${version}`)
this.logger.debug(`ForgeMod ${name} contains a version wildcard yet no MANIFEST.MF.. Defaulting to ${version}`)
}
entry.version = version
}
@@ -159,7 +170,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
modLoader: 'javafml',
loaderVersion: '',
mods: [{
modId: crudeInference.name.toLowerCase(),
modId: this.discernResult(claritasId, crudeInference.name.toLowerCase()),
version: crudeInference.version,
displayName: crudeInference.name,
description: ''

View File

@@ -5,12 +5,9 @@ import { McModInfo } from '../../../../forge/mcmodinfo'
import { McModInfoList } from '../../../../forge/mcmodinfolist'
import { BaseForgeModStructure } from '../forgemod.struct'
import { MinecraftVersion } from '../../../../../util/MinecraftVersion'
import { LoggerUtil } from '../../../../../util/LoggerUtil'
export class ForgeModStructure17 extends BaseForgeModStructure {
private static readonly logger = LoggerUtil.getLogger('ForgeModStructure (1.7)')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12])
@@ -21,18 +18,23 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, baseUrl)
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion)
}
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
return ForgeModStructure17.isForVersion(version, libraryVersion)
}
public getLoggerName(): string {
return 'ForgeModStructure (1.7)'
}
protected async getModuleId(name: string, path: string): Promise<string> {
const fmData = await this.getForgeModMetadata(name, path)
return this.generateMavenIdentifier(fmData.modid, fmData.version)
return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.modid, fmData.version)
}
protected async getModuleName(name: string, path: string): Promise<string> {
return capitalize((await this.getForgeModMetadata(name, path)).name)
@@ -50,7 +52,7 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
zip.on('error', err => reject(err))
zip.on('ready', () => {
try {
const res = this.processZip(zip, name)
const res = this.processZip(zip, name, path)
zip.close()
resolve(res)
return
@@ -69,7 +71,7 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
})
}
private processZip(zip: StreamZip, name: string): McModInfo {
private processZip(zip: StreamZip, name: string, path: string): McModInfo {
// Optifine is a tweak that can be loaded as a forge mod. It does not
// appear to contain a mcmod.info class. This a special case we will
// account for.
@@ -113,38 +115,50 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
}
} catch (err) {
ForgeModStructure17.logger.error(`ForgeMod ${name} contains an invalid mcmod.info file.`)
this.logger.error(`ForgeMod ${name} contains an invalid mcmod.info file.`)
}
} else {
ForgeModStructure17.logger.error(`ForgeMod ${name} does not contain mcmod.info file.`)
this.logger.error(`ForgeMod ${name} does not contain mcmod.info file.`)
}
const cRes = this.claritasResult[path]
if(cRes == null) {
this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`)
this.logger.error('Is this mod malformated or does Claritas need an update?')
}
const claritasId = cRes?.id
const claritasVersion = cRes?.version
const claritasName = cRes?.name
// Validate
const crudeInference = this.attemptCrudeInference(name)
if(this.forgeModMetadata[name] != null) {
const x = this.forgeModMetadata[name]!
if(x.modid == null || x.modid === '' || x.modid === this.EXAMPLE_MOD_ID) {
x.modid = crudeInference.name.toLowerCase()
x.name = crudeInference.name
x.modid = this.discernResult(claritasId, crudeInference.name.toLowerCase())
x.name = this.discernResult(claritasName, crudeInference.name)
}
// Ex. @VERSION@, ${version}
if(this.forgeModMetadata[name]!.version != null) {
const isVersionWildcard = this.forgeModMetadata[name]!.version.indexOf('@') > -1 || this.forgeModMetadata[name]!.version.indexOf('$') > -1
if(isVersionWildcard) {
x.version = crudeInference.version
x.version = this.discernResult(claritasVersion, crudeInference.version)
}
} else {
x.version = crudeInference.version
x.version = this.discernResult(claritasVersion, crudeInference.version)
}
} else {
this.forgeModMetadata[name] = ({
modid: crudeInference.name.toLowerCase(),
name: crudeInference.name,
version: crudeInference.version
modid: this.discernResult(claritasId, crudeInference.name.toLowerCase()),
name: this.discernResult(claritasName, crudeInference.name),
version: this.discernResult(claritasVersion, crudeInference.version)
}) as McModInfo
}

View File

@@ -3,23 +3,29 @@ import { Type, TypeMetadata } from 'helios-distribution-types'
import { Stats } from 'fs-extra'
import { join } from 'path'
import { resolve } from 'url'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
export class LibraryStructure extends ModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'libraries', baseUrl, Type.Library, (name: string) => {
super(absoluteRoot, relativeRoot, 'libraries', baseUrl, minecraftVersion, Type.Library, (name: string) => {
return name.toLowerCase().endsWith(TypeMetadata[this.type].defaultExtension!)
})
}
public getLoggerName(): string {
return 'LibraryStructure'
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleId(name: string, path: string): Promise<string> {
const inference = this.attemptCrudeInference(name)
return this.generateMavenIdentifier(inference.name, inference.version)
return this.generateMavenIdentifier(this.getDefaultGroup(), inference.name, inference.version)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async getModuleName(name: string, path: string): Promise<string> {

View File

@@ -6,6 +6,8 @@ import { resolve } from 'url'
import { capitalize } from '../../../../util/stringutils'
import { LiteMod } from '../../../liteloader/litemod'
import { ToggleableModuleStructure } from './toggleablemodule.struct'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
import { LibraryType } from '../../../claritas/ClaritasLibraryType'
export class LiteModStructure extends ToggleableModuleStructure {
@@ -14,14 +16,19 @@ export class LiteModStructure extends ToggleableModuleStructure {
constructor(
absoluteRoot: string,
relativeRoot: string,
baseUrl: string
baseUrl: string,
minecraftVersion: MinecraftVersion
) {
super(absoluteRoot, relativeRoot, 'litemods', baseUrl, Type.LiteMod)
super(absoluteRoot, relativeRoot, 'litemods', baseUrl, minecraftVersion, Type.LiteMod)
}
public getLoggerName(): string {
return 'LiteModStructure'
}
protected async getModuleId(name: string, path: string): Promise<string> {
const liteModData = await this.getLiteModMetadata(name, path)
return this.generateMavenIdentifier(liteModData.name, `${liteModData.version}-${liteModData.mcversion}`)
return this.generateMavenIdentifier(this.getClaritasGroup(path), liteModData.name, `${liteModData.version}-${liteModData.mcversion}`)
}
protected async getModuleName(name: string, path: string): Promise<string> {
return capitalize((await this.getLiteModMetadata(name, path)).name)
@@ -35,6 +42,10 @@ export class LiteModStructure extends ToggleableModuleStructure {
return null
}
protected getClaritasType(): LibraryType {
return LibraryType.LITELOADER
}
private getLiteModMetadata(name: string, path: string): Promise<LiteMod> {
return new Promise((resolve, reject) => {
if (!Object.prototype.hasOwnProperty.call(this.liteModMetadata, name)) {

View File

@@ -3,17 +3,35 @@ import { lstat, pathExists, readdir, readFile, Stats } from 'fs-extra'
import { Module, Type, TypeMetadata } from 'helios-distribution-types'
import { resolve } from 'path'
import { BaseModelStructure } from '../basemodel.struct'
import { LibraryType } from '../../../claritas/ClaritasLibraryType'
import { ClaritasResult, ClaritasModuleMetadata } from '../../../claritas/ClaritasResult'
import { ClaritasWrapper } from '../../../../util/java/ClaritasWrapper'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
export interface ModuleCandidate {
file: string
filePath: string
stats: Stats
}
export interface ClaritasException {
exceptionName: string
proxyMetadata: ClaritasModuleMetadata
}
export abstract class ModuleStructure extends BaseModelStructure<Module> {
private readonly crudeRegex = /(.+?)-(.+).[jJ][aA][rR]/
protected readonly DEFAULT_VERSION = '0.0.0'
protected claritasResult!: ClaritasResult
constructor(
absoluteRoot: string,
relativeRoot: string,
structRoot: string,
baseUrl: string,
protected minecraftVersion: MinecraftVersion,
protected type: Type,
protected filter?: ((name: string, path: string, stats: Stats) => boolean)
) {
@@ -22,14 +40,18 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
public async getSpecModel(): Promise<Module[]> {
if (this.resolvedModels == null) {
this.resolvedModels = await this._doModuleRetrieval(this.containerDirectory)
this.resolvedModels = await this._doModuleRetrieval(await this._doModuleDiscovery(this.containerDirectory))
}
return this.resolvedModels
}
protected generateMavenIdentifier(name: string, version: string): string {
return `generated.${this.type.toLowerCase()}:${name}:${version}@${TypeMetadata[this.type].defaultExtension}`
protected getDefaultGroup(): string {
return `generated.${this.type.toLowerCase()}`
}
protected generateMavenIdentifier(group: string, id: string, version: string): string {
return `${group}:${id}:${version}@${TypeMetadata[this.type].defaultExtension}`
}
protected attemptCrudeInference(name: string): { name: string, version: string } {
@@ -47,6 +69,18 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
}
}
protected getClaritasGroup(path: string): string {
return this.claritasResult[path]?.group || this.getDefaultGroup()
}
protected getClaritasExceptions(): ClaritasException[] {
return []
}
protected getClaritasType(): LibraryType | null {
return null
}
protected async abstract getModuleId(name: string, path: string): Promise<string>
protected async abstract getModuleName(name: string, path: string): Promise<string>
protected async abstract getModuleUrl(name: string, path: string, stats: Stats): Promise<string>
@@ -71,9 +105,9 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
return mdl
}
protected async _doModuleRetrieval(scanDirectory: string): Promise<Module[]> {
protected async _doModuleDiscovery(scanDirectory: string): Promise<ModuleCandidate[]> {
const accumulator: Module[] = []
const moduleCandidates: ModuleCandidate[] = []
if (await pathExists(scanDirectory)) {
const files = await readdir(scanDirectory)
@@ -82,13 +116,63 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
const stats = await lstat(filePath)
if (stats.isFile()) {
if(this.filter == null || this.filter(file, filePath, stats)) {
accumulator.push(await this.parseModule(file, filePath, stats))
moduleCandidates.push({file, filePath, stats})
}
}
}
}
return moduleCandidates
}
protected async _doModuleRetrieval(moduleCandidates: ModuleCandidate[], options?: {
preProcess?: (candidate: ModuleCandidate) => void
postProcess?: (module: Module) => void
}): Promise<Module[]> {
const accumulator: Module[] = []
if(moduleCandidates.length > 0) {
// Invoke Claritas
if(this.getClaritasType() != null) {
const claritasExecutor = new ClaritasWrapper()
let claritasCandidates = moduleCandidates
const exceptionCandidates: [ModuleCandidate, ClaritasException][] = []
for(const exception of this.getClaritasExceptions()) {
const exceptionCandidate = moduleCandidates.find((value) => value.file.toLowerCase().indexOf(exception.exceptionName) > -1)
if(exceptionCandidate != null) {
exceptionCandidates.push([exceptionCandidate, exception])
claritasCandidates = claritasCandidates.filter((value) => value.file.toLowerCase().indexOf(exception.exceptionName) === -1)
}
}
this.claritasResult = await claritasExecutor.execute(
this.getClaritasType()!,
this.minecraftVersion,
claritasCandidates.map(entry => entry.filePath)
)
if(this.claritasResult == null) {
this.logger.error('Failed to process Claritas result!')
} else {
for(const [candidate, exception] of exceptionCandidates) {
this.claritasResult[candidate.filePath] = exception.proxyMetadata
}
}
}
// Process Modules
for(const candidate of moduleCandidates) {
options?.preProcess?.(candidate)
const mdl = await this.parseModule(candidate.file, candidate.filePath, candidate.stats)
options?.postProcess?.(mdl)
accumulator.push(mdl)
}
}
return accumulator
}

View File

@@ -1,7 +1,8 @@
import { ModuleStructure } from './module.struct'
import { ModuleStructure, ModuleCandidate } from './module.struct'
import { Type, Module } from 'helios-distribution-types'
import { Stats, mkdirs } from 'fs-extra'
import { resolve } from 'path'
import { MinecraftVersion } from '../../../../util/MinecraftVersion'
export enum ToggleableNamespace {
@@ -11,6 +12,10 @@ export enum ToggleableNamespace {
}
export interface ToggleableModuleCandidate extends ModuleCandidate {
namespace: ToggleableNamespace
}
export abstract class ToggleableModuleStructure extends ModuleStructure {
private activeNamespace: string | undefined
@@ -20,10 +25,11 @@ export abstract class ToggleableModuleStructure extends ModuleStructure {
relativeRoot: string,
structRoot: string,
baseUrl: string,
protected type: Type,
protected filter?: ((name: string, path: string, stats: Stats) => boolean)
minecraftVersion: MinecraftVersion,
type: Type,
filter?: ((name: string, path: string, stats: Stats) => boolean)
) {
super(absoluteRoot, relativeRoot, structRoot, baseUrl, type, filter)
super(absoluteRoot, relativeRoot, structRoot, baseUrl, minecraftVersion, type, filter)
}
public async init(): Promise<void> {
@@ -36,13 +42,21 @@ export abstract class ToggleableModuleStructure extends ModuleStructure {
public async getSpecModel(): Promise<Module[]> {
if (this.resolvedModels == null) {
this.resolvedModels = []
const moduleCandidates: ToggleableModuleCandidate[] = []
for(const value of Object.values(ToggleableNamespace)) {
this.activeNamespace = value
const models = await this._doModuleRetrieval(resolve(this.containerDirectory, value))
models.forEach(this.getNamespaceMapper(value))
this.resolvedModels = this.resolvedModels.concat(models)
moduleCandidates.push(...(await super._doModuleDiscovery(resolve(this.containerDirectory, value))).map(val => ({...val, namespace: value})))
}
this.resolvedModels = await this._doModuleRetrieval(moduleCandidates, {
preProcess: (candidate) => {
this.activeNamespace = (candidate as ToggleableModuleCandidate).namespace
},
postProcess: (module) => {
this.getNamespaceMapper(this.activeNamespace as ToggleableNamespace)(module)
}
})
// Cleanup
this.activeNamespace = undefined
}

View File

@@ -9,12 +9,9 @@ import { MiscFileStructure } from './module/file.struct'
import { LiteModStructure } from './module/litemod.struct'
import { LibraryStructure } from './module/library.struct'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
import { LoggerUtil } from '../../../util/LoggerUtil'
export class ServerStructure extends BaseModelStructure<Server> {
private static readonly logger = LoggerUtil.getLogger('ServerStructure')
private readonly ID_REGEX = /(.+-(.+)$)/
private readonly SERVER_META_FILE = 'servermeta.json'
@@ -25,6 +22,10 @@ export class ServerStructure extends BaseModelStructure<Server> {
super(absoluteRoot, '', 'servers', baseUrl)
}
public getLoggerName(): string {
return 'ServerStructure'
}
public async getSpecModel(): Promise<Server[]> {
if (this.resolvedModels == null) {
this.resolvedModels = await this._doSeverRetrieval()
@@ -45,7 +46,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
const relativeServerRoot = join(this.relativeRoot, effectiveId)
if (await pathExists(absoluteServerRoot)) {
ServerStructure.logger.error('Server already exists! Aborting.')
this.logger.error('Server already exists! Aborting.')
return
}
@@ -66,7 +67,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
}
if (options.liteloaderVersion != null) {
const lms = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const lms = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
await lms.init()
serverMetaOpts.liteloaderVersion = options.liteloaderVersion
}
@@ -74,10 +75,10 @@ export class ServerStructure extends BaseModelStructure<Server> {
const serverMeta: ServerMeta = getDefaultServerMeta(id, minecraftVersion.toString(), serverMetaOpts)
await writeFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), JSON.stringify(serverMeta, null, 2))
const libS = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const libS = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
await libS.init()
const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
await mfs.init()
}
@@ -93,8 +94,8 @@ export class ServerStructure extends BaseModelStructure<Server> {
const match = this.ID_REGEX.exec(file)
if (match == null) {
ServerStructure.logger.warn(`Server directory ${file} does not match the defined standard.`)
ServerStructure.logger.warn('All server ids must end with -<minecraft version> (ex. -1.12.2)')
this.logger.warn(`Server directory ${file} does not match the defined standard.`)
this.logger.warn('All server ids must end with -<minecraft version> (ex. -1.12.2)')
continue
}
@@ -110,7 +111,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
}
if (!iconUrl) {
ServerStructure.logger.warn(`No icon file found for server ${file}.`)
this.logger.warn(`No icon file found for server ${file}.`)
}
// Read server meta
@@ -146,16 +147,16 @@ export class ServerStructure extends BaseModelStructure<Server> {
if(serverMeta.liteloader) {
const liteModStruct = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const liteModStruct = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
const liteModModules = await liteModStruct.getSpecModel()
modules.push(...liteModModules)
}
const libraryStruct = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const libraryStruct = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
const libraryModules = await libraryStruct.getSpecModel()
modules.push(...libraryModules)
const fileStruct = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl)
const fileStruct = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion)
const fileModules = await fileStruct.getSpecModel()
modules.push(...fileModules)
@@ -174,7 +175,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
})
} else {
ServerStructure.logger.warn(`Path ${file} in server directory is not a directory!`)
this.logger.warn(`Path ${file} in server directory is not a directory!`)
}
}
return accumulator

View File

@@ -18,6 +18,10 @@ export class LibRepoStructure extends BaseMavenRepo {
super(absoluteRoot, relativeRoot, 'lib')
}
public getLoggerName(): string {
return 'LibRepoStructure'
}
public getLocalForge(version: string, classifier?: string): string {
return this.getArtifactByComponents(
LibRepoStructure.FORGE_GROUP,

View File

@@ -17,6 +17,10 @@ export class RepoStructure extends BaseFileStructure {
this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot)
}
public getLoggerName(): string {
return 'RepoStructure'
}
public async init(): Promise<void> {
super.init()
await this.libRepoStruct.init()

View File

@@ -12,6 +12,10 @@ export class VersionRepoStructure extends BaseFileStructure {
super(absoluteRoot, relativeRoot, 'versions')
}
public getLoggerName(): string {
return 'VersionRepoStructure'
}
public getFileName(minecraftVersion: MinecraftVersion, forgeVersion: string): string {
return `${minecraftVersion}-forge-${forgeVersion}`
}

View File

@@ -5,7 +5,7 @@ import { basename, join } from 'path'
import { VersionManifestFG2 } from '../../../model/forge/VersionManifestFG2'
import { LibRepoStructure } from '../../../model/struct/repo/librepo.struct'
import { MavenUtil } from '../../../util/maven'
import { PackXZExtractWrapper } from '../../../util/PackXZExtractWrapper'
import { PackXZExtractWrapper } from '../../../util/java/PackXZExtractWrapper'
import { VersionUtil } from '../../../util/versionutil'
import { ForgeResolver } from '../forge.resolver'
import { MinecraftVersion } from '../../../util/MinecraftVersion'
@@ -227,7 +227,8 @@ export class ForgeGradle2Adapter extends ForgeResolver {
}
ForgeGradle2Adapter.logger.debug('Spawning PackXZExtract.')
await PackXZExtractWrapper.extractUnpack(files)
const packXZExecutor = new PackXZExtractWrapper()
await packXZExecutor.extractUnpack(files)
ForgeGradle2Adapter.logger.debug('All files extracted, calculating hashes..')
for (const entry of processingQueue) {

View File

@@ -7,7 +7,7 @@ import { LibRepoStructure } from '../../../model/struct/repo/librepo.struct'
import { pathExists, remove, mkdirs, copy, writeFile, readFile, lstat, move, writeJson } from 'fs-extra'
import { join, basename, dirname } from 'path'
import { spawn } from 'child_process'
import { JavaUtil } from '../../../util/javautil'
import { JavaUtil } from '../../../util/java/javautil'
import { VersionManifestFG3 } from '../../../model/forge/VersionManifestFG3'
import { MavenUtil } from '../../../util/maven'
import { createHash } from 'crypto'

View File

@@ -1,47 +0,0 @@
import { spawn } from 'child_process'
import { join } from 'path'
import { JavaUtil } from './javautil'
import { LoggerUtil } from './LoggerUtil'
export class PackXZExtractWrapper {
private static readonly logger = LoggerUtil.getLogger('PackXZExtract')
public static getPackXZExtract(): string {
return join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
}
public static extractUnpack(paths: string[]): Promise<void> {
return PackXZExtractWrapper.execute('-packxz', paths)
}
public static extract(paths: string[]): Promise<void> {
return PackXZExtractWrapper.execute('-xz', paths)
}
public static unpack(paths: string[]): Promise<void> {
return PackXZExtractWrapper.execute('-pack', paths)
}
private static execute(command: string, paths: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(JavaUtil.getJavaExecutable(), [
'-jar',
PackXZExtractWrapper.getPackXZExtract(),
command,
paths.join(',')
])
child.stdout.on('data', (data) => PackXZExtractWrapper.logger.info(data.toString('utf8').trim()))
child.stderr.on('data', (data) => PackXZExtractWrapper.logger.error(data.toString('utf8').trim()))
child.on('close', code => {
PackXZExtractWrapper.logger.info('Exited with code', code)
resolve()
})
child.on('error', (err) => {
PackXZExtractWrapper.logger.info('Error during process execution', err)
reject(err)
})
})
}
}

View File

@@ -42,7 +42,7 @@ export class VersionSegmentedRegistry {
): BaseForgeModStructure {
for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IML) {
if (impl.isForVersion(minecraftVersion, forgeVersion)) {
return new impl(absoluteRoot, relativeRoot, baseUrl)
return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion)
}
}
throw new Error(`No forge mod structure found for Minecraft ${minecraftVersion}!`)

View File

@@ -0,0 +1,32 @@
import { JarExecutor } from './JarExecutor'
import { join } from 'path'
import { ClaritasResult } from '../../model/claritas/ClaritasResult'
import { MinecraftVersion } from '../MinecraftVersion'
import { LibraryType } from '../../model/claritas/ClaritasLibraryType'
export class ClaritasWrapper extends JarExecutor<ClaritasResult> {
constructor() {
super('Claritas')
this.stdoutListeners.push((data) => {
const clean = data.toString('utf8').trim() as string
const spike = 'results::'
if(clean.startsWith(spike)) {
this.lastExecutionResult = JSON.parse(clean.substr(spike.length)) as ClaritasResult
}
})
}
protected getJarPath(): string {
return join(process.cwd(), 'libraries', 'java', 'Claritas.jar')
}
public execute(libraryType: LibraryType, mcVersion: MinecraftVersion, absoluteJarPaths: string[]): Promise<ClaritasResult> {
return super.executeJar(
'--absoluteJarPaths', absoluteJarPaths.join(','),
'--libraryType', libraryType,
'--mcVersion', mcVersion.toString()
)
}
}

View File

@@ -0,0 +1,48 @@
import { JavaUtil } from './javautil'
import { Logger } from 'winston'
import { spawn } from 'child_process'
import { LoggerUtil } from '../LoggerUtil'
export abstract class JarExecutor<T> {
protected readonly logger: Logger
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected stdoutListeners: ((chunk: any) => void)[] = []
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected stderrListeners: ((chunk: any) => void)[] = []
protected lastExecutionResult!: T
protected constructor(loggerName: string) {
this.logger = LoggerUtil.getLogger(loggerName)
}
protected abstract getJarPath(): string
protected executeJar(...args: string[]): Promise<T> {
this.lastExecutionResult = undefined!
return new Promise((resolve, reject) => {
const child = spawn(JavaUtil.getJavaExecutable(), [
'-jar',
this.getJarPath(),
...args
])
child.stdout.on('data', (data) => this.logger.info(data.toString('utf8').trim()))
this.stdoutListeners.forEach(l => child.stdout.on('data', l))
child.stderr.on('data', (data) => this.logger.error(data.toString('utf8').trim()))
this.stderrListeners.forEach(l => child.stderr.on('data', l))
child.on('close', code => {
this.logger.info('Exited with code', code)
resolve(this.lastExecutionResult)
})
child.on('error', (err) => {
this.logger.info('Error during process execution', err)
reject(err)
})
})
}
}

View File

@@ -0,0 +1,30 @@
import { join } from 'path'
import { JarExecutor } from './JarExecutor'
export class PackXZExtractWrapper extends JarExecutor<void> {
constructor() {
super('PackXZExtract')
}
protected getJarPath(): string {
return join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
}
protected execute(command: string, paths: string[]): Promise<void> {
return super.executeJar(command, paths.join(','))
}
public extractUnpack(paths: string[]): Promise<void> {
return this.execute('-packxz', paths)
}
public extract(paths: string[]): Promise<void> {
return this.execute('-xz', paths)
}
public unpack(paths: string[]): Promise<void> {
return this.execute('-pack', paths)
}
}