mess around with a rust agent version with just rev shell.
This commit is contained in:
@@ -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