Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions .dagger/modules/e2e/main.dang
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion dagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"source": "dang"
},
"include": [
"!runtime",
"!.dagger",
"!future",
"!docs"
Expand Down
55 changes: 54 additions & 1 deletion mod.dang
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("#<externals>")
let closing = (opening[1] ?? "").split("#</externals>")
if (opening.length != 2 or closing.length != 2) {
raise runtimeSourcePath + " must fence its module-source reads with exactly one #<externals> 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 #<externals> 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.
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions python-sdk.dang
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 16 additions & 10 deletions runtime/main.dang
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,24 @@ type PythonSdkRuntime {
let uvCacheVolume: String! = "modpython-uv"
let pipCacheVolume: String! = "modpython-pip"

#<externals>
# 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] ?? ""
}
}
#</externals>

"""
Container for executing the Python module runtime. introspectionJson is
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(":")
Expand Down