Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
24 changes: 19 additions & 5 deletions packages/js-sdk/src/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
calculateFilesHash,
getCallerDirectory,
getCallerFrame,
normalizeCopySourcePath,
padOctal,
readDockerignore,
readGCPServiceAccountJSON,
Expand Down Expand Up @@ -357,8 +358,10 @@ export class TemplateBase
const srcs = Array.isArray(src) ? src : [src]

for (const src of srcs) {
const { normalizedSrc, contextPathForInstruction } =
normalizeCopySourcePath(src.toString(), this.fileContextPath)
const args = [
src.toString(),
normalizedSrc,
dest.toString(),
options?.user ?? '',
options?.mode ? padOctal(options.mode) : '',
Expand All @@ -370,6 +373,7 @@ export class TemplateBase
force: options?.forceUpload || this.forceNextLayer,
forceUpload: options?.forceUpload,
resolveSymlinks: options?.resolveSymlinks,
contextPath: contextPathForInstruction,
})
}

Expand Down Expand Up @@ -477,6 +481,7 @@ export class TemplateBase
type: InstructionType.RUN,
args,
force: this.forceNextLayer,
contextPath: this.fileContextPath.toString(),
})

this.collectStackTrace()
Expand All @@ -488,6 +493,7 @@ export class TemplateBase
type: InstructionType.WORKDIR,
args: [workdir.toString()],
force: this.forceNextLayer,
contextPath: this.fileContextPath.toString(),
})

this.collectStackTrace()
Expand All @@ -499,6 +505,7 @@ export class TemplateBase
type: InstructionType.USER,
args: [user],
force: this.forceNextLayer,
contextPath: this.fileContextPath.toString(),
})

this.collectStackTrace()
Expand Down Expand Up @@ -679,6 +686,7 @@ export class TemplateBase
type: InstructionType.ENV,
args: Object.entries(envs).flatMap(([key, value]) => [key, value]),
force: this.forceNextLayer,
contextPath: this.fileContextPath.toString(),
})
this.collectStackTrace()
return this
Expand Down Expand Up @@ -901,6 +909,9 @@ export class TemplateBase
throw new Error('Source path and files hash are required')
}

const contextPathForInstruction =
instruction.contextPath ?? this.fileContextPath.toString()

const forceUpload = instruction.forceUpload
let stackTrace = undefined
if (index + 1 >= 0 && index + 1 < this.stackTraces.length) {
Expand All @@ -923,11 +934,11 @@ export class TemplateBase
await uploadFile(
{
fileName: src,
fileContextPath: this.fileContextPath.toString(),
fileContextPath: contextPathForInstruction,
url,
ignorePatterns: [
...this.fileIgnorePatterns,
...readDockerignore(this.fileContextPath.toString()),
...readDockerignore(contextPathForInstruction),
],
resolveSymlinks: instruction.resolveSymlinks ?? RESOLVE_SYMLINKS,
},
Expand Down Expand Up @@ -990,6 +1001,9 @@ export class TemplateBase
throw new Error('Source path and destination path are required')
}

const contextPathForInstruction =
instruction.contextPath ?? this.fileContextPath.toString()

let stackTrace = undefined
if (index + 1 >= 0 && index + 1 < this.stackTraces.length) {
stackTrace = this.stackTraces[index + 1]
Expand All @@ -1000,12 +1014,12 @@ export class TemplateBase
filesHash: await calculateFilesHash(
src,
dest,
this.fileContextPath.toString(),
contextPathForInstruction,
[
...this.fileIgnorePatterns,
...(runtime === 'browser'
? []
: readDockerignore(this.fileContextPath.toString())),
: readDockerignore(contextPathForInstruction)),
],
instruction.resolveSymlinks ?? RESOLVE_SYMLINKS,
stackTrace
Expand Down
4 changes: 4 additions & 0 deletions packages/js-sdk/src/template/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export type Instruction = {
forceUpload?: true
filesHash?: string
resolveSymlinks?: boolean
/**
* Base path used for resolving and hashing the instruction's source files.
*/
contextPath?: string
}

/**
Expand Down
51 changes: 49 additions & 2 deletions packages/js-sdk/src/template/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from 'node:crypto'
import fs from 'node:fs'
import fs, { PathLike } from 'node:fs'
import path from 'node:path'
import { dynamicImport, dynamicRequire } from '../utils'
import { BASE_STEP_NAME, FINALIZE_STEP_NAME } from './consts'
Expand Down Expand Up @@ -33,6 +33,49 @@ function normalizePath(path: string): string {
return path.replace(/\\/g, '/')
}

/**
* Normalize a COPY source path into a context-relative pattern and its context.
*
* - Absolute sources: context is the source's directory; normalized path is the
* relative path from that directory ('.' when the source is the directory itself).
* - Relative sources: context defaults to `fileContextPath`; if the path escapes
* that context (e.g., '../../../foo'), use the escaped path's directory instead.
* - Always returns POSIX separators for glob/tar friendliness.
*/
export function normalizeCopySourcePath(
src: string,
fileContextPath: PathLike
): {
normalizedSrc: string
contextPathForInstruction: string
} {
const defaultContext = path.resolve(fileContextPath.toString())
const absoluteSrc = path.isAbsolute(src)
? src
: path.resolve(defaultContext, src)

let contextPathForInstruction = path.isAbsolute(src)
? path.dirname(absoluteSrc)
: defaultContext

// If a relative path escapes the default context, fall back to its own directory
if (!path.isAbsolute(src)) {
const relativeToDefault = path.relative(defaultContext, absoluteSrc)
if (relativeToDefault.startsWith('..')) {
contextPathForInstruction = path.dirname(absoluteSrc)
}
}

// Normalize the source path to forward slashes
const normalizedSrc =
normalizePath(path.relative(contextPathForInstruction, absoluteSrc)) || '.'

return {
normalizedSrc,
contextPathForInstruction,
}
}

/**
* Get all files for a given path and ignore patterns.
*
Expand Down Expand Up @@ -276,7 +319,11 @@ export async function tarFileStream(
true
)

const filePaths = allFiles.map((file) => file.relativePosix())
const filePaths = allFiles.map((file) => {
const rel = path.relative(fileContextPath, file.fullpath())
const normalized = normalizePath(rel || '.')
return normalized
})

return create(
{
Expand Down
17 changes: 17 additions & 0 deletions packages/js-sdk/tests/template/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,20 @@ buildTemplateTest(
await buildTemplate(template)
}
)

buildTemplateTest(
'build template with absolute paths',
async ({ buildTemplate }) => {
const packageTxt = path.resolve(process.cwd(), folderPath, 'test.txt')

const template = Template()
// using base image to avoid re-building ubuntu:22.04 image
.fromBaseImage()
.skipCache()
.copy(packageTxt, 'text.txt', { forceUpload: true })
.copy('../../../../package.json', 'package.json', { forceUpload: true })
.runCmd(['ls -l .', 'cat text.txt', 'cat package.json'])

await buildTemplate(template, {}, defaultBuildLogger())
}
)
16 changes: 13 additions & 3 deletions packages/python-sdk/e2b/template/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
read_dockerignore,
read_gcp_service_account_json,
get_caller_frame,
normalize_copy_source_path,
)
from types import TracebackType

Expand Down Expand Up @@ -65,8 +66,11 @@ def copy(
srcs = [src] if isinstance(src, (str, Path)) else src

for src_item in srcs:
normalized_src, context_path_for_instruction = normalize_copy_source_path(
str(src_item), self._template._file_context_path
)
args = [
str(src_item),
normalized_src,
str(dest),
user or "",
pad_octal(mode) if mode else "",
Expand All @@ -78,6 +82,7 @@ def copy(
"force": force_upload or self._template._force_next_layer,
"forceUpload": force_upload,
"resolveSymlinks": resolve_symlinks,
"contextPath": context_path_for_instruction,
}

self._template._instructions.append(instruction)
Expand Down Expand Up @@ -1275,6 +1280,7 @@ def _instructions_with_hashes(
"force": instruction["force"],
"forceUpload": instruction.get("forceUpload"),
"resolveSymlinks": instruction.get("resolveSymlinks"),
"contextPath": instruction.get("contextPath"),
}

if instruction["type"] == InstructionType.COPY:
Expand All @@ -1289,13 +1295,17 @@ def _instructions_with_hashes(
raise ValueError("Source path and destination path are required")

resolve_symlinks = instruction.get("resolveSymlinks")
context_path_for_instruction = (
instruction.get("contextPath") or self._file_context_path
)

step["filesHash"] = calculate_files_hash(
src,
dest,
self._file_context_path,
context_path_for_instruction,
[
*self._file_ignore_patterns,
*read_dockerignore(self._file_context_path),
*read_dockerignore(context_path_for_instruction),
],
resolve_symlinks
if resolve_symlinks is not None
Expand Down
1 change: 1 addition & 0 deletions packages/python-sdk/e2b/template/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Instruction(TypedDict):
forceUpload: NotRequired[Optional[Literal[True]]]
filesHash: NotRequired[Optional[str]]
resolveSymlinks: NotRequired[Optional[bool]]
contextPath: NotRequired[Optional[str]]


class GenericDockerRegistry(TypedDict):
Expand Down
34 changes: 34 additions & 0 deletions packages/python-sdk/e2b/template/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,40 @@ def normalize_path(path: str) -> str:
return path.replace(os.sep, "/")


def normalize_copy_source_path(src: str, file_context_path: str) -> (str, str):
"""
Normalize a COPY source path into a context-relative pattern and its context.

- Absolute sources: context is the source's directory; normalized path is the
relative path from that directory ('.' when the source is the directory itself).
- Relative sources: context defaults to file_context_path; if the path escapes
that context (e.g., '../../../foo'), use the escaped path's directory instead.
- Always returns POSIX separators for glob/tar friendliness.
"""
default_context = os.path.abspath(file_context_path)
absolute_src = (
src
if os.path.isabs(src)
else os.path.abspath(os.path.join(default_context, src))
)

context_path_for_instruction = (
os.path.dirname(absolute_src) if os.path.isabs(src) else default_context
)

if not os.path.isabs(src):
relative_to_default = os.path.relpath(absolute_src, default_context)
if relative_to_default.startswith(".."):
context_path_for_instruction = os.path.dirname(absolute_src)

normalized_src = (
normalize_path(os.path.relpath(absolute_src, context_path_for_instruction))
or "."
)

return normalized_src, context_path_for_instruction


def get_all_files_in_path(
src: str,
context_path: str,
Expand Down
12 changes: 9 additions & 3 deletions packages/python-sdk/e2b/template_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ async def _build(
force_upload = file_upload.get("forceUpload")
files_hash = file_upload.get("filesHash", None)
resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS)
context_path_for_instruction = (
file_upload.get("contextPath") or template._template._file_context_path
)
resolved_symlinks = (
resolve_symlinks if resolve_symlinks is not None else RESOLVE_SYMLINKS
)

if src is None or files_hash is None:
raise ValueError("Source path and files hash are required")
Expand All @@ -109,13 +115,13 @@ async def _build(
await upload_file(
api_client,
src,
template._template._file_context_path,
context_path_for_instruction,
file_info.url,
[
*template._template._file_ignore_patterns,
*read_dockerignore(template._template._file_context_path),
*read_dockerignore(context_path_for_instruction),
],
resolve_symlinks,
resolved_symlinks,
stack_trace,
)
if on_build_logs:
Expand Down
12 changes: 9 additions & 3 deletions packages/python-sdk/e2b/template_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ def _build(
force_upload = file_upload.get("forceUpload")
files_hash = file_upload.get("filesHash", None)
resolve_symlinks = file_upload.get("resolveSymlinks", RESOLVE_SYMLINKS)
context_path_for_instruction = (
file_upload.get("contextPath") or template._template._file_context_path
)
resolved_symlinks = (
resolve_symlinks if resolve_symlinks is not None else RESOLVE_SYMLINKS
)

if src is None or files_hash is None:
raise ValueError("Source path and files hash are required")
Expand All @@ -109,13 +115,13 @@ def _build(
upload_file(
api_client,
src,
template._template._file_context_path,
context_path_for_instruction,
file_info.url,
[
*template._template._file_ignore_patterns,
*read_dockerignore(template._template._file_context_path),
*read_dockerignore(context_path_for_instruction),
],
resolve_symlinks,
resolved_symlinks,
stack_trace,
)
if on_build_logs:
Expand Down
Loading