Skip to content
Merged
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
8 changes: 8 additions & 0 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1256,21 +1256,29 @@ Swift packages are defined at a project level, and then linked to individual tar
- `branch: master`
- `revision: xxxxxx`
- [ ] **github** : **String**- this is an optional helper you can use for github repos. Instead of specifying the full url in `url` you can just specify the github org and repo
- [ ] **traits**: **[String]** - Optional Swift package traits to enable for this package reference. Trait names are written to the Xcode project in the specified order.

### 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. Use `""` to specify the project root.
- [ ] **excludeFromProject** : **String**- Optional flag to exclude the package from the generated project (useful if the package is already added via xcworkspace and the project is not intended for standalone use), defaults to `false`
- [ ] **traits**: **[String]** - Optional Swift package traits to enable for this package reference. Use the top-level `packages` mapping because the legacy `localPackages` syntax cannot specify traits.

```yml
packages:
Yams:
url: https://github.com/jpsim/Yams
from: 2.0.0
traits:
- StrictConcurrency
Ink:
github: JohnSundell/Ink
from: 0.5.0
Sentry:
path: ../..
traits:
- NoUIFramework
RxClient:
path: ../RxClient
AppFeature:
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved

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

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.10.1"),
.package(url: "https://github.com/tuist/XcodeProj.git", exact: "9.14.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/freddi-kit/ArtifactBundleGen", exact: "0.0.8")
Expand Down
2 changes: 1 addition & 1 deletion Sources/ProjectSpec/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ extension Project {
packages.merge(localPackages.reduce(into: [String: SwiftPackage]()) {
// Project name will be obtained by resolved abstractpath's lastComponent for dealing with some path case, like "../"
let packageName = (basePath + Path($1).normalize()).lastComponent
$0[packageName] = .local(path: $1, group: nil, excludeFromProject: false)
$0[packageName] = .local(path: $1, group: nil, excludeFromProject: false, traits: nil)
}
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ProjectSpec/SpecValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ extension Project {
}

for (name, package) in packages {
if case let .local(path, _, _) = package, !(basePath + Path(path).normalize()).exists {
if case let .local(path, _, _, _) = package, !(basePath + Path(path).normalize()).exists {
errors.append(.invalidLocalPackage(name))
}
}
Expand Down
26 changes: 20 additions & 6 deletions Sources/ProjectSpec/SwiftPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public enum SwiftPackage: Equatable {

static let githubPrefix = "https://github.com/"

case remote(url: String, versionRequirement: VersionRequirement)
case local(path: String, group: String?, excludeFromProject: Bool)
case remote(url: String, versionRequirement: VersionRequirement, traits: [String]? = nil)
case local(path: String, group: String?, excludeFromProject: Bool, traits: [String]? = nil)

public var isLocal: Bool {
if case .local = self {
Expand All @@ -23,10 +23,17 @@ public enum SwiftPackage: Equatable {
extension SwiftPackage: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
var traits: [String]?
if jsonDictionary["traits"] != nil {
// Need to assign the trait to a non-optional variable first to resolve method overloading ambiguity
let decodedTraits: [String] = try jsonDictionary.json(atKeyPath: "traits", invalidItemBehaviour: .fail)
traits = decodedTraits
}

if let path: String = jsonDictionary.json(atKeyPath: "path") {
let customLocation: String? = jsonDictionary.json(atKeyPath: "group")
let excludeFromProject: Bool = jsonDictionary.json(atKeyPath: "excludeFromProject") ?? false
self = .local(path: path, group: customLocation, excludeFromProject: excludeFromProject)
self = .local(path: path, group: customLocation, excludeFromProject: excludeFromProject, traits: traits)
} else {
let versionRequirement: VersionRequirement = try VersionRequirement(jsonDictionary: jsonDictionary)
try Self.validateVersion(versionRequirement: versionRequirement)
Expand All @@ -37,7 +44,7 @@ extension SwiftPackage: JSONObjectConvertible {
} else {
url = try jsonDictionary.json(atKeyPath: "url")
}
self = .remote(url: url, versionRequirement: versionRequirement)
self = .remote(url: url, versionRequirement: versionRequirement, traits: traits)
}
}

Expand Down Expand Up @@ -68,7 +75,7 @@ extension SwiftPackage: JSONEncodable {
public func toJSONValue() -> Any {
var dictionary: JSONDictionary = [:]
switch self {
case .remote(let url, let versionRequirement):
case .remote(let url, let versionRequirement, let traits):
if url.hasPrefix(Self.githubPrefix) {
dictionary["github"] = url.replacingOccurrences(of: Self.githubPrefix, with: "")
} else {
Expand All @@ -91,11 +98,18 @@ extension SwiftPackage: JSONEncodable {
case .revision(let revision):
dictionary["revision"] = revision
}

if let traits {
dictionary["traits"] = traits
}
return dictionary
case let .local(path, group, excludeFromProject):
case let .local(path, group, excludeFromProject, traits):
dictionary["path"] = path
dictionary["group"] = group
dictionary["excludeFromProject"] = excludeFromProject
if let traits {
dictionary["traits"] = traits
}
}

return dictionary
Expand Down
8 changes: 4 additions & 4 deletions Sources/XcodeGenKit/PBXProjGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,12 @@ public class PBXProjGenerator {

for (name, package) in project.packages {
switch package {
case let .remote(url, versionRequirement):
let packageReference = XCRemoteSwiftPackageReference(repositoryURL: url, versionRequirement: versionRequirement)
case let .remote(url, versionRequirement, traits):
let packageReference = XCRemoteSwiftPackageReference(repositoryURL: url, versionRequirement: versionRequirement, traits: traits)
packageReferences[name] = packageReference
addObject(packageReference)
case let .local(path, group, excludeFromProject):
let packageReference = XCLocalSwiftPackageReference(relativePath: path)
case let .local(path, group, excludeFromProject, traits):
let packageReference = XCLocalSwiftPackageReference(relativePath: path, traits: traits)
localPackageReferences[name] = packageReference

if !excludeFromProject {
Expand Down
2 changes: 1 addition & 1 deletion Sources/XcodeGenKit/SchemeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public class SchemeGenerator {
switch target.location {
case .package(let packageName):
guard let package = self.project.getPackage(packageName),
case let .local(path, _, _) = package else {
case let .local(path, _, _, _) = package else {
throw SchemeGenerationError.missingPackage(packageName)
}
return XCScheme.BuildableReference(
Expand Down
73 changes: 69 additions & 4 deletions Tests/ProjectSpecTests/SpecLoadingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1485,18 +1485,19 @@ class SpecLoadingTests: XCTestCase {

$0.it("parses packages") {
let project = Project(name: "spm", packages: [
"package1": .remote(url: "package.git", versionRequirement: .exact("1.2.2")),
"package1": .remote(url: "package.git", versionRequirement: .exact("1.2.2"), traits: ["FeatureA", "FeatureB"]),
"package2": .remote(url: "package.git", versionRequirement: .upToNextMajorVersion("1.2.2")),
"package3": .remote(url: "package.git", versionRequirement: .upToNextMinorVersion("1.2.2")),
"package4": .remote(url: "package.git", versionRequirement: .branch("master")),
"package5": .remote(url: "package.git", versionRequirement: .revision("x")),
"package6": .remote(url: "package.git", versionRequirement: .range(from: "1.2.0", to: "1.2.5")),
"package7": .remote(url: "package.git", versionRequirement: .exact("1.2.2")),
"package8": .remote(url: "package.git", versionRequirement: .upToNextMajorVersion("4.0.0-beta.5")),
"package9": .local(path: "package/package", group: nil, excludeFromProject: false),
"package9": .local(path: "package/package", group: nil, excludeFromProject: false, traits: ["NoUIFramework", "Networking"]),
"package10": .remote(url: "https://github.com/yonaskolb/XcodeGen", versionRequirement: .exact("1.2.2")),
"XcodeGen": .local(path: "../XcodeGen", group: nil, excludeFromProject: false),
"package11": .local(path: "../XcodeGen", group: "Packages/Feature", excludeFromProject: false),
"package12": .remote(url: "empty-traits.git", versionRequirement: .exact("1.0.0"), traits: []),
], options: .init(localPackagesGroup: "MyPackages"))

let dictionary: [String: Any] = [
Expand All @@ -1505,24 +1506,88 @@ class SpecLoadingTests: XCTestCase {
"localPackagesGroup": "MyPackages",
],
"packages": [
"package1": ["url": "package.git", "exactVersion": "1.2.2"],
"package1": ["url": "package.git", "exactVersion": "1.2.2", "traits": ["FeatureA", "FeatureB"]],
"package2": ["url": "package.git", "majorVersion": "1.2.2"],
"package3": ["url": "package.git", "minorVersion": "1.2.2"],
"package4": ["url": "package.git", "branch": "master"],
"package5": ["url": "package.git", "revision": "x"],
"package6": ["url": "package.git", "minVersion": "1.2.0", "maxVersion": "1.2.5"],
"package7": ["url": "package.git", "version": "1.2.2"],
"package8": ["url": "package.git", "majorVersion": "4.0.0-beta.5"],
"package9": ["path": "package/package"],
"package9": ["path": "package/package", "traits": ["NoUIFramework", "Networking"]],
"package10": ["github": "yonaskolb/XcodeGen", "exactVersion": "1.2.2"],
"package11": ["path": "../XcodeGen", "group": "Packages/Feature"],
"package12": ["url": "empty-traits.git", "exactVersion": "1.0.0", "traits": [String]()],
],
"localPackages": ["../XcodeGen"],
]
let parsedSpec = try getProjectSpec(dictionary)
try expect(parsedSpec) == project
}

$0.it("encodes package traits while preserving omitted, empty, and non-empty values") {
func jsonDictionary(for package: SwiftPackage) throws -> [String: Any] {
try unwrap(package.toJSONValue() as? [String: Any])
}

let remoteWithoutTraits = try jsonDictionary(for: .remote(
url: "package.git",
versionRequirement: .exact("1.0.0")
))
let remoteWithEmptyTraits = try jsonDictionary(for: .remote(
url: "package.git",
versionRequirement: .exact("1.0.0"),
traits: []
))
let remoteWithTraits = try jsonDictionary(for: .remote(
url: "package.git",
versionRequirement: .exact("1.0.0"),
traits: ["FeatureA", "FeatureB"]
))
let localWithoutTraits = try jsonDictionary(for: .local(
path: "../Package",
group: nil,
excludeFromProject: false
))
let localWithEmptyTraits = try jsonDictionary(for: .local(
path: "../Package",
group: nil,
excludeFromProject: false,
traits: []
))
let localWithTraits = try jsonDictionary(for: .local(
path: "../Package",
group: nil,
excludeFromProject: false,
traits: ["NoUIFramework", "Networking"]
))

XCTAssertFalse(remoteWithoutTraits.keys.contains("traits"))
XCTAssertEqual(remoteWithEmptyTraits["traits"] as? [String], [])
XCTAssertEqual(remoteWithTraits["traits"] as? [String], ["FeatureA", "FeatureB"])
XCTAssertFalse(localWithoutTraits.keys.contains("traits"))
XCTAssertEqual(localWithEmptyTraits["traits"] as? [String], [])
XCTAssertEqual(localWithTraits["traits"] as? [String], ["NoUIFramework", "Networking"])
}

$0.it("rejects invalid package traits") {
let invalidPackages: [[String: Any]] = [
[
"url": "package.git",
"exactVersion": "1.0.0",
"traits": "FeatureA",
],
[
"path": "../Package",
"traits": ["FeatureA", 1],
],
]

for dictionary in invalidPackages {
try expect(expression: { _ = try SwiftPackage(jsonDictionary: dictionary) }).toThrow()
}
}

$0.it("parses old local package format") {
let project = Project(name: "spm", packages: [
"XcodeGen": .local(path: "../XcodeGen", group: nil, excludeFromProject: false),
Expand Down
10 changes: 8 additions & 2 deletions Tests/XcodeGenKitTests/ProjectGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1565,9 +1565,9 @@ class ProjectGeneratorTests: XCTestCase {
)

let project = Project(name: "test", targets: [app], packages: [
"XcodeGen": .remote(url: "http://github.com/yonaskolb/XcodeGen", versionRequirement: .branch("master")),
"XcodeGen": .remote(url: "http://github.com/yonaskolb/XcodeGen", versionRequirement: .branch("master"), traits: ["FeatureA", "FeatureB"]),
"Codability": .remote(url: "http://github.com/yonaskolb/Codability", versionRequirement: .exact("1.0.0")),
"Yams": .local(path: "../Yams", group: nil, excludeFromProject: false),
"Yams": .local(path: "../Yams", group: nil, excludeFromProject: false, traits: ["NoUIFramework", "Networking"]),
], options: .init(localPackagesGroup: "MyPackages"))

let pbxProject = try project.generatePbxProj(specValidate: false)
Expand All @@ -1577,17 +1577,22 @@ class ProjectGeneratorTests: XCTestCase {

try expect(projectSpecDependency.package?.name) == "XcodeGen"
try expect(projectSpecDependency.package?.versionRequirement) == .branch("master")
try expect(projectSpecDependency.package?.traits) == ["FeatureA", "FeatureB"]

let codabilityDependency = try unwrap(nativeTarget.packageProductDependencies?.first(where: { $0.productName == "Codability" }))

try expect(codabilityDependency.package?.name) == "Codability"
try expect(codabilityDependency.package?.versionRequirement) == .exact("1.0.0")
try expect(codabilityDependency.package?.traits).beNil()

let localPackagesGroup = try unwrap(try pbxProject.getMainGroup().children.first(where: { $0.name == "MyPackages" }) as? PBXGroup)

let yamsLocalPackageFile = try unwrap(pbxProject.fileReferences.first(where: { $0.path == "../Yams" }))
try expect(localPackagesGroup.children.contains(yamsLocalPackageFile)) == true
try expect(yamsLocalPackageFile.lastKnownFileType) == "folder"

let yamsPackageReference = try unwrap(pbxProject.rootObject?.localPackages.first(where: { $0.relativePath == "../Yams" }))
try expect(yamsPackageReference.traits) == ["NoUIFramework", "Networking"]
}

$0.it("generates local swift packages") {
Expand All @@ -1610,6 +1615,7 @@ class ProjectGeneratorTests: XCTestCase {
let localPackageReference = try unwrap(pbxProject.rootObject?.localPackages.first)
try expect(pbxProject.rootObject?.localPackages.count) == 1
try expect(localPackageReference.relativePath) == "../XcodeGen"
try expect(localPackageReference.traits).beNil()

let frameworkPhases = nativeTarget.buildPhases.compactMap { $0 as? PBXFrameworksBuildPhase }

Expand Down