Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
### Added
- Added `buildArchitectures` to scheme build options and target schemes, to control Xcode's "Override Architectures" scheme setting #1642 @arhxam
- Added FAQ documentation on how to add an Xcode capability, such as In-App Purchase #1644 @Hokila
- Added experimental support for the JSON project format Xcode 27.2 writes by default. Set `options.projectFormat: json` to write `project.xcproj` instead of `project.pbxproj`. The generated project opens in Xcode 27 or later only

### Changed
- XcodeGen now requires macOS 14 and Swift 6.1, which the XcodeProj 9.17 upgrade behind the JSON project format brings with it
- Generated schemes now default to `buildArchitectures: matchRunDestination` ("Match Run Destination"), matching Xcode's default for new schemes. Previously schemes used each target's architecture settings. Set `buildArchitectures: useTargetSettings` to keep the previous behaviour #1642 @yonaskolb

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,15 @@ Note that target names can also be changed by adding a `name` property to a targ
- [ ] **tabWidth**: **Int** - If this is specified, the Xcode project will override the user's setting for indent width in number of spaces.
- [ ] **xcodeVersion**: **String** - The version of Xcode. This defaults to the latest version periodically. You can specify it in the format `0910` or `9.1`
- [ ] **projectFormat**: **String** - The version of Xcode project. By default this is set to `xcode16_0`
- `json`: experimental, the JSON format Xcode 27.2 writes. Stored as `project.xcproj` and opens only in Xcode 27 and later
- `xcode16_3`: Xcode 16.3
- `xcode16_0`: Xcode 16.0
- `xcode15_3`: Xcode 15.3
- `xcode15_0`: Xcode 15.0
- `xcode14_0`: Xcode 14.0

`json` is built on [XcodeProj's experimental support](https://github.com/tuist/XcodeProj/blob/main/Documentation/json-project-format.md) for [Apple's format](https://developer.apple.com/documentation/xcode/updating-your-xcode-project-configuration-file-format). Two things still generate a project Xcode will not open, so open yours before you commit it: a `copyFiles` [target source](#target-source) ([#1181](https://github.com/tuist/XcodeProj/pull/1181)), and a dependency through a [project reference](#project-reference) ([#1182](https://github.com/tuist/XcodeProj/pull/1182)). Both are fixed upstream but not yet released.

- [ ] **deploymentTarget**: **[[Platform](#platform): String]** - A project wide deployment target can be specified for each platform otherwise the default SDK version in Xcode will be used. This will be overridden by any custom build settings that set the deployment target eg `IPHONEOS_DEPLOYMENT_TARGET`. Target specific deployment targets can also be set with [Target](#target).deploymentTarget.
- [ ] **disabledValidations**: **[String]** - A list of validations that can be disabled if they're too strict for your use case. By default this is set to an empty array. Currently these are the available options:
- `missingConfigs`: Disable errors for configurations in yaml files that don't exist in the project itself. This can be useful if you include the same yaml file in different projects
Expand Down
13 changes: 11 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PackageDescription

let package = Package(
name: "XcodeGen",
platforms: [.macOS(.v11)],
platforms: [.macOS(.v14)],
products: [
.executable(name: "xcodegen", targets: ["XcodeGen"]),
.library(name: "XcodeGenKit", targets: ["XcodeGenKit"]),
Expand All @@ -16,7 +16,7 @@ let package = Package(
.package(url: "https://github.com/yonaskolb/JSONUtilities.git", from: "4.2.0"),
.package(url: "https://github.com/kylef/Spectre.git", from: "0.9.2"),
.package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.0"),
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "9.14.0"),
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "9.17.1"),
.package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.3"),
.package(url: "https://github.com/mxcl/Version", from: "2.0.0"),
.package(url: "https://github.com/freddi-kit/ArtifactBundleGen", exact: "0.0.8")
Expand Down
1 change: 1 addition & 0 deletions Sources/ProjectSpec/Linkage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ extension Target {
.application,
.bundle,
.commandLineTool,
.hostBuildTool,
.instrumentsPackage,
.intentsServiceExtension,
.messagesApplication,
Expand Down
10 changes: 9 additions & 1 deletion Sources/XcodeGenKit/FileWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ public class FileWriter {
if projectPath.exists {
try projectPath.copy(tempPath)
}
try xcodeProject.write(path: tempPath, override: true)
let isJSON = project.projectFormat.isJSON
if isJSON {
// Discarded: encoding the property list is what settles XcodeProj's object identifiers,
// without which `project.xcproj` gets fresh UUIDs per run. Drop with XcodeProj#1180.
_ = try xcodeProject.pbxproj.dataRepresentation(outputSettings: PBXOutputSettings())
}
try xcodeProject.write(path: tempPath, override: true, format: isJSON ? .xcproj : .pbxproj)
// The copy above may carry the previous format's file, which would win the next read.
try? (isJSON ? XcodeProj.pbxprojPath(tempPath) : XcodeProj.xcprojPath(tempPath)).delete()
try? projectPath.delete()
try tempPath.copy(projectPath)
try? tempPath.delete()
Expand Down
11 changes: 9 additions & 2 deletions Sources/XcodeGenKit/ProjectFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ public extension ProjectFormat {
}

public enum ProjectFormat: String {
case json
case xcode16_3
case xcode16_0
case xcode15_3
case xcode15_0
case xcode14_0

public var isJSON: Bool {
self == .json
}

public var objectVersion: UInt {
switch self {
// `project.xcproj` stores no object version; 77 is what XcodeProj assigns reading one back.
case .json: 77
case .xcode16_3: 90
case .xcode16_0: 77
case .xcode15_3: 63
Expand All @@ -22,13 +29,13 @@ public enum ProjectFormat: String {
public var preferredProjectObjectVersion: UInt? {
switch self {
case .xcode16_3, .xcode16_0: objectVersion
case .xcode15_3, .xcode15_0, .xcode14_0: nil
case .json, .xcode15_3, .xcode15_0, .xcode14_0: nil
}
}

public var compatibilityVersion: String? {
switch self {
case .xcode16_3, .xcode16_0: nil
case .json, .xcode16_3, .xcode16_0: nil
case .xcode15_3: "Xcode 15.3"
case .xcode15_0: "Xcode 15.0"
case .xcode14_0: "Xcode 14.0"
Expand Down
64 changes: 64 additions & 0 deletions Tests/XcodeGenKitTests/ProjectGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,70 @@ class ProjectGeneratorTests: XCTestCase {
}
}

func testProjectFormat() throws {

func writeProject(format: String?, to basePath: Path) throws -> Path {
try basePath.mkpath()
let project = Project(
basePath: basePath,
name: "MyProject",
targets: [framework],
options: SpecOptions(projectFormat: format)
)
let xcodeProject = try ProjectGenerator(project: project).generateXcodeProject(userName: "someUser")
try FileWriter(project: project).writeXcodeProject(xcodeProject)
return project.defaultProjectPath
}

describe {

$0.it("writes project.pbxproj by default") {
let basePath = Path.temporary + "projectFormat/default"
try? basePath.delete()
let projectPath = try writeProject(format: nil, to: basePath)

try expect(XcodeProj.pbxprojPath(projectPath).exists).beTrue()
try expect(XcodeProj.xcprojPath(projectPath).exists).beFalse()
}

$0.it("writes project.xcproj for the json format") {
let basePath = Path.temporary + "projectFormat/json"
try? basePath.delete()
let projectPath = try writeProject(format: "json", to: basePath)

try expect(XcodeProj.xcprojPath(projectPath).exists).beTrue()
try expect(XcodeProj.pbxprojPath(projectPath).exists).beFalse()

let written = try XcodeProj(path: projectPath)
try expect(written.projectFormat) == .xcproj
try expect(written.pbxproj.nativeTargets.map(\.name)) == [framework.name]
}

$0.it("writes the same project.xcproj on every generation") {
let basePath = Path.temporary + "projectFormat/jsonRepeated"
try? basePath.delete()
let projectPath = try writeProject(format: "json", to: basePath)
let first: Data = try XcodeProj.xcprojPath(projectPath).read()

_ = try writeProject(format: "json", to: basePath)
let second: Data = try XcodeProj.xcprojPath(projectPath).read()

try expect(first) == second
}

$0.it("removes the file of the format it replaces") {
let basePath = Path.temporary + "projectFormat/switching"
try? basePath.delete()
let projectPath = try writeProject(format: "json", to: basePath)
try expect(XcodeProj.xcprojPath(projectPath).exists).beTrue()

_ = try writeProject(format: "xcode16_0", to: basePath)
try expect(XcodeProj.pbxprojPath(projectPath).exists).beTrue()
try expect(XcodeProj.xcprojPath(projectPath).exists).beFalse()
}
}
}

func testConfigGenerator() {
describe {

Expand Down
Loading