mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2024-10-05 16:57:30 +03:00
Compare commits
18 Commits
bb2baabb07
...
78e9008555
Author | SHA1 | Date | |
---|---|---|---|
|
78e9008555 | ||
|
7eb5e9bd06 | ||
|
f51719ce29 | ||
|
1b0720d139 | ||
|
576739bcb5 | ||
|
aa79a3ed0b | ||
|
d99e448647 | ||
|
274ce7342c | ||
|
17acb4dc61 | ||
|
447cc7f9ac | ||
|
9816466703 | ||
|
bc08f53505 | ||
|
0301741002 | ||
|
ecb9b560ae | ||
|
632ca2d419 | ||
|
1645d419ce | ||
|
6694943ad8 | ||
|
9df3e62734 |
33
CHANGELOG.md
33
CHANGELOG.md
@ -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
|
||||
|
@ -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)
|
||||
|
10
Docs/FAQ.md
10
Docs/FAQ.md
@ -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.
|
||||
|
@ -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:
|
||||
|
8
Makefile
8
Makefile
@ -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 \
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
]),
|
||||
|
26
README.md
26
README.md
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,6 +402,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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -409,6 +412,7 @@ extension Scheme.ExecutionAction: JSONEncodable {
|
||||
"script": script,
|
||||
"name": name,
|
||||
"settingsTarget": settingsTarget,
|
||||
"shell": shell
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
44
Sources/XcodeGenCLI/Commands/CacheCommand.swift
Normal file
44
Sources/XcodeGenCLI/Commands/CacheCommand.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ public class XcodeGenCLI {
|
||||
description: "Generates Xcode projects",
|
||||
commands: [
|
||||
generateCommand,
|
||||
CacheCommand(version: version),
|
||||
DumpCommand(version: version),
|
||||
]
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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" {
|
||||
|
@ -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?
|
||||
|
@ -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.
|
||||
|
@ -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 = [
|
||||
|
@ -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]
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user