From 2761afb1e485e4c505a0b031c9e0d87501517995 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 1 Sep 2026 10:15:31 +0200 Subject: [PATCH 1/4] runtime: confine module-source reads to a marked externals block The runtime reads its own module source in exactly three places: the two Dockerfile image pins and the runtime.py entrypoint script. Gather them, plus the imageFrom helper they use, into one marker-delimited block, and write the entrypoint from its contents instead of mounting the source file. The SDK's generator can then embed this runtime into a generated module by replacing the block with literals, leaving a fully self-contained file. Signed-off-by: Yves Brissaud --- runtime/main.dang | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/runtime/main.dang b/runtime/main.dang index 0096b0f..62613ad 100644 --- a/runtime/main.dang +++ b/runtime/main.dang @@ -16,9 +16,24 @@ type PythonSdkRuntime { let uvCacheVolume: String! = "modpython-uv" let pipCacheVolume: String! = "modpython-pip" + # + # Everything between these markers reads this runtime's own module source; + # Mod.generate replaces the whole block with literals when it embeds this + # file into a generated module, so the copy is fully self-contained. # Committed single-FROM Dockerfiles so Dependabot keeps the digests fresh. let defaultBaseImage: String! { imageFrom("images/base/Dockerfile") } let defaultUvImage: String! { imageFrom("images/uv/Dockerfile") } + let runtimeScriptContents: String! { currentModule.source.file("runtime.py").contents } + + 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] ?? "" + } + } + # """ Container for executing the Python module runtime. introspectionJson is @@ -46,7 +61,7 @@ type PythonSdkRuntime { let packageName = packageNameFor(modName, cfg) let built = base(baseImage, uvImage, cfg) - .withFile(runtimeExecutablePath, currentModule.source.file("runtime.py"), permissions: 493) + .withNewFile(runtimeExecutablePath, runtimeScriptContents, permissions: 493) .withEntrypoint([runtimeExecutablePath]) .withWorkdir(join(contextDirPath, subPath)) .withMountedDirectory(contextDirPath, withoutVenv(contextDir, source, subPath)) @@ -260,15 +275,6 @@ type PythonSdkRuntime { if (rel == "" or rel == ".") { base } else { base + "/" + rel } } - 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 imageTag(ref: String!): String! { let named = ref.split("@").takeFirst(1).join("") let parts = named.split(":") From 3abde18f183ba26cf884f47e0fba763ae14e2709 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 1 Sep 2026 10:19:19 +0200 Subject: [PATCH 2/4] python-sdk: emit a self-contained runtime.dang next to dagger-module.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generation now assembles the module runtime into a single file and adds it to every dagger-module.toml module's changeset: runtime/main.dang with its externals block replaced by literals — the image pins extracted from the runtime's Dockerfiles and the entrypoint script inlined as a string. Assembly fails loudly when the runtime source grows a module-source read outside the fenced block, so the emitted file stays self-contained by construction. The engine reads the file straight from the module root and evaluates it in-process, without resolving a runtime module. The authoring module's source needs runtime/ back in its include list; the exclusion predates the vendored library's move to sdk/, and the runtime directory is five small files today. Signed-off-by: Yves Brissaud --- dagger.json | 1 - mod.dang | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/dagger.json b/dagger.json index 6fac37c..899c771 100644 --- a/dagger.json +++ b/dagger.json @@ -5,7 +5,6 @@ "source": "dang" }, "include": [ - "!runtime", "!.dagger", "!future", "!docs" diff --git a/mod.dang b/mod.dang index 6617ed0..e427fd0 100644 --- a/mod.dang +++ b/mod.dang @@ -66,7 +66,7 @@ type Mod { let generated = if (isModern) { # Generated here: the runtime generates nothing, so the engine's # generated context would be empty. - vendoredDir(stagedWs) + vendoredDir(stagedWs).withNewFile(embeddedRuntimeFilename, embeddedRuntime) } else { # A pre-1.0 module is still generated by the engine's builtin Python SDK. stagedWs @@ -101,6 +101,57 @@ type Mod { ) } + """ + The module runtime as a single self-contained file: the live runtime source + with its marked externals block — the only part that reads the runtime's own + module source — replaced by literals. The engine reads this file from the + module root and evaluates it in-process, resolving no runtime module at all. + """ + let embeddedRuntime: String! { + let source = currentModule.source.file(runtimeSourcePath).contents + let opening = source.split("#") + let closing = (opening[1] ?? "").split("#") + if (opening.length != 2 or closing.length != 2) { + raise runtimeSourcePath + " must fence its module-source reads with exactly one # block" + } + let assembled = "# Code generated by dagger. DO NOT EDIT.\n" + + (opening[0] ?? "") + + embeddedExternals + + (closing[1] ?? "") + if (assembled.contains("currentModule")) { + raise runtimeSourcePath + " reads the module source outside the # block; the embedded runtime would not be self-contained" + } + assembled + } + + """ + Literal replacements for the runtime's externals block: the image pins from + the runtime's Dockerfiles and the inlined entrypoint script. + """ + let embeddedExternals: String! { + "# Pinned when the SDK generated this file.\n" + + " let defaultBaseImage: String! = " + dangQuoted(runtimeImageFrom("runtime/images/base/Dockerfile")) + "\n" + + " let defaultUvImage: String! = " + dangQuoted(runtimeImageFrom("runtime/images/uv/Dockerfile")) + "\n" + + " let runtimeScriptContents: String! = " + dangQuoted(currentModule.source.file("runtime/runtime.py").contents) + } + + """ + The same extraction as the runtime's own imageFrom, run against this + module's source at generation time. + """ + let runtimeImageFrom(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 dangQuoted(text: String!): String! { + "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\"" + } + """ Bindings generated from a module's schema, straight from the synced environment: `uv run --isolated` built a throwaway one per module. @@ -180,6 +231,8 @@ type Mod { let vendorDirName: String! = "sdk" let generatedBindingsPath: String! = "src/dagger/client/gen.py" let schemaPath: String! = "/schema.json" + let embeddedRuntimeFilename: String! = "runtime.dang" + let runtimeSourcePath: String! = "runtime/main.dang" # 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. From 3dc5b7a06db4a4266a42e9a3d016508da4753872 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 1 Sep 2026 10:19:53 +0200 Subject: [PATCH 3/4] python-sdk: target the embedded runtime for new modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dagger module init now records embed:runtime.dang as the module's runtime source. The engine reads the named file from the directory holding dagger-module.toml — where generate emits it — and evaluates it in-process with its native Dang interpreter, the fastest runtime-loading path there is: no runtime module is resolved, fetched or loaded, and the unpinned git-ref exposure that pointing at this repository's runtime module would have carried never materializes. Engines that predate the embed runtime source reject it, so this flip must not ship before an engine release understands the form. Signed-off-by: Yves Brissaud --- .dagger/modules/e2e/main.dang | 8 ++++---- python-sdk.dang | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 2ddc72e..0b9eb1e 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -323,12 +323,12 @@ type E2e { } """ - New modules still target the engine's builtin runtime: they move to this - repository's own only once `runtime/` is on the default branch, where the - recorded ref resolves. + New modules embed this repository's runtime: generate writes runtime.dang + next to dagger-module.toml and the engine evaluates it in-process, so no + runtime module is resolved at load. """ pub targetRuntimeCheck: Void @check { - assert(pythonSdk.targetRuntime == "python", "targetRuntime should be the python builtin runtime") + assert(pythonSdk.targetRuntime == "embed:runtime.dang", "targetRuntime should embed the generated runtime.dang") null } diff --git a/python-sdk.dang b/python-sdk.dang index e34dcbb..eea6abc 100644 --- a/python-sdk.dang +++ b/python-sdk.dang @@ -8,9 +8,10 @@ type PythonSdk { pub skipGenerateFilename: String! = ".dagger-python-sdk-skip-generate" """ - Runtime source to write into modules created by this SDK. + Runtime source to write into modules created by this SDK: the engine reads + the named file from the module root and evaluates it in-process. """ - pub targetRuntime: String! { "python" } + pub targetRuntime: String! { "embed:runtime.dang" } """ Return every managed Python SDK module in the client's cwd scope: every module From c7bcd971409cbd9db54745704de35d4e266308d6 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 1 Sep 2026 10:21:21 +0200 Subject: [PATCH 4/4] e2e: cover the embedded runtime tomlGenerateCheck asserts what generation emits: runtime.dang lands next to dagger-module.toml, declares moduleRuntime, carries the generated marker, the pinned images and the inlined entrypoint script, and reads no module source. embedRuntimeCallCheck rebases the runtime fixture onto the embed form with a freshly generated runtime.dang and drives a call through a released CLI; it stays red until a released engine understands the embed runtime source, and its docstring says so. Signed-off-by: Yves Brissaud --- .dagger/modules/e2e/main.dang | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 0b9eb1e..5bce734 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -291,6 +291,15 @@ type E2e { "generate", ) + let runtimeDangPath = tomlGenerateModulePath + "/runtime.dang" + assertAdded(changes, runtimeDangPath) + let runtimeDang = changes.layer.file(runtimeDangPath).contents + assertContains(runtimeDang, generatedMarkerContents, "the embedded runtime is missing the generated-file marker") + assertContains(runtimeDang, "pub moduleRuntime(", "the embedded runtime does not declare moduleRuntime") + assertContains(runtimeDang, "@sha256:", "the embedded runtime lost its pinned images") + assertContains(runtimeDang, "dagger.mod.cli", "the embedded runtime lost its inlined entrypoint script") + assertNotContains(runtimeDang, "currentModule", "the embedded runtime must not read a module source") + null } @@ -366,6 +375,36 @@ type E2e { null } + """ + A module whose runtime source is embed:runtime.dang builds and runs: the + runtime fixture is rebased onto the embed form with a freshly generated + runtime.dang, then driven through a released CLI. Known red until a released + engine understands the embed runtime source. + """ + pub embedRuntimeCallCheck(ws: Workspace!): Void @check { + let embedded = pythonSdk + .mod(ws, path: tomlGenerateModulePath) + .generate + .layer + .file(tomlGenerateModulePath + "/runtime.dang") + + let tree = ws + .directory("/") + .withNewFile( + runtimeModulePath + "/dagger-module.toml", + "name = \"runtime-app\"\nengineVersion = \"v1.0.0-0\"\n\n[runtime]\nsource = \"embed:runtime.dang\"\n", + ) + .withFile(runtimeModulePath + "/runtime.dang", embedded) + + let run = sdkSdk + .target(tree, ".") + .runInstalled(["call", "-m", runtimeFixturePath, "greeting"]) + run.assertSuccess + assertContains(run.stdout, runtimeGreeting, "the module did not run on the embedded runtime") + + null + } + """ The fixture as seen from sdk-sdk's scratch workspace, where this repository is vendored.