UNPKG

@bscotch/stitch-launcher

Version:

Manage GameMaker IDE and runtime installations for fast switching between versions.

281 lines 13.2 kB
var GameMakerIde_1; import { __decorate, __metadata } from "tslib"; import { Pathy } from '@bscotch/pathy'; import { ok } from 'assert'; import { exec } from 'child_process'; import { GameMakerComponent } from './GameMakerComponent.js'; import { cleanVersionString, createStaticTracer, download, listDefaultMacrosPaths, listInstalledIdes, listInstalledRuntimes, runIdeInstaller, setActiveRuntime, trace, } from './utility.js'; import { logger } from './log.js'; export class GameMakerRunningIde { exePath; projectYypPath; runtimeVersion; process; constructor(exePath, projectYypPath, runtimeVersion) { this.exePath = exePath; this.projectYypPath = projectYypPath; this.runtimeVersion = runtimeVersion; this.process = exec(`"${exePath}" "${projectYypPath}"`); } waitForClose(cb) { return new Promise((resolve) => { const events = ['exit', 'close', 'disconnect', 'error']; const callback = () => { events.forEach((event) => this.process.off(event, callback)); return resolve(cb?.()); }; events.forEach((event) => this.process.on(event, callback)); }); } close() { this.process.kill(); } } const ideClassStaticTracer = (methodName) => createStaticTracer('GameMakerIde', methodName); export class GameMakerIdeError extends Error { code; constructor(message, code) { super(message); this.code = code; this.name = 'GameMakerIdeError'; Error.captureStackTrace(this, this.constructor); } } export function assert(claim, message = 'Assertion failed', code = 'UNKNOWN') { if (!claim) { throw new GameMakerIdeError(message, code); } } let GameMakerIde = class GameMakerIde extends GameMakerComponent { static { GameMakerIde_1 = this; } static error = GameMakerIdeError; constructor(info) { super(info); } async openProject(projectYypPath, runtimeVersion, options) { const projectPath = Pathy.asInstance(projectYypPath); const user = await this.activeUser(); assert(user, 'No active user', 'LOGIN_REQUIRED'); const pairedRuntimeVersion = await this.pairedRuntimeVersion(); runtimeVersion ||= pairedRuntimeVersion; assert(runtimeVersion, `Could not find a runtime version for IDE v${this.version}, and none was specified.`, 'RUNTIME_NOT_FOUND'); // Assert that this runtime version is in the feed const release = await GameMakerComponent.findRelease({ runtimeVersion }); assert(release, `Could not find runtime version ${runtimeVersion}`, 'RUNTIME_NOT_FOUND'); // If the runtime version is the paired one, // and it is not already installed, then the IDE // will install on boot and set it to active. // Therefore we can either do NOTHING, or just // need to unset the runtime.json>active field. const installedRuntime = (await listInstalledRuntimes()).find((r) => r.version === runtimeVersion); if (installedRuntime) { await setActiveRuntime(installedRuntime); } assert(installedRuntime || runtimeVersion === pairedRuntimeVersion, `The specified Runtime was neither already installed nor a match for the specified IDE.`, 'RUNTIME_NOT_FOUND'); // Open the project with the given IDE console.log(`Opening project "${projectYypPath}" using IDE v${this.version} with Runtime v${runtimeVersion}.\n\nThe window might take a few seconds to appear, and might not steal focus!\n\nThis command will exit when the IDE is closed.`); // Prevent the IDE from annoying the user with // suggestions to update. if (options?.disableUpdatePrompt) { await GameMakerIde_1.disableUpdatePrompt(); } await GameMakerComponent.ensureOfficialRuntimeFeeds(); return new GameMakerRunningIde(this.executablePath.absolute, projectPath.toString({ format: 'win32' }), runtimeVersion); } /** * Each GameMaker IDE lists the runtime version * that it is paired with. In theory this is the * most-compatible runtime version. */ async pairedRuntimeVersion() { const runtimeVersionFile = this.directory.join('matching.runtime'); // Get the value specified in the actual install (this is the most-correct one) if (await runtimeVersionFile.exists()) { const version = (await runtimeVersionFile.read()).trim(); ok(version.match(/^\d+\.\d+\.\d+\.\d+$/), `Invalid runtime version: ${version}`); return version; } // If that fails, fall back on the version in the feed const release = await GameMakerComponent.findRelease({ ideVersion: this.version, }); assert(release, `Could not find release for IDE v${this.version}`); return release?.runtime.version; } /** * Install the specified IDE version. Only * one IDE version can be installed at a time * (per stable and beta channels), so this * may clobber the currently-installed IDE. * * If this version is already installed, no action * is taken. If this version's installer is already * downloaded, it will not be re-downloaded. If it * is *not* downloaded, then it will be downloaded. */ static async install(version, options) { version = cleanVersionString(version); const release = await GameMakerComponent.findRelease({ ideVersion: version, }); ok(release, `Could not find version ${version} in the IDE feed`); ok(release.ide.link, `Could not find a download link for version ${version}`); let installedVersion = await GameMakerIde_1.findInstalled(version); if (!installedVersion || options?.force) { // See if it's installed to PROGRAMFILES, // just not yet to Stitch. let directlyInstalled = await GameMakerIde_1.findDirectlyInstalled(version, options?.programFiles); if (!directlyInstalled || options?.force) { // Download & install! const installerPath = GameMakerIde_1.cachedIdeInstallerPath(version); await download(release.ide.link, installerPath); await runIdeInstaller(installerPath); // Make sure this version is now installed directlyInstalled = await GameMakerIde_1.findDirectlyInstalled(version, options?.programFiles); ok(directlyInstalled, `Could not find version ${version} after installation. Installation might have gone to an unexpected location or the installer might have failed.`); await installerPath.delete(); } // Copy over to Stitch console.log("Copying installed files to Stitch's cache..."); await directlyInstalled.directory.copy(GameMakerIde_1.cachedIdeDirectory(version)); installedVersion = await GameMakerIde_1.findInstalled(version); } ok(installedVersion, `Could not find version ${version} after installation.`); return installedVersion; } static async findInstalled(version) { const installedIdeVersions = await GameMakerIde_1.listInstalled(); return installedIdeVersions.find((v) => v.version === version); } static async listInstalled() { await GameMakerIde_1.defaultCachedIdeParentDirectory.ensureDirectory(); return await GameMakerIde_1.listInstalledInDir(GameMakerIde_1.defaultCachedIdeParentDirectory); } /** * Prevent the IDE from showing an update prompt on boot. */ static async disableUpdatePrompt() { const macroFilePaths = await listDefaultMacrosPaths(); for (const path of macroFilePaths) { const macros = (await path.exists()) ? await path.read() : {}; const backupPath = path.changeExtension('.bk.json'); if (!(await backupPath.exists())) { console.log('COPYING TO', backupPath.absolute); await path.copy(backupPath); } // Set it to a syntactically correct, but // non-existent RSS feed. macros.updateURI = 'http://gms.yoyogames.com/update-win-NuBeta-TOTALLY-FAKE.rss'; await path.write(macros); } } static async findDirectlyInstalled(version, programFiles) { const installedIdeVersions = await GameMakerIde_1.listDirectlyInstalled(programFiles); return installedIdeVersions.find((v) => v.version === version); } /** * Check PROGRAMFILES for installed IDE versions. These are the result of running * the GameMaker IDE installers. The installed * content end up being separately cached by * Stitch to allow parallel installs * (see {@link listInstalled}). */ static async listDirectlyInstalled(programFiles = process.env.PROGRAMFILES) { return await GameMakerIde_1.listInstalledInDir(programFiles); } static async listInstalledInDir(parentDir) { const tracer = ideClassStaticTracer('listInstalledInDir'); const releases = await GameMakerIde_1.listReleases(); tracer(`Searching for folders with GameMaker .exe files in "${parentDir}"`); const ideExecutables = await listInstalledIdes(parentDir); tracer(`Found ${ideExecutables.length} GameMaker .exe files`); const ideVersions = await Promise.all(ideExecutables.map(async (executablePath) => { const possibleFileNames = /^(IDE|GameMaker(Studio2?)?(-(Beta|LTS))?)\.dll$/; const parentDir = executablePath.up(); tracer(`Parsing version information from GameMaker installation in "${parentDir}"`); const dllFile = (await parentDir.listChildren()).filter((p) => { const match = p.basename.match(possibleFileNames); return !!match; })[0]; ok(await dllFile?.exists(), `Could not find DLL file for ${executablePath}`); // The main DLL file is binary, but it contains // plaintext version strings for the IDE version. const version = (await dllFile.read()).match(/\b((23|2|20\d{2})\.\d{1,4}\.\d{1,4}\.\d{1,4})\b/u)?.[1]; ok(version, `Could not find a version string in ${dllFile.absolute}`); const matchingFeedVersion = releases.find((v) => v.ide.version === version); if (!matchingFeedVersion) { logger.warn(`Found local install of GameMaker ${version}, but that version is not in the feed.`); return; } return new GameMakerIde_1({ version, channel: matchingFeedVersion.channel, executablePath, directory: executablePath.up(), publishedAt: new Date(matchingFeedVersion.publishedAt), feedUrl: matchingFeedVersion.ide.feedUrl, }); })); return ideVersions.filter((v) => v); } /** * The parent folder inside of which all IDE * installs are cached in their own separate * folders. For the path to a specific IDE * version, use {@link cachedIdeDirectory}. */ static get defaultCachedIdeParentDirectory() { return GameMakerIde_1.cacheDir.join('ide'); } static cachedIdeInstallerPath(version) { return GameMakerIde_1.defaultCachedIdeParentDirectory.join(`gamemaker-${version}.exe`); } static cachedIdeDirectory(version) { return GameMakerIde_1.defaultCachedIdeParentDirectory.join(`gamemaker-${version}`); } }; __decorate([ trace, __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], GameMakerIde, "install", null); __decorate([ trace, __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], GameMakerIde, "findInstalled", null); __decorate([ trace, __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], GameMakerIde, "listInstalled", null); __decorate([ trace, __metadata("design:type", Function), __metadata("design:paramtypes", [String, String]), __metadata("design:returntype", Promise) ], GameMakerIde, "findDirectlyInstalled", null); __decorate([ trace, __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], GameMakerIde, "listDirectlyInstalled", null); __decorate([ trace, __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], GameMakerIde, "listInstalledInDir", null); GameMakerIde = GameMakerIde_1 = __decorate([ trace, __metadata("design:paramtypes", [Object]) ], GameMakerIde); export { GameMakerIde }; //# sourceMappingURL=GameMakerIde.js.map