-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmod.dang
More file actions
289 lines (260 loc) · 9.44 KB
/
Copy pathmod.dang
File metadata and controls
289 lines (260 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""
A Dagger module that uses the Python SDK.
"""
type Mod {
"""
Workspace-root-relative path of this module root.
"""
pub rootPath: String!
"""
The workspace this module belongs to.
"""
let ws: Workspace!
"""
Whether the module gets a static entrypoint, from the SDK setting or from
the manifest it already has.
"""
pub dangEntrypoint: Boolean!
"""
Module root relative to the client's cwd.
"""
pub path: String! {
let cwd = ws.cwd.trimPrefix("/").trimSuffix("/")
if (rootPath == cwd) {
"."
} else if (cwd == "") {
rootPath
} else if (rootPath.trimPrefix(cwd + "/") != rootPath) {
rootPath.trimPrefix(cwd + "/")
} else if (rootPath == "." or cwd.trimPrefix(rootPath + "/") != cwd) {
let depth = if (rootPath == ".") { 0 } else { rootPath.split("/").length }
cwd.split("/").dropFirst(depth).map { segment => ".." }.join("/")
} else {
rootPath
}
}
"""
Manage this module's Python build configuration (pyproject.toml).
"""
pub config: ModConfig! {
ModConfig(path: rootPath, ws: ws)
}
"""
Generate this module.
"""
pub generate: Changeset! {
generated.changes(ws)
}
"""
The workspace with this module's generated files merged in. On the static
path the entrypoint manifest is written here too, so a module generated
directly gets the same files as one generated through the SDK scope.
"""
pub generated: Workspace! {
let files = if (isModern) {
# Generated here: the runtime generates nothing, so the engine's
# generated context would be empty.
vendoredDir
} else {
# A pre-1.0 module is still generated by the engine's builtin Python SDK.
ws
.moduleSource("/" + rootPath)
.generatedContextDirectory
.directory(rootPath)
}
# Merge, don't replace: the generated context holds only generated files.
let merged = ws.withDirectory("/" + rootPath, files)
let entrypointPath = vendorDirName + "/" + entrypointDirName
if (dangEntrypoint) {
entrypointManifest(merged, entrypointPath)
} else if (ws.directory("/" + rootPath).exists(entrypointPath)) {
merged.withoutDirectory("/" + rootPath + "/" + entrypointPath)
} else {
merged
}
}
"""
Write the manifest of a static module: the name and the Dang entrypoint,
and nothing the runtime manifest keeps. sdk-helpers owns the manifest's
shape, so the SDK never writes the TOML itself.
"""
let entrypointManifest(target: Workspace!, entrypointPath: String!): Workspace! {
sdkHelpers.moduleManifest
.withName(name: moduleName)
.withDangEntrypoint(source: "./" + entrypointPath)
.generate(target.withWorkdir(rootPath), lock: false, legacyJson: false)
.withWorkdir(".")
}
"""
Whether this module uses the 1.0 dagger-module.toml config.
"""
let isModern: Boolean! {
ws.directory("/", include: [manifestPath]).exists(manifestPath)
}
let manifestPath: String! {
if (rootPath == ".") { "dagger-module.toml" } else { rootPath + "/dagger-module.toml" }
}
let hasEntrypointManifest: Boolean! {
isModern and ManifestToml(ws.file("/" + manifestPath).contents).hasEntrypoint
}
let moduleName: String! {
let m = ws.file("/" + manifestPath).contents.match("(?m)^\\s*name\\s*=\\s*\"([^\"]*)\"")
if (m == null) {
raise "no module name in " + manifestPath
} else {
m.captures[0] ?? ""
}
}
"""
The workspace the module's schema is read from and its container is built
in: the module's own manifest with the entrypoint table taken out.
An engine that does not know the table serves a static entrypoint manifest
the oldest schema view. An engine that loads manifest version 2 would resolve
the entrypoint here, shared or static, and build the module from source it
has not generated yet. Either way the runtime manifest points back at the
module being built. Everything else the manifest holds stays, the
dependencies above all: the bindings are generated from this schema.
"""
let stagingWs: Workspace! {
if (hasEntrypointManifest) {
let staged = ManifestToml(ws.file("/" + manifestPath).contents).withoutEntrypoint
let loaded = sdkHelpers.moduleManifest(loadToml: staged)
# A static entrypoint manifest holds no runtime to fall back on.
let runnable = if (staged.contents.containsMatch("(?m)^\\s*\\[runtime\\]")) {
loaded
} else {
loaded.withLegacyPythonRuntime
}
runnable.withName(name: moduleName)
.generate(ws.withWorkdir(rootPath), lock: false, legacyJson: false)
.withWorkdir(".")
} else {
ws
}
}
"""
The client library with the module's bindings, in a module-rooted directory
holding only `sdk/` so it merges onto the module without touching anything else.
"""
let vendoredDir: Directory! {
let schemaJSON = stagingWs.moduleSource("/" + rootPath).introspectionSchemaJSON
let vendored = library.withFile(generatedBindingsPath, bindings(schemaJSON))
let files = if (dangEntrypoint) {
vendored.withDirectory(entrypointDirName, entrypointDir(vendored))
} else {
vendored
}
directory.withDirectory(vendorDirName, files)
}
"""
The entrypoint the engine loads instead of calling a runtime: the module's
own container renders its types, and the container build travels with it.
"""
let entrypointDir(vendored: Directory!): Directory! {
let staged = stagingWs.withDirectory("/" + rootPath + "/" + vendorDirName, vendored)
pythonSdkRuntime
.moduleRuntime(modSource: staged.moduleSource("/" + rootPath), introspectionJson: null)
.withExec(["python", "-m", "dagger.mod", "entrypoint", "--name", moduleName, "--path", rootPath, "--output", entrypointOutput])
.directory(entrypointOutput)
.withNewFile("build.dang", buildDang)
}
"""
runtime/build.dang with its reads of this module's source inlined, so the
copy in a generated entrypoint needs nothing but itself.
"""
let buildDang: String! {
let pins = "let defaultBaseImage: String! = \"" + imageFrom("runtime/images/base/Dockerfile") + "\"\n" +
" let defaultUvImage: String! = \"" + imageFrom("runtime/images/uv/Dockerfile") + "\""
let inlined = currentModule.source.file("runtime/build.dang").contents
.replaceMatches("(?s)#<externals>.*?#</externals>", pins)
if (inlined.contains("currentModule")) {
raise "runtime/build.dang reads currentModule outside its externals block"
} else {
inlined
}
}
let imageFrom(path: String!): String! {
let line = currentModule.source.file(path).contents.match("(?m)^FROM\\s+(\\S+)")
if (line == null) {
raise "no FROM line in " + path
} else {
line.captures[0] ?? ""
}
}
"""
Bindings generated from a module's schema, straight from the synced
environment: `uv run --isolated` built a throwaway one per module.
"""
let bindings(schemaJSON: File!): File! {
codegenEnv
.withMountedFile(schemaPath, schemaJSON)
.withExec([
codegenPython, "-m", "codegen", "generate", "-i", schemaPath, "-o", "/gen.py",
])
.file("/gen.py")
}
"""
The code generator's environment, synced once per SDK version and shared by
every module that generates.
"""
let codegenEnv: Container! {
codegenBase.withExec(["uv", "sync", "--frozen", "--no-dev", "--package", "codegen"])
}
let codegenBase: Container! {
container
.from(codegenImage)
.withoutEntrypoint
.withMountedCache("/root/.cache/uv", cacheVolume("python-sdk-uv"))
.withEnvVariable("UV_LINK_MODE", "copy")
.withEnvVariable("UV_COMPILE_BYTECODE", "1")
.withDirectory("/sdk", codegenSource)
.withWorkdir("/sdk")
}
let codegenPython: String! = "/sdk/.venv/bin/python"
let codegenSource: Directory! {
currentModule.source.directory("sdk").filter(include: [
"pyproject.toml",
"uv.lock",
"src/**/*.py",
"src/**/*.typed",
"codegen/pyproject.toml",
"codegen/**/*.py",
])
}
"""
What a module vendors: the importable library and its license. The generator
runs here, never in a module, and the lock pins the generator's environment.
"""
let library: Directory! {
currentModule.source
.directory("sdk")
.filter(include: [
"LICENSE",
"README.md",
"src/**/*.py",
"src/**/*.typed",
# An optional import that provisions an engine; a module already has one.
"!src/dagger/provisioning/**",
])
.withFile("pyproject.toml", libraryPyproject)
}
"""
pyproject.toml without its development sections: vendored verbatim, it names
the absent codegen workspace member and uv refuses to install the library.
"""
let libraryPyproject: File! {
codegenBase
.withFile(stripScriptPath, currentModule.source.file("helpers/vendor-pyproject/strip_dev_sections.py"))
.withExec(["python", stripScriptPath, "pyproject.toml", "/library-pyproject.toml"])
.file("/library-pyproject.toml")
}
let stripScriptPath: String! = "/strip-dev-sections.py"
let vendorDirName: String! = "sdk"
let entrypointDirName: String! = "entrypoint"
let entrypointOutput: String! = "/dagger/entrypoint"
let generatedBindingsPath: String! = "src/dagger/client/gen.py"
let schemaPath: String! = "/schema.json"
# musl runs the generator ~0.3s slower than glibc, but the glibc image is
# 25 MiB larger to pull, which costs more on the first generate.
let codegenImage: String! = "ghcr.io/astral-sh/uv:python3.14-alpine"
}