Files
poshmanager/server/services/intuneWinBuilder.js
2026-06-25 12:05:53 -05:00

81 lines
3.4 KiB
JavaScript

import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js';
// Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft
// Win32 Content Prep Tool (IntuneWinAppUtil.exe) on Windows, or an optional
// cross-platform packager via INTUNEWIN_BUILD_COMMAND. The command construction
// is pure and unit-tested; the spawn wrapper is thin.
export function resolveToolPath() {
return config.intunewinUtilPath || path.join(config.toolsDir, 'IntuneWinAppUtil.exe');
}
// Where to put the tool and whether we can build here.
export function builderAvailability() {
if (config.intunewinBuildCommand) {
return { available: true, mode: 'external-command', toolPath: null };
}
const toolPath = resolveToolPath();
if (!fs.existsSync(toolPath)) {
return { available: false, mode: 'bundled', toolPath, reason: `IntuneWinAppUtil.exe not found at ${toolPath}. Place it in the tools directory or set INTUNEWIN_UTIL_PATH / INTUNEWIN_BUILD_COMMAND.` };
}
if (process.platform !== 'win32') {
return { available: false, mode: 'bundled', toolPath, reason: 'IntuneWinAppUtil.exe requires Windows. Set INTUNEWIN_BUILD_COMMAND to use a cross-platform packager.' };
}
return { available: true, mode: 'bundled', toolPath };
}
// Pure: produce the spawn descriptor for the configured packager.
export function buildPackagerInvocation({ toolPath, sourceFolder, setupFile, outputDir, commandTemplate }) {
if (commandTemplate) {
const command = commandTemplate
.split('{source}').join(sourceFolder)
.split('{setup}').join(setupFile)
.split('{output}').join(outputDir);
return { shell: true, command };
}
return {
shell: false,
command: toolPath,
args: ['-c', sourceFolder, '-s', setupFile, '-o', outputDir, '-q']
};
}
// IntuneWinAppUtil names the output after the setup file's base name.
export function expectedOutputFile(outputDir, setupFile) {
return path.join(outputDir, `${path.parse(setupFile).name}.intunewin`);
}
export function buildIntuneWin({ sourceFolder, setupFile, outputDir }) {
return new Promise((resolve, reject) => {
const availability = builderAvailability();
if (!availability.available) return reject(new Error(availability.reason || 'Packager not available'));
if (!sourceFolder || !fs.existsSync(sourceFolder)) return reject(new Error(`Source folder not found: ${sourceFolder}`));
if (!setupFile) return reject(new Error('A setup file (entry installer) is required'));
fs.mkdirSync(outputDir, { recursive: true });
const invocation = buildPackagerInvocation({
toolPath: availability.toolPath,
sourceFolder,
setupFile,
outputDir,
commandTemplate: config.intunewinBuildCommand
});
const child = invocation.shell
? spawn(invocation.command, { shell: true })
: spawn(invocation.command, invocation.args, { shell: false });
let stderr = '';
child.stderr.on('data', (chunk) => { stderr += String(chunk); });
child.on('error', (error) => reject(new Error(`Packager failed to start: ${error.message}`)));
child.on('close', (code) => {
const outputFile = expectedOutputFile(outputDir, setupFile);
if (code === 0 && fs.existsSync(outputFile)) return resolve({ outputFile });
reject(new Error(`Packager exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
});
});
}