UNPKG

rollup-plugin-glsl-optimize

Version:

Import GLSL source files as strings. Pre-processed, validated and optimized with Khronos Group SPIRV-Tools. Supports glslify.

368 lines (341 loc) 10.4 kB
import {platform, arch, EOL} from 'os'; import {spawn} from 'child_process'; import * as path from 'path'; import * as fsSync from 'fs'; import {bufferAndErrLines, bufferAndOutLines, bufferLines, parseLines, writeLines} from './lines.js'; import {default as envPaths} from 'env-paths'; import {settings} from '../../settings.js'; const binFolder = settings.BIN_PATH; const rootFolder = settings.PROJECT_ROOT; let _pkg; /** * @internal * Workaround since node won't ES6 import json * @return {any} Contents of package.json */ export const getPkg = () => { if (!_pkg) { try { _pkg = loadJSON('package.json'); } catch (err) { _pkg = {name: 'unknown'}; } } return _pkg; }; /** * @internal * @param {string} file relative to package root * @return {any} */ export const loadJSON = (file) => JSON.parse(fsSync.readFileSync( path.resolve(rootFolder, file), {encoding: 'utf8'})); /** * @typedef {'Validator'|'Optimizer'|'Cross'} GLSLToolVals * @typedef {'glslangValidatorPath'|'glslangOptimizerPath'|'glslangCrossPath'} GLSLToolPathKeys * @typedef {{[P in GLSLToolPathKeys]?: string}} GLSLToolPathConfig * @typedef {{name: string, optionKey: GLSLToolPathKeys, envKey: string, url: string, distPath?: string, path?: string}} GLSLSingleToolConfig * @typedef {{[P in GLSLToolVals]: GLSLSingleToolConfig}} GLSLToolConfig * @type {GLSLToolConfig} */ const ToolConfig = { Validator: { name: 'glslangValidator', optionKey: 'glslangValidatorPath', envKey: 'GLSLANG_VALIDATOR', url: 'https://github.com/KhronosGroup/glslang', }, Optimizer: { name: 'spirv-opt', optionKey: 'glslangOptimizerPath', envKey: 'GLSLANG_OPTIMIZER', url: 'https://github.com/KhronosGroup/SPIRV-Tools', }, Cross: { name: 'spriv-cross', optionKey: 'glslangCrossPath', envKey: 'GLSLANG_CROSS', url: 'https://github.com/KhronosGroup/SPIRV-Cross', }, }; /** * @typedef {'win64'|'ubuntu64'|'macos64'} PlatformTag * @typedef {{[P in GLSLToolVals]: string}} PerPlatformDistPaths * @typedef {{[P in PlatformTag]: PerPlatformDistPaths}} PlatformDistPaths * @type {PlatformDistPaths} */ const ToolDistPaths = { win64: { Validator: `glslangValidator.exe`, Optimizer: `spirv-opt.exe`, Cross: `spirv-cross.exe`, }, ubuntu64: { Validator: `glslangValidator`, Optimizer: `spirv-opt`, Cross: `spirv-cross`, }, macos64: { Validator: `glslangValidator`, Optimizer: `spirv-opt`, Cross: `spirv-cross`, }, }; /** * @typedef {Object} BinarySource * @property {string} folderPath * @property {string} tag * @property {string[]} fileList */ /** @type {PlatformTag} */ let _platTag = undefined; let _platConfigured = false; /** * @internal * @return {string?} */ export function getPlatTag() { if (!_platTag) { if (arch() === 'x64') { switch (platform()) { case 'win32': _platTag = 'win64'; break; case 'linux': _platTag = 'ubuntu64'; break; case 'darwin': _platTag = 'macos64'; break; } } } return _platTag; } /** * @internal * @return {BinarySource?} */ export function configurePlatformBinaries() { if (!_platConfigured) { _platConfigured = true; getPlatTag(); if (_platTag) { (/** @type {[GLSLToolVals, string][]} */(Object.entries(ToolDistPaths[_platTag]))) .forEach(([tool, file]) => ToolConfig[tool].distPath = `${_platTag}${path.sep}${file}`); } } return _platTag ? { folderPath: path.join(binFolder, _platTag), tag: _platTag, fileList: Object.values(ToolConfig).map((tool) => path.join(binFolder, tool.distPath) ?? ''), } : null; } /** * @param {GLSLToolVals[]} kinds */ function errorMissingTools(kinds) { let errMsg = `Khronos tool binaries could not be found:\n`; for (const kind of kinds) { const config = ToolConfig[kind]; errMsg += `${config.name} not found, searched path: '${config.path ?? ''}'\n` + toolInfo(config); } throw new Error(errMsg); } /** @param {GLSLSingleToolConfig} config */ const toolInfo = (config) => `${config.name} : configure with the environment variable ${ config.envKey} (or the option ${config.optionKey})\n${config.url}\n`; /** @internal */ export const allToolInfo = () => Object.values(ToolConfig).map(toolInfo).join('\n'); /** * @internal * @param {Partial<import('./glslProcess').GLSLToolOptions>} options * @param {GLSLToolVals[]} [required] */ export function configureTools(options, required = /** @type {GLSLToolVals[]} */(Object.keys(ToolConfig))) { configurePlatformBinaries(); /** @type {GLSLToolVals[]} */ const missingKinds = []; for (const kind of required) { const tool = ToolConfig[kind]; const toolPath = process.env[tool.envKey] || options[tool.optionKey] || tool.distPath; if (!toolPath) { console.warn(`Khronos ${tool.name} binary not shipped for this platform`); } else { tool.path = path.resolve(binFolder, toolPath); } if (!tool.path || !fsSync.existsSync(tool.path)) { missingKinds.push(kind); } } if (missingKinds.length) { errorMissingTools(missingKinds); } } /** * @param {GLSLToolVals} kind * @return {string} Path to tool if found */ function getToolPath(kind) { const validatorPath = ToolConfig[kind].path; if (!validatorPath) errorMissingTools([kind]); return validatorPath; } /** * @internal * @param {GLSLToolVals} kind * @param {string} workingDir * @param {string[]} args */ export function launchTool(kind, workingDir, args) { const toolBin = getToolPath(kind); return launchToolPath(toolBin, workingDir, args); } /** * @typedef {{code: number, signal: NodeJS.Signals}} ToolExitStatus */ /** * @internal * @param {string} path * @param {string} workingDir * @param {string[]} args */ export function launchToolPath(path, workingDir, args) { const toolProcess = spawn(path, args, { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'], shell: false, windowsVerbatimArguments: true, }, ); toolProcess.on('error', (err) => { throw new Error(`${path}: failed to launch${err?.message?` : ${err.message}`:''}`); }); /** @type {Promise<ToolExitStatus>} */ const exitPromise = new Promise((resolve, reject) => { toolProcess.on('exit', (code, signal) => { resolve({code, signal}); }); }); return {toolProcess, exitPromise}; } /** * @internal * @param {{toolProcess: import('child_process').ChildProcess, exitPromise: Promise<ToolExitStatus>}} param0 * @param {string} [input] * @param {boolean} [echo] */ export async function waitForToolBuffered({toolProcess, exitPromise}, input = undefined, echo = false) { const stderrPromise = echo ? bufferAndErrLines(parseLines(toolProcess.stderr)) : bufferLines(parseLines(toolProcess.stderr)); const stdoutPromise = echo ? bufferAndOutLines(parseLines(toolProcess.stdout)) : bufferLines(parseLines(toolProcess.stdout)); if (input !== undefined) { await writeLines(toolProcess.stdin, input); } const exitStatus = await exitPromise; const outLines = await stdoutPromise; const errLines = await stderrPromise; return { error: exitStatus.signal !== null || (exitStatus.code && exitStatus.code !== 0), exitMessage: `exit status: ${exitStatus.code || 'n/a'} ${exitStatus.signal || ''}`, exitStatus, outLines, errLines, }; } /** * @internal * @param {{toolProcess: import('child_process').ChildProcess, exitPromise: Promise<ToolExitStatus>}} param0 * @param {string} [input] */ export async function waitForTool({toolProcess, exitPromise}, input = undefined) { toolProcess.stderr.pipe(process.stderr); toolProcess.stdout.pipe(process.stdout); if (input !== undefined) { await writeLines(toolProcess.stdin, input); } const exitStatus = await exitPromise; return { error: exitStatus.signal !== null || (exitStatus.code && exitStatus.code !== 0), exitMessage: `exit status: ${exitStatus.code || 'n/a'} ${exitStatus.signal || ''}`, exitStatus, }; } export function printToolDiagnostic(lines) { for (const line of lines) { if (line.length && line !== 'stdin') { console.error(line); } } } /** * @internal * @param {string} path * @param {string} workingDir * @param {string} title * @param {string[]} args */ export async function runTool(path, workingDir, title, args) { const toolResult = await waitForTool(launchToolPath(path, workingDir, args)); if (toolResult.error) { const errMsg = `${title} failed: ${path} ${toolResult.exitMessage}`; console.error(errMsg); throw new Error(errMsg); } return toolResult; } /** * @internal * @param {string} path * @param {string} workingDir * @param {string} title * @param {string[]} args */ export async function runToolBuffered(path, workingDir, title, args) { const toolResult = await waitForToolBuffered(launchToolPath(path, workingDir, args)); if (toolResult.error) { printToolDiagnostic(toolResult.outLines); printToolDiagnostic(toolResult.errLines); const errMsg = `${title} failed: ${path} ${toolResult.exitMessage}`; console.error(errMsg); throw new Error(errMsg); } return { out: toolResult.outLines ? toolResult.outLines.join(EOL) : '', err: toolResult.errLines ? toolResult.errLines.join(EOL) : '', }; } const argEscapeWindows = (pattern) => { const buf = []; for (const char of pattern) { switch (char) { case '"': buf.push('\\', '"'); break; // case '"': buf.push('\''); break; // case ' ': buf.push('\\', 's'); break; default: buf.push(char); } } return buf.join(''); }; const argQuoteWindows = (val) => `"${argEscapeWindows(val)}"`.split(' '); const argVerbatim = (val) => [val]; /** @internal */ export const argQuote = platform() === 'win32' ? argQuoteWindows : argVerbatim; let _cachePath; /** @internal */ export const getCachePath = () => { if (!_cachePath) { _cachePath = envPaths(getPkg().name).cache; } return _cachePath; }; let _npmCommand; /** * @internal * @param {string[]} args * @param {string} [workingDir] */ export async function npmCommand(args, workingDir = settings.PROJECT_ROOT) { if (!_npmCommand) { _npmCommand = platform() === 'win32' ? 'npm.cmd' : 'npm'; } return runTool(_npmCommand, workingDir, 'npm', args); }