190 lines
7.0 KiB
JavaScript
190 lines
7.0 KiB
JavaScript
import { existsSync, readFileSync } from 'node:fs';
|
|
import { dirname, join, normalize, resolve, win32 } from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
const command = packageJson?.scripts?.['verify:installer-shell'] || '';
|
|
const isWindowsHost = process.platform === 'win32' || process.env.VENDOO_TEST_WINDOWS_SHELL === '1';
|
|
|
|
function fail(message) {
|
|
console.error(`Installer-Shell-Portabilitätsgate fehlgeschlagen: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (/^\s*sh\s+/i.test(command)) {
|
|
fail('package.json darf den POSIX-Befehl sh nicht direkt voraussetzen.');
|
|
}
|
|
if (command !== 'node tools/verify-installer-shell.mjs') {
|
|
fail(`Unerwarteter npm-Vertrag: ${command}`);
|
|
}
|
|
|
|
const shellContract = readFileSync(join(root, 'tools', 'verify-installer-shell.sh'), 'utf8');
|
|
for (const marker of [
|
|
'VENDOO_INSTALLER_SHELL_HOST',
|
|
'MINGW*|MSYS*|CYGWIN*',
|
|
'POSIX-Modus 0600',
|
|
'show_file',
|
|
'assert_env_permissions',
|
|
'--exclude=.git',
|
|
]) {
|
|
if (!shellContract.includes(marker)) fail(`Shell-Testvertrag fehlt: ${marker}`);
|
|
}
|
|
|
|
function run(file, args, options = {}) {
|
|
return spawnSync(file, args, {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
windowsHide: true,
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
function addCandidate(set, value) {
|
|
if (!value) return;
|
|
const candidate = normalize(value.replace(/^"|"$/g, '').trim());
|
|
if (candidate) set.add(candidate);
|
|
}
|
|
|
|
function addGitRootCandidates(set, gitRoot) {
|
|
if (!gitRoot) return;
|
|
addCandidate(set, join(gitRoot, 'bin', 'sh.exe'));
|
|
addCandidate(set, join(gitRoot, 'usr', 'bin', 'sh.exe'));
|
|
}
|
|
|
|
function pathApiFor(value) {
|
|
return /^[a-zA-Z]:[\\/]/.test(value) ? win32 : { normalize, dirname };
|
|
}
|
|
|
|
function deriveGitRootFromExecutable(executable) {
|
|
const raw = executable.replace(/^"|"$/g, '').trim();
|
|
const api = pathApiFor(raw);
|
|
const clean = api.normalize(raw);
|
|
const parent = api.dirname(clean);
|
|
const leaf = parent.toLowerCase().split(/[\\/]/).pop();
|
|
if (['cmd', 'bin', 'usr'].includes(leaf)) {
|
|
return api.dirname(parent);
|
|
}
|
|
return api.dirname(clean);
|
|
}
|
|
|
|
function deriveGitRootFromExecPath(execPath) {
|
|
const raw = execPath.replace(/^"|"$/g, '').trim();
|
|
const api = pathApiFor(raw);
|
|
const clean = api.normalize(raw);
|
|
const lowered = clean.toLowerCase().replace(/\\/g, '/');
|
|
for (const marker of ['/mingw64/', '/mingw32/', '/usr/libexec/', '/libexec/']) {
|
|
const index = lowered.indexOf(marker);
|
|
if (index > 0) return clean.slice(0, index);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findWindowsGitBash() {
|
|
const candidates = new Set();
|
|
addCandidate(candidates, process.env.VENDOO_SH);
|
|
addCandidate(candidates, process.env.SHELL);
|
|
|
|
for (const base of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA]) {
|
|
if (!base) continue;
|
|
addGitRootCandidates(candidates, join(base, base === process.env.LOCALAPPDATA ? 'Programs' : '', 'Git'));
|
|
}
|
|
|
|
const whereGit = run('where.exe', ['git.exe']);
|
|
if (whereGit.status === 0) {
|
|
for (const line of whereGit.stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
|
addGitRootCandidates(candidates, deriveGitRootFromExecutable(line));
|
|
}
|
|
}
|
|
|
|
const execPath = run('git.exe', ['--exec-path']);
|
|
if (execPath.status === 0) {
|
|
const gitRoot = deriveGitRootFromExecPath(execPath.stdout);
|
|
if (gitRoot) addGitRootCandidates(candidates, gitRoot);
|
|
}
|
|
|
|
const whereSh = run('where.exe', ['sh.exe']);
|
|
if (whereSh.status === 0) {
|
|
for (const line of whereSh.stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
|
addCandidate(candidates, line);
|
|
}
|
|
}
|
|
|
|
return [...candidates].find((candidate) => existsSync(candidate)) || null;
|
|
}
|
|
|
|
// Regression contracts for common Git-for-Windows layouts.
|
|
const standardRoot = deriveGitRootFromExecutable('C:\\Program Files\\Git\\cmd\\git.exe');
|
|
if (standardRoot.toLowerCase() !== win32.normalize('C:\\Program Files\\Git').toLowerCase()) {
|
|
fail(`Git-Root aus cmd\\git.exe falsch abgeleitet: ${standardRoot}`);
|
|
}
|
|
const execRoot = deriveGitRootFromExecPath('C:\\Program Files\\Git\\mingw64\\libexec\\git-core');
|
|
if (!execRoot || execRoot.toLowerCase() !== win32.normalize('C:\\Program Files\\Git').toLowerCase()) {
|
|
fail(`Git-Root aus git --exec-path falsch abgeleitet: ${execRoot}`);
|
|
}
|
|
|
|
let shell;
|
|
if (isWindowsHost) {
|
|
shell = process.platform === 'win32' ? findWindowsGitBash() : 'sh';
|
|
if (!shell) {
|
|
fail('Git Bash wurde trotz installiertem Git nicht gefunden. Erwartet wird bin\\sh.exe oder usr\\bin\\sh.exe innerhalb der Git-for-Windows-Installation.');
|
|
}
|
|
} else {
|
|
const probe = run('sh', ['-c', 'printf vendoo-shell-ok']);
|
|
if (probe.status !== 0 || probe.stdout !== 'vendoo-shell-ok') {
|
|
fail('Eine funktionsfähige POSIX-Shell wurde nicht gefunden.');
|
|
}
|
|
shell = 'sh';
|
|
}
|
|
|
|
if (isWindowsHost) {
|
|
for (const script of ['setup.sh', 'tools/verify-installer-shell.sh']) {
|
|
const syntax = run(shell, ['-n', script], {
|
|
env: {
|
|
...process.env,
|
|
NO_COLOR: '1',
|
|
FORCE_COLOR: '0',
|
|
MSYS2_ARG_CONV_EXCL: '*',
|
|
},
|
|
timeout: 60_000,
|
|
});
|
|
if (syntax.stdout) process.stdout.write(syntax.stdout);
|
|
if (syntax.stderr) process.stderr.write(syntax.stderr);
|
|
if (syntax.error) fail(`Shell-Syntaxprüfung konnte nicht gestartet werden (${script}): ${syntax.error.message}`);
|
|
if (syntax.signal) fail(`Shell-Syntaxprüfung wurde durch Signal ${syntax.signal} beendet (${script}).`);
|
|
if (syntax.status !== 0) {
|
|
fail(`Shell-Syntaxprüfung fehlgeschlagen (${script}, Exit-Code ${syntax.status}). Verwendete Shell: ${shell}`);
|
|
}
|
|
}
|
|
console.log('Installer-Shell-Portabilitätsgate erfolgreich: Git Bash hat setup.sh und den Linux-Funktionstest syntaktisch geprüft; die funktionale Docker-/NAS-/VPS-Simulation läuft im verpflichtenden Linux-CI-Gate.');
|
|
process.exit(0);
|
|
}
|
|
|
|
const result = run(shell, ['tools/verify-installer-shell.sh'], {
|
|
env: {
|
|
...process.env,
|
|
NO_COLOR: '1',
|
|
FORCE_COLOR: '0',
|
|
VENDOO_INSTALLER_SHELL_HOST: 'posix',
|
|
},
|
|
timeout: 180_000,
|
|
});
|
|
|
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
if (result.error) fail(`Shell-Gate konnte nicht gestartet werden: ${result.error.message}`);
|
|
if (result.signal) fail(`Shell-Gate wurde durch Signal ${result.signal} beendet.`);
|
|
if (result.status !== 0) {
|
|
const stdout = String(result.stdout || '').trim();
|
|
const stderr = String(result.stderr || '').trim();
|
|
const diagnostics = [
|
|
stderr ? `Shell-Fehlerausgabe:\n${stderr}` : '',
|
|
stdout ? `Shell-Standardausgabe:\n${stdout}` : '',
|
|
].filter(Boolean).join('\n\n');
|
|
fail(`Shell-Gate endete mit Exit-Code ${result.status}. Verwendete Shell: ${shell}${diagnostics ? `\n${diagnostics}` : ''}`);
|
|
}
|
|
|
|
console.log(`Installer-Shell-Portabilitätsgate erfolgreich: ${shell} führte die vollständige Linux-/NAS-/VPS-Simulation mit strikter POSIX-Rechteprüfung aus.`);
|