Skip to content

Commit 7408594

Browse files
committed
fix(test-suite): isolate local builds with :fhevm-local image tags
Prevent docker compose build from overwriting registry images when --override builds locally. Local builds now use :fhevm-local tags, keeping real registry images untouched. Also deduplicates services sharing the same image tag (fixes buildx "already exists" error) and skips compat CLI args for locally-built coprocessor services.
1 parent d859891 commit 7408594

File tree

1 file changed

+45
-3
lines changed

1 file changed

+45
-3
lines changed

test-suite/fhevm/src/artifacts.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,29 @@ const ensureWritableDir = async (dir: string) => {
6363
await fs.chmod(dir, 0o777);
6464
};
6565

66+
const LOCAL_BUILD_TAG = "fhevm-local";
67+
6668
const loadComposeDoc = async (component: string) =>
6769
YAML.parse(await fs.readFile(path.join(TEMPLATE_COMPOSE_DIR, `${component}-docker-compose.yml`), "utf8")) as ComposeDoc;
6870

71+
const overriddenServicesForComponent = (state: State, component: string) =>
72+
new Set(
73+
state.overrides.flatMap((o) =>
74+
GROUP_BUILD_COMPONENTS[o.group].includes(component) ? GROUP_BUILD_SERVICES[o.group] : [],
75+
),
76+
);
77+
78+
const retagLocal = (image: unknown) =>
79+
typeof image === "string" ? image.replace(/:([^:]+)$/, `:${LOCAL_BUILD_TAG}`) : image;
80+
81+
const applyBuildPolicy = (service: Record<string, unknown>, isOverridden: boolean) => {
82+
if (isOverridden) {
83+
service.image = retagLocal(service.image);
84+
} else {
85+
delete service.build;
86+
}
87+
};
88+
6989
const appendVolume = (service: Record<string, unknown>, value: string) => {
7090
const volumes = Array.isArray(service.volumes) ? [...service.volumes] : [];
7191
if (!volumes.includes(value)) {
@@ -184,12 +204,17 @@ const applyInstanceAdjustments = (
184204
const buildCoprocessorOverride = async (state: State) => {
185205
const doc = rewriteComposePaths(await loadComposeDoc("coprocessor"));
186206
const next = structuredClone(doc);
207+
const overridden = overriddenServicesForComponent(state, "coprocessor");
187208
const services: Record<string, Record<string, unknown>> = {};
188209
const baseOverride = state.topology.instances["coprocessor-0"];
189210
const baseEnv = await readEnvFile(envPath("coprocessor"));
190211
const compat = compatPolicyForState(state);
212+
// Skip compat args for overridden services — local builds use HEAD code, not the resolved version.
213+
const compatArgs = overridden.size ? {} : compat.coprocessorArgs;
191214
for (const [name, service] of Object.entries(doc.services)) {
192-
services[name] = applyInstanceAdjustments(service, envPath("coprocessor"), baseEnv, baseOverride, compat.coprocessorArgs);
215+
const adjusted = applyInstanceAdjustments(service, envPath("coprocessor"), baseEnv, baseOverride, compatArgs);
216+
applyBuildPolicy(adjusted, overridden.has(name));
217+
services[name] = adjusted;
193218
}
194219
for (let index = 1; index < state.topology.count; index += 1) {
195220
const prefix = `coprocessor${index}-`;
@@ -202,9 +227,10 @@ const buildCoprocessorOverride = async (state: State) => {
202227
envPath(`coprocessor.${index}`),
203228
instanceEnv,
204229
override,
205-
compat.coprocessorArgs,
230+
compatArgs,
206231
);
207232
cloned.container_name = prefix + suffix;
233+
applyBuildPolicy(cloned, overridden.has(name));
208234
if (cloned.depends_on && typeof cloned.depends_on === "object") {
209235
cloned.depends_on = Object.fromEntries(
210236
Object.entries(cloned.depends_on as Record<string, unknown>).map(([dep, value]) => [
@@ -225,9 +251,11 @@ const buildComposeOverride = async (component: string, state: State) => {
225251
return buildCoprocessorOverride(state);
226252
}
227253
const doc = rewriteComposePaths(structuredClone(await loadComposeDoc(component)));
254+
const overridden = overriddenServicesForComponent(state, component);
228255
const envVars = await readEnvFile(envPath(component));
229256
for (const [name, service] of Object.entries(doc.services)) {
230257
Object.assign(service, interpolateComposeValue(service, envVars));
258+
applyBuildPolicy(service, overridden.has(name));
231259
if (component === "gateway-sc") {
232260
if (name === "gateway-sc-add-network") {
233261
service.command = ["npx hardhat task:addHostChainsToGatewayConfig --use-internal-proxy-address true"];
@@ -456,8 +484,22 @@ const maybeBuild = async (
456484
if (!services.length) {
457485
continue;
458486
}
487+
// Deduplicate services by image tag — buildx fails when two services
488+
// produce the same output tag in a single `docker compose build`.
489+
const seen = new Set<string>();
490+
const deduped = services.filter((s) => {
491+
const img = doc.services[s]?.image;
492+
if (typeof img !== "string" || seen.has(img)) {
493+
return false;
494+
}
495+
seen.add(img);
496+
return true;
497+
});
459498
log(`[build] ${override.group} (${component})`);
460-
await deps.liveRunner([...dockerArgs(component), "build", ...services], { env: await composeEnv(state) });
499+
for (const ref of await imageRefsForServices(component, deduped)) {
500+
await deps.runner(["docker", "image", "rm", "-f", ref], { allowFailure: true });
501+
}
502+
await deps.liveRunner([...dockerArgs(component), "build", ...deduped], { env: await composeEnv(state) });
461503
await rememberBuiltImages(state, component, override.group, services, deps, saveState);
462504
}
463505
}

0 commit comments

Comments
 (0)