Repository navigation
Expand file tree
/
Copy pathmod.dang
More file actions
310 lines (282 loc) · 11.1 KB
/
Copy pathmod.dang
File metadata and controls
310 lines (282 loc) · 11.1 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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
"""
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!
"""
How the engine runs this module: "generated" for an entrypoint written into
it, "remote" for the one this SDK publishes, "runtime" for a runtime the
manifest names. A module keeps the kind its manifest has.
"""
pub entrypointMode: String!
"""
Whether this generation converts a module the builtin Python runtime ran,
whose code expects the global client.
"""
let migrating: Boolean!
"""
The engine version the scope's code is generated against, empty for the
engine's own view. Only a module that keeps a runtime of its own runs on a
version of its own, and only the caller knows which runtime the manifest
names.
"""
let engineVersion: String!
"""
The module scope's clients, as the engine hands them to generation. Null
reads them from the workspace config, for a module generated directly.
"""
let clients: [ModuleSource!]
"""
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 scope generated. On the static path the
entrypoint manifest and the entrypoint are written here too, so a module
generated directly gets the same files as one generated through the SDK
scope.
"""
pub generated: Workspace! {
if (isModern == false) {
# A pre-1.0 module is still generated by the engine's builtin Python SDK.
# Merge, don't replace: the generated context holds only generated files.
ws.withDirectory("/" + rootPath, ws
.moduleSource("/" + rootPath)
.generatedContextDirectory
.directory(rootPath))
} else {
let scope = Scope(
path: rootPath,
ws: ws,
clients: clients ?? declaredClients,
isModule: true,
engineVersion: engineVersion,
migrating: migrating,
)
# The scope writes sdk/ whole, so an entrypoint from an earlier static
# generation is gone unless it is written again. It also relocks
# uv.lock, which the derived resources are checked against.
let scoped = scope.generated
if (entrypointMode == "generated") {
let outside = PyprojectHelper().outsidePaths(scoped, rootPath)
checkResourcesLoad(scoped, outside)
let entrypointPath = vendorDirName + "/" + entrypointDirName
entrypointManifest(scoped.withDirectory("/" + rootPath + "/" + entrypointPath, entrypointDir(scoped, patterns(outside))), entrypointPath)
} else if (entrypointMode == "remote") {
checkNoResources(PyprojectHelper().outsidePaths(scoped, rootPath))
scoped
} else {
scoped
}
}
}
"""
The clients the workspace config declares on this module's scope. The
config names a local client by its path from the workspace root, starting
with "./", and a Git client by its ref.
"""
let declaredClients: [ModuleSource!]! {
ws.sdk(name: currentModule.name)
.clients.{{name, source}}
.filter { client => scopeOf(client.name) == rootPath }
.map { client =>
if (client.source.hasPrefix(".") or client.source.hasPrefix("/")) {
ws.moduleSource("/" + scopeOf(client.source))
} else {
moduleSource(client.source)
}
}
}
let scopeOf(path: String!): String! {
let trimmed = path.trimPrefix("./").trimPrefix("/").trimSuffix("/")
if (trimmed == "") { "." } else { trimmed }
}
"""
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 manifestConfig: JSONValue! {
json.withContents((JSON.encode(TOML.decode(ws.file("/" + manifestPath).contents)) :: Dagger.JSON!))
}
let moduleName: String! {
if (manifestConfig.fields.contains("name")) {
manifestConfig.field(["name"]).asString
} else {
raise "no module name in " + manifestPath
}
}
"""
The generated workspace with the module's own manifest, entrypoint table
taken out, which the module's container is built from.
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. So the runtime manifest points back at the module being built.
"""
let staging(target: Workspace!, outside: [String!]!): 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 (manifestConfig.fields.contains("runtime")) {
loaded
} else {
loaded.withLegacyPythonRuntime
}
# The build that renders the types reads the path sources outside the
# module directory, which the entrypoint's sources will return.
let included = outside.reduce(runnable) { manifest, pattern => manifest.withLegacyInclude(path: pattern) }
included.withName(name: moduleName)
.generate(target.withWorkdir(rootPath), lock: false, legacyJson: false)
.withWorkdir(".")
} else {
target
}
}
"""
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(generated: Workspace!, outside: [String!]!): Directory! {
pythonSdkRuntime
.moduleRuntime(modSource: staging(generated, outside).moduleSource("/" + rootPath), introspectionJson: null)
.withExec(["python", "-m", "dagger.mod", "entrypoint", "--name", moduleName, "--output", entrypointOutput] +
outside.map { pattern => "--outside=" + pattern })
.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] ?? ""
}
}
let patterns(outside: [OutsidePath!]!): [String!]! {
outside.map { path => path.pattern }
}
"""
Refuse derived resources this workspace cannot load: a path that holds no
file, or only what .gitignore drops from a local module, loads nothing, and
sources reads each path as a directory, which a wheel is not. A symlink is
imported without the path it names, so a path through one is refused before
it reads as missing or as a file.
"""
let checkResourcesLoad(target: Workspace!, outside: [OutsidePath!]!): Void {
outside.each { path =>
let named = "module \"" + moduleName + "\" builds with " + path.pattern + ", which"
let prefixes = path.rooted.split("/").reduce([] :: [String!]!) { found, part =>
found + [if (found.length == 0) { part } else { (found[found.length - 1] ?? "") + "/" + part }]
}
let links = prefixes.filter { prefix =>
target.directory("/", include: [prefix], exclude: [prefix + "/**"]).exists(prefix, expectedType: ExistsType.SYMLINK_TYPE)
}
if (links.length > 0) {
raise named + " goes through the symlink " + (links[0] ?? "") + " in the workspace; outside path sources through a symlink are not supported: point the path source at the directory it names"
}
let selected = target.directory("/", include: [path.rooted], exclude: ["**/.git"])
if (files(selected).length == 0) {
raise named + " holds no file in the workspace"
}
if (selected.exists(path.rooted, expectedType: ExistsType.DIRECTORY_TYPE) == false) {
raise named + " is a file; outside file sources, such as a wheel, are not supported: point the path source at a directory, such as the project that builds it"
}
if (files(target.directory("/", include: [path.rooted], exclude: ["**/.git"], gitignore: true)).length == 0) {
raise named + " holds only files that .gitignore ignores"
}
null
}
null
}
"""
Refuse derived resources on a module that runs on an entrypoint this SDK
does not generate into it: only that entrypoint returns them from its
sources, so the module would fail at call time.
"""
let checkNoResources(outside: [OutsidePath!]!): Void {
if (outside.length > 0) {
raise "module \"" + moduleName + "\" builds with files outside its directory (" + patterns(outside).join(", ") +
"), which only the entrypoint this SDK generates into a module loads; it runs on the entrypoint " +
ManifestToml(ws.file("/" + manifestPath).contents).entrypointSource + " instead"
}
null
}
"""
A directory whose files a filter removed is still listed, so it does not
count.
"""
let files(dir: Directory!): [String!]! {
dir.glob("**").filter { path => path.hasSuffix("/") == false }
}
let vendorDirName: String! = "sdk"
let entrypointDirName: String! = "entrypoint"
let entrypointOutput: String! = "/dagger/entrypoint"
}