Skip to content

Commit b1e2701

Browse files
mishushakovgraphite-app[bot]dobrac
authored
Correct file hashing logic in the SDK (#916)
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Jakub Dobry <jakub.dobry8@gmail.com>
1 parent 64e8f05 commit b1e2701

22 files changed

Lines changed: 549 additions & 234 deletions

File tree

.changeset/metal-penguins-begin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@e2b/python-sdk': patch
3+
'e2b': patch
4+
---
5+
6+
fix template build files copy cache invalidation

packages/js-sdk/src/template/buildApi.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { ApiClient, paths, handleApiError } from '../api'
1+
import { ApiClient, handleApiError, paths } from '../api'
2+
import { stripAnsi } from '../utils'
23
import { BuildError, FileUploadError } from './errors'
34
import { LogEntry } from './types'
45
import { getBuildStepIndex, tarFileStreamUpload } from './utils'
5-
import { stripAnsi } from '../utils'
66

77
type RequestBuildInput = {
88
alias: string
@@ -91,14 +91,16 @@ export async function uploadFile(
9191
fileName: string
9292
fileContextPath: string
9393
url: string
94+
resolveSymlinks: boolean
9495
},
95-
stackTrace?: string
96+
stackTrace: string | undefined
9697
) {
97-
const { fileName, url, fileContextPath } = options
98+
const { fileName, url, fileContextPath, resolveSymlinks } = options
9899
try {
99100
const { contentLength, uploadStream } = await tarFileStreamUpload(
100101
fileName,
101-
fileContextPath
102+
fileContextPath,
103+
resolveSymlinks
102104
)
103105

104106
// The compiler assumes this is Web fetch API, but it's actually Node.js fetch API

packages/js-sdk/src/template/consts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export const BASE_STEP_NAME = 'base'
88
* 3. Caller method (eg. copy(), fromImage(), etc.)
99
*/
1010
export const STACK_TRACE_DEPTH = 3
11+
12+
export const RESOLVE_SYMLINKS = false

packages/js-sdk/src/template/index.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
uploadFile,
1010
waitForBuildFinish,
1111
} from './buildApi'
12-
import { STACK_TRACE_DEPTH } from './consts'
12+
import { RESOLVE_SYMLINKS, STACK_TRACE_DEPTH } from './consts'
1313
import { parseDockerfile } from './dockerfileParser'
1414
import { ReadyCmd } from './readycmd'
1515
import {
@@ -228,18 +228,30 @@ export class TemplateBase
228228
copy(
229229
src: string,
230230
dest: string,
231-
options?: { forceUpload?: true; user?: string; mode?: number }
232-
): TemplateBuilder
233-
copy(
234-
items: CopyItem[],
235-
options?: { forceUpload?: true; user?: string; mode?: number }
231+
options?: {
232+
forceUpload?: true
233+
user?: string
234+
mode?: number
235+
resolveSymlinks?: boolean
236+
}
236237
): TemplateBuilder
238+
copy(items: CopyItem[]): TemplateBuilder
237239
copy(
238240
srcOrItems: string | CopyItem[],
239241
destOrOptions?:
240242
| string
241-
| { forceUpload?: true; user?: string; mode?: number },
242-
options?: { forceUpload?: true; user?: string; mode?: number }
243+
| {
244+
forceUpload?: true
245+
user?: string
246+
mode?: number
247+
resolveSymlinks?: boolean
248+
},
249+
options?: {
250+
forceUpload?: true
251+
user?: string
252+
mode?: number
253+
resolveSymlinks?: boolean
254+
}
243255
): TemplateBuilder {
244256
if (runtime === 'browser') {
245257
throw new Error('Browser runtime is not supported for copy')
@@ -254,6 +266,7 @@ export class TemplateBase
254266
mode: options?.mode,
255267
user: options?.user,
256268
forceUpload: options?.forceUpload,
269+
resolveSymlinks: options?.resolveSymlinks,
257270
},
258271
]
259272
for (const item of items) {
@@ -269,6 +282,7 @@ export class TemplateBase
269282
args,
270283
force: item.forceUpload ?? this.forceNextLayer,
271284
forceUpload: item.forceUpload,
285+
resolveSymlinks: item.resolveSymlinks,
272286
})
273287
}
274288

@@ -610,6 +624,7 @@ export class TemplateBase
610624
fileName: src,
611625
fileContextPath: this.fileContextPath,
612626
url,
627+
resolveSymlinks: instruction.resolveSymlinks ?? RESOLVE_SYMLINKS,
613628
},
614629
stackTrace
615630
)
@@ -689,6 +704,7 @@ export class TemplateBase
689704
? []
690705
: readDockerignore(this.fileContextPath)),
691706
],
707+
instruction.resolveSymlinks ?? RESOLVE_SYMLINKS,
692708
stackTrace
693709
),
694710
}

packages/js-sdk/src/template/types.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type Instruction = {
1515
force: boolean
1616
forceUpload?: boolean
1717
filesHash?: string
18+
resolveSymlinks?: boolean
1819
}
1920

2021
export type CopyItem = {
@@ -23,6 +24,7 @@ export type CopyItem = {
2324
forceUpload?: boolean
2425
user?: string
2526
mode?: number
27+
resolveSymlinks?: boolean
2628
}
2729

2830
// Interface for the initial state
@@ -75,13 +77,15 @@ export interface TemplateBuilder {
7577
copy(
7678
src: string,
7779
dest: string,
78-
options?: { forceUpload?: true; user?: string; mode?: number }
80+
options?: {
81+
forceUpload?: true
82+
user?: string
83+
mode?: number
84+
resolveSymlinks?: boolean
85+
}
7986
): TemplateBuilder
8087

81-
copy(
82-
items: CopyItem[],
83-
options?: { forceUpload?: true; user?: string; mode?: number }
84-
): TemplateBuilder
88+
copy(items: CopyItem[]): TemplateBuilder
8589

8690
remove(
8791
path: string,

packages/js-sdk/src/template/utils.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export async function calculateFilesHash(
2121
src: string,
2222
dest: string,
2323
contextPath: string,
24-
ignorePatterns?: string[],
25-
stackTrace?: string
24+
ignorePatterns: string[],
25+
resolveSymlinks: boolean,
26+
stackTrace: string | undefined
2627
): Promise<string> {
2728
const { glob } = await dynamicGlob()
2829
const srcPath = path.join(contextPath, src)
@@ -44,13 +45,47 @@ export async function calculateFilesHash(
4445
throw error
4546
}
4647

48+
// Hash stats
49+
const hashStats = (stats: fs.Stats) => {
50+
hash.update(stats.mode.toString())
51+
hash.update(stats.uid.toString())
52+
hash.update(stats.gid.toString())
53+
hash.update(stats.size.toString())
54+
hash.update(stats.mtimeMs.toString())
55+
}
56+
4757
for (const file of files) {
48-
if (!file.isFile()) {
49-
continue
58+
// Add a relative path to hash calculation
59+
const relativePath = path.relative(contextPath, file.fullpath())
60+
hash.update(relativePath)
61+
62+
// Add stat information to hash calculation
63+
if (file.isSymbolicLink()) {
64+
// If the symlink is broken, it will return undefined, otherwise it will return a stats object of the target
65+
const stats = fs.statSync(file.fullpath(), { throwIfNoEntry: false })
66+
const shouldFollow =
67+
resolveSymlinks && (stats?.isFile() || stats?.isDirectory())
68+
69+
if (!shouldFollow) {
70+
const stats = fs.lstatSync(file.fullpath())
71+
72+
hashStats(stats)
73+
74+
const content = fs.readlinkSync(file.fullpath())
75+
hash.update(content)
76+
77+
continue
78+
}
5079
}
5180

52-
const content = fs.readFileSync(file.fullpath())
53-
hash.update(new Uint8Array(content))
81+
const stats = fs.statSync(file.fullpath())
82+
83+
hashStats(stats)
84+
85+
if (stats.isFile()) {
86+
const content = fs.readFileSync(file.fullpath())
87+
hash.update(new Uint8Array(content))
88+
}
5489
}
5590

5691
return hash.digest('hex')
@@ -105,34 +140,48 @@ export function padOctal(mode: number): string {
105140
return mode.toString(8).padStart(4, '0')
106141
}
107142

108-
export async function tarFileStream(fileName: string, fileContextPath: string) {
143+
export async function tarFileStream(
144+
fileName: string,
145+
fileContextPath: string,
146+
resolveSymlinks: boolean
147+
) {
109148
const { globSync } = await dynamicGlob()
110149
const { create } = await dynamicTar()
111-
const files = globSync(fileName, { cwd: fileContextPath, nodir: false })
150+
const files = globSync(fileName, { cwd: fileContextPath })
112151

113152
return create(
114153
{
115154
gzip: true,
116155
cwd: fileContextPath,
156+
follow: resolveSymlinks,
117157
},
118158
files
119159
)
120160
}
121161

122162
export async function tarFileStreamUpload(
123163
fileName: string,
124-
fileContextPath: string
164+
fileContextPath: string,
165+
resolveSymlinks: boolean
125166
) {
126167
// First pass: calculate the compressed size without buffering
127-
const sizeCalculationStream = await tarFileStream(fileName, fileContextPath)
168+
const sizeCalculationStream = await tarFileStream(
169+
fileName,
170+
fileContextPath,
171+
resolveSymlinks
172+
)
128173
let contentLength = 0
129174
for await (const chunk of sizeCalculationStream as unknown as AsyncIterable<Buffer>) {
130175
contentLength += chunk.length
131176
}
132177

133178
return {
134179
contentLength,
135-
uploadStream: await tarFileStream(fileName, fileContextPath),
180+
uploadStream: await tarFileStream(
181+
fileName,
182+
fileContextPath,
183+
resolveSymlinks
184+
),
136185
}
137186
}
138187

packages/js-sdk/tests/setup.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1-
import { Sandbox } from '../src'
1+
import { Sandbox, Template, TemplateClass } from '../src'
22
import { test as base } from 'vitest'
33
import { template } from './template'
4+
import { randomUUID } from 'node:crypto'
45

56
interface SandboxFixture {
67
sandbox: Sandbox
78
template: string
89
sandboxTestId: string
910
}
1011

12+
interface BuildTemplateFixture {
13+
buildTemplate: (template: TemplateClass) => Promise<void>
14+
}
15+
16+
function buildTemplate(template: TemplateClass, skipCache?: boolean) {
17+
return Template.build(template, {
18+
alias: randomUUID(),
19+
cpuCount: 1,
20+
memoryMB: 1024,
21+
skipCache: skipCache,
22+
})
23+
}
24+
1125
export const sandboxTest = base.extend<SandboxFixture>({
1226
template,
1327
sandboxTestId: [
@@ -41,6 +55,16 @@ export const sandboxTest = base.extend<SandboxFixture>({
4155
],
4256
})
4357

58+
export const buildTemplateTest = base.extend<BuildTemplateFixture>({
59+
buildTemplate: [
60+
// eslint-disable-next-line no-empty-pattern
61+
async ({}, use) => {
62+
await use(buildTemplate)
63+
},
64+
{ auto: true },
65+
],
66+
})
67+
4468
export const isDebug = process.env.E2B_DEBUG !== undefined
4569
export const isIntegrationTest = process.env.E2B_INTEGRATION_TEST !== undefined
4670

0 commit comments

Comments
 (0)