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!, _ flags: Int32, _ errfunc: (@convention(c) (UnsafePointer?, Int32) -> Int32)!, _ vector_ptr: UnsafeMutablePointer!) -> 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?= 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 }