Repository navigation
Expand file tree
/
Copy pathmod.dang
More file actions
239 lines (215 loc) · 7.68 KB
/
Copy pathmod.dang
File metadata and controls
239 lines (215 loc) · 7.68 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
"""
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 {
# The scope writes sdk/ whole, so an entrypoint from an earlier static
# generation is gone unless it is written again.
let scoped = Scope(
path: rootPath,
ws: ws,
clients: clients ?? declaredClients,
isModule: true,
engineVersion: engineVersion,
migrating: migrating,
).generated
if (entrypointMode == "generated") {
let entrypointPath = vendorDirName + "/" + entrypointDirName
entrypointManifest(scoped.withDirectory("/" + rootPath + "/" + entrypointPath, entrypointDir(scoped)), entrypointPath)
} 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!): 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
}
runnable.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!): Directory! {
pythonSdkRuntime
.moduleRuntime(modSource: staging(generated).moduleSource("/" + rootPath), introspectionJson: null)
.withExec(["python", "-m", "dagger.mod", "entrypoint", "--name", moduleName, "--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] ?? ""
}
}
let vendorDirName: String! = "sdk"
let entrypointDirName: String! = "entrypoint"
let entrypointOutput: String! = "/dagger/entrypoint"
}