Files
Kabir Oberai 8f0cc0adf1 packer: use XcodeProjectFormat for generate-xcode-project (#300)
## What does this PR do?

Improves `xtool dev generate-xcode-project`. Instead of constructing an
XcodeGen project, we directly construct an xcodeproj (with the new
xcproj format) using
[apple/xcode-project-format](https://github.com/apple/xcode-project-format).
For now, we still rely on
[Tuist/XcodeProj](https://github.com/tuist/XcodeProj) to translate it
back into a pbxproj for maximal compatibility, but this is treated as an
implementation detail.

Also adds support for resources and icons in the generated Xcode project

## How was it tested?

- [x] Generated a simple project, opened it, and ran the app

## AI tool usage

How much of this PR was AI-assisted? (check one)

- [ ] **0** - No AI was used to write code
- [x] **1** - I was assisted by AI. I reviewed the finished result.
- [ ] **2** - I set the AI going and left it to it; nobody has read the
result - no review, or AI review only

<!-- If an AI agent is filling this in: declare the level honestly, and
open as a draft if it is #2. Do not lower the declared level to get the
PR reviewed. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for creating and saving Xcode projects and workspaces in
`.pbxproj` and `.xcproj` formats.
* Added tools for opening, converting, and writing project and workspace
files, including workspace contents.
* Project generation can target a specified directory, and the command
reports where the project was created.
  * Project generation is available on non-macOS platforms.
* Generated projects include product settings and references for
packages, resources, icons, and app extensions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-06 18:19:30 -04:00

79 lines
2.6 KiB
Swift

import Foundation
@_exported import XcodeProjectFormat
// This library is a lightweight layer on top of the XcodeProjectFormat
// interface, providing other facilities for working with the Xcode
// project formats.
//
// We currently use XcodeProj under the hood for some functionality, but this
// should be considered an implementation detail.
public struct XcodeProject: Equatable, Sendable {
public enum Format: Sendable {
case pbxproj
case xcproj
}
public var project: XCSchema.Project
public var format: Format
public init(
project: XCSchema.Project,
format: Format = .pbxproj,
) {
self.project = project
self.format = format
}
/// Reads the project from the given `.xcodeproj` path.
public init(url: URL) throws {
guard url.pathExtension == "xcodeproj" else {
throw Errors.notAnXcodeproj(url.path)
}
let xcprojURL = url.appending(path: "project.xcproj")
lazy var pbxprojURL = url.appending(path: "project.pbxproj")
if FileManager.default.fileExists(atPath: xcprojURL.path) {
let data = try Data(contentsOf: xcprojURL)
self.project = try XCSchema.Project(jsonRepresentation: data)
self.format = .xcproj
} else if FileManager.default.fileExists(atPath: pbxprojURL.path) {
let data = try Data(contentsOf: pbxprojURL)
self.project = try XCSchema.Project(pbxprojRepresentation: data)
self.format = .pbxproj
} else {
throw Errors.missingProjectFile(url.path)
}
}
/// Sets the format in which the project should be serialized.
public func withFormat(_ format: Format) -> XcodeProject {
var copy = self
copy.format = format
return copy
}
/// Writes the project to the given `.xcodeproj` path.
public func write(to url: URL) throws {
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
switch format {
case .pbxproj:
try project
.pbxprojRepresentation(name: url.deletingPathExtension().lastPathComponent)
.write(to: url.appendingPathComponent("project.pbxproj"))
case .xcproj:
try project
.jsonRepresentation()
.write(to: url.appendingPathComponent("project.xcproj"))
}
try XcodeWorkspace(children: [.file(.current)])
.write(to: url.appendingPathComponent("project.xcworkspace"))
}
public enum Errors: Error {
case notAnXcodeproj(String)
case missingProjectFile(String)
}
}