← Files ModRetro Chromatic PluginARCHIVED FILE

scripts/setup-emulator.mjs

18.8 KB · Oct 3, 2026 · 06:38 UTC

↓ Download file

#!/usr/bin/env node

import { spawnSync as defaultSpawnSync } from "node:child_process";
import { accessSync, constants, lstatSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const DEFAULT_SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
const TRUSTED_UV_REGISTRY_VARIABLES = new Set([
  "UV_INDEX_URL",
  "UV_DEFAULT_INDEX",
  "UV_EXTRA_INDEX_URL",
]);
const UNSAFE_EXACT_VARIABLES = new Set([
  "BASH_ENV",
  "ENV",
  "VIRTUAL_ENV",
  "VIRTUAL_ENV_PROMPT",
  "PIP_CONFIG_FILE",
  "PIP_PREFIX",
  "PIP_TARGET",
]);

const SETUP_USAGE = "Usage: node scripts/setup-emulator.mjs [--dry-run] [--help]\n" +
  "Legacy developer setup for the optional PyBoy environment. --dry-run prints an unverified plan without running tools.\n" +
  "For installed plugins, use the packaged setup.mjs (or setup.sh/setup.ps1) entrypoint and an external dependency root.";

export function parseEmulatorSetupArguments(arguments_) {
  const selected = { dryRun: false, help: false };
  for (const argument of arguments_) {
    if (argument === "--dry-run") selected.dryRun = true;
    else if (argument === "--help" || argument === "-h") selected.help = true;
    else throw new Error(`Unknown emulator setup option: ${argument}`);
  }
  return selected;
}

function emulatorSetupPlan(options) {
  const platform = options.platform ?? process.platform;
  const architecture = options.arch ?? process.arch;
  const environment = options.environment ?? process.env;
  const scriptDirectory = path.resolve(options.scriptDirectory ?? DEFAULT_SCRIPT_DIRECTORY);
  const toolchainRoot = path.resolve(options.cwd ?? process.cwd(), options.toolchainRoot ??
    environmentValue(environment, "GB_STUDIO_TOOLCHAIN_ROOT", platform) ?? path.dirname(scriptDirectory));
  const pythonVersion = options.pythonVersion ?? environmentValue(environment, "GB_STUDIO_PYTHON_VERSION", platform) ?? "3.13";
  if (!/^\d+\.\d+(?:\.\d+)?$/.test(pythonVersion)) {
    throw new Error(`The requested emulator Python version must be an explicit numeric version: ${pythonVersion}`);
  }
  const runtimeDirectory = path.join(toolchainRoot, ".local", "pyboy-venv");
  writeTo(options.stdout ?? process.stdout, `[dry-run] Plan only; no tools were run and dependency health is unverified.\n` +
    `[dry-run] Target ${platform}-${architecture}: Python ${pythonVersion}, PyBoy 2.7.0, Pillow >=11,<13.\n` +
    `[dry-run] Would verify/reuse or prepare the PyBoy environment at ${runtimeDirectory}.\n` +
    "[dry-run] Applying this legacy helper requires trusted uv when installation or repair is needed; installed plugins should use packaged setup.mjs with an external dependency root.");
  return { dryRun: true, verified: false, platform, architecture, toolchainRoot, runtimeDirectory, installed: false };
}

function assertMutableToolchain(filesystem, toolchainRoot) {
  let candidate = path.resolve(toolchainRoot);
  for (;;) {
    const receipt = path.join(candidate, ".codex-plugin", "local-payload.json");
    const manifest = path.join(candidate, ".codex-plugin", "plugin.json");
    let immutable = tryInspect(filesystem, receipt) !== undefined;
    const metadata = tryInspect(filesystem, manifest);
    if (!immutable && metadata?.isSymbolicLink()) {
      throw new Error("Legacy setup cannot establish a mutable root through a linked plugin manifest. Use packaged setup.mjs with an external dependency root.");
    }
    if (!immutable && metadata?.isFile() && !metadata.isSymbolicLink()) {
      const version = JSON.parse(filesystem.readFileSync(manifest, "utf8")).version;
      immutable = typeof version === "string" && version.includes("+codex.payload.");
    }
    if (immutable) {
      throw new Error("Legacy setup cannot write into an immutable plugin payload or cache. " +
        "Use packaged setup.mjs (or setup.sh/setup.ps1) with an external dependency root.");
    }
    const parent = path.dirname(candidate);
    if (parent === candidate) return;
    candidate = parent;
  }
}

/** Preserve enterprise registry/proxy configuration without inheriting interpreter redirects. */
export function sanitizeEmulatorSetupEnvironment(environment = process.env, platform = process.platform) {
  const sanitized = {};
  const acceptedWindowsNames = new Map();
  for (const [name, value] of Object.entries(environment)) {
    if (value === undefined) continue;
    const canonicalName = name.toUpperCase();
    if (
      UNSAFE_EXACT_VARIABLES.has(canonicalName) ||
      canonicalName.startsWith("NODE_") ||
      canonicalName.startsWith("PYTHON") ||
      canonicalName.startsWith("CONDA_") ||
      (canonicalName.startsWith("UV_") && !TRUSTED_UV_REGISTRY_VARIABLES.has(canonicalName))
    ) {
      continue;
    }
    if (platform === "win32") {
      const existingValue = acceptedWindowsNames.get(canonicalName);
      if (existingValue !== undefined) {
        if (existingValue !== value) {
          throw new Error(`Conflicting case-insensitive Windows environment values for ${canonicalName}.`);
        }
        continue;
      }
      acceptedWindowsNames.set(canonicalName, value);
    }
    sanitized[name] = value;
  }
  return sanitized;
}

function environmentValue(environment, name, platform) {
  if (platform !== "win32") return environment[name];
  const normalizedName = name.toUpperCase();
  const matches = Object.entries(environment).filter(
    ([candidate, value]) => value !== undefined && candidate.toUpperCase() === normalizedName,
  );
  if (matches.length > 1 && new Set(matches.map(([, value]) => value)).size > 1) {
    throw new Error(`Conflicting case-insensitive Windows environment values for ${name}.`);
  }
  return matches[0]?.[1];
}

function normalizedPhysicalPath(candidate, platform) {
  const normalized = path.resolve(candidate);
  return platform === "win32" ? normalized.toLowerCase() : normalized;
}

function isInsideDirectory(directory, candidate, platform) {
  const relativePath = path.relative(
    normalizedPhysicalPath(directory, platform),
    normalizedPhysicalPath(candidate, platform),
  );
  return (
    relativePath === "" ||
    (relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath))
  );
}

function tryInspect(filesystem, candidate) {
  try {
    return filesystem.lstatSync(candidate);
  } catch (error) {
    if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return undefined;
    throw error;
  }
}

function assertPhysicalEntry(filesystem, candidate, expectedKind, platform, label) {
  const entry = tryInspect(filesystem, candidate);
  if (entry === undefined) {
    throw new Error(
      `The existing PyBoy environment is missing a regular ${label}: ${candidate}. ` +
        "Repair or relocate it manually; setup will not remove or recreate it.",
    );
  }
  if (entry.isSymbolicLink()) {
    throw new Error(
      `The ${label} must be a real ${expectedKind}, not a symbolic link or reparse point: ${candidate}. ` +
        "Repair or relocate it manually; setup will not replace it.",
    );
  }
  const hasExpectedKind = expectedKind === "directory" ? entry.isDirectory() : entry.isFile();
  if (!hasExpectedKind) {
    throw new Error(
      `The ${label} must be a real ${expectedKind}: ${candidate}. ` +
        "Repair or relocate it manually; setup will not remove or recreate it.",
    );
  }

  const physicalPath = filesystem.realpathSync(candidate);
  if (normalizedPhysicalPath(physicalPath, platform) !== normalizedPhysicalPath(candidate, platform)) {
    throw new Error(
      `The ${label} resolves through a symbolic link or reparse point: ${candidate}. ` +
        "Repair or relocate it manually; setup will not replace it.",
    );
  }
  return physicalPath;
}

function inspectExistingRuntime(filesystem, runtimeDirectory, platform) {
  assertPhysicalEntry(filesystem, runtimeDirectory, "directory", platform, "PyBoy environment");
  assertPhysicalEntry(
    filesystem,
    path.join(runtimeDirectory, "pyvenv.cfg"),
    "file",
    platform,
    "pyvenv.cfg",
  );
  const executableDirectory = path.join(runtimeDirectory, "Scripts");
  assertPhysicalEntry(filesystem, executableDirectory, "directory", platform, "Scripts directory");
  const pythonPath = path.join(executableDirectory, "python.exe");
  assertPhysicalEntry(filesystem, pythonPath, "file", platform, "Scripts/python.exe");
  filesystem.accessSync(pythonPath, constants.F_OK);
  return pythonPath;
}

function resolveUvExecutable({ filesystem, environment, platform, currentDirectory, selectedProject }) {
  const configuredPath = environmentValue(environment, "PATH", platform);
  if (!configuredPath) {
    throw new Error("The optional emulator setup requires a trusted uv.exe on PATH: https://docs.astral.sh/uv/");
  }

  const rejectedRoots = [currentDirectory, selectedProject].filter(Boolean);
  for (const rawEntry of configuredPath.split(";")) {
    const entry = rawEntry.trim().replace(/^"(.*)"$/, "$1");
    if (!entry || !(path.isAbsolute(entry) || path.win32.isAbsolute(entry))) continue;

    let directory;
    try {
      directory = filesystem.realpathSync(entry);
    } catch {
      continue;
    }
    if (rejectedRoots.some((root) => isInsideDirectory(root, directory, platform))) continue;

    const candidate = path.join(directory, "uv.exe");
    const metadata = tryInspect(filesystem, candidate);
    if (metadata === undefined || metadata.isSymbolicLink() || !metadata.isFile()) continue;

    let executable;
    try {
      executable = filesystem.realpathSync(candidate);
    } catch {
      continue;
    }
    if (normalizedPhysicalPath(executable, platform) !== normalizedPhysicalPath(candidate, platform)) continue;
    return executable;
  }

  throw new Error(
    "The optional emulator setup requires a trusted uv.exe on an absolute PATH entry outside the selected project: " +
      "https://docs.astral.sh/uv/",
  );
}

function writeTo(target, content) {
  if (!content) return;
  target.write(content.endsWith("\n") ? content : `${content}\n`);
}

function verificationProgram(pythonVersion) {
  const expectedParts = pythonVersion.split(".").slice(0, 2).map(Number);
  return [
    "import sys",
    "import pyboy",
    "import PIL",
    "from importlib.metadata import version",
    `expected_python = (${expectedParts[0]}, ${expectedParts[1]})`,
    "if sys.version_info[:2] != expected_python:",
    "    raise RuntimeError(f'Expected Python {expected_python[0]}.{expected_python[1]}, found {sys.version.split()[0]}')",
    "pyboy_version = version('pyboy')",
    "pillow_version = version('Pillow')",
    "if pyboy_version != '2.7.0':",
    "    raise RuntimeError(f'Expected PyBoy 2.7.0, found {pyboy_version}')",
    "try:",
    "    pillow_major = int(pillow_version.split('.', 1)[0])",
    "except (TypeError, ValueError) as error:",
    "    raise RuntimeError(f'Invalid Pillow version: {pillow_version}') from error",
    "if pillow_major not in (11, 12):",
    "    raise RuntimeError(f'Expected Pillow >=11,<13, found {pillow_version}')",
    "print('Python ' + sys.version.split()[0])",
    "print('PyBoy ' + pyboy_version)",
    "print('Pillow ' + pillow_version)",
  ].join("\n");
}

/** Install or verify the plugin-owned Windows x64 PyBoy environment without touching Unix setup. */
export function setupEmulator(options = {}) {
  const selected = parseEmulatorSetupArguments(options.argv ?? options.args ?? []);
  if (selected.help) {
    writeTo(options.stdout ?? process.stdout, SETUP_USAGE);
    return { help: true, installed: false };
  }
  if (selected.dryRun) return emulatorSetupPlan(options);
  const platform = options.platform ?? process.platform;
  if (platform !== "win32") {
    throw new Error("Native Node emulator setup is only supported on Windows; use runSetupEmulator on Unix.");
  }
  const architecture = options.arch ?? process.arch;
  if (architecture !== "x64") {
    throw new Error(`The native Windows PyBoy environment currently requires x64; received ${architecture}.`);
  }

  const filesystem = {
    accessSync,
    lstatSync,
    mkdirSync,
    readFileSync,
    realpathSync,
    ...options.filesystem,
  };
  const parentEnvironment = options.environment ?? process.env;
  const childEnvironment = sanitizeEmulatorSetupEnvironment(parentEnvironment, platform);
  const spawn = options.spawnSync ?? defaultSpawnSync;
  const stdout = options.stdout ?? process.stdout;
  const stderr = options.stderr ?? process.stderr;
  const currentDirectory = options.cwd ?? process.cwd();

  let trustedDirectory;
  try {
    trustedDirectory = filesystem.realpathSync(path.resolve(options.scriptDirectory ?? DEFAULT_SCRIPT_DIRECTORY));
  } catch {
    throw new Error("The trusted plugin scripts directory must be an existing physical directory.");
  }

  const configuredToolchain = options.toolchainRoot ?? environmentValue(parentEnvironment, "GB_STUDIO_TOOLCHAIN_ROOT", platform);
  let toolchainRoot;
  try {
    toolchainRoot = filesystem.realpathSync(path.resolve(configuredToolchain ?? path.dirname(trustedDirectory)));
    if (!filesystem.lstatSync(toolchainRoot).isDirectory()) throw new Error("not a directory");
  } catch {
    throw new Error(`The selected game toolchain root must be an existing directory: ${configuredToolchain ?? path.dirname(trustedDirectory)}`);
  }
  assertMutableToolchain(filesystem, toolchainRoot);

  const pythonVersion = options.pythonVersion ?? environmentValue(parentEnvironment, "GB_STUDIO_PYTHON_VERSION", platform) ?? "3.13";
  if (!/^\d+\.\d+(?:\.\d+)?$/.test(pythonVersion)) {
    throw new Error(`The requested emulator Python version must be an explicit numeric version: ${pythonVersion}`);
  }

  const localDirectory = path.join(toolchainRoot, ".local");
  const existingLocal = tryInspect(filesystem, localDirectory);
  if (existingLocal !== undefined) {
    assertPhysicalEntry(filesystem, localDirectory, "directory", platform, "managed .local directory");
  }
  const runtimeDirectory = path.join(localDirectory, "pyboy-venv");
  const existingRuntime = tryInspect(filesystem, runtimeDirectory) !== undefined;
  let pythonPath;
  if (existingRuntime) {
    pythonPath = inspectExistingRuntime(filesystem, runtimeDirectory, platform);
  }

  const subprocessOptions = {
    cwd: trustedDirectory,
    env: childEnvironment,
    encoding: "utf8",
    shell: false,
    windowsHide: true,
  };

  function verify() {
    const result = spawn(pythonPath, ["-I", "-c", verificationProgram(pythonVersion)], {
      ...subprocessOptions,
      stdio: "pipe",
      maxBuffer: 1024 * 1024,
    });
    writeTo(stdout, result.stdout);
    writeTo(stderr, result.stderr);
    if (result.error) {
      writeTo(stderr, `Unable to verify the existing PyBoy interpreter: ${result.error.message}`);
      return false;
    }
    return result.status === 0 && result.signal == null;
  }

  if (existingRuntime && verify()) {
    writeTo(stdout, `Verified the optional PyBoy runtime at ${pythonPath}`);
    return { pythonPath, runtimeDirectory, created: false, repaired: false, reused: true };
  }

  if (existingRuntime) {
    writeTo(stderr, "The existing PyBoy runtime requires dependency repair; preserving its virtual environment.");
  }

  const uvExecutable = resolveUvExecutable({
    filesystem,
    environment: childEnvironment,
    platform,
    currentDirectory,
    selectedProject: environmentValue(parentEnvironment, "GB_STUDIO_PROJECT_ROOT", platform),
  });

  function runUv(args) {
    const result = spawn(uvExecutable, args, { ...subprocessOptions, stdio: "inherit" });
    if (result.error) {
      throw new Error(`Unable to run the trusted uv executable: ${result.error.message}`);
    }
    if (result.status !== 0 || result.signal != null) {
      throw new Error(
        `uv ${args[0]} failed with ${result.signal ?? `exit code ${result.status ?? "unknown"}`}; ` +
          "the existing environment was not removed or recreated.",
      );
    }
  }

  if (!existingRuntime) {
    if (existingLocal === undefined) {
      try {
        filesystem.mkdirSync(localDirectory, { mode: 0o700 });
      } catch (error) {
        if (error?.code !== "EEXIST") throw error;
      }
    }
    assertPhysicalEntry(filesystem, localDirectory, "directory", platform, "managed .local directory");
    runUv([
      "venv",
      "--no-project",
      "--no-config",
      "--directory",
      trustedDirectory,
      "--python",
      pythonVersion,
      runtimeDirectory,
    ]);
    assertPhysicalEntry(filesystem, localDirectory, "directory", platform, "managed .local directory");
    pythonPath = inspectExistingRuntime(filesystem, runtimeDirectory, platform);
  } else {
    pythonPath = inspectExistingRuntime(filesystem, runtimeDirectory, platform);
  }

  runUv([
    "pip",
    "install",
    "--no-config",
    "--no-sources",
    "--directory",
    trustedDirectory,
    "--python",
    pythonPath,
    "pyboy==2.7.0",
    "pillow>=11,<13",
  ]);

  assertPhysicalEntry(filesystem, localDirectory, "directory", platform, "managed .local directory");
  pythonPath = inspectExistingRuntime(filesystem, runtimeDirectory, platform);
  if (!verify()) {
    throw new Error(
      "The PyBoy environment could not import its required Python 3.13, PyBoy 2.7.0, and Pillow >=11,<13 runtimes. " +
        "Repair the existing environment and rerun setup; it was not removed or recreated.",
    );
  }

  writeTo(stdout, `Verified the optional PyBoy runtime at ${pythonPath}`);
  return {
    pythonPath,
    runtimeDirectory,
    created: !existingRuntime,
    repaired: existingRuntime,
    reused: false,
  };
}

/** Preserve the existing Unix bootstrap while exposing one portable npm entrypoint. */
export function runSetupEmulator(options = {}) {
  const arguments_ = options.argv ?? process.argv.slice(2);
  const selected = parseEmulatorSetupArguments(arguments_);
  if (selected.help) {
    writeTo(options.stdout ?? process.stdout, SETUP_USAGE);
    return 0;
  }
  if (selected.dryRun) {
    emulatorSetupPlan(options);
    return 0;
  }
  const platform = options.platform ?? process.platform;
  if (platform === "win32") {
    setupEmulator({ ...options, argv: arguments_ });
    return 0;
  }

  const scriptDirectory = path.resolve(options.scriptDirectory ?? DEFAULT_SCRIPT_DIRECTORY);
  const scriptPath = path.join(scriptDirectory, "setup-emulator.sh");
  const spawn = options.spawnSync ?? defaultSpawnSync;
  const result = spawn(scriptPath, arguments_, {
    cwd: options.cwd ?? process.cwd(),
    env: options.environment ?? process.env,
    stdio: options.stdio ?? "inherit",
    shell: false,
    windowsHide: true,
  });
  if (result.error) {
    throw new Error(`Unable to launch the existing Unix emulator setup: ${result.error.message}`);
  }
  if (result.signal != null) return 128 + (os.constants.signals[result.signal] ?? 0);
  return result.status ?? 1;
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  try {
    process.exitCode = runSetupEmulator();
  } catch (error) {
    process.stderr.write(`Emulator setup failed: ${error instanceof Error ? error.message : String(error)}\n`);
    process.exitCode = 1;
  }
}

SHA-256: fe78da4f75ccc8afa1105d0ca3ad1e26470aa1bf5c4c9cd6ba41624c9af58d8e