chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
/**
|
||||
* Prepare aioncore binary for packaging.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. GitHub Actions artifact download when AIONUI_BACKEND_RUN_ID is set
|
||||
* 2. GitHub release download (requires version or defaults to "latest")
|
||||
* 3. Complete local bundle from AIONUI_BACKEND_LOCAL_BUNDLE_DIR
|
||||
* 4. Local binary fallback from AIONUI_BACKEND_LOCAL_BINARY
|
||||
*
|
||||
* Output: {projectRoot}/resources/bundled-aioncore/{platform}-{arch}/
|
||||
* - aioncore[.exe]
|
||||
* - manifest.json
|
||||
* - managed-resources/...
|
||||
*
|
||||
* @module prepare-aioncore
|
||||
*/
|
||||
|
||||
const { execSync, execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const GITHUB_OWNER = 'iOfficeAI';
|
||||
const GITHUB_REPO = 'AionCore';
|
||||
|
||||
const ACTIONS_ARTIFACT_TARGETS = {
|
||||
'darwin-arm64': {
|
||||
artifactName: 'aioncore-manual-macos-arm64',
|
||||
manualPlatform: 'macos-arm64',
|
||||
},
|
||||
'darwin-x64': {
|
||||
artifactName: 'aioncore-manual-macos-x64',
|
||||
manualPlatform: 'macos-x64',
|
||||
},
|
||||
'linux-arm64': {
|
||||
artifactName: 'aioncore-manual-linux-arm64',
|
||||
manualPlatform: 'linux-arm64',
|
||||
},
|
||||
'linux-x64': {
|
||||
artifactName: 'aioncore-manual-linux-x64',
|
||||
manualPlatform: 'linux-x64',
|
||||
},
|
||||
'win32-arm64': {
|
||||
artifactName: 'aioncore-manual-windows-arm64',
|
||||
manualPlatform: 'windows-arm64',
|
||||
},
|
||||
'win32-x64': {
|
||||
artifactName: 'aioncore-manual-windows-x64',
|
||||
manualPlatform: 'windows-x64',
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ensureDirectory(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function removeDirectorySafe(dirPath) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyFileSafe(sourcePath, targetPath) {
|
||||
ensureDirectory(path.dirname(targetPath));
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
|
||||
function copyDirectorySafe(sourcePath, targetPath) {
|
||||
ensureDirectory(path.dirname(targetPath));
|
||||
fs.cpSync(sourcePath, targetPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function ensureExecutableMode(filePath) {
|
||||
if (process.platform === 'win32') return;
|
||||
try {
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function writeJson(filePath, payload) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
function getBinaryName(platform) {
|
||||
return platform === 'win32' ? 'aioncore.exe' : 'aioncore';
|
||||
}
|
||||
|
||||
function getActionsTarget(platform, arch) {
|
||||
return ACTIONS_ARTIFACT_TARGETS[`${platform}-${arch}`] || null;
|
||||
}
|
||||
|
||||
function getActionsArtifactName(platform, arch) {
|
||||
return getActionsTarget(platform, arch)?.artifactName || null;
|
||||
}
|
||||
|
||||
function getActionsManualPlatform(platform, arch) {
|
||||
return getActionsTarget(platform, arch)?.manualPlatform || `${platform}-${arch}`;
|
||||
}
|
||||
|
||||
function getActionsArtifactMissingMessage({ runId, platform, arch, expectedArtifactName, availableArtifactNames }) {
|
||||
const available =
|
||||
Array.isArray(availableArtifactNames) && availableArtifactNames.length > 0
|
||||
? availableArtifactNames.join(', ')
|
||||
: '(none)';
|
||||
return [
|
||||
`AionCore run ${runId} does not contain artifact [ ${expectedArtifactName} ] required for [ ${platform}-${arch} ].`,
|
||||
`Available artifacts: ${available}.`,
|
||||
`Re-run AionCore Manual Build with platform [ ${getActionsManualPlatform(platform, arch)} ] or all.`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function prepareManagedResources(binaryPath, targetDir) {
|
||||
const bundleOut = path.join(targetDir, 'managed-resources');
|
||||
const dataDir = path.join(targetDir, '.prepare-data');
|
||||
|
||||
removeDirectorySafe(bundleOut);
|
||||
removeDirectorySafe(dataDir);
|
||||
ensureDirectory(bundleOut);
|
||||
ensureDirectory(dataDir);
|
||||
|
||||
console.log(` Preparing managed resources under ${path.relative(process.cwd(), bundleOut)}`);
|
||||
execFileSync(binaryPath, ['--data-dir', dataDir, 'prepare-managed-resources', '--bundle-out', bundleOut], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
AIONUI_BUNDLED_MANAGED_RESOURCES: '',
|
||||
},
|
||||
});
|
||||
|
||||
removeDirectorySafe(dataDir);
|
||||
return bundleOut;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Source resolvers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve the actual version tag when "latest" is requested.
|
||||
* Uses GitHub API via `gh` CLI (needs GH_TOKEN in CI) or falls back to
|
||||
* `curl` with an optional Authorization header (GITHUB_TOKEN / GH_TOKEN).
|
||||
*/
|
||||
function resolveLatestTag() {
|
||||
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '';
|
||||
|
||||
// 1. Try gh CLI (honours GH_TOKEN automatically)
|
||||
try {
|
||||
const out = execSync(`gh api repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest --jq .tag_name`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
}).trim();
|
||||
if (out) return out;
|
||||
} catch {
|
||||
// gh CLI not available or no token — fall back to curl
|
||||
}
|
||||
|
||||
// 2. Curl with optional token to avoid rate-limit 403
|
||||
try {
|
||||
const authArgs = token ? ['-H', `Authorization: token ${token}`] : [];
|
||||
const args = ['-fsSL', ...authArgs, `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`];
|
||||
const out = execFileSync('curl', args, { encoding: 'utf-8', timeout: 15000 });
|
||||
const tag = JSON.parse(out).tag_name;
|
||||
if (tag) return tag;
|
||||
} catch {
|
||||
// network issue or rate-limited
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the release asset filename for the given platform/arch/tag.
|
||||
*
|
||||
* Expected asset naming convention:
|
||||
* aioncore-v0.1.0-aarch64-apple-darwin.tar.gz
|
||||
*/
|
||||
function getAssetName(platform, arch, tag) {
|
||||
const archMap = { x64: 'x86_64', arm64: 'aarch64' };
|
||||
const platformMap = {
|
||||
darwin: 'apple-darwin',
|
||||
linux: 'unknown-linux-gnu',
|
||||
win32: 'pc-windows-msvc',
|
||||
};
|
||||
const normalizedArch = archMap[arch];
|
||||
const normalizedPlatform = platformMap[platform];
|
||||
if (!normalizedArch || !normalizedPlatform) return null;
|
||||
const ext = platform === 'win32' ? '.zip' : '.tar.gz';
|
||||
return `aioncore-${tag}-${normalizedArch}-${normalizedPlatform}${ext}`;
|
||||
}
|
||||
|
||||
function getDownloadUrl(assetName, tag) {
|
||||
return `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download/${tag}/${assetName}`;
|
||||
}
|
||||
|
||||
function downloadFile(url, outputPath) {
|
||||
console.log(` Downloading aioncore from ${url}`);
|
||||
if (process.platform === 'win32') {
|
||||
const ps = `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${url}' -OutFile '${outputPath.replace(/'/g, "''")}'`;
|
||||
execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], {
|
||||
timeout: 120000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
execFileSync('curl', ['-L', '--fail', '--silent', '--show-error', '-o', outputPath, url], { timeout: 120000 });
|
||||
} catch {
|
||||
execFileSync('wget', ['-q', '-O', outputPath, url], { timeout: 120000 });
|
||||
}
|
||||
}
|
||||
|
||||
function extractArchive(archivePath, outputDir, platform) {
|
||||
ensureDirectory(outputDir);
|
||||
if (platform === 'win32' || archivePath.endsWith('.zip')) {
|
||||
if (platform === 'win32') {
|
||||
const ps = `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${outputDir.replace(/'/g, "''")}' -Force`;
|
||||
execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps]);
|
||||
} else {
|
||||
execFileSync('unzip', ['-o', archivePath, '-d', outputDir]);
|
||||
}
|
||||
} else {
|
||||
execFileSync('tar', ['-xzf', archivePath, '-C', outputDir]);
|
||||
}
|
||||
}
|
||||
|
||||
function findBinaryInDir(dir, binaryName) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isFile() && entry.name === binaryName) return fullPath;
|
||||
if (entry.isDirectory()) {
|
||||
const found = findBinaryInDir(fullPath, binaryName);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findAioncoreArchiveInDir(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (
|
||||
entry.isFile() &&
|
||||
entry.name.startsWith('aioncore-') &&
|
||||
(entry.name.endsWith('.zip') || entry.name.endsWith('.tar.gz'))
|
||||
) {
|
||||
return fullPath;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
const found = findAioncoreArchiveInDir(fullPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGitHubToken() {
|
||||
return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '';
|
||||
}
|
||||
|
||||
function githubApiGetJson(apiPath) {
|
||||
const token = getGitHubToken();
|
||||
|
||||
try {
|
||||
return JSON.parse(
|
||||
execFileSync('gh', ['api', apiPath], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
env: {
|
||||
...process.env,
|
||||
GH_TOKEN: token || process.env.GH_TOKEN,
|
||||
},
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// gh CLI not available or failed — fall back to curl.
|
||||
}
|
||||
|
||||
const headers = ['-H', 'Accept: application/vnd.github+json'];
|
||||
if (token) {
|
||||
headers.push('-H', `Authorization: Bearer ${token}`);
|
||||
}
|
||||
|
||||
const url = `https://api.github.com/${apiPath}`;
|
||||
const out = execFileSync('curl', ['-fsSL', ...headers, url], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
});
|
||||
return JSON.parse(out);
|
||||
}
|
||||
|
||||
function downloadFileWithAuth(url, outputPath) {
|
||||
const token = getGitHubToken();
|
||||
const headers = ['-H', 'Accept: application/vnd.github+json'];
|
||||
if (token) {
|
||||
headers.push('-H', `Authorization: Bearer ${token}`);
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync('curl', ['-L', '--fail', '--silent', '--show-error', ...headers, '-o', outputPath, url], {
|
||||
timeout: 120000,
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
// curl may be unavailable in some local environments; try gh before failing.
|
||||
}
|
||||
|
||||
execFileSync('gh', ['api', url, '--output', outputPath], {
|
||||
timeout: 120000,
|
||||
env: {
|
||||
...process.env,
|
||||
GH_TOKEN: token || process.env.GH_TOKEN,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function listActionsArtifacts(runId) {
|
||||
const response = githubApiGetJson(
|
||||
`repos/${GITHUB_OWNER}/${GITHUB_REPO}/actions/runs/${runId}/artifacts?per_page=100`
|
||||
);
|
||||
return Array.isArray(response?.artifacts) ? response.artifacts : [];
|
||||
}
|
||||
|
||||
function downloadAndExtractActionsArtifact(platform, arch, runId) {
|
||||
const expectedArtifactName = getActionsArtifactName(platform, arch);
|
||||
if (!expectedArtifactName) {
|
||||
throw new Error(`Unsupported AionCore Actions artifact target: ${platform}-${arch}`);
|
||||
}
|
||||
|
||||
const artifacts = listActionsArtifacts(runId);
|
||||
const availableArtifactNames = artifacts
|
||||
.map((artifact) => artifact.name)
|
||||
.filter(Boolean)
|
||||
.toSorted();
|
||||
const artifact = artifacts.find((candidate) => candidate.name === expectedArtifactName);
|
||||
if (!artifact) {
|
||||
throw new Error(
|
||||
getActionsArtifactMissingMessage({
|
||||
runId,
|
||||
platform,
|
||||
arch,
|
||||
expectedArtifactName,
|
||||
availableArtifactNames,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const tempDir = path.join(os.tmpdir(), 'aioncore-prepare-actions', runId, `${platform}-${arch}`);
|
||||
const artifactZipPath = path.join(tempDir, `${expectedArtifactName}.zip`);
|
||||
const artifactExtractDir = path.join(tempDir, 'artifact');
|
||||
const binaryExtractDir = path.join(tempDir, 'binary');
|
||||
|
||||
removeDirectorySafe(tempDir);
|
||||
ensureDirectory(tempDir);
|
||||
|
||||
const downloadUrl =
|
||||
artifact.archive_download_url ||
|
||||
`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/actions/artifacts/${artifact.id}/zip`;
|
||||
console.log(` Downloading aioncore from AionCore run ${runId} artifact ${expectedArtifactName}`);
|
||||
downloadFileWithAuth(downloadUrl, artifactZipPath);
|
||||
extractArchive(artifactZipPath, artifactExtractDir, platform);
|
||||
|
||||
const archivePath = findAioncoreArchiveInDir(artifactExtractDir);
|
||||
if (!archivePath) {
|
||||
throw new Error(`AionCore artifact ${expectedArtifactName} from run ${runId} does not contain an aioncore archive`);
|
||||
}
|
||||
|
||||
extractArchive(archivePath, binaryExtractDir, platform);
|
||||
|
||||
const binaryName = getBinaryName(platform);
|
||||
const binaryPath = findBinaryInDir(binaryExtractDir, binaryName);
|
||||
if (!binaryPath) {
|
||||
throw new Error(`Binary ${binaryName} not found in AionCore artifact ${expectedArtifactName} from run ${runId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
binaryPath,
|
||||
tempDir,
|
||||
artifactName: expectedArtifactName,
|
||||
archivePath,
|
||||
url: downloadUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function downloadAndExtract(platform, arch, tag) {
|
||||
const assetName = getAssetName(platform, arch, tag);
|
||||
if (!assetName) {
|
||||
throw new Error(`Unsupported aioncore target: ${platform}-${arch}`);
|
||||
}
|
||||
|
||||
const url = getDownloadUrl(assetName, tag);
|
||||
const tempDir = path.join(os.tmpdir(), 'aioncore-prepare', tag, `${platform}-${arch}`);
|
||||
const archivePath = path.join(tempDir, assetName);
|
||||
const extractDir = path.join(tempDir, 'extracted');
|
||||
|
||||
removeDirectorySafe(tempDir);
|
||||
ensureDirectory(tempDir);
|
||||
|
||||
downloadFile(url, archivePath);
|
||||
extractArchive(archivePath, extractDir, platform);
|
||||
|
||||
const binaryName = getBinaryName(platform);
|
||||
const binaryPath = findBinaryInDir(extractDir, binaryName);
|
||||
if (!binaryPath) {
|
||||
throw new Error(`Binary ${binaryName} not found in downloaded archive`);
|
||||
}
|
||||
|
||||
return { binaryPath, tempDir, url };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Prepare aioncore binary for packaging.
|
||||
*
|
||||
* @param {object} options - Configuration options
|
||||
* @param {string} options.projectRoot - Project root directory
|
||||
* @param {string} options.platform - Target platform (process.platform)
|
||||
* @param {string} options.arch - Target architecture (process.arch)
|
||||
* @param {string} options.version - Backend version (default: 'latest')
|
||||
* @returns {{ prepared: true; dir: string; sourceType: string }}
|
||||
*/
|
||||
function prepareAioncore(options) {
|
||||
const { projectRoot, platform, arch, version = 'latest' } = options;
|
||||
const runtimeKey = `${platform}-${arch}`;
|
||||
const actionsRunId = (process.env.AIONUI_BACKEND_RUN_ID || '').trim();
|
||||
|
||||
let tag = null;
|
||||
if (!actionsRunId) {
|
||||
// Resolve the actual version tag — release asset filenames include the tag.
|
||||
if (version === 'latest') {
|
||||
const resolved = resolveLatestTag();
|
||||
if (!resolved) {
|
||||
throw new Error('Failed to resolve latest aioncore release tag from GitHub API');
|
||||
}
|
||||
tag = resolved;
|
||||
console.log(`Resolved aioncore "latest" → ${tag}`);
|
||||
} else {
|
||||
tag = version.startsWith('v') ? version : `v${version}`;
|
||||
}
|
||||
}
|
||||
|
||||
const targetDir = path.join(projectRoot, 'resources', 'bundled-aioncore', runtimeKey);
|
||||
const binaryName = getBinaryName(platform);
|
||||
const targetBinaryPath = path.join(targetDir, binaryName);
|
||||
|
||||
console.log(
|
||||
`Preparing aioncore for ${runtimeKey} (${actionsRunId ? `actions run: ${actionsRunId}` : `version: ${tag}`})`
|
||||
);
|
||||
|
||||
removeDirectorySafe(targetDir);
|
||||
ensureDirectory(targetDir);
|
||||
|
||||
const localBundleDir = (process.env.AIONUI_BACKEND_LOCAL_BUNDLE_DIR || '').trim();
|
||||
if (localBundleDir) {
|
||||
const resolvedLocalBundleDir = path.resolve(localBundleDir);
|
||||
const localBinaryPath = path.join(resolvedLocalBundleDir, binaryName);
|
||||
const localManagedResourcesDir = path.join(resolvedLocalBundleDir, 'managed-resources');
|
||||
if (
|
||||
fs.existsSync(resolvedLocalBundleDir) &&
|
||||
fs.statSync(resolvedLocalBundleDir).isDirectory() &&
|
||||
fs.existsSync(localBinaryPath) &&
|
||||
fs.existsSync(localManagedResourcesDir)
|
||||
) {
|
||||
copyDirectorySafe(resolvedLocalBundleDir, targetDir);
|
||||
ensureExecutableMode(targetBinaryPath);
|
||||
const manifest = {
|
||||
platform,
|
||||
arch,
|
||||
version: tag || `actions-run-${actionsRunId}` || 'local-bundle',
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourceType: 'local-bundle',
|
||||
source: { path: resolvedLocalBundleDir },
|
||||
files: [binaryName, 'managed-resources/'],
|
||||
};
|
||||
writeJson(path.join(targetDir, 'manifest.json'), manifest);
|
||||
console.log(` Using local aioncore bundle: ${resolvedLocalBundleDir}`);
|
||||
return { prepared: true, dir: targetDir, sourceType: 'local-bundle' };
|
||||
}
|
||||
console.warn(` Local aioncore bundle is incomplete or missing: ${resolvedLocalBundleDir}`);
|
||||
}
|
||||
|
||||
let sourcePath = null;
|
||||
let sourceType = 'none';
|
||||
let sourceDetail = {};
|
||||
let tempDir = null;
|
||||
|
||||
// 1. Download from GitHub Actions artifacts when manual build run id is provided.
|
||||
if (actionsRunId) {
|
||||
const result = downloadAndExtractActionsArtifact(platform, arch, actionsRunId);
|
||||
sourcePath = result.binaryPath;
|
||||
tempDir = result.tempDir;
|
||||
sourceType = 'actions-artifact';
|
||||
sourceDetail = {
|
||||
runId: actionsRunId,
|
||||
artifactName: result.artifactName,
|
||||
url: result.url,
|
||||
};
|
||||
console.log(` Downloaded from GitHub Actions artifact`);
|
||||
}
|
||||
|
||||
// 2. Download from GitHub releases.
|
||||
if (!sourcePath && tag) {
|
||||
try {
|
||||
const result = downloadAndExtract(platform, arch, tag);
|
||||
sourcePath = result.binaryPath;
|
||||
tempDir = result.tempDir;
|
||||
sourceType = 'download';
|
||||
sourceDetail = { url: result.url };
|
||||
console.log(` Downloaded from GitHub releases`);
|
||||
} catch (error) {
|
||||
console.warn(` Download failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Use an explicitly supplied local cache when network download is unavailable.
|
||||
if (!sourcePath) {
|
||||
const localBinary = (process.env.AIONUI_BACKEND_LOCAL_BINARY || '').trim();
|
||||
if (localBinary) {
|
||||
const resolvedLocalBinary = path.resolve(localBinary);
|
||||
if (fs.existsSync(resolvedLocalBinary) && fs.statSync(resolvedLocalBinary).isFile()) {
|
||||
sourcePath = resolvedLocalBinary;
|
||||
sourceType = 'local-binary';
|
||||
sourceDetail = { path: resolvedLocalBinary };
|
||||
console.log(` Using local aioncore binary: ${resolvedLocalBinary}`);
|
||||
} else {
|
||||
console.warn(` Local aioncore binary not found: ${resolvedLocalBinary}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write result
|
||||
if (sourcePath) {
|
||||
copyFileSafe(sourcePath, targetBinaryPath);
|
||||
ensureExecutableMode(targetBinaryPath);
|
||||
const bundledManagedResourcesDir = prepareManagedResources(targetBinaryPath, targetDir);
|
||||
|
||||
// The release tag is the authoritative version — the aioncore
|
||||
// binary does not expose a --version flag (it has --app-version which
|
||||
// takes a value, not a self-report).
|
||||
const manifest = {
|
||||
platform,
|
||||
arch,
|
||||
version: tag || `actions-run-${actionsRunId}`,
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourceType,
|
||||
source: sourceDetail,
|
||||
files: [binaryName, 'managed-resources/'],
|
||||
};
|
||||
|
||||
writeJson(path.join(targetDir, 'manifest.json'), manifest);
|
||||
console.log(
|
||||
` Bundled aioncore prepared: resources/bundled-aioncore/${runtimeKey}/${binaryName} [source=${sourceType}]`
|
||||
);
|
||||
console.log(` Bundled managed resources prepared: ${bundledManagedResourcesDir}`);
|
||||
|
||||
if (tempDir) removeDirectorySafe(tempDir);
|
||||
return { prepared: true, dir: targetDir, sourceType };
|
||||
}
|
||||
|
||||
throw new Error(`aioncore binary not found for ${runtimeKey} (tag: ${tag})`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getActionsArtifactMissingMessage,
|
||||
getActionsArtifactName,
|
||||
prepareAioncore,
|
||||
};
|
||||
@@ -0,0 +1,254 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function backendBinaryName(platform) {
|
||||
return platform === 'win32' ? 'aioncore.exe' : 'aioncore';
|
||||
}
|
||||
|
||||
function nodeBinaryName(platform) {
|
||||
return platform === 'win32' ? 'node.exe' : 'node';
|
||||
}
|
||||
|
||||
function nodeExecutableParts(platform) {
|
||||
return platform === 'win32' ? [nodeBinaryName(platform)] : ['bin', nodeBinaryName(platform)];
|
||||
}
|
||||
|
||||
function normalize(relativePath) {
|
||||
return relativePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function bundledPath(runtimeKey, ...parts) {
|
||||
return normalize(path.join('bundled-aioncore', runtimeKey, ...parts));
|
||||
}
|
||||
|
||||
function requireRelativePath(baseDir, runtimeKey, parts, checked, missing) {
|
||||
const relativePath = bundledPath(runtimeKey, ...parts);
|
||||
checked.push(relativePath);
|
||||
|
||||
if (!isFile(path.join(baseDir, ...parts))) {
|
||||
missing.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
function requireRelativeDirectory(baseDir, runtimeKey, parts, checked, missing) {
|
||||
const relativePath = bundledPath(runtimeKey, ...parts);
|
||||
checked.push(relativePath);
|
||||
|
||||
const fullPath = path.join(baseDir, ...parts);
|
||||
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
||||
missing.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
function readDirectories(root) {
|
||||
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return [];
|
||||
|
||||
return fs
|
||||
.readdirSync(root, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
function isFile(filePath) {
|
||||
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
||||
}
|
||||
|
||||
function requireFile(baseDir, runtimeKey, parts, checked, missing) {
|
||||
const relativePath = bundledPath(runtimeKey, ...parts);
|
||||
checked.push(relativePath);
|
||||
|
||||
if (!isFile(path.join(baseDir, ...parts))) {
|
||||
missing.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
function requireDirectory(baseDir, runtimeKey, parts, checked, missing) {
|
||||
const relativePath = bundledPath(runtimeKey, ...parts);
|
||||
checked.push(relativePath);
|
||||
|
||||
const fullPath = path.join(baseDir, ...parts);
|
||||
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
||||
missing.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyBundleManifest(baseDir, runtimeKey, electronPlatformName, targetArch, checked, missing) {
|
||||
const parts = ['manifest.json'];
|
||||
const relativePath = bundledPath(runtimeKey, ...parts);
|
||||
const manifestPath = path.join(baseDir, ...parts);
|
||||
checked.push(relativePath);
|
||||
|
||||
if (!isFile(manifestPath)) {
|
||||
missing.push(relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = readManifest(manifestPath);
|
||||
if (!manifest) {
|
||||
missing.push(`${relativePath}<invalid-json>`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (manifest.platform !== electronPlatformName) {
|
||||
missing.push(`${relativePath}<platform:${electronPlatformName}>`);
|
||||
}
|
||||
|
||||
if (manifest.arch !== targetArch) {
|
||||
missing.push(`${relativePath}<arch:${targetArch}>`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireManagedNode(baseDir, runtimeKey, platform, checked, missing) {
|
||||
const nodeRoot = path.join(baseDir, 'managed-resources', 'node');
|
||||
const versions = readDirectories(nodeRoot);
|
||||
const executableParts = nodeExecutableParts(platform);
|
||||
|
||||
if (versions.length === 0) {
|
||||
const relativePath = bundledPath(runtimeKey, 'managed-resources', 'node', '*', ...executableParts);
|
||||
checked.push(relativePath);
|
||||
missing.push(relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const version of versions) {
|
||||
requireFile(baseDir, runtimeKey, ['managed-resources', 'node', version, ...executableParts], checked, missing);
|
||||
}
|
||||
}
|
||||
|
||||
const CODEX_VENDOR_TRIPLE_BY_RUNTIME_KEY = {
|
||||
'win32-arm64': 'aarch64-pc-windows-msvc',
|
||||
'win32-x64': 'x86_64-pc-windows-msvc',
|
||||
};
|
||||
|
||||
function acpToolPlatformExecutableParts(platform, runtimeKey, toolId) {
|
||||
if (platform !== 'win32') return null;
|
||||
|
||||
if (toolId === 'codex-acp') {
|
||||
const vendorTriple = CODEX_VENDOR_TRIPLE_BY_RUNTIME_KEY[runtimeKey];
|
||||
if (!vendorTriple) return null;
|
||||
|
||||
return ['node_modules', '@openai', `codex-${runtimeKey}`, 'vendor', vendorTriple, 'bin', 'codex.exe'];
|
||||
}
|
||||
|
||||
if (toolId === 'claude-agent-acp') {
|
||||
return ['node_modules', '@anthropic-ai', `claude-agent-sdk-${runtimeKey}`, 'claude.exe'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readManifest(manifestPath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function requireManagedAcpTool(baseDir, runtimeKey, platform, toolId, checked, missing) {
|
||||
const toolRoot = path.join(baseDir, 'managed-resources', 'acp', toolId);
|
||||
const versions = readDirectories(toolRoot);
|
||||
|
||||
if (versions.length === 0) {
|
||||
const relativePath = bundledPath(runtimeKey, 'managed-resources', 'acp', toolId, '*', runtimeKey, 'manifest.json');
|
||||
checked.push(relativePath);
|
||||
missing.push(relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const version of versions) {
|
||||
const platformRoot = path.join(toolRoot, version, runtimeKey);
|
||||
const manifestRelativePath = bundledPath(
|
||||
runtimeKey,
|
||||
'managed-resources',
|
||||
'acp',
|
||||
toolId,
|
||||
version,
|
||||
runtimeKey,
|
||||
'manifest.json'
|
||||
);
|
||||
checked.push(manifestRelativePath);
|
||||
|
||||
const manifestPath = path.join(platformRoot, 'manifest.json');
|
||||
if (!isFile(manifestPath)) {
|
||||
missing.push(manifestRelativePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const manifest = readManifest(manifestPath);
|
||||
const entrypoint = typeof manifest?.entrypoint === 'string' ? manifest.entrypoint : null;
|
||||
if (!entrypoint) {
|
||||
missing.push(bundledPath(runtimeKey, 'managed-resources', 'acp', toolId, version, runtimeKey, '<entrypoint>'));
|
||||
continue;
|
||||
}
|
||||
|
||||
const entrypointRelativePath = bundledPath(
|
||||
runtimeKey,
|
||||
'managed-resources',
|
||||
'acp',
|
||||
toolId,
|
||||
version,
|
||||
runtimeKey,
|
||||
entrypoint
|
||||
);
|
||||
checked.push(entrypointRelativePath);
|
||||
|
||||
if (!isFile(path.join(platformRoot, entrypoint))) {
|
||||
missing.push(entrypointRelativePath);
|
||||
}
|
||||
|
||||
requireFile(
|
||||
baseDir,
|
||||
runtimeKey,
|
||||
['managed-resources', 'acp', toolId, version, runtimeKey, 'package.json'],
|
||||
checked,
|
||||
missing
|
||||
);
|
||||
requireFile(
|
||||
baseDir,
|
||||
runtimeKey,
|
||||
['managed-resources', 'acp', toolId, version, runtimeKey, 'package-lock.json'],
|
||||
checked,
|
||||
missing
|
||||
);
|
||||
requireDirectory(
|
||||
baseDir,
|
||||
runtimeKey,
|
||||
['managed-resources', 'acp', toolId, version, runtimeKey, 'node_modules'],
|
||||
checked,
|
||||
missing
|
||||
);
|
||||
|
||||
const platformExecutableParts = acpToolPlatformExecutableParts(platform, runtimeKey, toolId);
|
||||
if (platformExecutableParts) {
|
||||
requireFile(
|
||||
baseDir,
|
||||
runtimeKey,
|
||||
['managed-resources', 'acp', toolId, version, runtimeKey, ...platformExecutableParts],
|
||||
checked,
|
||||
missing
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verifyBundledAioncoreResources({ resourcesDir, electronPlatformName, targetArch }) {
|
||||
const runtimeKey = `${electronPlatformName}-${targetArch}`;
|
||||
const baseDir = path.join(resourcesDir, 'bundled-aioncore', runtimeKey);
|
||||
const checked = [];
|
||||
const missing = [];
|
||||
|
||||
requireRelativePath(baseDir, runtimeKey, [backendBinaryName(electronPlatformName)], checked, missing);
|
||||
verifyBundleManifest(baseDir, runtimeKey, electronPlatformName, targetArch, checked, missing);
|
||||
requireRelativeDirectory(baseDir, runtimeKey, ['managed-resources'], checked, missing);
|
||||
requireManagedNode(baseDir, runtimeKey, electronPlatformName, checked, missing);
|
||||
requireManagedAcpTool(baseDir, runtimeKey, electronPlatformName, 'codex-acp', checked, missing);
|
||||
requireManagedAcpTool(baseDir, runtimeKey, electronPlatformName, 'claude-agent-acp', checked, missing);
|
||||
|
||||
return { runtimeKey, checked, missing };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyBundledAioncoreResources,
|
||||
};
|
||||
Reference in New Issue
Block a user