mirror of
https://github.com/xtool-org/xtool.git
synced 2026-11-04 20:11:35 +01:00
## What does this PR do? An XcodeProject/XcodeWorkspace now knows its own location on disk, which is important when we start using these types to build PIF. ## How was it tested? - [x] Used in future PRs to build various Xcode projects ## 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 --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Improvements** * Projects and workspaces now retain their associated locations and save directly to those locations. * Generated project files preserve the configured project directory path, helping keep file references consistent when projects are written or reopened. * Projects and workspaces can be associated with a new location while retaining their existing contents. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
114 lines
3.9 KiB
Swift
114 lines
3.9 KiB
Swift
import Foundation
|
|
private import XcodeProj
|
|
@_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
|
|
/// The .xcodeproj bundle's location, including for projects constructed in memory.
|
|
public var url: URL
|
|
/// For pbxproj projects, an explicit source root.
|
|
public var projectDirPath = ""
|
|
|
|
public var sourceRoot: URL {
|
|
if projectDirPath.isEmpty {
|
|
url.deletingLastPathComponent()
|
|
} else {
|
|
URL(
|
|
filePath: projectDirPath,
|
|
directoryHint: .isDirectory,
|
|
relativeTo: url.deletingLastPathComponent(),
|
|
).standardizedFileURL
|
|
}
|
|
}
|
|
|
|
public init(
|
|
project: XCSchema.Project,
|
|
url: URL,
|
|
format: Format = .pbxproj,
|
|
) {
|
|
self.project = project
|
|
self.format = format
|
|
self.url = url.standardizedFileURL.resolvingSymlinksInPath()
|
|
}
|
|
|
|
/// Reads the project from the given `.xcodeproj` path.
|
|
public init(url: URL) throws {
|
|
guard url.pathExtension == "xcodeproj" else {
|
|
throw Errors.notAnXcodeproj(url.path)
|
|
}
|
|
let url = url.standardizedFileURL.resolvingSymlinksInPath()
|
|
self.url = url
|
|
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)
|
|
let pbxproj = try PBXProj(data: data)
|
|
self.project = try XCSchema.Project(jsonRepresentation: pbxproj.xcprojData())
|
|
self.projectDirPath = try pbxproj.rootProject()?.projectDirPath ?? ""
|
|
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
|
|
}
|
|
|
|
/// Returns a copy at a new bundle location.
|
|
public func withURL(_ url: URL) -> Self {
|
|
var copy = self
|
|
copy.url = url.standardizedFileURL.resolvingSymlinksInPath()
|
|
return copy
|
|
}
|
|
|
|
/// Writes the project to its current `.xcodeproj` URL.
|
|
public func write() throws {
|
|
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
|
|
switch format {
|
|
case .pbxproj:
|
|
try project.pbxprojRepresentation(
|
|
name: url.deletingPathExtension().lastPathComponent,
|
|
projectDirPath: projectDirPath,
|
|
).write(to: url.appendingPathComponent("project.pbxproj"))
|
|
case .xcproj:
|
|
guard projectDirPath.isEmpty else {
|
|
throw Errors.xcprojWithExplicitSourceRoot
|
|
}
|
|
try project
|
|
.jsonRepresentation()
|
|
.write(to: url.appendingPathComponent("project.xcproj"))
|
|
}
|
|
|
|
let workspaceURL = url.appendingPathComponent("project.xcworkspace")
|
|
try XcodeWorkspace(children: [.file(.current)], url: workspaceURL).write()
|
|
}
|
|
|
|
public enum Errors: Error {
|
|
case notAnXcodeproj(String)
|
|
case missingProjectFile(String)
|
|
case xcprojWithExplicitSourceRoot
|
|
}
|
|
}
|