From 57c7b5d9aeb549a20b93712b77db1ac2a554ab59 Mon Sep 17 00:00:00 2001
From: Nuno de Carvalho <4029254+nmcc24@users.noreply.github.com>
Date: Tue, 25 Nov 2025 11:14:38 +0000
Subject: [PATCH 1/4] Fix compilation issues on iOS platform

Currently, XcodeProj is not compilable on iOS platform. This is an issue when attempting to compile products that have XcodeProj as a (transitive) dependency
---
 Sources/XcodeProj/Extensions/Path+Extras.swift         | 2 +-
 Sources/XcodeProj/Extensions/String+md5.swift          | 2 +-
 Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift | 4 ++++
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/Sources/XcodeProj/Extensions/Path+Extras.swift b/Sources/XcodeProj/Extensions/Path+Extras.swift
index 4b76668eb..6327ac822 100644
--- a/Sources/XcodeProj/Extensions/Path+Extras.swift
+++ b/Sources/XcodeProj/Extensions/Path+Extras.swift
@@ -6,7 +6,7 @@ import PathKit
 // MARK: - Path extras.
 
 func systemGlob(_ pattern: UnsafePointer<CChar>!, _ flags: Int32, _ errfunc: (@convention(c) (UnsafePointer<CChar>?, Int32) -> Int32)!, _ vector_ptr: UnsafeMutablePointer<glob_t>!) -> Int32 {
-    #if os(macOS)
+    #if os(macOS) || os(iOS)
         return Darwin.glob(pattern, flags, errfunc, vector_ptr)
     #else
         return Glibc.glob(pattern, flags, errfunc, vector_ptr)
diff --git a/Sources/XcodeProj/Extensions/String+md5.swift b/Sources/XcodeProj/Extensions/String+md5.swift
index 1c0afa871..04be85b4c 100644
--- a/Sources/XcodeProj/Extensions/String+md5.swift
+++ b/Sources/XcodeProj/Extensions/String+md5.swift
@@ -31,7 +31,7 @@ extension String {
             return self
         }
         #if canImport(CryptoKit)
-            if #available(OSX 10.15, *) {
+            if #available(OSX 10.15, iOS 13.0, *) {
                 return Insecure.MD5.hash(data: data)
                     .withUnsafeBytes { Array($0) }.hexString
             } else {
diff --git a/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift b/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift
index 5ea654b4c..277c451bf 100644
--- a/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift
+++ b/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift
@@ -3,6 +3,7 @@ import Foundation
 /// Returns the output of running `executable` with `args`. Throws an error if the process exits indicating failure.
 @discardableResult
 func checkedOutput(_ executable: String, _ args: [String]) throws -> String? {
+    #if os(macOS)
     let process = Process()
     let output = Pipe()
 
@@ -22,4 +23,7 @@ func checkedOutput(_ executable: String, _ args: [String]) throws -> String? {
     }
 
     return String(data: output.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
+    #else
+    return nil
+    #endif
 }

From 3ce1a9c8fd3c56185811ce00b0c5780403e367e5 Mon Sep 17 00:00:00 2001
From: Nuno de Carvalho <4029254+nmcc24@users.noreply.github.com>
Date: Sun, 1 Mar 2026 10:28:24 +0400
Subject: [PATCH 2/4] Skip tests on iOS platform that utilise unavailable APIs

---
 Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift | 4 ++++
 Tests/XcodeProjTests/Tests/testWrite.swift                | 4 ++++
 2 files changed, 8 insertions(+)

diff --git a/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift b/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift
index ed33c9749..156585c3f 100644
--- a/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift
+++ b/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift
@@ -45,6 +45,9 @@ final class XCSchemeManagementTests: XCTestCase {
     }
 
     func test_write_produces_no_diff() throws {
+        #if os(iOS)
+        throw XCTSkip("'Process' API is unavailable on iOS platform")
+        #else
         let tmpDir = try Path.uniqueTemporary()
         defer {
             try? tmpDir.delete()
@@ -77,6 +80,7 @@ final class XCSchemeManagementTests: XCTestCase {
             let got = try checkedOutput("git", ["status"])
             XCTAssertTrue(got?.contains("nothing to commit") ?? false)
         }
+        #endif
     }
 
     private var xcschememanagementPath: Path {
diff --git a/Tests/XcodeProjTests/Tests/testWrite.swift b/Tests/XcodeProjTests/Tests/testWrite.swift
index 5362af16d..8a6152859 100644
--- a/Tests/XcodeProjTests/Tests/testWrite.swift
+++ b/Tests/XcodeProjTests/Tests/testWrite.swift
@@ -45,6 +45,9 @@ func testReadWriteProducesNoDiff(file _: StaticString = #file,
                                  from path: Path,
                                  initModel: (Path) throws -> some Writable) throws
 {
+    #if os(iOS)
+    throw XCTSkip("'Process' API is unavailable on iOS platform")
+    #else
     let tmpDir = try Path.uniqueTemporary()
     defer {
         try? tmpDir.delete()
@@ -69,4 +72,5 @@ func testReadWriteProducesNoDiff(file _: StaticString = #file,
         let diff = try XCTUnwrap(checkedOutput("git", ["diff"]))
         XCTAssertEqual(diff, "")
     }
+    #endif
 }

From 3e6c051c7c567b12bfb62225a5934a2537ef3332 Mon Sep 17 00:00:00 2001
From: Nuno de Carvalho <4029254+nmcc24@users.noreply.github.com>
Date: Thu, 16 Apr 2026 14:47:51 +0100
Subject: [PATCH 3/4] Update xcodeproj.yml

Add iOS simulator build and test stages to github workflow
---
 .github/workflows/xcodeproj.yml | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/xcodeproj.yml b/.github/workflows/xcodeproj.yml
index 14bdb8601..38cc99d7e 100644
--- a/.github/workflows/xcodeproj.yml
+++ b/.github/workflows/xcodeproj.yml
@@ -33,6 +33,14 @@ jobs:
       - run: mise use swift@6.0.2
       - name: Build
         run: swift build --configuration release
+  build-ios:
+    name: Build (iOS)
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v6
+      - name: Build for iOS Simulator
+        run: xcodebuild build -scheme XcodeProj -destination 'platform=iOS Simulator,name=iPhone 16'
+  
   test:
     name: Test (macOS / Xcode)
     runs-on: macos-latest
@@ -41,7 +49,6 @@ jobs:
       - uses: jdx/mise-action@v2
       - name: Run tests
         run: mise run test
-
   test-linux:
     name: Test (Linux)
     runs-on: ubuntu-latest
@@ -55,6 +62,13 @@ jobs:
           git config --global init.defaultBranch main
       - name: Test
         run: swift test
+  test-ios:
+    name: Test (iOS / Xcode)
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Run tests on iOS Simulator
+        run: xcodebuild test -scheme XcodeProj -destination 'platform=iOS Simulator,name=iPhone 16'
 
   lint:
     name: Lint

From 932e2844d868ed13d809b74f819aaccd9067371d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pedro=20Pi=C3=B1era=20Buend=C3=ADa?= <pedro@pepicrft.me>
Date: Fri, 5 Jun 2026 13:14:06 +0200
Subject: [PATCH 4/4] Fix iOS workflow checks

---
 .github/workflows/xcodeproj.yml               | 14 ++++-
 .../Extensions/XCTestCase+Shell.swift         | 36 ++++++------
 .../Scheme/XCSchemeManagementTests.swift      | 58 +++++++++----------
 Tests/XcodeProjTests/Tests/testWrite.swift    | 42 +++++++-------
 4 files changed, 79 insertions(+), 71 deletions(-)

diff --git a/.github/workflows/xcodeproj.yml b/.github/workflows/xcodeproj.yml
index 38cc99d7e..ea1151502 100644
--- a/.github/workflows/xcodeproj.yml
+++ b/.github/workflows/xcodeproj.yml
@@ -39,8 +39,7 @@ jobs:
     steps:
       - uses: actions/checkout@v6
       - name: Build for iOS Simulator
-        run: xcodebuild build -scheme XcodeProj -destination 'platform=iOS Simulator,name=iPhone 16'
-  
+        run: xcodebuild build -scheme XcodeProj -destination 'generic/platform=iOS Simulator'
   test:
     name: Test (macOS / Xcode)
     runs-on: macos-latest
@@ -67,8 +66,17 @@ jobs:
     runs-on: macos-latest
     steps:
       - uses: actions/checkout@v3
+      - name: Select iOS Simulator
+        run: |
+          simulator_id="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ { print $2; exit }')"
+          if [ -z "$simulator_id" ]; then
+            echo "No available iPhone simulator found"
+            xcrun simctl list devices available
+            exit 1
+          fi
+          echo "IOS_SIMULATOR_ID=$simulator_id" >> "$GITHUB_ENV"
       - name: Run tests on iOS Simulator
-        run: xcodebuild test -scheme XcodeProj -destination 'platform=iOS Simulator,name=iPhone 16'
+        run: xcodebuild test -scheme XcodeProj -destination "platform=iOS Simulator,id=$IOS_SIMULATOR_ID"
 
   lint:
     name: Lint
diff --git a/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift b/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift
index 277c451bf..95d8c881c 100644
--- a/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift
+++ b/Tests/XcodeProjTests/Extensions/XCTestCase+Shell.swift
@@ -3,27 +3,27 @@ import Foundation
 /// Returns the output of running `executable` with `args`. Throws an error if the process exits indicating failure.
 @discardableResult
 func checkedOutput(_ executable: String, _ args: [String]) throws -> String? {
-    #if os(macOS)
-    let process = Process()
-    let output = Pipe()
+    #if os(iOS)
+        return nil
+    #else
+        let process = Process()
+        let output = Pipe()
 
-    if executable.contains("/") {
-        process.launchPath = executable
-    } else {
-        process.launchPath = try checkedOutput("/usr/bin/which", [executable])?.trimmingCharacters(in: .newlines)
-    }
+        if executable.contains("/") {
+            process.launchPath = executable
+        } else {
+            process.launchPath = try checkedOutput("/usr/bin/which", [executable])?.trimmingCharacters(in: .newlines)
+        }
 
-    process.arguments = args
-    process.standardOutput = output
-    process.launch()
-    process.waitUntilExit()
+        process.arguments = args
+        process.standardOutput = output
+        process.launch()
+        process.waitUntilExit()
 
-    guard process.terminationStatus == 0 else {
-        throw NSError(domain: NSPOSIXErrorDomain, code: Int(process.terminationStatus))
-    }
+        guard process.terminationStatus == 0 else {
+            throw NSError(domain: NSPOSIXErrorDomain, code: Int(process.terminationStatus))
+        }
 
-    return String(data: output.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
-    #else
-    return nil
+        return String(data: output.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
     #endif
 }
diff --git a/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift b/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift
index 156585c3f..9a5f719d7 100644
--- a/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift
+++ b/Tests/XcodeProjTests/Scheme/XCSchemeManagementTests.swift
@@ -46,40 +46,40 @@ final class XCSchemeManagementTests: XCTestCase {
 
     func test_write_produces_no_diff() throws {
         #if os(iOS)
-        throw XCTSkip("'Process' API is unavailable on iOS platform")
+            throw XCTSkip("'Process' API is unavailable on iOS platform")
         #else
-        let tmpDir = try Path.uniqueTemporary()
-        defer {
-            try? tmpDir.delete()
-        }
+            let tmpDir = try Path.uniqueTemporary()
+            defer {
+                try? tmpDir.delete()
+            }
 
-        try tmpDir.chdir {
-            // Write
-            let plistPath = tmpDir + "xcschememanagement.plist"
-            let subject = XCSchemeManagement(
-                schemeUserState: [
-                    .init(name: "Test 0.xcscheme", shared: true, orderHint: 0, isShown: true),
-                    .init(name: "Test 1.xcscheme", shared: true, orderHint: 1, isShown: true),
-                    .init(name: "Test 2.xcscheme", shared: true, orderHint: 2, isShown: false),
-                    .init(name: "Test 3.xcscheme", shared: true, orderHint: 3, isShown: true),
-                ],
-                suppressBuildableAutocreation: [
-                    "E525238B16245A900012E2BA": .init(primary: true),
-                ]
-            )
-            try subject.write(path: plistPath, override: true)
+            try tmpDir.chdir {
+                // Write
+                let plistPath = tmpDir + "xcschememanagement.plist"
+                let subject = XCSchemeManagement(
+                    schemeUserState: [
+                        .init(name: "Test 0.xcscheme", shared: true, orderHint: 0, isShown: true),
+                        .init(name: "Test 1.xcscheme", shared: true, orderHint: 1, isShown: true),
+                        .init(name: "Test 2.xcscheme", shared: true, orderHint: 2, isShown: false),
+                        .init(name: "Test 3.xcscheme", shared: true, orderHint: 3, isShown: true),
+                    ],
+                    suppressBuildableAutocreation: [
+                        "E525238B16245A900012E2BA": .init(primary: true),
+                    ]
+                )
+                try subject.write(path: plistPath, override: true)
 
-            // Create a commit
-            try checkedOutput("git", ["init"])
-            try checkedOutput("git", ["add", "."])
-            try checkedOutput("git", ["commit", "-m", "test"])
+                // Create a commit
+                try checkedOutput("git", ["init"])
+                try checkedOutput("git", ["add", "."])
+                try checkedOutput("git", ["commit", "-m", "test"])
 
-            // Write again
-            try subject.write(path: plistPath, override: true)
+                // Write again
+                try subject.write(path: plistPath, override: true)
 
-            let got = try checkedOutput("git", ["status"])
-            XCTAssertTrue(got?.contains("nothing to commit") ?? false)
-        }
+                let got = try checkedOutput("git", ["status"])
+                XCTAssertTrue(got?.contains("nothing to commit") ?? false)
+            }
         #endif
     }
 
diff --git a/Tests/XcodeProjTests/Tests/testWrite.swift b/Tests/XcodeProjTests/Tests/testWrite.swift
index 8a6152859..56b45dd61 100644
--- a/Tests/XcodeProjTests/Tests/testWrite.swift
+++ b/Tests/XcodeProjTests/Tests/testWrite.swift
@@ -46,31 +46,31 @@ func testReadWriteProducesNoDiff(file _: StaticString = #file,
                                  initModel: (Path) throws -> some Writable) throws
 {
     #if os(iOS)
-    throw XCTSkip("'Process' API is unavailable on iOS platform")
+        throw XCTSkip("'Process' API is unavailable on iOS platform")
     #else
-    let tmpDir = try Path.uniqueTemporary()
-    defer {
-        try? tmpDir.delete()
-    }
+        let tmpDir = try Path.uniqueTemporary()
+        defer {
+            try? tmpDir.delete()
+        }
 
-    let fileName = path.lastComponent
-    let tmpPath = tmpDir + fileName
-    try path.copy(tmpPath)
+        let fileName = path.lastComponent
+        let tmpPath = tmpDir + fileName
+        try path.copy(tmpPath)
 
-    try tmpDir.chdir {
-        // Create a commit
-        try checkedOutput("git", ["init"])
-        try checkedOutput("git", ["add", "."])
-        try checkedOutput("git", [
-            "-c", "user.email=test@example.com", "-c", "user.name=Test User",
-            "commit", "-m", "test",
-        ])
+        try tmpDir.chdir {
+            // Create a commit
+            try checkedOutput("git", ["init"])
+            try checkedOutput("git", ["add", "."])
+            try checkedOutput("git", [
+                "-c", "user.email=test@example.com", "-c", "user.name=Test User",
+                "commit", "-m", "test",
+            ])
 
-        let object = try initModel(tmpPath)
-        try object.write(path: tmpPath, override: true)
+            let object = try initModel(tmpPath)
+            try object.write(path: tmpPath, override: true)
 
-        let diff = try XCTUnwrap(checkedOutput("git", ["diff"]))
-        XCTAssertEqual(diff, "")
-    }
+            let diff = try XCTUnwrap(checkedOutput("git", ["diff"]))
+            XCTAssertEqual(diff, "")
+        }
     #endif
 }
