mess around with a rust agent version with just rev shell.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Rust Lite Builder Plugin
|
||||
|
||||
This trusted server-only plugin registers Rust Lite as an agent build provider.
|
||||
Select **Rust Lite** from the Builder's **Agent Type** field, choose targets, and
|
||||
start the build normally. Output, history, downloads, expiration, rate limits,
|
||||
RBAC, and artifact hooks remain owned by the Overlord server.
|
||||
|
||||
The resulting client supports authenticated enrollment, TLS identity pinning,
|
||||
heartbeat, reconnect behavior, and interactive TTY console sessions. It
|
||||
intentionally contains no plugin loader or other remote command handlers.
|
||||
|
||||
During compilation the Build page shows estimated overall/target percentages,
|
||||
completed Cargo crates, elapsed time, the current compile or link/LTO phase,
|
||||
and a rolling ETA. Cargo does not expose exact link progress, so percentages
|
||||
and ETA are estimates. Completed target durations are retained in plugin data
|
||||
to improve later estimates, and the persistent Cargo target directory makes
|
||||
subsequent builds substantially faster.
|
||||
|
||||
The **Cargo Jobs** setting defaults to `0`, which leaves parallelism at Cargo's
|
||||
host-CPU default. Set a positive number only when the server needs an explicit
|
||||
resource cap. The plugin cache is separate from `Overlord-Lite/target`, so the
|
||||
first plugin build does not reuse artifacts from a manual source-tree build.
|
||||
|
||||
The server host needs Rust 1.88 or newer. Native-host builds work with the
|
||||
standard Rust installation. Cross-target builds additionally require the target
|
||||
from `rustup target add <triple>` and a compatible linker/toolchain.
|
||||
|
||||
The plugin locates `Overlord-Lite` beside the server runtime or under its
|
||||
`dist/` directory. Production package scripts copy that source automatically.
|
||||
|
||||
Builder releases compile in the selected server URL, agent token, and signed
|
||||
build tag. Runtime `OVERLORD_SERVER`, `OVERLORD_AGENT_TOKEN`, and
|
||||
`OVERLORD_BUILD_TAG` variables can still override those defaults.
|
||||
|
||||
Builds also embed the server's active TLS SPKI pins. This lets Lite clients
|
||||
trust the server's generated self-signed certificate without disabling
|
||||
certificate verification and protects later HTTPS plugin downloads too.
|
||||
|
||||
All provider builds use Cargo's `--release` profile. That profile uses
|
||||
`opt-level = "z"`, full LTO, one codegen unit, symbol stripping, and
|
||||
`panic = "abort"`.
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "PLUGIN_ID=rust-lite-builder"
|
||||
set "OUT=%~dp0%PLUGIN_ID%.zip"
|
||||
if exist "%OUT%" del /q "%OUT%"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "Compress-Archive -Path '%~dp0config.json','%~dp0rust-lite-builder.html','%~dp0rust-lite-builder.css','%~dp0rust-lite-builder.js','%~dp0server.js' -DestinationPath '%OUT%' -Force"
|
||||
if errorlevel 1 exit /b 1
|
||||
echo [ok] %OUT%
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT"
|
||||
rm -f rust-lite-builder.zip
|
||||
zip -q rust-lite-builder.zip config.json rust-lite-builder.html rust-lite-builder.css rust-lite-builder.js server.js
|
||||
echo "[ok] $ROOT/rust-lite-builder.zip"
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "Rust Lite Builder",
|
||||
"version": "1.2.0",
|
||||
"apiVersion": 2,
|
||||
"runtime": "server",
|
||||
"description": "Adds a dedicated builder for the authenticated Rust Lite agent with an interactive TTY console.",
|
||||
"build": {
|
||||
"label": "Rust Lite Builder",
|
||||
"description": "Builds the lightweight Rust client instead of the full Go agent.",
|
||||
"enabledByDefault": true,
|
||||
"provider": {
|
||||
"label": "Rust Lite",
|
||||
"description": "Authenticated sessions, TLS pinning, heartbeat, and an interactive TTY console.",
|
||||
"icon": "fa-brands fa-rust",
|
||||
"platforms": [
|
||||
"windows-amd64",
|
||||
"windows-arm64",
|
||||
"linux-amd64",
|
||||
"linux-arm64",
|
||||
"darwin-amd64",
|
||||
"darwin-arm64"
|
||||
]
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"key": "locked",
|
||||
"label": "Require Cargo.lock",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Build with --locked for reproducible dependency resolution."
|
||||
},
|
||||
{
|
||||
"key": "jobs",
|
||||
"label": "Cargo Jobs",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 32,
|
||||
"description": "Parallel Cargo jobs. Use 0 to let Cargo use the host's available CPUs."
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "use-rust-lite",
|
||||
"label": "Use Rust Lite Builder",
|
||||
"icon": "fa-brands fa-rust",
|
||||
"description": "Selects Rust Lite as the active agent builder.",
|
||||
"setBuild": {
|
||||
"buildProvider": "rust-lite-builder"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"navbar": {
|
||||
"label": "Rust Lite",
|
||||
"icon": "fa-brands fa-rust"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
.rust-lite-builder {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.rust-lite-builder header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.rust-lite-builder h2 {
|
||||
margin: 0.15rem 0 0.35rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.rust-lite-builder p {
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.rust-lite-builder .eyebrow {
|
||||
color: #fb923c;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rust-lite-builder button {
|
||||
border: 1px solid #c2410c;
|
||||
border-radius: 0.5rem;
|
||||
background: #9a3412;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 0.55rem 0.8rem;
|
||||
}
|
||||
|
||||
.rust-lite-builder pre {
|
||||
min-height: 8rem;
|
||||
overflow: auto;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.65rem;
|
||||
background: #0f172a;
|
||||
padding: 1rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<main class="rust-lite-builder">
|
||||
<header>
|
||||
<div>
|
||||
<p class="eyebrow">Server build provider</p>
|
||||
<h2>Rust Lite Builder</h2>
|
||||
<p>Builds the authenticated Rust agent with interactive TTY console support through the normal Overlord build workflow.</p>
|
||||
</div>
|
||||
<button id="rust-lite-refresh" type="button">Refresh status</button>
|
||||
</header>
|
||||
<pre id="rust-lite-status">Loading...</pre>
|
||||
</main>
|
||||
@@ -0,0 +1,21 @@
|
||||
const statusOutput = document.getElementById("rust-lite-status");
|
||||
const refreshButton = document.getElementById("rust-lite-refresh");
|
||||
|
||||
async function refreshStatus() {
|
||||
statusOutput.textContent = "Checking Rust Lite builder...";
|
||||
try {
|
||||
const response = await fetch("/api/plugins/rust-lite-builder/rpc", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ method: "status", params: {} }),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!body.ok) throw new Error(body.error || "Status check failed");
|
||||
statusOutput.textContent = JSON.stringify(body.result, null, 2);
|
||||
} catch (error) {
|
||||
statusOutput.textContent = `Error: ${error?.message || error}`;
|
||||
}
|
||||
}
|
||||
|
||||
refreshButton?.addEventListener("click", refreshStatus);
|
||||
refreshStatus();
|
||||
@@ -0,0 +1,415 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const TARGETS = {
|
||||
"windows-amd64": { windows: "x86_64-pc-windows-msvc", other: "x86_64-pc-windows-gnu", extension: ".exe" },
|
||||
"windows-arm64": { windows: "aarch64-pc-windows-msvc", other: "aarch64-pc-windows-gnullvm", extension: ".exe" },
|
||||
"linux-amd64": { all: "x86_64-unknown-linux-gnu", extension: "" },
|
||||
"linux-arm64": { all: "aarch64-unknown-linux-gnu", extension: "" },
|
||||
"darwin-amd64": { all: "x86_64-apple-darwin", extension: "" },
|
||||
"darwin-arm64": { all: "aarch64-apple-darwin", extension: "" },
|
||||
};
|
||||
|
||||
function rustTarget(platform) {
|
||||
const target = TARGETS[platform];
|
||||
if (!target) throw new Error(`Unsupported Rust Lite platform: ${platform}`);
|
||||
return process.platform === "win32" ? (target.windows || target.all) : (target.other || target.all);
|
||||
}
|
||||
|
||||
function findSource(runtimeRoot) {
|
||||
const candidates = [
|
||||
path.join(runtimeRoot, "Overlord-Lite"),
|
||||
path.join(runtimeRoot, "dist", "Overlord-Lite"),
|
||||
path.join(runtimeRoot, "..", "Overlord-Lite"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(path.join(candidate, "Cargo.toml"))) return candidate;
|
||||
}
|
||||
throw new Error(`Overlord-Lite source not found. Checked: ${candidates.join(", ")}`);
|
||||
}
|
||||
|
||||
async function capture(command, cwd) {
|
||||
const proc = Bun.spawn(command, { cwd, stdout: "pipe", stderr: "pipe" });
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
if (exitCode !== 0) throw new Error(stderr.trim() || `${command[0]} exited with ${exitCode}`);
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
async function streamPipe(stream, onLine) {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let pending = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
pending += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = pending.split(/\r?\n/);
|
||||
pending = lines.pop() || "";
|
||||
for (const line of lines) if (line.trim()) onLine(line);
|
||||
if (done) break;
|
||||
}
|
||||
if (pending.trim()) onLine(pending);
|
||||
}
|
||||
|
||||
function packageVersion(sourceDir) {
|
||||
const cargo = fs.readFileSync(path.join(sourceDir, "Cargo.toml"), "utf8");
|
||||
return cargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1] || "unknown";
|
||||
}
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes < 60) return remainingSeconds ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return remainingMinutes ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
||||
}
|
||||
|
||||
async function estimatePackageCount(sourceDir, target, locked) {
|
||||
const args = [
|
||||
"cargo",
|
||||
"tree",
|
||||
"--target",
|
||||
target,
|
||||
"--prefix",
|
||||
"none",
|
||||
"--format",
|
||||
"{p}",
|
||||
];
|
||||
if (locked) args.push("--locked");
|
||||
const output = await capture(args, sourceDir);
|
||||
const packages = new Set(
|
||||
output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.replace(/ \(\*\)$/, "").replace(/ \(proc-macro\)$/, "").trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
return packages.size;
|
||||
}
|
||||
|
||||
function readBuildHistory(dataDir) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(path.join(dataDir, "build-history.json"), "utf8"));
|
||||
return parsed && typeof parsed === "object" ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function recordBuildDuration(dataDir, target, durationMs) {
|
||||
const historyPath = path.join(dataDir, "build-history.json");
|
||||
const history = readBuildHistory(dataDir);
|
||||
const previous = Number(history[target]?.durationMs);
|
||||
const samples = Math.max(0, Number(history[target]?.samples) || 0) + 1;
|
||||
history[target] = {
|
||||
durationMs: Number.isFinite(previous)
|
||||
? Math.round(previous * 0.6 + durationMs * 0.4)
|
||||
: Math.round(durationMs),
|
||||
samples,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
try {
|
||||
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
||||
} catch {
|
||||
// Progress history is optional; a read-only data directory must not fail the build.
|
||||
}
|
||||
}
|
||||
|
||||
function createTargetProgress(ctx, options) {
|
||||
const DEFAULT_LINK_BUDGET_MS = 35_000;
|
||||
const startedAt = Date.now();
|
||||
const packageStates = new Map();
|
||||
let linking = false;
|
||||
let linkingStartedAt = null;
|
||||
let finalizing = false;
|
||||
let finished = false;
|
||||
let lastStatusAt = 0;
|
||||
let lastLoggedBucket = -1;
|
||||
|
||||
function snapshot() {
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const completedPackages = packageStates.size;
|
||||
const freshPackages = Array.from(packageStates.values()).filter(Boolean).length;
|
||||
const packageRatio = options.estimatedPackages > 0
|
||||
? Math.min(1, completedPackages / options.estimatedPackages)
|
||||
: 0;
|
||||
const historicalDurationMs = Number.isFinite(options.historicalDurationMs)
|
||||
? options.historicalDurationMs
|
||||
: null;
|
||||
const linkBudgetMs = historicalDurationMs === null
|
||||
? DEFAULT_LINK_BUDGET_MS
|
||||
: Math.max(10_000, historicalDurationMs * 0.4);
|
||||
const linkingElapsedMs = linkingStartedAt === null ? 0 : Date.now() - linkingStartedAt;
|
||||
let targetRatio = Math.min(0.7, packageRatio * 0.7);
|
||||
if (linking) {
|
||||
const estimatedLinkRatio = Math.min(0.95, linkingElapsedMs / linkBudgetMs);
|
||||
targetRatio = Math.max(targetRatio, 0.7 + estimatedLinkRatio * 0.28);
|
||||
}
|
||||
if (finalizing) targetRatio = Math.max(targetRatio, 0.98);
|
||||
if (finished) targetRatio = 1;
|
||||
const overallRatio = (options.targetIndex + targetRatio) / options.targetCount;
|
||||
|
||||
let predictedTargetMs = null;
|
||||
if (historicalDurationMs !== null && historicalDurationMs > 0) {
|
||||
predictedTargetMs = Math.max(elapsedMs, historicalDurationMs);
|
||||
}
|
||||
if (linking && historicalDurationMs === null) {
|
||||
predictedTargetMs = elapsedMs + Math.max(5_000, linkBudgetMs - linkingElapsedMs);
|
||||
} else if (!linking && elapsedMs >= 5_000 && packageRatio >= 0.05) {
|
||||
const rollingPrediction = elapsedMs / packageRatio + linkBudgetMs;
|
||||
predictedTargetMs = predictedTargetMs === null
|
||||
? rollingPrediction
|
||||
: predictedTargetMs * 0.65 + rollingPrediction * 0.35;
|
||||
}
|
||||
const etaMs = predictedTargetMs === null || finished
|
||||
? null
|
||||
: Math.max(
|
||||
0,
|
||||
predictedTargetMs - elapsedMs +
|
||||
(options.targetCount - options.targetIndex - 1) * predictedTargetMs,
|
||||
);
|
||||
|
||||
return {
|
||||
elapsedMs,
|
||||
completedPackages,
|
||||
freshPackages,
|
||||
targetRatio,
|
||||
overallRatio,
|
||||
etaMs,
|
||||
};
|
||||
}
|
||||
|
||||
function emit(force = false) {
|
||||
const now = Date.now();
|
||||
if (!force && now - lastStatusAt < 1_500) return;
|
||||
lastStatusAt = now;
|
||||
const state = snapshot();
|
||||
const targetPercent = Math.round(state.targetRatio * 100);
|
||||
const overallPercent = Math.round(state.overallRatio * 100);
|
||||
const phase = finalizing
|
||||
? "finalizing artifact"
|
||||
: linking
|
||||
? "linking + LTO"
|
||||
: "compiling";
|
||||
const pieces = [
|
||||
`Rust Lite ${options.platform} (${options.targetIndex + 1}/${options.targetCount})`,
|
||||
`${overallPercent}% overall`,
|
||||
`${targetPercent}% target`,
|
||||
phase,
|
||||
];
|
||||
if (options.estimatedPackages > 0) {
|
||||
pieces.push(`${state.completedPackages}/${options.estimatedPackages} crates`);
|
||||
}
|
||||
pieces.push(`${formatDuration(state.elapsedMs)} elapsed`);
|
||||
if (state.etaMs !== null && state.etaMs >= 1_000) {
|
||||
pieces.push(`ETA ~${formatDuration(state.etaMs)}`);
|
||||
}
|
||||
const text = pieces.join(" • ");
|
||||
ctx.build.status(
|
||||
text,
|
||||
Math.min(100, state.overallRatio * 100),
|
||||
state.etaMs === null ? undefined : Math.ceil(state.etaMs / 1000),
|
||||
);
|
||||
|
||||
const bucket = Math.floor(overallPercent / 10);
|
||||
if (bucket > lastLoggedBucket && overallPercent > 0) {
|
||||
lastLoggedBucket = bucket;
|
||||
ctx.build.output(`Progress: ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startedAt,
|
||||
noteArtifact(packageId, fresh) {
|
||||
const previous = packageStates.get(packageId);
|
||||
packageStates.set(packageId, previous === false ? false : fresh === true);
|
||||
emit();
|
||||
},
|
||||
markLinking() {
|
||||
linking = true;
|
||||
linkingStartedAt ??= Date.now();
|
||||
emit(true);
|
||||
},
|
||||
markFinalizing() {
|
||||
finalizing = true;
|
||||
emit(true);
|
||||
},
|
||||
markFinished() {
|
||||
finished = true;
|
||||
emit(true);
|
||||
return snapshot();
|
||||
},
|
||||
emit,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
setup(ctx) {
|
||||
ctx.log.info("Rust Lite build provider ready");
|
||||
},
|
||||
|
||||
async onBuild(ctx, payload) {
|
||||
const sourceDir = findSource(String(payload.runtimeRoot || ""));
|
||||
const outputDir = path.resolve(String(payload.outputDir || ""));
|
||||
const targetDir = path.join(ctx.dataDir, "cargo-target");
|
||||
const platforms = Array.isArray(payload.platforms) ? payload.platforms : [];
|
||||
if (platforms.length === 0) throw new Error("No Rust Lite platforms were requested");
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
const cargoVersion = await capture(["cargo", "--version"], sourceDir);
|
||||
ctx.build.output(`Using ${cargoVersion}`);
|
||||
ctx.build.output(`Rust Lite source: ${sourceDir}`);
|
||||
const settings = payload.settings && typeof payload.settings === "object" ? payload.settings : {};
|
||||
const tlsSpkiPins = Array.isArray(payload.tlsSpkiPins)
|
||||
? payload.tlsSpkiPins.map(String).filter(Boolean)
|
||||
: [];
|
||||
if (tlsSpkiPins.length > 0) {
|
||||
ctx.build.output(
|
||||
`TLS identity pinning: enabled (${tlsSpkiPins.length} trusted key${tlsSpkiPins.length === 1 ? "" : "s"})`,
|
||||
);
|
||||
} else {
|
||||
ctx.build.output("WARNING: No TLS SPKI pin is available for this build", "warn");
|
||||
}
|
||||
const requestedJobs = Math.trunc(Number(settings.jobs));
|
||||
const jobs = Number.isFinite(requestedJobs) && requestedJobs > 0
|
||||
? Math.max(1, Math.min(32, requestedJobs))
|
||||
: null;
|
||||
const version = packageVersion(sourceDir);
|
||||
const artifacts = [];
|
||||
|
||||
const history = readBuildHistory(ctx.dataDir);
|
||||
for (const [targetIndex, platform] of platforms.entries()) {
|
||||
const target = rustTarget(platform);
|
||||
const locked = settings.locked !== false;
|
||||
let estimatedPackages = 0;
|
||||
try {
|
||||
estimatedPackages = await estimatePackageCount(sourceDir, target, locked);
|
||||
} catch (error) {
|
||||
ctx.build.output(`Package-count estimate unavailable: ${error?.message || error}`, "warn");
|
||||
}
|
||||
const progress = createTargetProgress(ctx, {
|
||||
platform,
|
||||
targetIndex,
|
||||
targetCount: platforms.length,
|
||||
estimatedPackages,
|
||||
historicalDurationMs: Number(history[target]?.durationMs) || null,
|
||||
});
|
||||
progress.emit(true);
|
||||
ctx.build.output(`\n=== Rust Lite ${platform} (${target}) ===`);
|
||||
ctx.build.output(
|
||||
`Release profile: opt-level=z, LTO, 1 codegen unit, stripped symbols, panic=abort`,
|
||||
);
|
||||
ctx.build.output(`Cargo jobs: ${jobs === null ? "automatic (host CPU count)" : jobs}`);
|
||||
if (estimatedPackages > 0) ctx.build.output(`Estimated dependency graph: ${estimatedPackages} crates`);
|
||||
const args = [
|
||||
"cargo",
|
||||
"build",
|
||||
"--release",
|
||||
"--target",
|
||||
target,
|
||||
"--message-format=json-render-diagnostics",
|
||||
];
|
||||
if (jobs !== null) args.push("--jobs", String(jobs));
|
||||
if (locked) args.push("--locked");
|
||||
const env = {
|
||||
...process.env,
|
||||
CARGO_TARGET_DIR: targetDir,
|
||||
OVERLORD_LITE_DEFAULT_SERVER: String(payload.serverUrl || ""),
|
||||
OVERLORD_LITE_DEFAULT_AGENT_TOKEN: String(payload.agentToken || ""),
|
||||
OVERLORD_LITE_DEFAULT_BUILD_TAG: String(payload.buildTag || ""),
|
||||
OVERLORD_LITE_DEFAULT_TLS_SPKI_PINS: tlsSpkiPins.join(","),
|
||||
};
|
||||
const proc = Bun.spawn(args, {
|
||||
cwd: sourceDir,
|
||||
env,
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const progressTimer = setInterval(() => progress.emit(), 2_000);
|
||||
let exitCode;
|
||||
try {
|
||||
await Promise.all([
|
||||
streamPipe(proc.stdout, (line) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
ctx.build.output(line, "info");
|
||||
return;
|
||||
}
|
||||
if (message.reason === "compiler-artifact" && typeof message.package_id === "string") {
|
||||
progress.noteArtifact(message.package_id, message.fresh === true);
|
||||
} else if (message.reason === "compiler-message" && message.message?.rendered) {
|
||||
const level = message.message.level === "error"
|
||||
? "error"
|
||||
: message.message.level === "warning"
|
||||
? "warn"
|
||||
: "info";
|
||||
ctx.build.output(String(message.message.rendered).trimEnd(), level);
|
||||
}
|
||||
}),
|
||||
streamPipe(proc.stderr, (line) => {
|
||||
if (/^\s*Compiling\s+overlord-lite\b/i.test(line)) progress.markLinking();
|
||||
ctx.build.output(line, /^\s*error(?:\[|:)/i.test(line) ? "error" : "info");
|
||||
}),
|
||||
]);
|
||||
exitCode = await proc.exited;
|
||||
} finally {
|
||||
clearInterval(progressTimer);
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Cargo failed for ${platform} (exit ${exitCode}). Install the ${target} Rust target and its required cross-linker.`,
|
||||
);
|
||||
}
|
||||
|
||||
progress.markFinalizing();
|
||||
const extension = TARGETS[platform].extension;
|
||||
const builtPath = path.join(targetDir, target, "release", `overlord-lite${extension}`);
|
||||
if (!fs.existsSync(builtPath)) throw new Error(`Cargo did not create ${builtPath}`);
|
||||
const baseName = String(payload.outputName || "overlord-lite").replace(/[^A-Za-z0-9._-]/g, "") || "overlord-lite";
|
||||
const filename = `${baseName}-${platform}${extension}`;
|
||||
fs.copyFileSync(builtPath, path.join(outputDir, filename));
|
||||
artifacts.push({
|
||||
name: `Overlord Lite ${platform}`,
|
||||
filename,
|
||||
platform,
|
||||
version,
|
||||
});
|
||||
const completed = progress.markFinished();
|
||||
recordBuildDuration(ctx.dataDir, target, completed.elapsedMs);
|
||||
const compiledPackages = Math.max(0, completed.completedPackages - completed.freshPackages);
|
||||
ctx.build.output(
|
||||
`Completed ${platform} in ${formatDuration(completed.elapsedMs)} ` +
|
||||
`(${completed.freshPackages} cached, ${compiledPackages} compiled crates)`,
|
||||
"success",
|
||||
);
|
||||
ctx.build.output(`Artifact ready: ${filename}`, "success");
|
||||
}
|
||||
|
||||
return { artifacts };
|
||||
},
|
||||
|
||||
rpc: {
|
||||
async status(_ctx, _params) {
|
||||
let cargo = null;
|
||||
try {
|
||||
cargo = await capture(["cargo", "--version"], process.cwd());
|
||||
} catch (error) {
|
||||
cargo = `Unavailable: ${error?.message || error}`;
|
||||
}
|
||||
return {
|
||||
cargo,
|
||||
host: `${process.platform}-${process.arch}`,
|
||||
note: "Cross-target builds require the corresponding rustup target and linker.",
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user