Repository navigation
Expand file tree
/
Copy pathgo-sdk.dang
More file actions
660 lines (612 loc) · 27.3 KB
/
Copy pathgo-sdk.dang
File metadata and controls
660 lines (612 loc) · 27.3 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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
"""
Manage Dagger modules and module clients that use the Go SDK.
"""
type GoSdk {
"""Runtime SDK override, independent of a target's schema version. Unified outputs default to a published immutable runtime; legacy outputs retain their schema pin."""
clientVersion: String! = ""
"""
Reuse dagger.io/dagger/core types and one lazy shared session. Unset uses
unified clients for new entrypoint modules, while retaining the mode of
existing embedded and standalone packages. Explicit false keeps
embedded bindings; explicit true migrates them to shared core types.
Migrating embedded bindings also requires dangEntrypoint = true.
"""
unifiedClients: Boolean
"""
Generate the module's global `dag` for author code that still needs it.
It is omitted by default. Unified modules use core constructors such as
core.NewContainer() and dependency client constructors such as
mydep.New(); embedded bindings use dagger.Connect().
This setting applies to Dang entrypoints. Legacy runtime codegen keeps
its existing global API.
"""
globalClient: Boolean
"""
Starter template used when a new module scope is initialized.
Valid values are `default`, `empty`, and `legacy`.
"""
template: String! = "default"
"""
Generate a fat module that also writes the legacy dagger.json manifest.
The SDK-module interface requires Dagger v1.0.0-beta.12 or later.
"""
fat: Boolean! = false
"""Write selected dependency commits to the module manifests."""
lock: Boolean! = false
"""
In module scopes, generate each client two times: one for the module and
one standalone. Non-module scopes always generate standalone clients.
Temporary: this setting will be removed when the two clients become one.
"""
dualClients: Boolean! = false
"""
Generate a Dang entrypoint and a static dispatch command, so the engine
loads the module's types without running it. Supports package main and
importable packages with a go.mod in the module directory. Manifest v2
needs beta.14; collections need beta.15. Shared clients and cache policies
require later engine fixes. Unset enables entrypoints for new modules and keeps
the runner of existing modules. Explicit true migrates a supported existing
module; false keeps the Go runtime.
"""
dangEntrypoint: Boolean
"""Find the nearest Go module root from Workspace.cwd. Return null if no go.mod exists."""
findClientRoot(ws: Workspace!): String {
# A host-backed workspace's findUp stats the host. A new go.mod can
# exist only in the workspace edits until those edits are applied.
if (exists(ws, "go.mod")) {
return normalizePath(ws.cwd)
}
let goMod = ws.findUp("go.mod")
if (goMod == null) {
null
} else {
configDir(goMod, "go.mod")
}
}
"""
Generate the complete module and client state for one SDK scope.
Module scopes use the built-in Go module generator. With dualClients, they
also generate standalone clients in their nearest Go scope. Non-module
scopes always generate standalone clients. With dangEntrypoint, module
scopes get a Dang entrypoint and a manifest that names it instead of a
runtime.
"""
generateScope(
ws: Workspace!,
isModule: Boolean!,
name: String!,
clients: [ModuleSource!]!,
): Workspace! {
let hadManifest = exists(ws, "dagger-module.toml") or exists(ws, "dagger.json")
let entrypoint = isModule and usesEntrypoint(ws, hadManifest)
let unified = usesUnifiedModule(ws, entrypoint)
if (isModule and !entrypoint and globalClient == false) {
raise "globalClient = false requires dangEntrypoint = true; legacy Go runtime codegen retains its global client"
}
if (isModule and !entrypoint and unified and !hasCustomRunner(ws)) {
raise "unifiedClients = true requires dangEntrypoint = true for embedded module bindings; built-in Go runtime codegen cannot generate shared core types"
}
let initialized = if (isModule and !hadManifest) {
ws.withDirectory(".", renderedTemplate(ws, name, template, entrypoint, unified))
} else {
ws
}
if (isModule) {
checkEntrypointScope(initialized, hadManifest, clients, entrypoint, unified)
}
if (isModule and entrypoint) {
let withClients = if (unified) {
generateStandaloneClients(initialized, clients, true)
} else { initialized }
entrypointModule(withClients, name, unified)
} else if (isModule and hasCustomRunner(initialized)) {
# A runtime or entrypoint the user chose remains theirs. Only explicitly
# requested standalone clients are generated beside it.
if (clients.length == 0) { initialized } else {
generateStandaloneClients(initialized, clients, false)
}
} else if (isModule) {
let configured = generatedManifest(initialized, name, clients)
let generated = configured.withDirectory(".", generatedModuleCode(configured))
# generatedModuleCode excludes manifests, so the configured ones survive
# the merge. Rewriting them here would repeat the same workspace edit.
if (dualClients) {
generateStandaloneClients(generated, clients, false)
} else {
generated
}
} else {
# Standalone clients are the only output here, so dualClients does not apply.
generateStandaloneClients(initialized, clients, false)
}
}
"""Choose the new layout for fresh modules and retain existing runners."""
let usesEntrypoint(ws: Workspace!, hadManifest: Boolean!): Boolean! {
if (dangEntrypoint != null) {
dangEntrypoint ?? false
} else if (hadManifest) {
hasGeneratedEntrypoint(ws)
} else {
!fat and template != "legacy"
}
}
"""Existing embedded bindings keep their API unless migration is requested."""
let usesUnifiedModule(ws: Workspace!, entrypoint: Boolean!): Boolean! {
if (unifiedClients != null) { return unifiedClients ?? false }
if (!entrypoint) { return false }
if (exists(ws, "internal/dagger/dagger.gen.go")) {
ws.file("internal/dagger/dagger.gen.go").contents.contains("dagger.DefaultGraphQLClient(")
} else { true }
}
"""The SDK's generated entrypoint, rather than a runner chosen by the user."""
let hasGeneratedEntrypoint(ws: Workspace!): Boolean! {
let optionalManifest = decodedManifest(ws)
if (optionalManifest == null) { return false }
let manifest = optionalManifest :: Dagger.JSONValue!
if (manifest.fields.contains("entrypoint") == false) { return false }
let entrypoint = manifest.field(["entrypoint"])
stringField(entrypoint, "kind") == "dang" and
normalizePath(stringField(entrypoint, "source")) == entrypointPath and
ownsEntrypoint(ws)
}
let ownsEntrypoint(ws: Workspace!): Boolean! {
let path = entrypointPath + "/main.dang"
if (exists(ws, path)) {
# A user replacing the file takes precedence over old ownership.
return ws.file(path).contents.hasPrefix("# Code generated by dagger. Do not edit.\n")
}
if (!exists(ws, ".dagger-generated.json")) { return false }
let ownership = Dagger.json.withContents(ws.file(".dagger-generated.json").contents :: Dagger.JSON!)
ownership.fields.contains("files") and ownership.field(["files"]).fields.contains(path)
}
"""A custom runtime or entrypoint remains unchanged by automatic selection."""
let hasCustomRunner(ws: Workspace!): Boolean! {
let optionalManifest = if (exists(ws, "dagger-module.toml")) {
decodedManifest(ws)
} else if (exists(ws, "dagger.json")) {
Dagger.json.withContents(ws.file("dagger.json").contents :: Dagger.JSON!)
} else { null }
if (optionalManifest == null) { return false }
let manifest = optionalManifest :: Dagger.JSONValue!
let fields = manifest.fields
if (fields.contains("entrypoint") and !hasGeneratedEntrypoint(ws)) { return true }
let key = if (fields.contains("runtime")) { "runtime" } else if (fields.contains("sdk")) { "sdk" } else { "" }
if (key == "") { return false }
let source = runtimeSource(manifest.field([key]))
source != "" and source != "go"
}
"""Unified generation uses a published runtime independently of engine/schema versions."""
let runtimeVersion(unified: Boolean!): String! {
if (clientVersion != "") { clientVersion } else if (unified) {
"v1.0.0-beta.16.0.20261008202843-134540fec551"
} else { "" }
}
let normalizePath(p: String!): String! {
let trimmed = p.trimPrefix("/").trimPrefix("./").trimSuffix("/")
if (trimmed == "") { "." } else { trimmed }
}
let configDir(foundPath: String!, marker: String!): String! {
normalizePath(foundPath.trimPrefix("/").trimSuffix(marker))
}
let exists(ws: Workspace!, path: String!): Boolean! {
ws.directory(".", include: [path]).exists(path)
}
"""Generate standalone clients at the nearest Go scope without changing cwd."""
let generateStandaloneClients(
ws: Workspace!,
clients: [ModuleSource!]!,
moduleUnified: Boolean!,
): Workspace! {
let originalScope = normalizePath(ws.cwd)
let goScope = findClientRoot(ws)
if (goScope == null) {
if (clients.length == 0) { return ws }
raise "cannot generate Go clients without a go.mod"
} else {
let scoped = ws.withWorkdir(goScope)
let base = clientCodegenBase(scoped.file("go.mod")).withDirectory(
"/out",
scoped.directory(".", exclude: ["**/.git"]),
).withNewFile("/removed-clients.txt", "")
# Keep prior client outputs and go.mod updates in the same container.
# Only the completed batch needs to be merged into the workspace.
let generated = clients.reduce(base) { current, target =>
generateStandaloneClient(current, target, moduleUnified)
}
let pruned = generated.withExec([
"dagger-go-sdk-codegen", "prune-clients",
"--root", "/out/internal/dagger/clients",
"--keep", clients.map { target => target.moduleOriginalName }.join(","),
"--removed-list", "/removed-clients.txt",
"--prefix", "internal/dagger/clients",
])
# An entrypoint's author source already imports its module client,
# which the next step has not generated yet. Its bootstrap and final
# tidies run after that package exists; keep pruning here for empty batches.
let completed = if (moduleUnified) { pruned } else {
pruned.withWorkdir("/out").withExec(["go", "mod", "tidy"])
}
let merged = scoped.withDirectory(".", completed.directory("/out"))
let removed = pruned.file("/removed-clients.txt").contents.split("\n").filter { p => p != "" }
removed.reduce(merged) { current, p => current.withoutFile(p) }.withWorkdir(originalScope)
}
}
"""
Create the module manifests from the scope inputs and SDK defaults.
Fields of an existing manifest that the scope does not set are kept.
"""
let generatedManifest(
ws: Workspace!,
name: String!,
clients: [ModuleSource!]!,
): Workspace! {
sdkHelpers.loadModuleManifest(ws)
.withName(name)
.withLegacyGoRuntime
.withLegacyRuntimeDependencies(modules: clients)
.generate(ws, lock: lock, legacyJson: fat)
}
"""Run built-in Go codegen and discard its manifest files."""
let generatedModuleCode(ws: Workspace!): Directory! {
let scopePath = normalizePath(ws.cwd)
let sourcePath = if (scopePath == ".") { "/" } else { "/" + scopePath }
let source = ws
.withWorkdir(".")
.moduleSource(sourcePath)
let generatedRoot = source.generatedContextDirectory
let generated = if (scopePath == ".") {
generatedRoot
} else {
generatedRoot.directory(scopePath)
}
# Keep source navigation stable between Git and Directory workspaces without
# reconstructing the workspace or changing its resolved dependencies.
let stable = if (source.kind == GIT_SOURCE) {
codegenBuilder
.withDirectory("/out", generated)
.withFile("/schema.json", source.introspectionSchemaJSON)
.withExec([
"dagger-go-sdk-codegen", "normalize-source-links",
"--introspection-json-path", "/schema.json",
"--output", "/out",
"--scope", scopePath,
"--workspace-repo-url", source.htmlRepoURL,
"--workspace-pin", source.pin,
])
.directory("/out")
} else {
generated
}
let withoutTOML = if (stable.exists("dagger-module.toml")) {
stable.withoutFile("dagger-module.toml")
} else {
stable
}
if (withoutTOML.exists("dagger.json")) {
withoutTOML.withoutFile("dagger.json")
} else {
withoutTOML
}
}
"""
Refuse what a Dang entrypoint cannot carry, before anything is written.
Without dangEntrypoint, refuse a module generated with one: the Go runtime
cannot load the importable package it uses.
"""
let checkEntrypointScope(ws: Workspace!, hadManifest: Boolean!, clients: [ModuleSource!]!, entrypoint: Boolean!, unified: Boolean!): Void {
if (entrypoint) {
let enclosingRoot = findClientRoot(ws)
if (enclosingRoot != null and enclosingRoot != normalizePath(ws.cwd)) {
raise "Dang entrypoints need a go.mod in the module directory; keep dangEntrypoint = false for modules using a parent go.mod"
}
if (clients.length > 0 and !unified) {
raise "a Dang entrypoint cannot use other modules yet: a manifest with an entrypoint has no dependencies; set dangEntrypoint = false"
}
if (!hadManifest and template == "legacy") {
raise "the legacy template is not available with a Dang entrypoint; choose another template or set dangEntrypoint = false"
}
if (fat) {
raise "a fat module is not available with a Dang entrypoint: dagger.json would point older engines at the Go runtime, which cannot load an importable package; set fat = false or dangEntrypoint = false"
}
refuseManifestSettings("dagger-module.toml", decodedManifest(ws), "runtime")
let legacyJSON = if (exists(ws, "dagger.json")) {
Dagger.json.withContents(ws.file("dagger.json").contents :: Dagger.JSON!)
} else {
null
}
refuseManifestSettings("dagger.json", legacyJSON, "sdk")
} else if (hasGeneratedEntrypoint(ws)) {
raise "this module is generated with a Dang entrypoint, and the Go runtime cannot load its importable package; set dangEntrypoint = true, or change the package to main and remove the [entrypoint] table from dagger-module.toml"
}
null
}
"""
Refuse what a manifest sets that a manifest with an entrypoint drops:
sdk-helpers writes that manifest from the name and the entrypoint only.
"""
let refuseManifestSettings(fileName: String!, manifest: Dagger.JSONValue, runtimeKey: String!): Void {
if (manifest == null) {
return null
}
let objections = manifestObjections(manifest, runtimeKey)
if (objections.length > 0) {
raise fileName + " has settings a Dang entrypoint cannot carry (" + objections.join(", ") + "); remove them or set dangEntrypoint = false"
}
null
}
"""
The settings of a decoded manifest that a manifest with an entrypoint has
no place for. runtimeKey is `runtime` in dagger-module.toml and `sdk` in
dagger.json. A Go runtime is what dangEntrypoint replaces, so it is kept.
"""
let manifestObjections(manifest: Dagger.JSONValue!, runtimeKey: String!): [String!]! {
let fields = manifest.fields
let keys = ["include", "disableDefaultFunctionCaching"].filter { key =>
fields.contains(key)
}
# A module is rooted at its own directory, which is what `source = "."`
# says. Any other source is what the manifest cannot carry.
let source = if (fields.contains("source") and normalizePath(manifest.field(["source"]).asString) != ".") {
["source"]
} else {
[] :: [String!]!
}
let tables = ["codegen", "clients", "dependencies"].filter { key =>
fields.contains(key)
}
let runtime = if (fields.contains(runtimeKey) and runtimeSource(manifest.field([runtimeKey])) != "go") {
[runtimeKey]
} else {
[] :: [String!]!
}
keys + source + tables + runtime
}
"""
dagger-module.toml parsed as TOML, the way sdk-helpers reads it, or null
when the module has none.
"""
let decodedManifest(ws: Workspace!): Dagger.JSONValue {
if (exists(ws, "dagger-module.toml") == false) {
return null
}
let contents = ws.file("dagger-module.toml").contents
Dagger.json.withContents(JSON.encode(TOML.decode(contents)) :: Dagger.JSON!)
}
"""The source of a runtime table or sdk field, which dagger.json may give as a string."""
let runtimeSource(value: Dagger.JSONValue!): String! {
value.asString rescue {
err: Error => stringField(value, "source")
}
}
"""A string field of a JSON object, or empty when the field is missing."""
let stringField(value: Dagger.JSONValue!, key: String!): String! {
if (value.fields.contains(key)) { value.field([key]).asString } else { "" }
}
"""
Generate a module that the engine loads through a Dang entrypoint.
The schema comes from a staged runtime manifest that does not reach the
result. The generator writes the embedded client, the static dispatch
command, and the entrypoint into the module's Go root. sdk-helpers then
writes the manifest that names the entrypoint and removes dagger.json.
"""
let entrypointModule(ws: Workspace!, name: String!, unified: Boolean!): Workspace! {
let scopePath = normalizePath(ws.cwd)
# A module whose manifest names an entrypoint and no dependencies loads
# through that entrypoint, which is not generated yet. A fresh runtime
# manifest gives the engine a module it can serve a schema for.
let staged = sdkHelpers.moduleManifest
.withName(name: name)
.withLegacyGoRuntime
.generate(ws, lock: false, legacyJson: false)
let source = staged.withWorkdir(".").moduleSource(rootedPath(scopePath))
let schemaVersion = source.engineVersion
# The generator updates go.mod and go.sum at the Go root, which can be
# above the module. Without one, it creates go.mod in the module.
let goRoot = findClientRoot(ws) ?? scopePath
let moduleRoot = moduleSubpath(goRoot, scopePath)
let prefix = if (moduleRoot == ".") { "" } else { moduleRoot + "/" }
let workspaceRoot = ws.withWorkdir(goRoot)
let goVersion = if (exists(workspaceRoot, "go.mod")) {
requiredGoVersion(workspaceRoot.file("go.mod"))
} else { "1.26.1" }
let goImage = "golang:" + goVersion + "-alpine"
let inputs = moduleCodegenBuilder(goImage)
.withDirectory("/src", workspaceRoot.directory(".", exclude: ["**/.git"]))
.withFile("/schema.json", source.introspectionSchemaJSON)
let codegen = runTool(inputs, [
"module-codegen",
"--module-root", "/src/" + moduleRoot,
"--module-name", name,
"--introspection-json-path", "/schema.json",
"--schema-version", schemaVersion,
"--go-image", goImage,
"--dagger-version", if (unified) { runtimeVersion(true) } else { schemaVersion },
"--unified=" + JSON.encode(unified),
"--global-client=" + JSON.encode(globalClient ?? false),
"--removed-list", "/removed.txt",
])
let generated = runTool(codegen, [
"entrypoint-contract",
"--entrypoint-dir", "/src/" + prefix + entrypointPath,
"--introspection-json-path", "/schema.json",
]).directory("/src")
# Merge only what the generator owns, so the user's files are not
# written back.
let owned = directory.withDirectory(".", generated, include: [
"go.mod",
"go.sum",
prefix + "go.mod",
prefix + "go.sum",
prefix + "dagger.gen.go",
prefix + "dagger.dispatch.gen.go",
prefix + ".dagger-generated.json",
prefix + "internal/dagger/**",
prefix + "cmd/*-dispatch/**",
])
let merged = ws
.withWorkdir(goRoot)
.withDirectory(".", owned)
.withWorkdir(scopePath)
# The merge only adds files. The generated files the generator removed
# are removed here, relative to the module.
let removed = codegen.file("/removed.txt").contents.split("\n").filter { p => p != "" }
let pruned = removed.reduce(merged) { current, p => current.withoutFile(p) }
sdkHelpers.moduleManifest
.withName(name: name)
.withDangEntrypoint(source: "./" + entrypointPath)
.generate(pruned, lock: false, legacyJson: false)
}
"""
Run one generator tool. A failure raises the tool's own message: an exec
error reports only the exit code, and the message is what tells the user
how to fix the module.
"""
let runTool(ctr: Container!, args: [String!]!): Container! {
let ran = ctr.withExec(args, expect: ReturnType.ANY)
if (ran.exitCode != 0) {
# Each tool prints its error last, after its name.
let prefix = (args[0] ?? "") + ": "
raise prefix + ran.stderr.split(prefix).reduce("") { last, part => part }.trimSpace
}
ran
}
"""Directory of the generated Dang entrypoint, relative to the module."""
let entrypointPath: String! = "internal/dagger/entrypoint"
"""Path of a module below its Go root, both relative to the workspace root."""
let moduleSubpath(goRoot: String!, scopePath: String!): String! {
if (goRoot == scopePath) {
"."
} else if (goRoot == ".") {
scopePath
} else {
scopePath.trimPrefix(goRoot + "/")
}
}
let rootedPath(p: String!): String! {
if (p == ".") { "/" } else { "/" + p }
}
"""
Build the module generator and the entrypoint contract check.
The contract check embeds Dang, whose parser needs cgo.
"""
let moduleCodegenBuilder(goImage: String!): Container! {
container
.from(goImage)
.withoutEntrypoint
.withExec(["apk", "add", "--no-cache", "build-base"])
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helpers", currentModule.source.directory("helpers"))
.withDirectory("/cmd", currentModule.source.directory("cmd"))
.withWorkdir("/helpers/entrypoint-contract")
.withExec(["go", "build", "-o", "/usr/local/bin/entrypoint-contract", "."])
.withWorkdir("/helpers/module-codegen")
.withExec(["go", "build", "-o", "/usr/local/bin/module-codegen", "."])
}
"""Build the engine-free standalone client generator."""
let codegenBuilder: Container! {
container
.from("golang:1.26-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("cmd/dagger-go-sdk-codegen"))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/dagger-go-sdk-codegen", "."])
}
"""Use the newer of the consumer Go directives and the generator's minimum."""
let requiredGoVersion(goMod: File!): String! {
codegenBuilder.withFile("/consumer.go.mod", goMod)
.withExec(["dagger-go-sdk-codegen", "go-version", "--go-mod", "/consumer.go.mod", "--minimum", "1.26.1"])
.stdout.trimSpace
}
let clientCodegenBase(goMod: File!): Container! {
let goVersion = requiredGoVersion(goMod)
container.from("golang:" + goVersion + "-alpine").withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withFile("/usr/local/bin/dagger-go-sdk-codegen", codegenBuilder.file("/usr/local/bin/dagger-go-sdk-codegen"))
}
"""Serialize the target-module location used by the generated bootstrap."""
let boundModuleJSON(kindJSON: String!, path: String!, ref: String!, pin: String!): String! {
"{\"kind\":" + kindJSON + ",\"path\":" + JSON.encode(path) + ",\"ref\":" + JSON.encode(ref) + ",\"pin\":" + JSON.encode(pin) + "}"
}
"""Generate one target client in the batch's Go module at /out."""
let generateStandaloneClient(ctr: Container!, target: ModuleSource!, moduleUnified: Boolean!): Container! {
let targetName = target.moduleOriginalName
let outputPath = "internal/dagger/clients/" + targetName
# The existing package's transport tells auto whether it already uses
# shared core. New packages beside a legacy app retain embedded bindings;
# new entrypoint modules explicitly select their unified layout.
let generatedPath = "/out/" + outputPath + "/dagger.gen.go"
let existing = ctr.directory("/out").exists(outputPath + "/dagger.gen.go")
let retained = if (existing) {
ctr.file(generatedPath).contents.contains("dagger.ModuleGraphQLClient(")
} else { false }
let unified = unifiedClients ?? (moduleUnified or retained)
let metaJSON = "{\"engineVersion\":" + JSON.encode(target.engineVersion) +
",\"clientVersion\":" + JSON.encode(runtimeVersion(unified)) +
",\"module\":" + boundModuleJSON(
JSON.encode(target.kind),
target.sourceRootSubpath,
target.asString,
target.pin,
) + "}"
ctr
.withFile("/schema.json", target.clientSchemaIntrospectionJSON)
.withNewFile("/meta.json", metaJSON)
.withExec([
"dagger-go-sdk-codegen", "client",
"--introspection-json-path", "/schema.json",
"--client-meta-path", "/meta.json",
"--output", "/out/" + outputPath,
"--module-root", "/out",
"--unified=" + JSON.encode(unified),
"--removed-list", "/removed-clients.txt",
])
}
"""
Render one templates/<name> starter for a new module.
The Go runtime needs package main. A module with a Dang entrypoint is an
importable package named after the module. A starter with the global dag
calls it as before; the Go runtime always generates it. Without it,
embedded bindings declare their own core types, so only unified bindings
use core constructors.
"""
let renderedTemplate(ws: Workspace!, name: String!, selected: String!, entrypoint: Boolean!, unified: Boolean!): Directory! {
if (currentModule.source.exists("templates/" + selected) == false) {
raise "unknown init template: " + selected
} else {
let builder = container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("helpers/render-template"))
.withDirectory("/template", currentModule.source.directory("templates/" + selected))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/render-template", "."])
let rendered = if (entrypoint) {
# A fresh entrypoint owns its Go root even when the project above
# has go.mod. The starter and dispatcher must agree on this path.
if (!exists(ws, "go.mod")) {
builder.withExec(["render-template", "--importable-package", "--unified-clients=" + JSON.encode(unified), "--global-client=" + JSON.encode(globalClient ?? false), "--standalone-go-module", name, "/template", "/rendered"])
} else {
builder
.withFile("/go.mod", ws.file("go.mod"))
.withExec([
"render-template", "--importable-package",
"--unified-clients=" + JSON.encode(unified),
"--global-client=" + JSON.encode(globalClient ?? false),
"--go-mod", "/go.mod",
"--module-subpath", ".",
name, "/template", "/rendered",
])
}
} else {
builder.withExec(["render-template", "--global-client", name, "/template", "/rendered"])
}
rendered.directory("/rendered")
}
}
}