Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { API } from "./API";
import { SandboxClient } from "./SandboxClient";
import { retryWithDelay } from "./utils/api";
import { Tracer, SpanStatusCode } from "@opentelemetry/api";
import { sleep } from "./utils/sleep";

export class Sandbox {
private tracer?: Tracer;
Expand Down Expand Up @@ -319,4 +320,42 @@ export class Sandbox {
}
);
}

async getPintSandboxPorts(): Promise<number[]> {
Copy link
Contributor Author

@mojojoji mojojoji Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be removed and the client.ports.waitForPort function has to be used when the pint ports interface is implemented

const pintPortAPIUrl = `${this.pitcherManagerResponse.pitcherURL}/api/v1/ports`;
const response = await fetch(pintPortAPIUrl, {
headers: {
Authorization: `Bearer ${this.pitcherManagerResponse.pitcherToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch ports from Pint API: ${response.statusText}`);
}

const portData = await response.json();
const ports: number[] = portData.ports.map((portInfo: any) => portInfo.port);
return ports;
}

async waitForPortsToOpen(ports: number[], timeoutMs: number): Promise<void> {
const startTime = Date.now();

while (true) {
try {
const openPorts = await this.getPintSandboxPorts()
if (ports.every(port => openPorts.includes(port))) {
return;
}
} catch (e){
// Ignore errors and retry
console.log(`Error checking port ${ports.join(',')}, retrying...`, e);
}

if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for port ${ports.join(',')} to open`);
}

await sleep(1000);
}
}
}
205 changes: 200 additions & 5 deletions src/bin/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { getInferredApiKey } from "../../utils/constants";
import { hashDirectory as getFilePaths } from "../utils/files";
import { mkdir, writeFile } from "fs/promises";
import { sleep } from "../../utils/sleep";
import { buildDockerImage, prepareDockerBuild, pushDockerImage } from "../utils/docker";
import { randomUUID } from "crypto";

export type BuildCommandArgs = {
directory: string;
Expand Down Expand Up @@ -125,6 +127,12 @@ export const buildCommand: yargs.CommandModule<
describe: "Relative path to log file, if any",
type: "string",
})
.option("beta", {
describe: "Use the beta Docker build process",
type: "boolean",
// TOOD: Remove after releasing to customers as beta feature
hidden: true, // Do not show this flag in help
})
.positional("directory", {
describe: "Path to the project that we'll create a snapshot from",
type: "string",
Expand Down Expand Up @@ -174,6 +182,14 @@ export const buildCommand: yargs.CommandModule<
}),

handler: async (argv) => {

// Beta build process using Docker
// This uses the new architecture using bartender and gvisor
if (argv.beta) {
return betaCodeSandboxBuild(argv);
}

// Existing build process
const apiKey = getInferredApiKey();
const api = new API({ apiKey, instrumentation: instrumentedFetch });
const sdk = new CodeSandbox(apiKey);
Expand Down Expand Up @@ -234,8 +250,7 @@ export const buildCommand: yargs.CommandModule<
spinner.start(
updateSpinnerMessage(
index,
`Running setup ${steps.indexOf(step) + 1} / ${
steps.length
`Running setup ${steps.indexOf(step) + 1} / ${steps.length
} - ${step.name}...`
)
);
Expand Down Expand Up @@ -448,9 +463,9 @@ export const buildCommand: yargs.CommandModule<
argv.ci
? String(error)
: "Failed, please manually verify at https://codesandbox.io/s/" +
id +
" - " +
String(error)
id +
" - " +
String(error)
)
);

Expand Down Expand Up @@ -605,3 +620,183 @@ function createAlias(directory: string, alias: string) {
alias,
};
}

/**
* Build a CodeSandbox Template using Docker for use in gvisor-based sandboxes.
* @param argv arguments to csb build command
*/
export async function betaCodeSandboxBuild(argv: yargs.ArgumentsCamelCase<BuildCommandArgs>): Promise<void> {
let dockerFileCleanupFn: (() => Promise<void>) | undefined;

try {
const apiKey = getInferredApiKey();
const api = new API({ apiKey, instrumentation: instrumentedFetch });
const sdk = new CodeSandbox(apiKey);
const sandboxTier = argv.vmTier
? VMTier.fromName(argv.vmTier)
: VMTier.Micro;

const resolvedDirectory = path.resolve(argv.directory);

const registry = "registry.codesandbox.dev";
const repository = "templates";
const imageName = `image-${randomUUID().toLowerCase()}`;
const tag = "latest";
const fullImageName = `${registry}/${repository}/${imageName}:${tag}`;

let architecture = "amd64";
if (process.arch === "arm64" && process.env.CSB_BASE_URL === "https://api.codesandbox.dev") {
console.log("Using arm64 architecture for Docker build");
architecture = "arm64";
}

// Prepare Docker Build
const dockerBuildPrepareSpinner = ora({ stream: process.stdout });
dockerBuildPrepareSpinner.start("Preparing build environment...");

let dockerfilePath: string;

try {
const result = await prepareDockerBuild(resolvedDirectory, (output: string) => {
dockerBuildPrepareSpinner.text = `Preparing build environment: (${output})`;
});
dockerFileCleanupFn = result.cleanupFn;
dockerfilePath = result.dockerfilePath;

dockerBuildPrepareSpinner.succeed("Build environment ready.");
} catch (error) {
dockerBuildPrepareSpinner.fail(`Failed to prepare build environment: ${(error as Error).message}`);
throw error;
}


// Docker Build
const dockerBuildSpinner = ora({ stream: process.stdout });
dockerBuildSpinner.start("Building template docker image...");
try {
await buildDockerImage({
dockerfilePath,
imageName: fullImageName,
context: resolvedDirectory,
architecture,
onOutput: (output: string) => {
const cleanOutput = stripAnsiCodes(output);
dockerBuildSpinner.text = `Building template Docker image: (${cleanOutput})`;
},
});
} catch (error) {
dockerBuildSpinner.fail(`Failed to build template Docker image: ${(error as Error).message}`);
throw error;
}
dockerBuildSpinner.succeed("Template Docker image built successfully.");

// Push Docker Image
const imagePushSpinner = ora({ stream: process.stdout });
imagePushSpinner.start("Pushing template Docker image to CodeSandbox...");
try {
await pushDockerImage(
fullImageName,
(output: string) => {
const cleanOutput = stripAnsiCodes(output);
imagePushSpinner.text = `Pushing template Docker image to CodeSandbox: (${cleanOutput})`;
},
);
} catch (error) {
imagePushSpinner.fail(`Failed to push template Docker image: ${(error as Error).message}`);
throw error;
}
imagePushSpinner.succeed("Template Docker image pushed to CodeSandbox.");


// Create Template with Docker Image
const templateData = await api.createTemplate({
forkOf: argv.fromSandbox || getDefaultTemplateId(api.getClient()),
title: argv.name,
// We filter out sdk-templates on the dashboard
tags: ["sdk-template"],
// @ts-ignore
image: {
"registry": "registry.codesandbox.dev",
"repository": "templates",
"name": imageName,
"tag": "latest",
"architecture": architecture
},
});

// Create a memory snapshot from the template sandboxes
const templateBuildSpinner = ora({ stream: process.stdout });
templateBuildSpinner.start("Preparing template snapshot...");
try {

const sandboxId = templateData.sandboxes[0].id;

templateBuildSpinner.text = "Preparing template snapshot: Starting sandbox to create snapshot...";
const sandbox = await sdk.sandboxes.resume(sandboxId);

if (argv.ports && argv.ports.length > 0) {
templateBuildSpinner.text = `Preparing template snapshot: Waiting for ports ${argv.ports.join(', ')} to be ready...`;
await sandbox.waitForPortsToOpen(argv.ports, 30000);
} else {
templateBuildSpinner.text = `Preparing template snapshot: No ports specified, waiting 10 seconds for tasks to run...`;
await sleep(10000);
}

templateBuildSpinner.text = "Preparing template snapshot: Sandbox is ready. Creating snapshot...";
await sdk.sandboxes.hibernate(sandboxId);

templateBuildSpinner.succeed("Template snapshot created.");

} catch (error) {
templateBuildSpinner.fail(`Failed to create template reference and example: ${(error as Error).message}`);
throw error;
}

// Create alias if needed and output final instructions
const templateFinaliseSpinner = ora({ stream: process.stdout });
templateFinaliseSpinner.start(
`\n\nCreating template reference and example...`
);
let referenceString;
let id;

// Create alias if needed
if (argv.alias) {
const alias = createAlias(resolvedDirectory, argv.alias);
await api.assignVmTagAlias(alias.namespace, alias.alias, {
tag_id: templateData.tag,
});

id = `${alias.namespace}@${alias.alias}`;
referenceString = `Alias ${id} now referencing: ${templateData.tag}`;
} else {
id = templateData.tag;
referenceString = `Template created with tag: ${templateData.tag}`;
}

templateFinaliseSpinner.succeed(`${referenceString}\n\n
Create sandbox from template using

SDK:

sdk.sandboxes.create({
id: "${id}"
})

CLI:

csb sandboxes fork ${id}\n`

);

process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
} finally {
// Cleanup temporary Dockerfile if created
if (dockerFileCleanupFn) {
await dockerFileCleanupFn();
}
}
}
Loading