From 9901838a7fb24d572711c3e81b09f8849398d55c Mon Sep 17 00:00:00 2001 From: Pierre Chalamet Date: Mon, 16 Feb 2026 22:27:58 +0100 Subject: [PATCH 1/2] feat(scripting): add Env initialization for FScript 0.40.0 --- CHANGELOG.md | 2 + src/Terrabuild.Scripting.Tests/Scripting.fs | 15 +++++++ .../TestFiles/Env.fss | 21 +++++++++ src/Terrabuild.Scripting/Scripting.fs | 43 ++++++++++++++++++- .../Terrabuild.Scripting.fsproj | 4 +- 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/Terrabuild.Scripting.Tests/TestFiles/Env.fss diff --git a/CHANGELOG.md b/CHANGELOG.md index beed6296..92c80a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to Terrabuild are documented in this file. ## [Unreleased] +- Upgrade Terrabuild FScript runtime to 0.40.0 and initialize `Env` (`ScriptName`, `Arguments`) when loading `.fss` extension scripts. + ## [0.189.4-next] - Create annotated release tags in `release-prepare` so `git push --follow-tags` pushes releases diff --git a/src/Terrabuild.Scripting.Tests/Scripting.fs b/src/Terrabuild.Scripting.Tests/Scripting.fs index a010b289..d594d1f5 100644 --- a/src/Terrabuild.Scripting.Tests/Scripting.fs +++ b/src/Terrabuild.Scripting.Tests/Scripting.fs @@ -122,6 +122,21 @@ let invokeFScriptMethodWithStructuredArgumentsDefaults() = let res = invocable.Value.Invoke args res |> should equal "build|0|" +[] +let invokeFScriptMethodHasEnvInitialized() = + let root = NUnit.Framework.TestContext.CurrentContext.TestDirectory + let script = Terrabuild.Scripting.loadScript root [] "TestFiles/Env.fss" + let invocable = script.GetMethod("run") + let context = { ActionContext.Debug = false + ActionContext.CI = false + ActionContext.Command = "run" + ActionContext.Hash = "abc" + ActionContext.Directory = "TestFiles" + ActionContext.Batch = None } + let args = Value.Map (Map [ "context", Value.Object context ]) + let res = invocable.Value.Invoke args + res |> should equal "Env.fss|0" + [] let invokeFScriptMethodMissingContextFails() = let root = NUnit.Framework.TestContext.CurrentContext.TestDirectory diff --git a/src/Terrabuild.Scripting.Tests/TestFiles/Env.fss b/src/Terrabuild.Scripting.Tests/TestFiles/Env.fss new file mode 100644 index 00000000..84b43ba1 --- /dev/null +++ b/src/Terrabuild.Scripting.Tests/TestFiles/Env.fss @@ -0,0 +1,21 @@ +[] +let run (context: {| Command: string |}) = + let scriptName = + match Env.ScriptName with + | Some name -> name + | None -> "" + + $"{scriptName}|{Env.Arguments |> List.length}" + +type ExportFlag = + | Dispatch + | Default + | Batchable + | Never + | Local + | External + | Remote + +{ + [nameof run] = [] +} diff --git a/src/Terrabuild.Scripting/Scripting.fs b/src/Terrabuild.Scripting/Scripting.fs index f5f4eba9..f9b55c8f 100644 --- a/src/Terrabuild.Scripting/Scripting.fs +++ b/src/Terrabuild.Scripting/Scripting.fs @@ -549,11 +549,50 @@ let private toFScriptScript (loaded: FScript.Runtime.ScriptHost.LoadedScript) = let dispatchMethod, defaultMethod = Descriptor.ResolveDispatchAndDefault descriptor Script(FScript(loaded, descriptor, dispatchMethod, defaultMethod)) +let private toFScriptStringLiteral (value: string) = + let escaped = + value + .Replace("\\", "\\\\", StringComparison.Ordinal) + .Replace("\"", "\\\"", StringComparison.Ordinal) + $"\"{escaped}\"" + +let private prependEnvironmentBinding (scriptName: string option) (arguments: string list) (source: string) = + let scriptNameLiteral = + match scriptName with + | Some value -> $"Some {toFScriptStringLiteral value}" + | None -> "None" + + let argumentsLiteral = + match arguments with + | [] -> "[]" + | values -> + values + |> List.map toFScriptStringLiteral + |> String.concat "; " + |> sprintf "[%s]" + + let prelude = + String.concat + "\n" + [ "let asEnvironment (value: Environment) = value" + $"let Env = asEnvironment {{ ScriptName = {scriptNameLiteral}; Arguments = {argumentsLiteral} }}" + "" ] + + prelude + source + let private loadFScript (rootDirectory: string) (scriptFile: string) = let fullPath = Path.GetFullPath(scriptFile) let externs = FScript.Runtime.Registry.all { FScript.Runtime.HostContext.RootDirectory = rootDirectory } - let loaded = FScript.Runtime.ScriptHost.loadFile externs fullPath + let scriptName = Path.GetFileName(fullPath) |> Option.ofObj + let entrySource = File.ReadAllText(fullPath) |> prependEnvironmentBinding scriptName [] + let loaded = + FScript.Runtime.ScriptHost.loadSourceWithIncludes + externs + rootDirectory + fullPath + entrySource + (fun resolvedPath -> File.ReadAllText(resolvedPath) |> Some) toFScriptScript loaded let private loadFScriptFromSourceWithIncludes @@ -563,6 +602,8 @@ let private loadFScriptFromSourceWithIncludes (entrySource: string) (resolveImportedSource: string -> string option) = let externs = FScript.Runtime.Registry.all { FScript.Runtime.HostContext.RootDirectory = hostRootDirectory } + let scriptName = Path.GetFileName(entryFile) |> Option.ofObj + let entrySource = entrySource |> prependEnvironmentBinding scriptName [] let loaded = FScript.Runtime.ScriptHost.loadSourceWithIncludes externs diff --git a/src/Terrabuild.Scripting/Terrabuild.Scripting.fsproj b/src/Terrabuild.Scripting/Terrabuild.Scripting.fsproj index b20e9628..46d94c85 100644 --- a/src/Terrabuild.Scripting/Terrabuild.Scripting.fsproj +++ b/src/Terrabuild.Scripting/Terrabuild.Scripting.fsproj @@ -11,8 +11,8 @@ - - + + From a5150bf1c3dc8fccc13f923dcee437bc167e3d5e Mon Sep 17 00:00:00 2001 From: Pierre Chalamet Date: Mon, 16 Feb 2026 22:31:32 +0100 Subject: [PATCH 2/2] fix(scripting): keep imports first when injecting Env --- CHANGELOG.md | 1 + src/Terrabuild.Scripting/Scripting.fs | 39 ++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92c80a23..f4fd2052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Terrabuild are documented in this file. ## [Unreleased] - Upgrade Terrabuild FScript runtime to 0.40.0 and initialize `Env` (`ScriptName`, `Arguments`) when loading `.fss` extension scripts. +- Fix FScript `Env` prelude injection to preserve leading `import` directives in embedded scripts. ## [0.189.4-next] diff --git a/src/Terrabuild.Scripting/Scripting.fs b/src/Terrabuild.Scripting/Scripting.fs index f9b55c8f..74aeb43c 100644 --- a/src/Terrabuild.Scripting/Scripting.fs +++ b/src/Terrabuild.Scripting/Scripting.fs @@ -578,7 +578,44 @@ let private prependEnvironmentBinding (scriptName: string option) (arguments: st $"let Env = asEnvironment {{ ScriptName = {scriptNameLiteral}; Arguments = {argumentsLiteral} }}" "" ] - prelude + source + let newline = + if source.Contains("\r\n", StringComparison.Ordinal) then "\r\n" + else "\n" + + let lines = + source.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n') + + let isCommentOrBlank (line: string) = + let trimmed = line.Trim() + String.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("//", StringComparison.Ordinal) + + let isImport (line: string) = + line.TrimStart().StartsWith("import ", StringComparison.Ordinal) + + let mutable index = 0 + let mutable seenImport = false + let mutable keepScanning = true + + while index < lines.Length && keepScanning do + let line = lines[index] + if isImport line then + seenImport <- true + index <- index + 1 + elif isCommentOrBlank line then + index <- index + 1 + else + keepScanning <- false + + let insertionIndex = if seenImport then index else 0 + let before = lines |> Array.take insertionIndex |> String.concat newline + let after = lines |> Array.skip insertionIndex |> String.concat newline + + if insertionIndex = 0 then + prelude + source + elif String.IsNullOrEmpty(after) then + before + newline + prelude + else + before + newline + prelude + after let private loadFScript (rootDirectory: string) (scriptFile: string) =