@bscotch/stitch-launcher
Version:
Manage GameMaker IDE and runtime installations for fast switching between versions.
185 lines • 8.05 kB
JavaScript
import { Pathy } from '@bscotch/pathy';
import { arrayWrapped, formatTimestamp } from '@bscotch/utility/browser';
import { spawn } from 'child_process';
import { artifactExtensionForPlatform, currentOs, projectLogDirectory, } from './utility.js';
export async function executeGameMakerRuntimeInstallCommand(runtime, newRuntime) {
return await executeGameMakerCommand(runtime, 'runtime', ['Install', newRuntime.version], {
user: (await runtime.activeUserDirectory()).absolute,
runtimePath: runtime.directory.up().absolute,
runtimeUrl: newRuntime.feedUrl,
verbose: true,
});
}
export async function computeOptions(runtime, options) {
const target = options?.targetPlatform || 'windows';
const projectPath = new Pathy(options.project);
const projectDir = projectPath.up();
const outputDir = new Pathy(options?.outDir || projectDir);
const tempDir = new Pathy(projectDir).join('tmp');
const empath = (p) => `"${p}"`;
return {
project: empath(projectPath),
user: empath(await runtime.activeUserDirectory()),
runtimePath: empath(runtime.directory),
runtime: options?.yyc ? 'YYC' : 'VM',
config: options?.config,
verbose: !options?.quiet,
ignorecache: !!options?.noCache,
cache: empath(tempDir.join('igor/cache')),
temp: empath(tempDir.join('igor/temp')),
// For some reason the filename has to be there
// but only the directory is used...
of: empath(tempDir.join(`igor/out/${projectPath.name}.win`)),
tf: empath(outputDir.join(`${projectPath.name}.${artifactExtensionForPlatform(target)}`)),
};
}
export async function computeGameMakerCleanOptions(runtime, options) {
const target = options?.targetPlatform || 'windows';
const buildOptions = await computeOptions(runtime, options);
return {
target,
command: 'Clean',
options: buildOptions,
};
}
export async function computeGameMakerBuildOptions(runtime, options) {
const target = options?.targetPlatform || 'windows';
const command = options?.compile
? target === 'windows'
? 'PackageZip'
: 'Package'
: 'Run';
const buildOptions = await computeOptions(runtime, options);
return {
target,
command,
options: buildOptions,
};
}
// If I don't specify modules, I'll just get all of those I'm entitled to.
// The /rp points to the folder CONTAINING the runtime FOLDERS
// The /ru feed is required
// The other arguments are positional
export async function executeGameMakerBuildCommand(runtime, options) {
const { target, command, options: buildOptions, } = await computeGameMakerBuildOptions(runtime, options);
const results = await executeGameMakerCommand(runtime, target, command, buildOptions, options);
return results;
}
export async function executeGameMakerCleanCommand(runtime, options) {
const { target, command, options: buildOptions, } = await computeGameMakerCleanOptions(runtime, options);
const results = await executeGameMakerCommand(runtime, target, command, buildOptions, options);
return results;
}
export async function stringifyGameMakerBuildCommand(runtime, options) {
const { cmd, args } = await computeGameMakerBuildCommand(runtime, options);
const escapedCmd = cmd.replace(/[/\\]/g, '/').replace(/ /g, '\\ ');
return `${escapedCmd} ${args.join(' ')}`;
}
export async function stringifyGameMakerCleanCommand(runtime, options) {
const { cmd, args } = await computeGameMakerCleanCommand(runtime, options);
const escapedCmd = cmd.replace(/[/\\]/g, '/').replace(/ /g, '\\ ');
return `${escapedCmd} ${args.join(' ')}`;
}
export async function computeGameMakerCleanCommand(runtime, options) {
const { target, command, options: buildOptions, } = await computeGameMakerCleanOptions(runtime, options);
return computeGameMakerCommand(runtime, target, command, buildOptions);
}
export async function computeGameMakerBuildCommand(runtime, options) {
const { target, command, options: buildOptions, } = await computeGameMakerBuildOptions(runtime, options);
return computeGameMakerCommand(runtime, target, command, buildOptions);
}
export function computeGameMakerCommand(runtime, worker, command, executionOptions) {
let args = Object.entries(executionOptions)
.map((option) => {
const [key, value] = option;
if (typeof value === 'undefined') {
return;
}
const arg = `--${key}`;
if (value === false) {
return;
}
if (value === true) {
return arg;
}
return arg + `=${value}`;
})
.filter((x) => x);
const cmd = runtime.executablePath.absolute;
args = [...args, '--', worker, ...arrayWrapped(command)];
return {
cmd,
args,
};
}
export async function executeGameMakerCommand(runtime, worker, command, executionOptions, otherOptions) {
const childEnv = { ...process.env };
if (childEnv.PATH && currentOs === 'windows') {
//This is because node's the ENV contain the PATH variable that conflicts with MSBuild
//See https://github.com/dotnet/msbuild/issues/5726
delete childEnv.PATH;
}
const { cmd, args } = computeGameMakerCommand(runtime, worker, command, executionOptions);
console.log('🚀 Running GameMaker CLI command:');
console.log(cmd, ...args);
const child = spawn(cmd, args, {
env: childEnv,
stdio: 'pipe',
});
// Set up writeable file streams
const timestamp = formatTimestamp(new Date(), {
secondsPrecision: 0,
timeSeparator: '',
});
const logDir = await projectLogDirectory(executionOptions.project, otherOptions);
const logFilePathy = (fileName) => {
const logFileName = `${otherOptions?.excludeLogFileTimestamps ? '' : `${timestamp}.`}${fileName}.txt`;
const logFilePath = new Pathy(logDir.join(logFileName));
return logFilePath;
};
const results = {};
for (const pipe of ['stdout', 'stderr']) {
// Create a writeable filestream
child[pipe].on('data', (data) => {
const dataString = data.toString();
results[pipe] += dataString;
console[pipe === 'stderr' ? 'error' : 'log'](dataString);
});
}
return new Promise((resolve) => {
child.on('exit', async () => {
// The text "Igor complete." appears
// after the compile logs (if compile
// was successful) AND after the run
// (if the run exited normally).
const successMessage = 'Igor complete.';
const logParts = results.stdout.split(successMessage);
results.compileSucceeded = logParts.length > 1;
const wasRunnable = command === 'Run' && results.compileSucceeded;
const containedTwoIgorCompletes = logParts.length === 3;
results.runnerSucceeded = wasRunnable
? containedTwoIgorCompletes
: undefined;
// Add compiler & runner logs
for (const [index, source] of ['compiler', 'runner'].entries()) {
let content = logParts[index];
if (!content) {
continue;
}
const needsSuccessMessage = (index === 0 && results.compileSucceeded) ||
(index === 1 && results.runnerSucceeded);
if (needsSuccessMessage) {
content += successMessage;
}
const logName = `${source}Logs`;
results[logName] = content;
const logFileKey = `${source}LogsPath`;
const logFilePath = logFilePathy(source);
await logFilePath.write(content);
results[logFileKey] = logFilePath.absolute;
}
resolve(results);
});
});
}
//# sourceMappingURL=GameMakerRuntime.command.js.map