mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
306 lines
9.4 KiB
JavaScript
306 lines
9.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Prepare PortableGit (with bash.exe) under resources/mingit for Windows packaging/runtime.
|
|
*
|
|
* Features:
|
|
* - Cross-platform execution (macOS/Linux can prepare assets for Windows packaging)
|
|
* - Optional strict mode: --required (fail build if not prepared)
|
|
* - Offline archive support via LOBSTERAI_PORTABLE_GIT_ARCHIVE
|
|
* - Mirror URL override via LOBSTERAI_PORTABLE_GIT_URL
|
|
* - Unified extraction via 7zip-bin (path7za)
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
const { Readable } = require('stream');
|
|
const { pipeline } = require('stream/promises');
|
|
|
|
const GIT_VERSION = '2.47.1';
|
|
const PORTABLE_GIT_FILE = `PortableGit-${GIT_VERSION}-64-bit.7z.exe`;
|
|
const DEFAULT_PORTABLE_GIT_URL =
|
|
`https://github.com/git-for-windows/git/releases/download/v${GIT_VERSION}.windows.1/${PORTABLE_GIT_FILE}`;
|
|
|
|
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'resources', 'mingit');
|
|
const DEFAULT_ARCHIVE_PATH = path.join(PROJECT_ROOT, 'resources', PORTABLE_GIT_FILE);
|
|
const RUNTIME_DIRS = [
|
|
path.join('dev', 'shm'),
|
|
path.join('dev', 'mqueue'),
|
|
];
|
|
|
|
const DIRS_TO_PRUNE = [
|
|
'doc',
|
|
'ReleaseNotes.html',
|
|
'README.portable',
|
|
path.join('mingw64', 'doc'),
|
|
path.join('mingw64', 'share', 'doc'),
|
|
path.join('mingw64', 'share', 'gtk-doc'),
|
|
path.join('mingw64', 'share', 'man'),
|
|
path.join('mingw64', 'share', 'gitweb'),
|
|
path.join('mingw64', 'share', 'git-gui'),
|
|
path.join('mingw64', 'libexec', 'git-core', 'git-gui'),
|
|
path.join('mingw64', 'libexec', 'git-core', 'git-gui--askpass'),
|
|
path.join('usr', 'share', 'doc'),
|
|
path.join('usr', 'share', 'man'),
|
|
path.join('usr', 'share', 'vim'),
|
|
path.join('usr', 'share', 'perl5'),
|
|
path.join('usr', 'lib', 'perl5'),
|
|
];
|
|
|
|
function parseArgs(argv) {
|
|
return {
|
|
required: argv.includes('--required'),
|
|
};
|
|
}
|
|
|
|
function resolveInputPath(input) {
|
|
if (typeof input !== 'string') return null;
|
|
const trimmed = input.trim();
|
|
if (!trimmed) return null;
|
|
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
|
}
|
|
|
|
function isNonEmptyFile(filePath) {
|
|
try {
|
|
return fs.statSync(filePath).isFile() && fs.statSync(filePath).size > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getDirSize(dir) {
|
|
let size = 0;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
size += getDirSize(full);
|
|
} else {
|
|
size += fs.statSync(full).size;
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
|
|
function resolve7zaPath() {
|
|
let path7za;
|
|
try {
|
|
({ path7za } = require('7zip-bin'));
|
|
} catch (error) {
|
|
throw new Error(
|
|
'Missing dependency "7zip-bin". Run npm install and retry. '
|
|
+ `Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
}
|
|
|
|
if (!path7za || !fs.existsSync(path7za)) {
|
|
throw new Error(`7zip-bin executable not found: ${path7za || '(empty path)'}`);
|
|
}
|
|
|
|
return path7za;
|
|
}
|
|
|
|
function findPortableGitBash(baseDir = OUTPUT_DIR) {
|
|
const candidates = [
|
|
path.join(baseDir, 'bin', 'bash.exe'),
|
|
path.join(baseDir, 'usr', 'bin', 'bash.exe'),
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function downloadArchive(url, destination) {
|
|
const response = await fetch(url, { redirect: 'follow' });
|
|
if (!response.ok || !response.body) {
|
|
throw new Error(`Download failed (${response.status} ${response.statusText}) for ${url}`);
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
|
|
const tmpFile = `${destination}.download`;
|
|
try {
|
|
const stream = fs.createWriteStream(tmpFile);
|
|
await pipeline(Readable.fromWeb(response.body), stream);
|
|
|
|
if (!isNonEmptyFile(tmpFile)) {
|
|
throw new Error('Downloaded archive is empty.');
|
|
}
|
|
|
|
fs.renameSync(tmpFile, destination);
|
|
} catch (error) {
|
|
try {
|
|
fs.rmSync(tmpFile, { force: true });
|
|
} catch {
|
|
// ignore cleanup errors
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function pruneUnneededFiles() {
|
|
let prunedCount = 0;
|
|
for (const relPath of DIRS_TO_PRUNE) {
|
|
const fullPath = path.join(OUTPUT_DIR, relPath);
|
|
if (!fs.existsSync(fullPath)) continue;
|
|
try {
|
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
prunedCount++;
|
|
} catch (error) {
|
|
console.warn(`[setup-mingit] Warning: Could not remove ${relPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
console.log(`[setup-mingit] Pruned ${prunedCount} entries.`);
|
|
}
|
|
|
|
function extractArchive(archivePath) {
|
|
const sevenZip = resolve7zaPath();
|
|
if (fs.existsSync(OUTPUT_DIR)) {
|
|
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
|
|
console.log(`[setup-mingit] Extracting archive with 7zip-bin: ${archivePath}`);
|
|
const result = spawnSync(sevenZip, ['x', archivePath, `-o${OUTPUT_DIR}`, '-y'], {
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
if (result.status !== 0) {
|
|
throw new Error(`7zip extraction failed with exit code ${result.status}`);
|
|
}
|
|
}
|
|
|
|
function ensurePortableGitRuntimeDirs(required) {
|
|
const ensured = [];
|
|
for (const relPath of RUNTIME_DIRS) {
|
|
const fullPath = path.join(OUTPUT_DIR, relPath);
|
|
try {
|
|
fs.mkdirSync(fullPath, { recursive: true });
|
|
ensured.push(relPath);
|
|
} catch (error) {
|
|
const message = `Could not create runtime directory ${relPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
if (required) {
|
|
throw new Error(message);
|
|
}
|
|
console.warn(`[setup-mingit] Warning: ${message}`);
|
|
}
|
|
}
|
|
|
|
if (ensured.length > 0) {
|
|
console.log(`[setup-mingit] Ensured runtime directories: ${ensured.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
async function resolveArchive(required) {
|
|
const envArchive = resolveInputPath(process.env.LOBSTERAI_PORTABLE_GIT_ARCHIVE);
|
|
if (envArchive) {
|
|
if (!isNonEmptyFile(envArchive)) {
|
|
throw new Error(
|
|
`LOBSTERAI_PORTABLE_GIT_ARCHIVE points to an invalid file: ${envArchive}`
|
|
);
|
|
}
|
|
console.log(`[setup-mingit] Using local archive from LOBSTERAI_PORTABLE_GIT_ARCHIVE: ${envArchive}`);
|
|
return { archivePath: envArchive, source: 'env-archive' };
|
|
}
|
|
|
|
if (isNonEmptyFile(DEFAULT_ARCHIVE_PATH)) {
|
|
console.log(`[setup-mingit] Using cached archive: ${DEFAULT_ARCHIVE_PATH}`);
|
|
return { archivePath: DEFAULT_ARCHIVE_PATH, source: 'cache' };
|
|
}
|
|
|
|
const urlFromEnv = typeof process.env.LOBSTERAI_PORTABLE_GIT_URL === 'string'
|
|
? process.env.LOBSTERAI_PORTABLE_GIT_URL.trim()
|
|
: '';
|
|
const downloadUrl = urlFromEnv || DEFAULT_PORTABLE_GIT_URL;
|
|
|
|
try {
|
|
console.log(`[setup-mingit] Downloading PortableGit from: ${downloadUrl}`);
|
|
await downloadArchive(downloadUrl, DEFAULT_ARCHIVE_PATH);
|
|
const fileSizeMB = (fs.statSync(DEFAULT_ARCHIVE_PATH).size / 1024 / 1024).toFixed(1);
|
|
console.log(`[setup-mingit] Downloaded archive (${fileSizeMB} MB): ${DEFAULT_ARCHIVE_PATH}`);
|
|
return { archivePath: DEFAULT_ARCHIVE_PATH, source: 'download' };
|
|
} catch (error) {
|
|
if (required) {
|
|
throw new Error(
|
|
'Unable to obtain PortableGit archive. '
|
|
+ 'Set LOBSTERAI_PORTABLE_GIT_ARCHIVE to a local offline package or '
|
|
+ 'set LOBSTERAI_PORTABLE_GIT_URL to a reachable mirror. '
|
|
+ `Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
}
|
|
|
|
console.warn(
|
|
'[setup-mingit] PortableGit archive is not available; skip because --required is not set. '
|
|
+ `Reason: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function ensurePortableGit(options = {}) {
|
|
const required = Boolean(options.required);
|
|
const shouldRun = process.platform === 'win32' || required || process.env.LOBSTERAI_SETUP_MINGIT_FORCE === '1';
|
|
|
|
if (!shouldRun) {
|
|
console.log('[setup-mingit] Skip on non-Windows host (pass --required to force cross-platform preparation).');
|
|
return { ok: true, skipped: true, bashPath: null };
|
|
}
|
|
|
|
const existingBash = findPortableGitBash();
|
|
if (existingBash) {
|
|
ensurePortableGitRuntimeDirs(required);
|
|
console.log(`[setup-mingit] PortableGit already prepared: ${existingBash}`);
|
|
return { ok: true, skipped: false, bashPath: existingBash };
|
|
}
|
|
|
|
const archive = await resolveArchive(required);
|
|
if (!archive) {
|
|
return { ok: true, skipped: true, bashPath: null };
|
|
}
|
|
|
|
extractArchive(archive.archivePath);
|
|
const resolvedBash = findPortableGitBash();
|
|
if (!resolvedBash) {
|
|
throw new Error(
|
|
'PortableGit extraction finished but bash.exe is missing. '
|
|
+ `Checked: ${path.join(OUTPUT_DIR, 'bin', 'bash.exe')} and ${path.join(OUTPUT_DIR, 'usr', 'bin', 'bash.exe')}`
|
|
);
|
|
}
|
|
|
|
ensurePortableGitRuntimeDirs(required);
|
|
pruneUnneededFiles();
|
|
|
|
const finalSize = getDirSize(OUTPUT_DIR);
|
|
console.log(`[setup-mingit] PortableGit ready: ${resolvedBash}`);
|
|
console.log(`[setup-mingit] Total size: ~${(finalSize / 1024 / 1024).toFixed(1)} MB`);
|
|
|
|
return { ok: true, skipped: false, bashPath: resolvedBash };
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
await ensurePortableGit({ required: args.required });
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch((error) => {
|
|
console.error('[setup-mingit] ERROR:', error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
ensurePortableGit,
|
|
findPortableGitBash,
|
|
};
|