From 9a8e010e8261a01bce82c61f57ee91067bcdc788 Mon Sep 17 00:00:00 2001
From: Michalis Karagiorgos <m.karagiorgos@kaizengaming.com>
Date: Wed, 26 Nov 2025 16:34:24 +0200
Subject: [PATCH 1/4] fix https://github.com/tuist/XcodeProj/issues/921

---
 .../Test.xcodeproj/project.pbxproj            | 61 +++++++++----------
 .../xcshareddata/xcschemes/App.xcscheme       |  2 +-
 .../BuildPhase/PBXShellScriptBuildPhase.swift |  6 +-
 Sources/XcodeProj/Utils/Either.swift          | 31 ++++++++++
 .../PBXShellScriptBuildPhaseTests.swift       | 10 ---
 .../Objects/Project/PBXProjEncoderTests.swift |  4 ++
 Tests/XcodeProjTests/Tests/Fixtures.swift     |  5 ++
 7 files changed, 73 insertions(+), 46 deletions(-)
 create mode 100644 Sources/XcodeProj/Utils/Either.swift

diff --git a/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/project.pbxproj b/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/project.pbxproj
index 86898189c..9194eb0ae 100644
--- a/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/project.pbxproj
+++ b/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 56;
+	objectVersion = 90;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -48,15 +48,13 @@
 /* Begin PBXCopyFilesBuildPhase section */
 		BF31C3A479ECD7CBA0640A2F /* Embed Frameworks */ = {
 			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
 			dstPath = "";
-			dstSubfolderSpec = 10;
+			dstSubfolder = Frameworks;
 			files = (
 				C2317A164A6A4F635FE4CCD7 /* Framework1.framework in Embed Frameworks */,
 				3108C05C7B0C4FD6AC077294 /* Framework2.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
-			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXCopyFilesBuildPhase section */
 
@@ -67,18 +65,15 @@
 		6D728BF5C49E97251DA378AE /* Framework2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework2.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		87A3E8A0727A99EE88ED4E64 /* Framework1.xcodeproj */ = {isa = PBXFileReference; explicitFileType = "wrapper.pb-project"; includeInIndex = 0; name = Framework1.xcodeproj; path = ../Framework1/Framework1.xcodeproj; sourceTree = SOURCE_ROOT; };
 		E203E65EDD1A90FCBAA42C9E /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
-		F09268C492FF78A6C82868C6 /* App-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "App-Info.plist"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
 		B27C7CF4B617049554976EFD /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
 			files = (
 				5D8C48ABB7D5D013DDBD67AA /* Framework1.framework in Frameworks */,
 				C57CB036110A3D5DCC9E437A /* Framework2.framework in Frameworks */,
 			);
-			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXFrameworksBuildPhase section */
 
@@ -100,14 +95,6 @@
 			name = Products;
 			sourceTree = "<group>";
 		};
-		20C83B4860AC239B5E5FDF3C /* InfoPlists */ = {
-			isa = PBXGroup;
-			children = (
-				F09268C492FF78A6C82868C6 /* App-Info.plist */,
-			);
-			path = InfoPlists;
-			sourceTree = "<group>";
-		};
 		30CD57449DD0BAD1F51809C3 /* Products */ = {
 			isa = PBXGroup;
 			children = (
@@ -160,6 +147,7 @@
 				F1BC716AD69EACCBD2A2DDBC /* Resources */,
 				BF31C3A479ECD7CBA0640A2F /* Embed Frameworks */,
 				B27C7CF4B617049554976EFD /* Frameworks */,
+				0579081E2ED74460006A6AFB /* custom script */,
 			);
 			buildRules = (
 			);
@@ -168,8 +156,6 @@
 				CD591349E38A6A3EB5AA81DD /* PBXTargetDependency */,
 			);
 			name = App;
-			packageProductDependencies = (
-			);
 			productName = App;
 			productReference = E203E65EDD1A90FCBAA42C9E /* App.app */;
 			productType = "com.apple.product-type.application";
@@ -181,9 +167,9 @@
 			isa = PBXProject;
 			attributes = {
 				BuildIndependentTargetsInParallel = YES;
+				LastUpgradeCheck = 1640;
 			};
 			buildConfigurationList = C94346B8B75719D8319921C2 /* Build configuration list for PBXProject "Test" */;
-			compatibilityVersion = "Xcode 14.0";
 			developmentRegion = en;
 			hasScannedForEncodings = 0;
 			knownRegions = (
@@ -191,6 +177,7 @@
 				en,
 			);
 			mainGroup = 0767F0D2E9F7C0B23673A1B1;
+			preferredProjectObjectVersion = 90;
 			productRefGroup = 73B13978C93862E5AD69BCC8 /* Products */;
 			projectDirPath = "";
 			projectReferences = (
@@ -230,21 +217,31 @@
 /* Begin PBXResourcesBuildPhase section */
 		F1BC716AD69EACCBD2A2DDBC /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
-			buildActionMask = 2147483647;
 			files = (
 			);
-			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXResourcesBuildPhase section */
 
+/* Begin PBXShellScriptBuildPhase section */
+		0579081E2ED74460006A6AFB /* custom script */ = {
+			isa = PBXShellScriptBuildPhase;
+			name = "custom script";
+			shellPath = /bin/sh;
+			shellScript = (
+				"# comment",
+				"ls -la",
+				"ls -la",
+				"",
+			);
+		};
+/* End PBXShellScriptBuildPhase section */
+
 /* Begin PBXSourcesBuildPhase section */
 		2053EE4C6238D6C3AEB2ADB3 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
 			files = (
 				D2AE54DED6608D5B956A5E45 /* AppDelegate.swift in Sources */,
 			);
-			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXSourcesBuildPhase section */
 
@@ -262,7 +259,7 @@
 /* End PBXTargetDependency section */
 
 /* Begin XCBuildConfiguration section */
-		0B98A64D6F621FDCEEA0CE44 /* Debug */ = {
+		0B98A64D6F621FDCEEA0CE44 /* Debug configuration for PBXNativeTarget "App" */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@@ -287,7 +284,7 @@
 			};
 			name = Debug;
 		};
-		58BAFCCFAE3AD62113BBEDEF /* Debug */ = {
+		58BAFCCFAE3AD62113BBEDEF /* Debug configuration for PBXProject "Test" */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
@@ -326,6 +323,7 @@
 				DEBUG_INFORMATION_FORMAT = dwarf;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_DYNAMIC_NO_PIC = NO;
 				GCC_NO_COMMON_BLOCKS = YES;
@@ -346,7 +344,7 @@
 			};
 			name = Debug;
 		};
-		5904C1CCA86A83838723C772 /* Release */ = {
+		5904C1CCA86A83838723C772 /* Release configuration for PBXProject "Test" */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
@@ -385,6 +383,7 @@
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_NO_COMMON_BLOCKS = YES;
 				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -399,7 +398,7 @@
 			};
 			name = Release;
 		};
-		5E15950A8DD3B82ED1ED7849 /* Release */ = {
+		5E15950A8DD3B82ED1ED7849 /* Release configuration for PBXNativeTarget "App" */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@@ -429,19 +428,17 @@
 		BAA38452720711B406475F28 /* Build configuration list for PBXNativeTarget "App" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
-				0B98A64D6F621FDCEEA0CE44 /* Debug */,
-				5E15950A8DD3B82ED1ED7849 /* Release */,
+				0B98A64D6F621FDCEEA0CE44 /* Debug configuration for PBXNativeTarget "App" */,
+				5E15950A8DD3B82ED1ED7849 /* Release configuration for PBXNativeTarget "App" */,
 			);
-			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
 		C94346B8B75719D8319921C2 /* Build configuration list for PBXProject "Test" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
-				58BAFCCFAE3AD62113BBEDEF /* Debug */,
-				5904C1CCA86A83838723C772 /* Release */,
+				58BAFCCFAE3AD62113BBEDEF /* Debug configuration for PBXProject "Test" */,
+				5904C1CCA86A83838723C772 /* Release configuration for PBXProject "Test" */,
 			);
-			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
 /* End XCConfigurationList section */
diff --git a/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/xcshareddata/xcschemes/App.xcscheme
index aefdccd72..fc8b076c1 100644
--- a/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/xcshareddata/xcschemes/App.xcscheme
+++ b/Fixtures/Xcode16ProjectReferenceOrder/Test.xcodeproj/xcshareddata/xcschemes/App.xcscheme
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1010"
+   LastUpgradeVersion = "1640"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
index 4790dc45f..d2957bcdb 100644
--- a/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
+++ b/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
@@ -17,7 +17,7 @@ public final class PBXShellScriptBuildPhase: PBXBuildPhase {
     public var shellPath: String?
 
     /// Shell script.
-    public var shellScript: String?
+    public var shellScript: Either<String, [String]>?
 
     /// Show environment variables in the logs.
     public var showEnvVarsInLog: Bool
@@ -54,7 +54,7 @@ public final class PBXShellScriptBuildPhase: PBXBuildPhase {
                 inputFileListPaths: [String]? = nil,
                 outputFileListPaths: [String]? = nil,
                 shellPath: String = "/bin/sh",
-                shellScript: String? = nil,
+                shellScript: Either<String, [String]>? = nil,
                 buildActionMask: UInt = defaultBuildActionMask,
                 runOnlyForDeploymentPostprocessing: Bool = false,
                 showEnvVarsInLog: Bool = true,
@@ -122,7 +122,7 @@ extension PBXShellScriptBuildPhase: PlistSerializable {
         }
         dictionary["outputPaths"] = .array(outputPaths.map { .string(CommentedString($0)) })
         if let shellScript {
-            dictionary["shellScript"] = .string(CommentedString(shellScript))
+          dictionary["shellScript"] = .string(CommentedString(shellScript.toString()))
         }
         if let dependencyFile {
             dictionary["dependencyFile"] = .string(CommentedString(dependencyFile))
diff --git a/Sources/XcodeProj/Utils/Either.swift b/Sources/XcodeProj/Utils/Either.swift
new file mode 100644
index 000000000..f8c853add
--- /dev/null
+++ b/Sources/XcodeProj/Utils/Either.swift
@@ -0,0 +1,31 @@
+import Foundation
+
+public enum Either<L, R> {
+  case left(L)
+  case right(R)
+}
+
+extension Either: Decodable where L: Decodable, R: Decodable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.singleValueContainer()
+        if let leftValue = try? container.decode(L.self) {
+            self = .left(leftValue)
+        } else {
+            let rightValue = try container.decode(R.self)
+            self = .right(rightValue)
+        }
+    }
+}
+
+extension Either where L == String, R == [String] {
+    public func toString() -> String {
+        switch self {
+        case .left(let string):
+            return string
+        case .right(let stringArray):
+            return stringArray.joined(separator: "\n")
+        }
+    }
+}
+
+extension Either: Equatable where L: Equatable, R: Equatable {}
diff --git a/Tests/XcodeProjTests/Objects/BuildPhase/PBXShellScriptBuildPhaseTests.swift b/Tests/XcodeProjTests/Objects/BuildPhase/PBXShellScriptBuildPhaseTests.swift
index 18de54b2c..8a9282387 100644
--- a/Tests/XcodeProjTests/Objects/BuildPhase/PBXShellScriptBuildPhaseTests.swift
+++ b/Tests/XcodeProjTests/Objects/BuildPhase/PBXShellScriptBuildPhaseTests.swift
@@ -73,14 +73,4 @@ final class PBXShellScriptBuildPhaseTests: XCTestCase {
 
         XCTAssertNotEqual(noDiscovery, discovery)
     }
-
-    private func testDictionary() -> [String: Any] {
-        [
-            "files": ["files"],
-            "inputPaths": ["input"],
-            "outputPaths": ["output"],
-            "shellPath": "shellPath",
-            "shellScript": "shellScript",
-        ]
-    }
 }
diff --git a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift
index 61bf70205..9233b5b16 100644
--- a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift
+++ b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift
@@ -554,6 +554,10 @@
         private func loadProjectWithWrongProjectReferencesOrder() throws {
             proj = try PBXProj(data: projectWithWrongProjectReferencesOrder())
         }
+
+        private func loadProjectWithShellScriptBuildPhase() throws {
+            proj = try PBXProj(data: projectWithCustomShellScript())
+      }
     }
 
     // MARK: - Line validations
diff --git a/Tests/XcodeProjTests/Tests/Fixtures.swift b/Tests/XcodeProjTests/Tests/Fixtures.swift
index ed9dfe919..32a0e24fd 100644
--- a/Tests/XcodeProjTests/Tests/Fixtures.swift
+++ b/Tests/XcodeProjTests/Tests/Fixtures.swift
@@ -50,3 +50,8 @@ func projectWithWrongProjectReferencesOrder() throws -> Data {
     let iosProjectWithProjectReferences = fixturesPath() + "Xcode16ProjectReferenceOrder/Wrong.xcodeproj/project.pbxproj"
     return try Data(contentsOf: iosProjectWithProjectReferences.url)
 }
+
+func projectWithCustomShellScript() throws -> Data {
+    let iosProjectWithShellScript = fixturesPath() + "Xcode16ProjectReferenceOrder/Test.xcodeproj/project.pbxproj"
+    return try Data(contentsOf: iosProjectWithShellScript.url)
+}

From 07ed2fc633fd3b35a38d9b625ba98ae430e32b6c Mon Sep 17 00:00:00 2001
From: Michalis Karagiorgos <michaliskarag@gmail.com>
Date: Wed, 26 Nov 2025 17:02:37 +0200
Subject: [PATCH 2/4] fix wrong indentation

---
 .../XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
index d2957bcdb..d2bb24f9b 100644
--- a/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
+++ b/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
@@ -122,7 +122,7 @@ extension PBXShellScriptBuildPhase: PlistSerializable {
         }
         dictionary["outputPaths"] = .array(outputPaths.map { .string(CommentedString($0)) })
         if let shellScript {
-          dictionary["shellScript"] = .string(CommentedString(shellScript.toString()))
+            dictionary["shellScript"] = .string(CommentedString(shellScript.toString()))
         }
         if let dependencyFile {
             dictionary["dependencyFile"] = .string(CommentedString(dependencyFile))

From a3bbc5220b8bda19bf8fe12cb2395fdc2b1ecffb Mon Sep 17 00:00:00 2001
From: Michalis Karagiorgos <m.karagiorgos@kaizengaming.com>
Date: Wed, 26 Nov 2025 17:18:55 +0200
Subject: [PATCH 3/4] improve formatting for Either.swift

---
 Sources/XcodeProj/Utils/Either.swift          | 30 +++++++++----------
 .../Objects/Project/PBXProjEncoderTests.swift |  2 +-
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/Sources/XcodeProj/Utils/Either.swift b/Sources/XcodeProj/Utils/Either.swift
index f8c853add..3df65d6d3 100644
--- a/Sources/XcodeProj/Utils/Either.swift
+++ b/Sources/XcodeProj/Utils/Either.swift
@@ -6,26 +6,26 @@ public enum Either<L, R> {
 }
 
 extension Either: Decodable where L: Decodable, R: Decodable {
-    public init(from decoder: Decoder) throws {
-        let container = try decoder.singleValueContainer()
-        if let leftValue = try? container.decode(L.self) {
-            self = .left(leftValue)
-        } else {
-            let rightValue = try container.decode(R.self)
-            self = .right(rightValue)
-        }
+  public init(from decoder: Decoder) throws {
+    let container = try decoder.singleValueContainer()
+    if let leftValue = try? container.decode(L.self) {
+      self = .left(leftValue)
+    } else {
+      let rightValue = try container.decode(R.self)
+      self = .right(rightValue)
     }
+  }
 }
 
 extension Either where L == String, R == [String] {
-    public func toString() -> String {
-        switch self {
-        case .left(let string):
-            return string
-        case .right(let stringArray):
-            return stringArray.joined(separator: "\n")
-        }
+  public func toString() -> String {
+    switch self {
+    case .left(let string):
+        string
+    case .right(let stringArray):
+        stringArray.joined(separator: "\n")
     }
+  }
 }
 
 extension Either: Equatable where L: Equatable, R: Equatable {}
diff --git a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift
index 9233b5b16..af5233ce0 100644
--- a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift
+++ b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift
@@ -557,7 +557,7 @@
 
         private func loadProjectWithShellScriptBuildPhase() throws {
             proj = try PBXProj(data: projectWithCustomShellScript())
-      }
+        }
     }
 
     // MARK: - Line validations

From 001eebf7e06e18aae3703e2576401d3c238b58a2 Mon Sep 17 00:00:00 2001
From: Michalis Karagiorgos <m.karagiorgos@kaizengaming.com>
Date: Mon, 22 Dec 2025 17:52:17 +0200
Subject: [PATCH 4/4] remove Either and avoid introducing a breaking change for
 PBXShellScriptBuildPhase

---
 .../BuildPhase/PBXShellScriptBuildPhase.swift | 13 +++++---
 Sources/XcodeProj/Utils/Either.swift          | 31 -------------------
 2 files changed, 9 insertions(+), 35 deletions(-)
 delete mode 100644 Sources/XcodeProj/Utils/Either.swift

diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
index d2bb24f9b..c08f25ac3 100644
--- a/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
+++ b/Sources/XcodeProj/Objects/BuildPhase/PBXShellScriptBuildPhase.swift
@@ -17,7 +17,7 @@ public final class PBXShellScriptBuildPhase: PBXBuildPhase {
     public var shellPath: String?
 
     /// Shell script.
-    public var shellScript: Either<String, [String]>?
+    public var shellScript: String?
 
     /// Show environment variables in the logs.
     public var showEnvVarsInLog: Bool
@@ -54,7 +54,7 @@ public final class PBXShellScriptBuildPhase: PBXBuildPhase {
                 inputFileListPaths: [String]? = nil,
                 outputFileListPaths: [String]? = nil,
                 shellPath: String = "/bin/sh",
-                shellScript: Either<String, [String]>? = nil,
+                shellScript: String? = nil,
                 buildActionMask: UInt = defaultBuildActionMask,
                 runOnlyForDeploymentPostprocessing: Bool = false,
                 showEnvVarsInLog: Bool = true,
@@ -94,7 +94,12 @@ public final class PBXShellScriptBuildPhase: PBXBuildPhase {
         inputPaths = try (container.decodeIfPresent(.inputPaths)) ?? []
         outputPaths = try (container.decodeIfPresent(.outputPaths)) ?? []
         shellPath = try container.decodeIfPresent(.shellPath)
-        shellScript = try container.decodeIfPresent(.shellScript)
+        // Xcode 16.0 introduced a new format for shellScript, so we need to handle both cases.
+        if let scriptArray = try? container.decodeIfPresent([String].self, forKey: .shellScript) {
+            shellScript = scriptArray.joined(separator: "\n")
+        } else {
+            shellScript = try container.decodeIfPresent(.shellScript)
+        }
         showEnvVarsInLog = try container.decodeIntBoolIfPresent(.showEnvVarsInLog) ?? true
         alwaysOutOfDate = try container.decodeIntBoolIfPresent(.alwaysOutOfDate) ?? false
         dependencyFile = try container.decodeIfPresent(.dependencyFile)
@@ -122,7 +127,7 @@ extension PBXShellScriptBuildPhase: PlistSerializable {
         }
         dictionary["outputPaths"] = .array(outputPaths.map { .string(CommentedString($0)) })
         if let shellScript {
-            dictionary["shellScript"] = .string(CommentedString(shellScript.toString()))
+            dictionary["shellScript"] = .string(CommentedString(shellScript))
         }
         if let dependencyFile {
             dictionary["dependencyFile"] = .string(CommentedString(dependencyFile))
diff --git a/Sources/XcodeProj/Utils/Either.swift b/Sources/XcodeProj/Utils/Either.swift
deleted file mode 100644
index 3df65d6d3..000000000
--- a/Sources/XcodeProj/Utils/Either.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-import Foundation
-
-public enum Either<L, R> {
-  case left(L)
-  case right(R)
-}
-
-extension Either: Decodable where L: Decodable, R: Decodable {
-  public init(from decoder: Decoder) throws {
-    let container = try decoder.singleValueContainer()
-    if let leftValue = try? container.decode(L.self) {
-      self = .left(leftValue)
-    } else {
-      let rightValue = try container.decode(R.self)
-      self = .right(rightValue)
-    }
-  }
-}
-
-extension Either where L == String, R == [String] {
-  public func toString() -> String {
-    switch self {
-    case .left(let string):
-        string
-    case .right(let stringArray):
-        stringArray.joined(separator: "\n")
-    }
-  }
-}
-
-extension Either: Equatable where L: Equatable, R: Equatable {}
