diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang
index 2ddc72e..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
}
@@ -323,12 +332,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
}
@@ -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.
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.
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
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(":")