Files
2026-03-23 16:40:53 -05:00

176 lines
5.5 KiB
TypeScript

import { execSync } from "node:child_process";
import {
existsSync,
mkdirSync,
readFileSync,
symlinkSync,
readlinkSync,
rmSync,
} from "node:fs";
import { resolve, join } from "node:path";
import { e2eConfig } from "../e2e.config.ts";
import { extractPinnedWorkflowTemplatesRequirement } from "./setup-test-comfy-helpers";
const isWindows = process.platform === "win32";
const projectRoot = resolve(import.meta.dirname, "..");
const venvDir = resolve(projectRoot, e2eConfig.venvDir);
const comfyInstallDir = resolve(projectRoot, e2eConfig.comfyInstallDir);
const customNodesDir = resolve(projectRoot, e2eConfig.customNodesDir);
const pythonBin = isWindows
? join(venvDir, "Scripts", "python.exe")
: join(venvDir, "bin", "python");
const comfyBin = isWindows
? join(venvDir, "Scripts", "comfy.exe")
: join(venvDir, "bin", "comfy");
function tryExec(cmd: string): string | null {
try {
return execSync(cmd, {
cwd: projectRoot,
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf-8",
}).trim();
} catch {
return null;
}
}
function run(cmd: string, label: string) {
console.log(`[setup] ${label}...`);
console.log(`[setup] $ ${cmd}`);
execSync(cmd, { stdio: "inherit", cwd: projectRoot });
}
function getInstalledComfyRevision(): string | null {
const exactTag = tryExec(
`git -C "${comfyInstallDir}" describe --tags --exact-match HEAD`,
);
if (exactTag) return exactTag;
const gitHead = tryExec(`git -C "${comfyInstallDir}" rev-parse HEAD`);
return gitHead;
}
function installComfy(revision: string): void {
run(
`"${comfyBin}" --skip-prompt --workspace "${comfyInstallDir}" install --version "${revision}" --restore --cpu`,
`Installing ComfyUI ${revision}`,
);
}
function getPinnedWorkflowTemplatesRequirement(): string {
const requirementsPath = resolve(comfyInstallDir, "requirements.txt");
if (!existsSync(requirementsPath)) {
throw new Error(
`Pinned ComfyUI checkout is missing requirements.txt at ${requirementsPath}`,
);
}
return extractPinnedWorkflowTemplatesRequirement(
readFileSync(requirementsPath, "utf-8"),
);
}
// Step 1: Create venv (with --seed so pip is available for comfy-cli)
if (existsSync(pythonBin)) {
console.log("[setup] venv already exists, skipping.");
} else {
if (existsSync(venvDir)) {
console.log("[setup] Removing invalid venv directory before recreate...");
rmSync(venvDir, { recursive: true, force: true });
}
mkdirSync(resolve(projectRoot, e2eConfig.testComfyDir), { recursive: true });
run(`uv venv --seed "${venvDir}"`, "Creating Python venv (with pip)");
}
// Ensure pip is available (handles venvs created without --seed)
try {
execSync(`"${pythonBin}" -m pip --version`, { stdio: "ignore" });
} catch {
console.log("[setup] pip not found in venv, installing...");
run(
`uv pip install pip --python "${pythonBin}"`,
"Installing pip into venv",
);
}
// Step 2: Install comfy-cli
if (existsSync(comfyBin)) {
console.log("[setup] comfy-cli already installed, skipping.");
} else {
run(
`uv pip install comfy-cli --python "${pythonBin}"`,
"Installing comfy-cli",
);
}
// Step 3: Install ComfyUI
// Check for main.py directly in workspace (comfy clones repo as workspace root)
if (existsSync(join(comfyInstallDir, "main.py"))) {
console.log("[setup] ComfyUI repo already cloned.");
const installedRevision = getInstalledComfyRevision();
if (installedRevision === e2eConfig.comfyRevision) {
try {
execSync(`"${pythonBin}" -c "import torch"`, { stdio: "ignore" });
console.log(
`[setup] ComfyUI already pinned at ${e2eConfig.comfyRevision} and dependencies are installed.`,
);
} catch {
installComfy(e2eConfig.comfyRevision);
}
} else {
console.log(
`[setup] Syncing ComfyUI to ${e2eConfig.comfyRevision} (current: ${installedRevision ?? "unknown"})...`,
);
installComfy(e2eConfig.comfyRevision);
}
} else {
// Fresh install: clean up any invalid workspace state
if (existsSync(comfyInstallDir)) {
console.log(
"[setup] Removing invalid workspace directory before install...",
);
rmSync(comfyInstallDir, { recursive: true, force: true });
}
mkdirSync(resolve(comfyInstallDir, ".."), { recursive: true });
installComfy(e2eConfig.comfyRevision);
}
// Step 3b: Install the exact workflow templates package required by the
// pinned ComfyUI checkout. Do not hardcode or float this version here.
const workflowTemplatesRequirement = getPinnedWorkflowTemplatesRequirement();
run(
`uv pip install "${workflowTemplatesRequirement}" --python "${pythonBin}"`,
`Installing ${workflowTemplatesRequirement}`,
);
// Step 4: Symlink extension into custom_nodes
const symlinkTarget = resolve(customNodesDir, "comfy-node-organizer");
if (existsSync(symlinkTarget)) {
try {
const linkDest = readlinkSync(symlinkTarget);
const resolvedLink = resolve(customNodesDir, linkDest);
if (resolvedLink === projectRoot) {
console.log("[setup] Symlink already exists and is correct, skipping.");
} else {
console.log(
`[setup] WARNING: Symlink exists but points to ${resolvedLink}, expected ${projectRoot}`,
);
}
} catch {
console.log(
"[setup] WARNING: Path exists but is not a symlink. Manual cleanup may be needed.",
);
}
} else {
mkdirSync(customNodesDir, { recursive: true });
symlinkSync(projectRoot, symlinkTarget, isWindows ? "junction" : "dir");
console.log(`[setup] Created symlink: ${symlinkTarget} -> ${projectRoot}`);
}
console.log("[setup] Done! Run `pnpm test:e2e` to run e2e tests.");