Compare commits

...

20 Commits

Author SHA1 Message Date
Ernesto Cambuston
0dd28421ba
fix build 2024-07-03 09:49:20 -07:00
Ernesto Cambuston
72aa193348
Merge branch 'master' into erneestoc/test-macro-expansion-override 2024-07-03 09:41:58 -07:00
Ernesto Cambuston
f8842228c5
Test action macroExpansion allows unavailable buildable reference. (#1471)
* fix buildable ref

* fix test

* add test
2024-07-03 21:41:03 +10:00
Kohki Miki
7eb5e9bd06
Remove GraphViz feature (#1485)
* Remove GraphViz to pass build on Xcode 16

* Update documentations
2024-07-02 17:06:02 +10:00
Yonas Kolb
f51719ce29
Update cache hook docs 2024-05-21 11:28:45 +10:00
Yonas Kolb
1b0720d139 Update to 2.41.0 2024-05-20 21:37:00 +10:00
Yonas Kolb
576739bcb5
Add cache command (#1476)
* add cache command

* docs: update git hook info
2024-05-20 21:32:24 +10:00
Tyler Milner
aa79a3ed0b
Fix typo in README (#1452) 2024-05-20 21:26:31 +10:00
John Flanagan
d99e448647
Include folder (SPM packages) in group sorting logic (#1466) 2024-05-17 23:14:20 +10:00
Tatsuki Otsuka
274ce7342c
Disallow the "watchOS" supported destination for multiplatform apps (#1470)
* Reject multiplatform apps that support the watchOS destination

This commit also fixes existing test cases.

* Add test cases

* Update docs

* Update changelog
2024-05-17 23:00:09 +10:00
Wolfgang Lutz
17acb4dc61
Update Examples.md (#1472)
We no longer use XcodeGen for this (because we switched this to pure SPM)
2024-04-25 11:28:06 +10:00
Yonas Kolb
447cc7f9ac remove homebrew step from release process
now handled by a bot
2024-04-10 20:59:59 +10:00
Yonas Kolb
9816466703 Update to 2.40.1 2024-04-10 20:49:49 +10:00
Yonas Kolb
bc08f53505 Revert "add xcprivacy to no build phase default (#1464)"
This reverts commit ecb9b560ae.
2024-04-10 20:46:22 +10:00
Yonas Kolb
0301741002 Update to 2.40.0 2024-04-08 11:27:53 +10:00
Yonas Kolb
ecb9b560ae
add xcprivacy to no build phase default (#1464) 2024-04-08 11:14:32 +10:00
Hilton Campbell
632ca2d419
Enable adding local Swift packages to the project root (#1413)
* Enable adding local Swift packages to the project root

* Update CHANGELOG.md
2024-04-07 23:18:24 +10:00
Balazs Perlaki-Horvath
1645d419ce
Add shell to ExecuteAction (#1430)
* Add shell to ExecuteAction

* Update to 2.38.1

* Add shell argument to jsons
2024-04-07 22:38:04 +10:00
Yonas Kolb
6694943ad8
add watchOS supported destination to docs 2024-04-07 22:33:42 +10:00
Yonas Kolb
9df3e62734
fix commandLineArguments example 2024-04-07 21:27:24 +10:00
27 changed files with 394 additions and 269 deletions

View File

@ -2,6 +2,39 @@
## Next Version
- Removed `xcodegen dump --type graphviz` feature. @giginet
## 2.41.0
### Added
- Added `xcodegen cache` command that writes the cache. Useful for `post-commit` git hook integration #1476 @yonaskolb
### Changed
- Include folders in file sorting #1466 @jflan-dd
### Fixed
- Fixed `supportedDestinations` validation when it contains watchOS for multiplatform apps. #1470 @tatsuky
## 2.40.1
### Fixed
- Reverted `.xcprivacy` handling. They will now again be treated as resources by default @yonaskolb
## 2.40.0
### Added
- Added support for local Swift packages at the project root by specifying a "" group #1413 @hiltonc
- Added a custom `shell` to a scheme's pre and post actions #1430 @balazs-vimn
### Changed
- `.xcprivacy` files are now not added to any build phases by default #1464 @yonaskolb
## 2.39.1
### Added

View File

@ -2,7 +2,6 @@
These are a bunch of real world examples of XcodeGen project specs. Feel free to add your own via PR.
- [num42/RxUserDefaults](https://github.com/num42/RxUserDefaults/blob/master/Examples/CocoaPodsExample.yml)
- [toshi0383/Bitrise-iOS](https://github.com/toshi0383/Bitrise-iOS/blob/master/project.yml)
- [johndpope/swift-models](https://github.com/johndpope/swift-models/tree/stable/Inference)
- [atelier-socle/AppRepositoryTemplate](https://github.com/atelier-socle/AppRepositoryTemplate/blob/master/project.yml)

View File

@ -9,10 +9,14 @@ Absolutely. You will get the most out of XcodeGen by adding your project to your
>Note that you can run `xcodegen` as a step in your build process on CI.
## What happens when I switch branches
If files were added or removed in the new checkout you will most likely need to run `xcodegen` again so that your project will reference all your files. Unfortunately this is a manual step at the moment, but in the future this could be automated.
If files were added or removed in the new checkout you will most likely need to run `xcodegen` again so that your project will reference all your files.
For now you can always add xcodegen as a git `post-checkout` hook.
It's recommended to use `--use-cache` so that the project is not needlessly generated.
It's recommended to set up some [git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) to automate the process:
- run `xcodegen generate --use-cache` on the following hooks. This will make sure the project is up to date when checking out, merging and rebasing
- `post-checkout`
- `post-rewrite`
- `post-merge`
- run `xcodegen cache` on `pre-commit`. This will make sure that when switching branches the cache will be updated in case you made local changes, or are ammending a commit that added a new file.
## Can I use CocoaPods
Yes, you will just need to run `pod install` after the project is generated to integrate Cocoapods changes.

View File

@ -147,7 +147,7 @@ Note that target names can also be changed by adding a `name` property to a targ
- [ ] **transitivelyLinkDependencies**: **Bool** - If this is `true` then targets will link to the dependencies of their target dependencies. If a target should embed its dependencies, such as application and test bundles, it will embed these transitive dependencies as well. Some complex setups might want to set this to `false` and explicitly specify dependencies at every level. Targets can override this with [Target](#target).transitivelyLinkDependencies. Defaults to `false`.
- [ ] **generateEmptyDirectories**: **Bool** - If this is `true` then empty directories will be added to project too else will be missed. Defaults to `false`.
- [ ] **findCarthageFrameworks**: **Bool** - When this is set to `true`, all the individual frameworks for Carthage framework dependencies will automatically be found. This property can be overridden individually for each carthage dependency - for more details see See **findFrameworks** in the [Dependency](#dependency) section. Defaults to `false`.
- [ ] **localPackagesGroup**: **String** - The group name that local packages are put into. This defaults to `Packages`
- [ ] **localPackagesGroup**: **String** - The group name that local packages are put into. This defaults to `Packages`. Use `""` to specify the project root.
- [ ] **fileTypes**: **[String: [FileType](#filetype)]** - A list of default file options for specific file extensions across the project. Values in [Sources](#sources) will overwrite these settings.
- [ ] **preGenCommand**: **String** - A bash command to run before the project has been generated. If the project isn't generated due to no changes when using the cache then this won't run. This is useful for running things like generating resources files before the project is regenerated.
- [ ] **postGenCommand**: **String** - A bash command to run after the project has been generated. If the project isn't generated due to no changes when using the cache then this won't run. This is useful for running things like `pod install` only if the project is actually regenerated.
@ -483,6 +483,7 @@ This will provide a mix of default build settings for the chosen platform destin
- `macOS`
- `macCatalyst`
- `visionOS`
- `watchOS`
```yaml
targets:
@ -499,7 +500,7 @@ targets:
destinationFilters: [iOS]
```
Note that the definition of supported destinations can be applied to every type of bundle making everything more easy to manage (app targets, unit tests, UI tests etc).
Note that the definition of supported destinations can be applied to almost every type of bundle making everything more easy to manage (app targets, unit tests, UI tests etc). App targets currently do not support the watchOS destination. Create a separate target using `platform` for watchOS apps. See Apple's [Configuring a multiplatform app](https://developer.apple.com/documentation/xcode/configuring-a-multiplatform-app-target) for details.
### Sources
@ -1140,12 +1141,16 @@ schemes:
MyTarget2: [run, archive]
run:
config: prod-debug
commandLineArguments: "--option value"
commandLineArguments:
"-MyEnabledArg": true
"-MyDisabledArg": false
environmentVariables:
RUN_ENV_VAR: VALUE
test:
config: prod-debug
commandLineArguments: "--option testValue"
commandLineArguments:
"-MyEnabledArg": true
"-MyDisabledArg": false
gatherCoverageData: true
coverageTargets:
- MyTarget1
@ -1240,7 +1245,7 @@ Swift packages are defined at a project level, and then linked to individual tar
### Local Package
- [x] **path**: **String** - the path to the package in local. The path must be directory with a `Package.swift`.
- [ ] **group** : **String**- Optional path that specifies the location where the package will live in your xcode project.
- [ ] **group** : **String**- Optional path that specifies the location where the package will live in your xcode project. Use `""` to specify the project root.
```yml
packages:

View File

@ -1,14 +1,12 @@
TOOL_NAME = XcodeGen
export EXECUTABLE_NAME = xcodegen
VERSION = 2.39.1
VERSION = 2.41.0
PREFIX = /usr/local
INSTALL_PATH = $(PREFIX)/bin/$(EXECUTABLE_NAME)
SHARE_PATH = $(PREFIX)/share/$(EXECUTABLE_NAME)
CURRENT_PATH = $(PWD)
REPO = https://github.com/yonaskolb/$(TOOL_NAME)
RELEASE_TAR = $(REPO)/archive/$(VERSION).tar.gz
SHA = $(shell curl -L -s $(RELEASE_TAR) | shasum -a 256 | sed 's/ .*//')
SWIFT_BUILD_FLAGS = --disable-sandbox -c release --arch arm64 --arch x86_64
BUILD_PATH = $(shell swift build $(SWIFT_BUILD_FLAGS) --show-bin-path)
EXECUTABLE_PATH = $(BUILD_PATH)/$(EXECUTABLE_NAME)
@ -42,10 +40,6 @@ release:
publish: archive brew
echo "published $(VERSION)"
brew:
brew update
brew bump-formula-pr --url=$(RELEASE_TAR) XcodeGen
archive: build
./scripts/archive.sh "$(EXECUTABLE_PATH)"
swift package plugin --allow-writing-to-package-directory generate-artifact-bundle \

View File

@ -18,15 +18,6 @@
"version" : "0.0.6"
}
},
{
"identity" : "graphviz",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftDocOrg/GraphViz.git",
"state" : {
"revision" : "70bebcf4597b9ce33e19816d6bbd4ba9b7bdf038",
"version" : "0.2.0"
}
},
{
"identity" : "jsonutilities",
"kind" : "remoteSourceControl",

View File

@ -19,7 +19,6 @@ let package = Package(
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "8.13.0"),
.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/SwiftDocOrg/GraphViz.git", exact: "0.2.0"),
.package(url: "https://github.com/freddi-kit/ArtifactBundleGen", exact: "0.0.6")
],
targets: [
@ -41,7 +40,6 @@ let package = Package(
"XcodeProj",
"PathKit",
"XcodeGenCore",
"GraphViz",
], resources: [
.copy("SettingPresets")
]),

View File

@ -35,7 +35,6 @@ The project spec is a YAML or JSON file that defines your targets, configuration
- ✅ Distribute your spec amongst multiple files for easy **sharing** and overriding
- ✅ Easily create **multi-platform** frameworks
- ✅ Integrate **Carthage** frameworks without any work
- ✅ Export **Dependency Diagrams** to view in [Graphviz](https://www.graphviz.org)
Given an example project spec:
@ -113,7 +112,7 @@ swift run xcodegen
Add the following to your Package.swift file's dependencies:
```swift
.package(url: "https://github.com/yonaskolb/XcodeGen.git", from: "2.39.1"),
.package(url: "https://github.com/yonaskolb/XcodeGen.git", from: "2.41.0"),
```
And then import wherever needed: `import XcodeGenKit`
@ -136,28 +135,7 @@ Options:
- **--use-cache**: Used to prevent unnecessarily generating the project. If this is set, then a cache file will be written to when a project is generated. If `xcodegen` is later run but the spec and all the files it contains are the same, the project won't be generated.
- **--cache-path**: A custom path to use for your cache file. This defaults to `~/.xcodegen/cache/{PROJECT_SPEC_PATH_HASH}`
There are other commands as well such as `xcodegen dump` which lets out output the resolved spec in many different formats, or write it to a file. Use `xcodegen help` to see more detailed usage information.
## Dependency Diagrams
<details>
<summary>Click to expand!</summary>
#### How to export dependency diagrams:
To stdout:
```
xcodegen dump --type graphviz
```
To a file:
```
xcodegen dump --type graphviz --file Graph.viz
```
During implementation, `graphviz` formatting was validated using [GraphvizOnline](https://dreampuf.github.io/GraphvizOnline/), [WebGraphviz](http://www.webgraphviz.com), and [Graphviz on MacOS](https://graphviz.org).
</details>
There are other commands as well such as `xcodegen dump` which lets one output the resolved spec in many different formats, or write it to a file. Use `xcodegen help` to see more detailed usage information.
## Editing
```shell

View File

@ -8,4 +8,3 @@
1. Push commit and tag to github
1. Create release from tag on GitHub using the version number and relevant changelog contents
1. Run `make archive` and upload `xcodegen.zip` and `xcodegen.artifactbundle.zip` to the github release
1. Run `make brew` which will open a PR on homebrew core

View File

@ -88,10 +88,12 @@ public struct Scheme: Equatable {
public var script: String
public var name: String
public var settingsTarget: String?
public init(name: String, script: String, settingsTarget: String? = nil) {
public var shell: String?
public init(name: String, script: String, shell: String? = nil, settingsTarget: String? = nil) {
self.script = script
self.name = name
self.settingsTarget = settingsTarget
self.shell = shell
}
}
@ -403,6 +405,7 @@ extension Scheme.ExecutionAction: JSONObjectConvertible {
script = try jsonDictionary.json(atKeyPath: "script")
name = jsonDictionary.json(atKeyPath: "name") ?? "Run Script"
settingsTarget = jsonDictionary.json(atKeyPath: "settingsTarget")
shell = jsonDictionary.json(atKeyPath: "shell")
}
}
@ -412,6 +415,7 @@ extension Scheme.ExecutionAction: JSONEncodable {
"script": script,
"name": name,
"settingsTarget": settingsTarget,
"shell": shell
]
}
}

View File

@ -185,6 +185,12 @@ extension Project {
errors.append(.unexpectedTargetPlatformForSupportedDestinations(target: target.name, platform: target.platform))
}
if let supportedDestinations = target.supportedDestinations,
target.type.isApp,
supportedDestinations.contains(.watchOS) {
errors.append(.containsWatchOSDestinationForMultiplatformApp(target: target.name))
}
if target.supportedDestinations?.contains(.macOS) == true,
target.supportedDestinations?.contains(.macCatalyst) == true {

View File

@ -19,6 +19,7 @@ public struct SpecValidationError: Error, CustomStringConvertible {
case invalidTargetSchemeTest(target: String, testTarget: String)
case invalidTargetPlatformForSupportedDestinations(target: String)
case unexpectedTargetPlatformForSupportedDestinations(target: String, platform: Platform)
case containsWatchOSDestinationForMultiplatformApp(target: String)
case multipleMacPlatformsInSupportedDestinations(target: String)
case missingTargetPlatformInSupportedDestinations(target: String, platform: Platform)
case invalidSchemeTarget(scheme: String, target: String, action: String)
@ -66,6 +67,8 @@ public struct SpecValidationError: Error, CustomStringConvertible {
return "Target \(target.quoted) has multiple definitions of mac platforms in supported destinations"
case let .missingTargetPlatformInSupportedDestinations(target, platform):
return "Target \(target.quoted) has platform \(platform.rawValue.quoted) that is missing in supported destinations"
case let .containsWatchOSDestinationForMultiplatformApp(target):
return "Multiplatform app \(target.quoted) cannot contain watchOS in \"supportedDestinations\". Create a separate target using \"platform\" for watchOS apps"
case let .invalidConfigFile(configFile, config):
return "Invalid config file \(configFile.quoted) for config \(config.quoted)"
case let .invalidSchemeTarget(scheme, target, action):

View File

@ -3,6 +3,6 @@ import ProjectSpec
import XcodeGenCLI
import Version
let version = Version("2.39.1")
let version = Version("2.41.0")
let cli = XcodeGenCLI(version: version)
cli.execute()

View File

@ -0,0 +1,44 @@
import Foundation
import PathKit
import ProjectSpec
import SwiftCLI
import XcodeGenKit
import XcodeProj
import Version
class CacheCommand: ProjectCommand {
@Key("--cache-path", description: "Where the cache file will be loaded from and save to. Defaults to ~/.xcodegen/cache/{SPEC_PATH_HASH}")
var cacheFilePath: Path?
init(version: Version) {
super.init(version: version,
name: "cache",
shortDescription: "Write the project cache")
}
override func execute(specLoader: SpecLoader, projectSpecPath: Path, project: Project) throws {
let cacheFilePath = self.cacheFilePath ?? Path("~/.xcodegen/cache/\(projectSpecPath.absolute().string.md5)").absolute()
var cacheFile: CacheFile?
// generate cache
do {
cacheFile = try specLoader.generateCacheFile()
} catch {
throw GenerationError.projectSpecParsingError(error)
}
// write cache
if let cacheFile = cacheFile {
do {
try cacheFilePath.parent().mkpath()
try cacheFilePath.write(cacheFile.string)
success("Wrote cache to \(cacheFilePath)")
} catch {
info("Failed to write cache: \(error.localizedDescription)")
}
}
}
}

View File

@ -41,15 +41,13 @@ class DumpCommand: ProjectCommand {
output = try Yams.dump(object: project.toJSONDictionary())
case .summary:
output = project.debugDescription
case .graphviz:
output = GraphVizGenerator().generateModuleGraphViz(targets: project.targets)
}
if let file = file {
try file.parent().mkpath()
try file.write(output)
} else {
stdout.print(output)
success(output)
}
}
}
@ -61,7 +59,6 @@ private enum DumpType: String, ConvertibleFromString, CaseIterable {
case parsedJSON = "parsed-json"
case parsedYaml = "parsed-yaml"
case summary
case graphviz
static var defaultValue: DumpType { .yaml }
}

View File

@ -8,9 +8,6 @@ import Version
class GenerateCommand: ProjectCommand {
@Flag("-q", "--quiet", description: "Suppress all informational and success output")
var quiet: Bool
@Flag("-c", "--use-cache", description: "Use a cache for the xcodegen spec. This will prevent unnecessarily generating the project if nothing has changed")
var useCache: Bool
@ -46,7 +43,7 @@ class GenerateCommand: ProjectCommand {
Path("~/.xcodegen/cache/\(projectSpecPath.absolute().string.md5)").absolute()
var cacheFile: CacheFile?
// read cache
// generate cache
if useCache || self.cacheFilePath != nil {
do {
cacheFile = try specLoader.generateCacheFile()
@ -138,22 +135,4 @@ class GenerateCommand: ProjectCommand {
try Task.run(bash: command, directory: projectDirectory.absolute().string)
}
}
func info(_ string: String) {
if !quiet {
stdout.print(string)
}
}
func warning(_ string: String) {
if !quiet {
stdout.print(string.yellow)
}
}
func success(_ string: String) {
if !quiet {
stdout.print(string.green)
}
}
}

View File

@ -12,6 +12,9 @@ class ProjectCommand: Command {
let name: String
let shortDescription: String
@Flag("-q", "--quiet", description: "Suppress all informational and success output")
var quiet: Bool
@Key("-s", "--spec", description: "The path to the project spec file. Defaults to project.yml. (It is also possible to link to multiple spec files by comma separating them. Note that all other flags will be the same.)")
var spec: String?
@ -58,4 +61,22 @@ class ProjectCommand: Command {
}
func execute(specLoader: SpecLoader, projectSpecPath: Path, project: Project) throws {}
func info(_ string: String) {
if !quiet {
stdout.print(string)
}
}
func warning(_ string: String) {
if !quiet {
stdout.print(string.yellow)
}
}
func success(_ string: String) {
if !quiet {
stdout.print(string.green)
}
}
}

View File

@ -15,6 +15,7 @@ public class XcodeGenCLI {
description: "Generates Xcode projects",
commands: [
generateCommand,
CacheCommand(version: version),
DumpCommand(version: version),
]
)

View File

@ -1,66 +0,0 @@
import DOT
import Foundation
import GraphViz
import ProjectSpec
extension Dependency {
var graphVizName: String {
switch self.type {
case .bundle, .package, .sdk, .framework, .carthage:
return "[\(self.type)]\\n\(reference)"
case .target:
return reference
}
}
}
extension Dependency.DependencyType: CustomStringConvertible {
public var description: String {
switch self {
case .bundle: return "bundle"
case .package: return "package"
case .framework: return "framework"
case .carthage: return "carthage"
case .sdk: return "sdk"
case .target: return "target"
}
}
}
extension Node {
init(target: Target) {
self.init(target.name)
self.shape = .box
}
init(dependency: Dependency) {
self.init(dependency.reference)
self.shape = .box
self.label = dependency.graphVizName
}
}
public class GraphVizGenerator {
public init() {}
public func generateModuleGraphViz(targets: [Target]) -> String {
return DOTEncoder().encode(generateGraph(targets: targets))
}
func generateGraph(targets: [Target]) -> Graph {
var graph = Graph(directed: true)
targets.forEach { target in
target.dependencies.forEach { dependency in
let from = Node(target: target)
graph.append(from)
let to = Node(dependency: dependency)
graph.append(to)
var edge = Edge(from: from, to: to)
edge.style = .dashed
graph.append(edge)
}
}
return graph
}
}

View File

@ -631,8 +631,8 @@ public class PBXProjGenerator {
}
if let order = groupOrdering?.order {
let files = group.children.filter { $0 is PBXFileReference }
var groups = group.children.filter { $0 is PBXGroup }
let files = group.children.filter { !$0.isGroupOrFolder }
var groups = group.children.filter { $0.isGroupOrFolder }
var filteredGroups = [PBXFileElement]()
@ -1626,6 +1626,10 @@ extension Platform {
}
extension PBXFileElement {
/// - returns: `true` if the element is a group or a folder reference. Likely an SPM package.
var isGroupOrFolder: Bool {
self is PBXGroup || (self as? PBXFileReference)?.lastKnownFileType == "folder"
}
public func getSortOrder(groupSortPosition: SpecOptions.GroupSortPosition) -> Int {
if type(of: self).isa == "PBXGroup" {

View File

@ -208,7 +208,12 @@ public class SchemeGenerator {
.first { settingsTarget == $0.buildableReference.blueprintName }?
.buildableReference
}
return XCScheme.ExecutionAction(scriptText: action.script, title: action.name, environmentBuildable: environmentBuildable)
return XCScheme.ExecutionAction(
scriptText: action.script,
title: action.name,
shellToInvoke: action.shell,
environmentBuildable: environmentBuildable
)
}
let schemeTarget: ProjectTarget?
@ -228,15 +233,6 @@ public class SchemeGenerator {
let buildableReference = buildActionEntries.first(where: { $0.buildableReference.blueprintName == schemeTarget?.name })?.buildableReference ?? buildActionEntries.first!.buildableReference
let runnables = makeProductRunnables(for: schemeTarget, buildableReference: buildableReference)
let testMacroExpansion: XCScheme.BuildableReference = buildActionEntries.first(
where: { value in
if let macroExpansion = scheme.test?.macroExpansion {
return value.buildableReference.blueprintName == macroExpansion
}
return false
}
)?.buildableReference ?? buildableReference
let buildAction = XCScheme.BuildAction(
buildActionEntries: buildActionEntries,
preActions: scheme.build.preActions.map(getExecutionAction),
@ -290,6 +286,17 @@ public class SchemeGenerator {
let testPlans = scheme.test?.testPlans.enumerated().map { index, testPlan in
XCScheme.TestPlanReference(reference: "container:\(testPlan.path)", default: defaultTestPlanIndex == index)
} ?? []
let testBuildableEntries = buildActionEntries.filter({ $0.buildFor.contains(.testing) }) + testBuildTargetEntries
let testMacroExpansionBuildableRef = testBuildableEntries.map(\.buildableReference).contains(buildableReference) ? buildableReference : testBuildableEntries.first?.buildableReference
let testMacroExpansion: XCScheme.BuildableReference = buildActionEntries.first(
where: { value in
if let macroExpansion = scheme.test?.macroExpansion {
return value.buildableReference.blueprintName == macroExpansion
}
return false
}
)?.buildableReference ?? testMacroExpansionBuildableRef ?? buildableReference
let testAction = XCScheme.TestAction(
buildConfiguration: scheme.test?.config ?? defaultDebugConfig.name,

View File

@ -59,7 +59,7 @@ class SourceGenerator {
}
let absolutePath = project.basePath + path.normalize()
// Get the local package's relative path from the project root
let fileReferencePath = try? absolutePath.relativePath(from: projectDirectory ?? project.basePath).string
@ -72,8 +72,12 @@ class SourceGenerator {
)
)
let parentGroups = parentGroup.components(separatedBy: "/")
createParentGroups(parentGroups, for: fileReference)
if parentGroup == "" {
rootGroups.insert(fileReference)
} else {
let parentGroups = parentGroup.components(separatedBy: "/")
createParentGroups(parentGroups, for: fileReference)
}
}
/// Collects an array complete of all `SourceFile` objects that make up the target based on the provided `TargetSource` definitions.

View File

@ -203,6 +203,32 @@ class ProjectSpecTests: XCTestCase {
try expectValidationError(project, .unexpectedTargetPlatformForSupportedDestinations(target: "target1", platform: .watchOS))
}
$0.it("watchOS in multiplatform app's supported destinations") {
var project = baseProject
project.targets = [
Target(
name: "target1",
type: .application,
platform: .auto,
supportedDestinations: [.watchOS]
)
]
try expectValidationError(project, .containsWatchOSDestinationForMultiplatformApp(target: "target1"))
}
$0.it("watchOS in non-app's supported destinations") {
var project = baseProject
project.targets = [
Target(
name: "target1",
type: .framework,
platform: .auto,
supportedDestinations: [.watchOS]
)
]
try expectNoValidationError(project, .containsWatchOSDestinationForMultiplatformApp(target: "target1"))
}
$0.it("multiple definitions of mac platform in supported destinations") {
var project = baseProject
project.targets = [

View File

@ -1,104 +0,0 @@
import ProjectSpec
import Spectre
@testable import XcodeGenKit
import XCTest
private let app = Target(
name: "MyApp",
type: .application,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_1": "VALUE"]),
dependencies: [
Dependency(type: .target, reference: "MyInternalFramework"),
Dependency(type: .bundle, reference: "Resources"),
Dependency(type: .carthage(findFrameworks: true, linkType: .static), reference: "MyStaticFramework"),
Dependency(type: .carthage(findFrameworks: true, linkType: .dynamic), reference: "MyDynamicFramework"),
Dependency(type: .framework, reference: "MyExternalFramework"),
Dependency(type: .package(products: ["MyPackage"]), reference: "MyPackage"),
Dependency(type: .sdk(root: "MySDK"), reference: "MySDK"),
]
)
private let framework = Target(
name: "MyFramework",
type: .framework,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
private let uiTest = Target(
name: "MyAppUITests",
type: .uiTestBundle,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_3": "VALUE"]),
dependencies: [Dependency(type: .target, reference: "MyApp")]
)
private let targets = [app, framework, uiTest]
class GraphVizGeneratorTests: XCTestCase {
func testGraphOutput() throws {
describe {
let graph = GraphVizGenerator().generateGraph(targets: targets)
$0.it("generates the expected number of nodes") {
try expect(graph.nodes.count) == 16
}
$0.it("generates box nodes") {
try expect(graph.nodes.filter { $0.shape == .box }.count) == 16
}
$0.it("generates the expected carthage nodes") {
try expect(graph.nodes.filter { $0.label?.contains("[carthage]") ?? false }.count) == 2
}
$0.it("generates the expected sdk nodes") {
try expect(graph.nodes.filter { $0.label?.contains("[sdk]") ?? false }.count) == 1
}
$0.it("generates the expected Framework nodes") {
try expect(graph.nodes.filter { $0.label?.contains("[framework]") ?? false }.count) == 1
}
$0.it("generates the expected package nodes") {
try expect(graph.nodes.filter { $0.label?.contains("[package]") ?? false }.count) == 1
}
$0.it("generates the expected bundle nodes") {
try expect(graph.nodes.filter { $0.label?.contains("[bundle]") ?? false }.count) == 1
}
$0.it("generates the expected edges") {
try expect(graph.edges.count) == 8
}
$0.it("generates dashed edges") {
try expect(graph.edges.filter { $0.style == .dashed }.count) == 8
}
$0.it("generates the expected output") {
let output = GraphVizGenerator().generateModuleGraphViz(targets: targets)
try expect(output) == """
digraph {
MyApp [shape=box]
MyInternalFramework [label=MyInternalFramework shape=box]
MyApp [shape=box]
Resources [label="[bundle]\\nResources" shape=box]
MyApp [shape=box]
MyStaticFramework [label="[carthage]\\nMyStaticFramework" shape=box]
MyApp [shape=box]
MyDynamicFramework [label="[carthage]\\nMyDynamicFramework" shape=box]
MyApp [shape=box]
MyExternalFramework [label="[framework]\\nMyExternalFramework" shape=box]
MyApp [shape=box]
MyPackage [label="[package]\\nMyPackage" shape=box]
MyApp [shape=box]
MySDK [label="[sdk]\\nMySDK" shape=box]
MyAppUITests [shape=box]
MyApp [label=MyApp shape=box]
MyApp -> MyInternalFramework [style=dashed]
MyApp -> Resources [style=dashed]
MyApp -> MyStaticFramework [style=dashed]
MyApp -> MyDynamicFramework [style=dashed]
MyApp -> MyExternalFramework [style=dashed]
MyApp -> MyPackage [style=dashed]
MyApp -> MySDK [style=dashed]
MyAppUITests -> MyApp [style=dashed]
}
"""
}
}
}
}

View File

@ -259,6 +259,96 @@ class PBXProjGeneratorTests: XCTestCase {
.map { $0.nameOrPath }
try expect(screenGroups) == ["mainScreen1.swift", "mainScreen2.swift", "View", "Presenter", "Interactor", "Entities", "Assembly"]
}
$0.it("sorts SPM packages") {
var options = SpecOptions()
options.groupSortPosition = .top
options.groupOrdering = [
GroupOrdering(
order: [
"Sources",
"Resources",
"Tests",
"Packages",
"Support files",
"Configurations",
]
),
GroupOrdering(
pattern: "Packages",
order: [
"FeatureA",
"FeatureB",
"Common",
]
),
]
let directories = """
Configurations:
- file.swift
Resources:
- file.swift
Sources:
- MainScreen:
- mainScreen1.swift
- mainScreen2.swift
- Assembly:
- file.swift
- Entities:
- file.swift
- Interactor:
- file.swift
- Presenter:
- file.swift
- View:
- file.swift
Support files:
- file.swift
Packages:
- Common:
- Package.swift
- FeatureA:
- Package.swift
- FeatureB:
- Package.swift
Tests:
- file.swift
UITests:
- file.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Configurations", "Resources", "Sources", "Support files", "Tests", "UITests"])
let project = Project(
basePath: directoryPath,
name: "Test",
targets: [target],
packages: [
"Common": .local(path: "Packages/Common", group: nil),
"FeatureA": .local(path: "Packages/FeatureA", group: nil),
"FeatureB": .local(path: "Packages/FeatureB", group: nil),
],
options: options
)
let projGenerator = PBXProjGenerator(project: project)
let pbxProj = try project.generatePbxProj()
let group = try pbxProj.getMainGroup()
projGenerator.setupGroupOrdering(group: group)
let mainGroups = group.children.map { $0.nameOrPath }
try expect(mainGroups) == ["Sources", "Resources", "Tests", "Packages", "Support files", "Configurations", "UITests", "Products"]
let packages = group.children
.first { $0.nameOrPath == "Packages" }
.flatMap { $0 as? PBXGroup }?
.children
.map(\.nameOrPath)
try expect(packages) == ["FeatureA", "FeatureB", "Common"]
}
}
}

View File

@ -485,8 +485,8 @@ class ProjectGeneratorTests: XCTestCase {
try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer"
}
$0.it("supportedDestinations merges settings - iOS, watchOS") {
let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.iOS, .watchOS])
$0.it("supportedDestinations merges settings - iOS, watchOS (framework)") {
let target = Target(name: "Target", type: .framework, platform: .auto, supportedDestinations: [.iOS, .watchOS])
let project = Project(name: "", targets: [target])
let pbxProject = try project.generatePbxProj()
@ -499,8 +499,8 @@ class ProjectGeneratorTests: XCTestCase {
try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true
}
$0.it("supportedDestinations merges settings - visionOS, watchOS") {
let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.visionOS, .watchOS])
$0.it("supportedDestinations merges settings - visionOS, watchOS (framework)") {
let target = Target(name: "Target", type: .framework, platform: .auto, supportedDestinations: [.visionOS, .watchOS])
let project = Project(name: "", targets: [target])
let pbxProject = try project.generatePbxProj()
@ -1642,6 +1642,74 @@ class ProjectGeneratorTests: XCTestCase {
try expect(file.product?.productName) == "XcodeGen"
}
$0.it("generates local swift packages at the top level") {
let app = Target(
name: "MyApp",
type: .application,
platform: .iOS,
dependencies: [
Dependency(type: .package(products: []), reference: "XcodeGen"),
]
)
let project = Project(name: "test", targets: [app], packages: ["XcodeGen": .local(path: "../XcodeGen", group: "")])
let pbxProject = try project.generatePbxProj(specValidate: false)
let nativeTarget = try unwrap(pbxProject.nativeTargets.first(where: { $0.name == app.name }))
let localPackageFile = try unwrap(pbxProject.fileReferences.first(where: { $0.path == "../XcodeGen" }))
try expect(localPackageFile.lastKnownFileType) == "folder"
let mainGroup = try pbxProject.getMainGroup()
try expect(mainGroup.children.contains(localPackageFile)) == true
let frameworkPhases = nativeTarget.buildPhases.compactMap { $0 as? PBXFrameworksBuildPhase }
guard let frameworkPhase = frameworkPhases.first else {
return XCTFail("frameworkPhases should have more than one")
}
guard let file = frameworkPhase.files?.first else {
return XCTFail("frameworkPhase should have file")
}
try expect(file.product?.productName) == "XcodeGen"
}
$0.it("generates local swift package group at the top level") {
let app = Target(
name: "MyApp",
type: .application,
platform: .iOS,
dependencies: [
Dependency(type: .package(products: []), reference: "XcodeGen"),
]
)
let project = Project(name: "test", targets: [app], packages: ["XcodeGen": .local(path: "../XcodeGen", group: nil)], options: .init(localPackagesGroup: ""))
let pbxProject = try project.generatePbxProj(specValidate: false)
let nativeTarget = try unwrap(pbxProject.nativeTargets.first(where: { $0.name == app.name }))
let localPackageFile = try unwrap(pbxProject.fileReferences.first(where: { $0.path == "../XcodeGen" }))
try expect(localPackageFile.lastKnownFileType) == "folder"
let mainGroup = try pbxProject.getMainGroup()
try expect(mainGroup.children.contains(localPackageFile)) == true
let frameworkPhases = nativeTarget.buildPhases.compactMap { $0 as? PBXFrameworksBuildPhase }
guard let frameworkPhase = frameworkPhases.first else {
return XCTFail("frameworkPhases should have more than one")
}
guard let file = frameworkPhase.files?.first else {
return XCTFail("frameworkPhase should have file")
}
try expect(file.product?.productName) == "XcodeGen"
}
$0.it("generates info.plist") {
let plist = Plist(path: "Info.plist", attributes: ["UISupportedInterfaceOrientations": ["UIInterfaceOrientationPortrait", "UIInterfaceOrientationLandscapeLeft"]])
let tempPath = Path.temporary + "info"

View File

@ -514,6 +514,46 @@ class SchemeGeneratorTests: XCTestCase {
try expect(xcscheme.launchAction?.macroExpansion?.buildableName) == "MyApp.app"
}
$0.it("generates scheme with macroExpansion from tests when the main target is not part of the scheme") {
let app = Target(
name: "MyApp",
type: .application,
platform: .iOS,
dependencies: []
)
let mockApp = Target(
name: "MockApp",
type: .application,
platform: .iOS,
dependencies: []
)
let testBundle = Target(
name: "TestBundle",
type: .unitTestBundle,
platform: .iOS
)
let appTarget = Scheme.BuildTarget(target: .local(app.name), buildTypes: [.running])
let mockAppTarget = Scheme.BuildTarget(target: .local(mockApp.name), buildTypes: [.testing])
let testBundleTarget = Scheme.BuildTarget(target: .local(testBundle.name), buildTypes: [.testing])
let scheme = Scheme(
name: "TestScheme",
build: Scheme.Build(targets: [appTarget, mockAppTarget, testBundleTarget]),
run: Scheme.Run(config: "Debug", macroExpansion: "MyApp")
)
let project = Project(
name: "test",
targets: [app, mockApp, testBundle],
schemes: [scheme]
)
let xcodeProject = try project.generateXcodeProject()
let xcscheme = try unwrap(xcodeProject.sharedData?.schemes.first)
try expect(xcscheme.testAction?.macroExpansion?.buildableName) == "MockApp.app"
}
$0.it("generates scheme with test target of local swift package") {
let targetScheme = TargetScheme(
testTargets: [Scheme.Test.TestTarget(targetReference: TestableTargetReference(name: "XcodeGenKitTests", location: .package("XcodeGen")))])