Repository: swiftlang/swift-build
Pull request #255: Restore environment variables exposed to SwiftPM build tool invocations
Author: login: owenv
Created: 2025-03-05T22:11:56Z | Merged: 2025-03-06T03:44:20Z

rdar://145763371




Diff:
diff --git a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
index 8f53edccf..2d52b24ec 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
@@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
+                    environmentAssignments = environmentAssignments.filter { $0.key.lowercased() != "path" }
+                }
+                for (key, value) in customTask.environment {
+                    environmentAssignments[context.settings.globalScope.evaluate(key)] = context.settings.globalScope.evaluate(value)
+                }
+                let environment = EnvironmentBindings(environmentAssignments)
                 let workingDirectory = customTask.workingDirectory.map { Path(context.settings.globalScope.evaluate($0)).normalize() } ?? context.defaultWorkingDirectory
                 let inputPaths = customTask.inputFilePaths.map { Path(context.settings.globalScope.evaluate($0)).normalize() }
                 let inputs = inputPaths.map { delegate.createNode($0) }
diff --git a/Tests/SWBTaskConstructionTests/CustomTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/CustomTaskConstructionTests.swift
index 126c7c213..c8cd71e96 100644
--- a/Tests/SWBTaskConstructionTests/CustomTaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/CustomTaskConstructionTests.swift
@@ -179,4 +179,53 @@ fileprivate struct CustomTaskConstructionTests: CoreBasedTests {
             }
         }
     }
+
+    @Test(.requireSDKs(.host))
+    func customTaskInjectsShellScriptEnvironment() async throws {
+        let testProject = TestProject(
+            "aProject",
+            groupTree: TestGroup(
+                "SomeFiles", path: "Sources",
+                children: [
+                    TestFile("input.txt"),
+                    TestFile("main.c"),
+                ]),
+            buildConfigurations: [
+                TestBuildConfiguration(
+                    "Debug",
+                    buildSettings: [
+                        "GENERATE_INFOPLIST_FILE": "YES",
+                        "PRODUCT_NAME": "$(TARGET_NAME)",
+                        "SDKROOT": "auto",
+                        "MY_SETTING": "FOO",
+                    ]),
+            ],
+            targets: [
+                TestStandardTarget(
+                    "CoreFoo", type: .framework,
+                    buildPhases: [
+                        TestSourcesBuildPhase(["main.c"])
+                    ],
+                    customTasks: [
+                        TestCustomTask(
+                            commandLine: ["tool", "-foo", "-bar"],
+                            environment: ["ENVVAR": "VALUE"],
+                            workingDirectory: Path.root.join("working/directory").str,
+                            executionDescription: "My Custom Task",
+                            inputs: ["$(SRCROOT)/Sources/input.txt"],
+                            outputs: [Path.root.join("output").str],
+                            enableSandboxing: false,
+                            preparesForIndexing: false)
+                    ]
+                ),
+            ])
+        let tester = try await TaskConstructionTester(getCore(), testProject)
+        await tester.checkBuild(runDestination: .host) { results in
+            results.checkNoDiagnostics()
+
+            results.checkTask(.matchRule(["CustomTask", "My Custom Task"])) { task in
+                task.checkEnvironment(["ENVVAR": "VALUE", "MY_SETTING": "FOO"])
+            }
+        }
+    }
 }


Reviews:
user: login: jakepetroules
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T22:50:00Z
user: login: neonichu
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T22:53:58Z
user: login: jakepetroules
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T00:31:15Z
user: login: owenv
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T00:33:07Z
user: login: jakepetroules
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T00:36:34Z
user: login: jakepetroules
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T00:38:14Z
user: login: owenv
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T00:38:24Z
user: login: jakepetroules
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T00:40:58Z

Review comments:
diff_hunk: @@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
path: Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
user: login: jakepetroules
body: what's the goal here?
created_at: 2025-03-06T00:31:14Z
author_association: COLLABORATOR
diff_hunk: @@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
path: Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
user: login: owenv
body: Exporting PATH via build setting ends up causing issues on Windows with running executables.
created_at: 2025-03-06T00:33:07Z
author_association: COLLABORATOR
diff_hunk: @@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
path: Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
user: login: jakepetroules
body: Because it overwrites the system environment, or what?
created_at: 2025-03-06T00:36:34Z
author_association: COLLABORATOR
diff_hunk: @@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
path: Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
user: login: jakepetroules
body: I wonder if it's worth stripping unconditionally; the main use was inspecting SDKROOT anyways, and you can get at DEVELOPER_DIR, TOOLCHAIN_DIR, etc, to find anything else presumably relevant.
created_at: 2025-03-06T00:38:14Z
author_association: COLLABORATOR
diff_hunk: @@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
path: Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
user: login: owenv
body: yep, in some tests we end up with a mix of different Path and PATH
created_at: 2025-03-06T00:38:24Z
author_association: COLLABORATOR
diff_hunk: @@ -22,7 +22,14 @@ final class CustomTaskProducer: PhasedTaskProducer, TaskProducer {
             for customTask in context.configuredTarget?.target.customTasks ?? [] {
                 
                 let commandLine = customTask.commandLine.map { context.settings.globalScope.evaluate($0) }
-                let environment = EnvironmentBindings(customTask.environment.map { (context.settings.globalScope.evaluate($0.0), context.settings.globalScope.evaluate($0.1)) })
+                var environmentAssignments = computeScriptEnvironment(.shellScriptPhase, scope: context.settings.globalScope, settings: context.settings, workspaceContext: context.workspaceContext)
+                if context.workspaceContext.core.hostOperatingSystem != .macOS {
path: Sources/SWBTaskConstruction/TaskProducers/OtherTaskProducers/CustomTaskProducer.swift
user: login: jakepetroules
body: > yep, in some tests we end up with a mix of different Path and PATH

Ah, of course. We have #186 to improve that situation.
created_at: 2025-03-06T00:40:58Z
author_association: COLLABORATOR

PR comments:
user: login: owenv
created_at: 2025-03-05T22:12:03Z
body: @swift-ci test
author_association: COLLABORATOR
user: login: owenv
created_at: 2025-03-06T00:19:58Z
body: @swift-ci test
author_association: COLLABORATOR
user: login: owenv
created_at: 2025-03-06T00:29:15Z
body: @swift-ci test
author_association: COLLABORATOR