Repository navigation
Expand file tree
/
Copy pathmain.dang.tmpl
More file actions
533 lines (504 loc) · 20.2 KB
/
Copy pathmain.dang.tmpl
File metadata and controls
533 lines (504 loc) · 20.2 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
"""
Manage Dagger modules that use the Java SDK, and the clients of plain Maven
projects.
A module vendors the Java SDK as source and commits every file generation
writes, so the engine generates nothing when it loads the module. A new module
runs on a Dang entrypoint generated into it. An existing module keeps this
repository's build/package-only runtime (`github.com/dagger/java-sdk/runtime`)
until dangEntrypoint = true moves it.
"""
type JavaSdk {
"""
Starter under templates/<template> for a module generated without source: <placeholder-list-templates>
"""
template: String! = "default"
"""
Commit the compiled Dagger Java SDK as a jar into each generated module so its
runtime build compiles only the module's own code against the jar instead of
recompiling the vendored SDK sources. The sources stay checked in for IDE use.
Opt-in (set `settings.vendorSdkJar = true` under `[modules.java-sdk]` in the
workspace dagger.toml) because it adds a committed binary artifact.
"""
vendorSdkJar: Boolean! = false
"""
Record the commit each git client resolved to as a pin in the module's
manifest, so a generated module resolves the same dependencies later. Off by
default because a pinned client stops following the branch or tag it was added
at until the pin is refreshed. It applies to a module on the runtime: a client
of a module on the Dang entrypoint always loads the commit it was generated
against.
"""
lock: Boolean! = false
"""
Generate a Dang entrypoint, so the engine reads the module's types without
building it and calls its functions through the dispatch the annotation
processor generates. Unset gives new modules the entrypoint and keeps the
runtime of existing ones; true migrates an existing module; false keeps the
module on github.com/dagger/java-sdk/runtime. A module with the entrypoint
gets one package per client under clients/<name>, and its dagger-module.toml
holds its name and the entrypoint only.
"""
dangEntrypoint: Boolean
"""
Runtime source written into the dagger-module.toml of a new module on the
runtime.
"""
let targetRuntime: String! = "github.com/dagger/java-sdk/runtime"
"""
Where the Dang entrypoint is generated, relative to the module.
"""
let entrypointPath: String! = "src/generated/dang/entrypoint"
"""
The first line of every file of the generated entrypoint, which tells it
apart from one the user wrote.
"""
let entrypointHeader: String! = "# This file has been generated by dagger-java-sdk. DO NOT EDIT.\n"
"""
The Java client root containing the workspace cwd: the directory of the
nearest pom.xml, relative to the workspace root. Every Java module has one at
its root, and the SDK vendored under a module carries none of its own, so the
nearest hit is always the owning module. Null when there is none — a project
built with anything but Maven is not a root this SDK can serve.
"""
findClientRoot(ws: Workspace!): String {
let found = ws.findUp("pom.xml")
if (found == null) {
null
} else {
normalizePath(found.trimSuffix("pom.xml"))
}
}
"""
Generate one SDK scope: the module at the workspace cwd, when the scope has
one. A scope with no config is scaffolded from the configured template first.
Every module then gets a dagger-module.toml from the manifest builder and is
generated; a pre-1.0 dagger.json is migrated into it and removed, so two
manifest files cannot disagree. On the runtime, the scope's module clients
become the module's dependencies, so the generated bindings include their
types. With the Dang entrypoint, each client is generated under
clients/<name> and loads its target when the module first calls it, and the
manifest names the entrypoint and nothing else. A scope without a module, a
plain Maven project, gets the same sdk/ and clients/ tree for its clients,
compiled by a profile of its pom.xml, and no module config.
"""
generateScope(
ws: Workspace!,
isModule: Boolean!,
name: String!,
clients: [ModuleSource!]!,
): Workspace! {
checkEngine
if (isModule == false) {
if (clients.length == 0) {
return ws
}
let scope = normalizePath(ws.cwd)
if (scopeHasFile(ws, scope, "pom.xml") == false) {
raise "the Java scope " + scope + " has no pom.xml: its clients are generated into a Maven project"
}
Mod(
rootPath: scope,
ws: ws,
isModule: false,
vendorSdkJar: false,
dangEntrypoint: null,
clients: clients,
).generated
} else {
let scope = normalizePath(ws.cwd)
# The rest works with the cwd at the workspace root: the engine resolves a
# module's local dependencies to workspace-root-relative paths and then
# reads them relative to Workspace.cwd (dagger/dagger ResolveDepToSource),
# so at the scope's cwd a dependency is looked up under the module itself.
# The scope cwd is restored on the way out because the engine rejects a
# generateScope result whose cwd is not the scope.
let rooted = ws.withWorkdir(".")
let hadManifest = hasModuleConfig(ws, scope)
let initialized = if (hadManifest) {
rooted
} else {
rooted.withDirectory(scopeRef(scope), moduleTemplate(name))
}
if (usesEntrypoint(initialized, scope, hadManifest)) {
entrypointModule(initialized, scope, name, clients)
} else {
if (hasGeneratedEntrypoint(initialized, scope)) {
raise "this module runs on its generated Dang entrypoint: set dangEntrypoint = true, " +
"or remove [entrypoint] from dagger-module.toml to move it to the runtime"
}
let configured = scopeManifest(initialized, scope, name, clients)
Mod(
rootPath: scope,
ws: configured,
isModule: true,
vendorSdkJar: vendorSdkJar,
dangEntrypoint: null,
clients: [],
).generated.withWorkdir(scope)
}
}
}
"""
Whether the module gets the Dang entrypoint. The setting decides when it is
set; otherwise a new module gets it, and an existing one keeps what it has.
"""
let usesEntrypoint(ws: Workspace!, scope: String!, hadManifest: Boolean!): Boolean! {
if (dangEntrypoint != null) {
dangEntrypoint ?? false
} else if (hadManifest) {
hasGeneratedEntrypoint(ws, scope)
} else {
true
}
}
"""
Generate a module on the Dang entrypoint. The module's schema is read through
a staged manifest on the engine's builtin dang runtime, because the engine
loads a module with an entrypoint through that entrypoint, which may not be
generated yet, and the builtin runtime resolves nothing remote. The staged
manifest has no dependencies, so the schema is core alone, and each client
is generated from its own schema. The manifest builder then writes the
manifest that names the entrypoint.
"""
let entrypointModule(
ws: Workspace!,
scope: String!,
name: String!,
clients: [ModuleSource!]!,
): Workspace! {
refuseEntrypointSettings(ws, scope, "dagger-module.toml", "runtime", clients)
refuseEntrypointSettings(ws, scope, "dagger.json", "sdk", clients)
let staged = sdkHelpers.moduleManifest
.withName(name: name)
.withLegacyDangRuntime
.generate(ws.withWorkdir(scope), legacyJson: false)
.withWorkdir(".")
let generated = Mod(
rootPath: scope,
ws: staged,
isModule: true,
vendorSdkJar: vendorSdkJar,
dangEntrypoint: entrypointPath,
clients: clients,
).generated
sdkHelpers.moduleManifest
.withName(name: name)
.withDangEntrypoint(source: "./" + entrypointPath)
.generate(generated.withWorkdir(scope), legacyJson: false)
}
"""
Refuse a setting of a manifest that the entrypoint manifest has no place for,
before anything is written: the manifest builder writes the name and the
entrypoint only. A runtime is what the entrypoint replaces, so this SDK's own
and the engine's builtin java runtime are accepted. A dependency is dropped
when a client of the scope covers it, and refused otherwise: the module
reaches other modules only through its clients, so dropping it would lose it.
"""
let refuseEntrypointSettings(
ws: Workspace!,
scope: String!,
fileName: String!,
runtimeKey: String!,
clients: [ModuleSource!]!,
): Void {
if (scopeHasFile(ws, scope, fileName) == false) {
return null
}
let contents = ws.file(scopeRef(scope, fileName)).contents
let manifest = if (fileName == "dagger.json") {
json.withContents((contents :: Dagger.JSON!))
} else {
json.withContents((JSON.encode(TOML.decode(contents)) :: Dagger.JSON!))
}
let kept = ["$schema", "name", "engineVersion", "dependencies"]
let refused = manifest.fields.filter { field =>
if (kept.contains(field)) {
false
} else if (field == runtimeKey) {
let runtime = manifest.field([runtimeKey])
let source = (runtime.asString rescue runtime.field(["source"]).asString) rescue ""
source != "java" and source.split("@")[0] != targetRuntime
} else if (field == "entrypoint") {
hasGeneratedEntrypoint(ws, scope) == false
} else {
true
}
}
if (refused.length > 0) {
raise fileName + " has settings a Dang entrypoint cannot carry (" + refused.join(", ") +
"): remove them, or set dangEntrypoint = false"
}
let uncovered = uncoveredDependencies(manifest, clients)
if (uncovered.length > 0) {
raise fileName + " depends on " + uncovered.join(", ") + ", which the scope has no client for: " +
"a module on the Dang entrypoint reaches other modules through its clients. " +
"Add a client for each with dagger module client add, remove it from " + fileName +
", or set dangEntrypoint = false"
}
null
}
"""
The dependencies a manifest records that no client of the scope covers, each
by its name, or by its source when it has none. A client covers the
dependency named after its module, and a nameless one on its git ref.
"""
let uncoveredDependencies(manifest: JSONValue!, clients: [ModuleSource!]!): [String!]! {
if (manifest.fields.contains("dependencies") == false) {
return [] :: [String!]!
}
let names = clients.map { client => client.moduleName } + clients.map { client => client.moduleOriginalName }
let refs = clients.map { client => client.asString }
let entries = manifest.field(["dependencies"]).contents :: String!
# dagger.json also accepts a dependency as a bare source string.
let bare = ((JSON.decode(entries) :: [String!]!).length >= 0) rescue { err: Error => false }
if (bare) {
return (JSON.decode(entries) :: [String!]!).filter { source => refs.contains(source) == false }
}
(JSON.decode(entries) :: [{{name: String, source: String}}!]!)
.map { dependency =>
let name = dependency.name ?? ""
let source = dependency.source ?? ""
if (name != "") {
if (names.contains(name)) { "" } else { name }
} else if (refs.contains(source)) {
""
} else {
source
}
}
.filter { label => label != "" }
}
"""
Whether the module's manifest names the entrypoint this SDK generates, at the
path it generates it to. A main.dang there without the generated header is the
user's own; a missing one is regenerated.
"""
let hasGeneratedEntrypoint(ws: Workspace!, scope: String!): Boolean! {
if (scopeHasFile(ws, scope, "dagger-module.toml") == false) {
return false
}
let manifest = json.withContents(
(JSON.encode(TOML.decode(ws.file(scopeRef(scope, "dagger-module.toml")).contents)) :: Dagger.JSON!),
)
if (manifest.fields.contains("entrypoint") == false) {
return false
}
let table = manifest.field(["entrypoint"])
let kind = (table.field(["kind"]).asString) rescue ""
let source = (table.field(["source"]).asString) rescue ""
let mainPath = entrypointPath + "/main.dang"
kind == "dang" and normalizePath(source) == entrypointPath and (
scopeHasFile(ws, scope, mainPath) == false or
ws.file(scopeRef(scope, mainPath)).contents.hasPrefix(entrypointHeader)
)
}
"""
Write the module's dagger-module.toml from the manifest it already has and the
complete client set, and drop a pre-1.0 dagger.json once its contents have
moved across. Fields this SDK does not own are carried over by loading the
existing manifest rather than building one from nothing.
"""
let scopeManifest(
ws: Workspace!,
scope: String!,
name: String!,
clients: [ModuleSource!]!,
): Workspace! {
let base = if (scopeHasFile(ws, scope, "dagger-module.toml")) {
sdkHelpers.moduleManifest(loadToml: ws.file(scopeRef(scope, "dagger-module.toml")))
} else if (scopeHasFile(ws, scope, "dagger.json")) {
sdkHelpers.moduleManifest(loadJson: ws.file(scopeRef(scope, "dagger.json")))
} else {
sdkHelpers.moduleManifest(loadToml: seedManifestFile(name))
}
# Cleared structurally rather than by name: withoutLegacyRuntimeDependency
# matches an unnamed dependency on its source, not on the module name it
# resolves to, and resolving the recorded dependencies to read their names
# would fail generation on one stale entry instead of dropping it.
let configured = clients
.reduce(base.withName(name: name).withoutLegacyRuntimeDependencies) { manifest, client =>
manifest.withLegacyRuntimeDependency(module: client)
}
# The builder records a local client relative to ws.cwd, so it runs from the
# scope while the caller keeps working from the workspace root. legacyJson is
# off because a module of this SDK is configured by its dagger-module.toml
# alone — the builder deletes the dagger.json one was migrated from.
configured.generate(ws.withWorkdir(scope), lock: lock, legacyJson: false).withWorkdir(".")
}
"""
The dagger-module.toml a new module on the runtime starts from.
The builder has one runtime setter per builtin runtime, and this SDK targets
its own repository's runtime, so the runtime is named by loading a seed file:
the builder accepts a non-builtin runtime on a manifest loaded from a config
file and rejects it on one built from nothing. The caller loads this, so what
finally lands is the builder's own rendering, not these bytes.
"""
let seedManifestFile(name: String!): File! {
let seed = "name = \"" + name + "\"\n"
+ "engineVersion = \"" + engineVersion + "\"\n"
+ "\n"
+ "[runtime]\n"
+ " source = \"" + targetRuntime + "\"\n"
directory.withNewFile("dagger-module.toml", seed).file("dagger-module.toml")
}
"""
The live engine version, without the build metadata that changes on every
engine build.
"""
let engineVersion: String! { version.split("+")[0] ?? version }
"""
The first engine release this SDK runs on.
"""
let engineFloor: String! = "v1.0.0-beta.18"
"""
Why this SDK refuses an engine of the given version, or the empty string when
it runs on it. Only a release older than the floor is refused: a version this
cannot read, a development build among them, passes, so an unexpected format
never blocks a user. The engine's own gate cannot do this: it treats every
prerelease of v1.0.0 as v1.0.0.
"""
engineRefusal(engine: String!): String! {
if (releasePrecedes(engine, engineFloor)) {
"this Java SDK needs Dagger engine " + engineFloor + " or later, and this engine is " +
engine + ". Upgrade the engine."
} else {
""
}
}
"""
Refuse an engine older than the floor before generation reads or writes
anything, so the user reads why instead of an unknown field later.
"""
let checkEngine: Void {
let refusal = engineRefusal(version)
if (refusal != "") {
raise refusal
}
null
}
"""
Whether release `a` comes before release `b` in semver precedence. False when
either is not a plain `vX.Y.Z[-pre][+build]` or is a development build.
"""
let releasePrecedes(a: String!, b: String!): Boolean! {
let pattern = Regexp("^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z.]+))?(?:\\+[0-9A-Za-z.-]+)?$")
let ma = a.match(pattern)
let mb = b.match(pattern)
if (ma == null) {
false
} else if (mb == null) {
false
} else if (a.contains("dev") or b.contains("dev")) {
false
} else {
let ca = ma.captures
let cb = mb.captures
let coreA = [ca[0] ?? "", ca[1] ?? "", ca[2] ?? ""]
let coreB = [cb[0] ?? "", cb[1] ?? "", cb[2] ?? ""]
let preA = ca[3] ?? ""
let preB = cb[3] ?? ""
if (identifiersPrecede(coreA, coreB, 0)) {
true
} else if (identifiersPrecede(coreB, coreA, 0)) {
false
} else if (preA == "") {
false
} else if (preB == "") {
true
} else {
identifiersPrecede(preA.split("."), preB.split("."), 0)
}
}
}
"""
Semver precedence of dot-separated identifiers from index `i`: numbers by
value and before words, words by ASCII, and a shorter list first when one is a
prefix of the other.
"""
let identifiersPrecede(a: [String!]!, b: [String!]!, i: Int!): Boolean! {
if (i >= a.length) {
i < b.length
} else if (i >= b.length) {
false
} else {
let x = a[i] ?? ""
let y = b[i] ?? ""
let xNumber = x.containsMatch("^[0-9]+$")
let yNumber = y.containsMatch("^[0-9]+$")
if (x == y) {
identifiersPrecede(a, b, i + 1)
} else if (xNumber and yNumber) {
# Space sorts before every digit, so padding to one width orders by value.
x.padLeft(20) < y.padLeft(20)
} else {
xNumber or (yNumber == false and x < y)
}
}
}
"""
The files of a new module: the configured template rendered for the module
name. An empty template name selects the default.
"""
let moduleTemplate(name: String!): Directory! {
let selected = if (template == "") { "default" } else { template }
if (currentModule.source.exists("templates/" + selected) == false) {
raise "unknown template: " + template
} else {
renderedTemplate(name, selected)
}
}
"""
Render a Java template, substituting the requested module name.
"""
let renderedTemplate(name: String!, template: String!): Directory! {
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-java-template"),
)
# target/ is gitignored, so it is absent from a clean checkout but present
# in a developer's — and the renderer substitutes the module name into
# every file it walks, including .class bytes, producing a corrupt class
# that then breaks the scaffolded module's build.
.withDirectory(
"/template",
currentModule.source.directory("templates/" + template),
exclude: ["**/target/**"],
)
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/render-java-template", "."])
.withExec(["render-java-template", name, "/template", "/rendered"])
.directory("/rendered")
}
"""
Whether a module config, dagger-module.toml or the pre-1.0 dagger.json, exists
at a workspace-root-relative scope.
"""
let hasModuleConfig(ws: Workspace!, scope: String!): Boolean! {
scopeHasFile(ws, scope, "dagger-module.toml") or scopeHasFile(ws, scope, "dagger.json")
}
let scopeHasFile(ws: Workspace!, scope: String!, filename: String!): Boolean! {
let path = if (scope == ".") { filename } else { scope + "/" + filename }
ws.directory("/", include: [path]).exists(path)
}
"""
A scope path, or a file under it, as a workspace-root-absolute path — the form
Workspace resolves from the workspace root rather than from the client's cwd.
"""
let scopeRef(scope: String!, sub: String! = ""): String! {
let root = if (scope == ".") { "" } else { "/" + scope }
if (sub == "") {
if (root == "") { "/" } else { root }
} else {
root + "/" + sub
}
}
let normalizePath(path: String!): String! {
let normalized = path.trimPrefix("./").trimPrefix("/").trimSuffix("/")
if (normalized == "") { "." } else { normalized }
}
}