Repository: PumasAI/QuartoNotebookRunner.jl (julia)

Issue:
Copying and overriding ENV variables
Currently, the server process copies ENV variables from quarto, but the worker processes from Malt do not copy ENV variables from the server. So one currently has no option to set them, global env variables will not have an effect on the workers. So one cannot influence JULIA_LOAD_PATH, JULIA_DEPOT_PATH, etc. for the workers.

Pull request description:
Also add ENV vars `QUARTONOTEBOOKRUNNER_EXEFLAGS` and `QUARTONOTEBOOKRUNNER_ENV` for providing defaults for these values when not set. These are considered internals and are subject to change at any time.

Fixes #41 (that enough for what you needed there @jkrumbiegel?)

Patch:
diff --git a/src/server.jl b/src/server.jl
index f3bec49d..76100886 100644
--- a/src/server.jl
+++ b/src/server.jl
@@ -4,18 +4,21 @@ mutable struct File
     worker::Malt.Worker
     path::String
     exeflags::Vector{String}
+    env::Vector{String}
 
-    function File(path::String)
+    function File(path::String, options::Union{String,Dict{String,Any}})
         if isfile(path)
             _, ext = splitext(path)
             if ext in (".jl", ".qmd")
                 path = isabspath(path) ? path : abspath(path)
 
-                _, frontmatter = raw_text_chunks(path)
-                exeflags = frontmatter["julia"]["exeflags"]
+                options = _parsed_options(options)
+                _, file_frontmatter = raw_text_chunks(path)
+                merged_options = _extract_relevant_options(file_frontmatter, options)
+                exeflags, env = _exeflags_and_env(merged_options)
 
-                worker = cd(() -> Malt.Worker(; exeflags), dirname(path))
-                file = new(worker, path, exeflags)
+                worker = cd(() -> Malt.Worker(; exeflags, env), dirname(path))
+                file = new(worker, path, exeflags, env)
                 init!(file)
                 return file
             else
@@ -31,6 +34,12 @@ mutable struct File
     end
 end
 
+function _exeflags_and_env(options)
+    exeflags = options["format"]["metadata"]["julia"]["exeflags"]
+    env = String.(options["format"]["metadata"]["julia"]["env"])
+    return exeflags, env
+end
+
 struct Server
     workers::Dict{String,File}
 
@@ -48,10 +57,10 @@ function init!(file::File)
 end
 
 function refresh!(file::File, options::Dict)
-    exeflags = options["format"]["metadata"]["julia"]["exeflags"]
-    if exeflags != file.exeflags
+    exeflags, env = _exeflags_and_env(options)
+    if exeflags != file.exeflags || env != file.env
         Malt.stop(file.worker)
-        file.worker = cd(() -> Malt.Worker(; exeflags), dirname(file.path))
+        file.worker = cd(() -> Malt.Worker(; exeflags, env), dirname(file.path))
         file.exeflags = exeflags
         init!(file)
     end
@@ -443,9 +452,11 @@ end
 
 function default_frontmatter()
     D = Dict{String,Any}
+    exeflags = JSON3.read(get(ENV, "QUARTONOTEBOOKRUNNER_EXEFLAGS", "[]"), Vector{String})
+    env = JSON3.read(get(ENV, "QUARTONOTEBOOKRUNNER_ENV", "[]"), Vector{String})
     return D(
         "fig-format" => "png",
-        "julia" => D("exeflags" => []),
+        "julia" => D("exeflags" => exeflags, "env" => env),
         "execute" => D("error" => true),
     )
 end
@@ -746,8 +757,8 @@ is_julia_toplevel(node) =
     node.t.info == "{julia}" &&
     node.parent.t isa CommonMark.Document
 
-function loadfile!(server::Server, path::String)
-    file = File(path)
+function loadfile!(server::Server, path::String, options::Union{String,Dict{String,Any}})
+    file = File(path, options)
     server.workers[path] = file
     return file
 end
@@ -761,7 +772,7 @@ function run!(
 )
     file = get!(server.workers, file) do
         @debug "file not loaded, loading first." file
-        loadfile!(server, file)
+        loadfile!(server, file, options)
     end
     return evaluate!(file, output; showprogress, options)
 end


Test patch:
diff --git a/test/examples/env_and_exeflags.qmd b/test/examples/env_and_exeflags.qmd
new file mode 100644
index 00000000..09b6ccb3
--- /dev/null
+++ b/test/examples/env_and_exeflags.qmd
@@ -0,0 +1,13 @@
+---
+title: env
+julia:
+    env: ["FOO=BAR"]
+---
+
+```{julia}
+ENV["FOO"]
+```
+
+```{julia}
+printstyled("red"; color = :red)
+```
diff --git a/test/testsets/env_and_exeflags.jl b/test/testsets/env_and_exeflags.jl
new file mode 100644
index 00000000..8a133fe6
--- /dev/null
+++ b/test/testsets/env_and_exeflags.jl
@@ -0,0 +1,25 @@
+include("../utilities/prelude.jl")
+
+test_example(joinpath(@__DIR__, "../examples/env_and_exeflags.qmd")) do json
+    cells = json["cells"]
+    @test length(cells) == 4
+
+    cell = cells[2]
+    @test cell["outputs"][1]["data"]["text/plain"] == "\"BAR\""
+
+    cell = cells[4]
+    @test cell["outputs"][1]["text"] == "red"
+end
+
+withenv("QUARTONOTEBOOKRUNNER_EXEFLAGS" => """["--color=yes"]""") do
+    test_example(joinpath(@__DIR__, "../examples/env_and_exeflags.qmd")) do json
+        cells = json["cells"]
+        @test length(cells) == 4
+
+        cell = cells[2]
+        @test cell["outputs"][1]["data"]["text/plain"] == "\"BAR\""
+
+        cell = cells[4]
+        @test cell["outputs"][1]["text"] == "\e[31mred\e[39m"
+    end
+end