diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackage.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackage.xcscheme index e35be09..193f14c 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackage.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackage.xcscheme @@ -67,15 +67,15 @@ isEnabled = "YES"> diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackages.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackages.xcscheme deleted file mode 100644 index feb960f..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/GeneratePackages.xcscheme +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/Config/RemoteDependencies.json b/Example/Config/Dependencies.json similarity index 99% rename from Example/Config/RemoteDependencies.json rename to Example/Config/Dependencies.json index bba5446..622c989 100644 --- a/Example/Config/RemoteDependencies.json +++ b/Example/Config/Dependencies.json @@ -21,4 +21,4 @@ "branch": "master" } ] -} +} \ No newline at end of file diff --git a/Example/Config/Dependencies.yaml b/Example/Config/Dependencies.yaml new file mode 100644 index 0000000..070a25e --- /dev/null +++ b/Example/Config/Dependencies.yaml @@ -0,0 +1,13 @@ +dependencies: +- name: RemoteDependencyA + url: https://github.com/DependencyA + version: 1.0.0 +- name: RemoteDependencyB + url: https://github.com/DependencyB + version: 2.0.0 +- name: RemoteDependencyC + url: https://github.com/DependencyC + revision: abcde1235kjh +- name: RemoteDependencyD + url: https://github.com/DependencyC + branch: master \ No newline at end of file diff --git a/Example/Packages/Example/Example.json b/Example/Packages/Example.json similarity index 99% rename from Example/Packages/Example/Example.json rename to Example/Packages/Example.json index cfb6c60..21026ab 100755 --- a/Example/Packages/Example/Example.json +++ b/Example/Packages/Example.json @@ -69,4 +69,4 @@ "path": "../LocalXCFramework.xcframework" } ] -} +} \ No newline at end of file diff --git a/Example/Packages/Example.yaml b/Example/Packages/Example.yaml new file mode 100644 index 0000000..0ea54ea --- /dev/null +++ b/Example/Packages/Example.yaml @@ -0,0 +1,38 @@ +name: Example +platforms: + - ".iOS(.v15)" + - ".macOS(.v13)" +swiftToolsVersion: '5.10' +swiftLanguageVersions: + - '5.10' + - '6.0' +products: + - productType: library + name: Example + targets: + - Example +localDependencies: + - name: MyLocalFramework + path: "../MyFrameworks" +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB + - name: RemoteDependencyC +targets: + - name: Example + targetType: target + dependencies: + - name: RemoteDependencyA + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: UnitTests + targetType: testTarget + dependencies: + - name: Example + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources +localBinaryTargets: + - name: LocalXCFramework + path: "../LocalXCFramework.xcframework" diff --git a/Example/Packages/Example/Package.swift b/Example/Packages/Example/Package.swift deleted file mode 100644 index a6f61ee..0000000 --- a/Example/Packages/Example/Package.swift +++ /dev/null @@ -1,71 +0,0 @@ -// swift-tools-version: 5.7 - -// This file was automatically generated by PackageGenerator and untracked -// PLEASE DO NOT EDIT MANUALLY - -import PackageDescription - -let package = Package( - name: "Example", - defaultLocalization: "en", - platforms: [ - .iOS(.v15), - .macOS(.v13), - ], - products: [ - .library( - name: "Example", - targets: ["Example"] - ), - ], - dependencies: [ - .package( - path: "../MyFrameworks" - ), - .package( - url: "https://github.com/DependencyA", - exact: "1.0.0" - ), - .package( - url: "https://github.com/DependencyB", - exact: "2.0.0" - ), - .package( - url: "https://github.com/DependencyC", - revision: "abcde1235kjh" - ), - ], - targets: [ - .target( - name: "Example", - dependencies: [ - .product(name: "RemoteDependencyA", package: "RemoteDependencyA"), - .target(name: "LocalXCFramework"), - ], - path: "Framework/Sources", - resources: [ - .process("Resources") - ], - plugins: [ - ] - ), - .testTarget( - name: "UnitTests", - dependencies: [ - .byName(name: "Example"), - .product(name: "RemoteDependencyB", package: "RemoteDependencyB"), - .target(name: "LocalXCFramework"), - ], - path: "Tests/Sources", - resources: [ - .process("Resources") - ], - plugins: [ - ] - ), - .binaryTarget( - name: "LocalXCFramework", - path: "../LocalXCFramework.xcframework" - ), - ] -) \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index b690223..353bc1c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -71,6 +71,15 @@ "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1", "version" : "1.1.4" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 5ddc628..7798f00 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"), .package(url: "https://github.com/SwiftGen/StencilSwiftKit", from: "2.8.0"), - .package(url: "https://github.com/JohnSundell/ShellOut", from: "2.3.0") + .package(url: "https://github.com/JohnSundell/ShellOut", from: "2.3.0"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6") ], targets: [ .executableTarget( @@ -21,6 +22,7 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ShellOut", package: "ShellOut"), .product(name: "StencilSwiftKit", package: "StencilSwiftKit"), + .product(name: "Yams", package: "Yams") ], path: "Sources"), .testTarget( @@ -28,7 +30,7 @@ let package = Package( dependencies: ["PackageGenerator"], path: "Tests", resources: [ - .process("Resources") + .copy("Resources") ] ) ] diff --git a/README.md b/README.md index a65cf58..c5df1a9 100644 --- a/README.md +++ b/README.md @@ -2,54 +2,18 @@ ![Build Status](https://github.com/justeattakeaway/PackageGenerator/actions/workflows/run_tests.yml/badge.svg?branch=main) -A tool to generate `Package.swift` files using a custom DSL allowing version alignment of dependencies across packages. +A CLI tool to generate `Package.swift` files using a custom DSL allowing version alignment of dependencies across packages. ## Usage -`PackageGenerator` uses [ArgumentParser](https://github.com/apple/swift-argument-parser) and [Stencil](https://stencil.fuller.li/). +`PackageGenerator` uses [ArgumentParser](https://github.com/apple/swift-argument-parser) and [Stencil](https://stencil.fuller.li/). The tool provides a single `generate-package` command requiring the following options: -The command `generate-package` requires the following arguments: +- `--spec`: Path to a package spec file (supported formats: json, yaml) +- `--dependencies`: Path to a dependencies file (supported formats: json, yaml) +- `--template`: Path to a template file (supported formats: stencil) -- `path`: Path to the folder containing the packages. -- `template-path`: Path to the Stencil template. -- `dependencies-path`: Path to the `RemoteDependencies.json` file. - -`RemoteDependencies.json` should contain the list of remote dependencies used by your packages. E.g. - -```json -{ - "dependencies": [ - { - "name": "Alamofire", - "url": "https://github.com/Alamofire/Alamofire", - "version": "5.6.1" - }, - { - "name": "ViewInspector", - "url": "https://github.com/nalexn/ViewInspector", - "version": "0.9.2" - }, - { - "name": "ViewInspector", - "url": "https://github.com/nalexn/ViewInspector", - "version": "0.9.2" - }, - { - "name": "SnapshotTesting", - "url": "https://github.com/pointfreeco/swift-snapshot-testing", - "branch": "master" - }, - { - "name": "Fastlane", - "url": "https://github.com/fastlane/fastlane.git", - "revision": "2c4f29fe161c5998e30000f96d23384fd0eebe90" - } - ] -} -``` - -Packages should be contained in respective folders inside a packages folder and provide a `.json` spec. E.g. +Here are spec examples in both json and yaml: ```json { @@ -82,12 +46,9 @@ Packages should be contained in respective folders inside a packages folder and "name": "ViewInspector", "version": "1.2.3" }, - { - "name": "Fastlane" - }, { "name": "SnapshotTesting" - }, + } ], "targets": [ { @@ -96,9 +57,6 @@ Packages should be contained in respective folders inside a packages folder and "dependencies": [ { "name": "Alamofire" - }, - { - "name": "Fastlane" } ], "sourcesPath": "Framework/Sources", @@ -126,19 +84,98 @@ Packages should be contained in respective folders inside a packages folder and } ``` -> Note that `PackageGenerator` will automatically retrieve `url` & ( `version` || `branch` || `revision` ) values for `remoteDependencies` from the `RemoteDependencies.json` file. If you need to override those values, you can set them in the package spec. +```yaml +name: Example +swiftToolsVersion: '5.10' +swiftLanguageVersions: + - '5.10' + - '6.0' +products: + - name: Example + productType: library + targets: + - ExampleTarget +localDependencies: + - name: ExampleLocalDependency + path: "../LocalDependencies" +remoteDependencies: + - name: Alamofire + - name: ViewInspector + version: 1.2.3 + - name: SnapshotTesting +targets: + - name: ExampleTarget + targetType: target + dependencies: + - name: Alamofire + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: ExampleTargetTests + targetType: testTarget + dependencies: + - name: ExampleTarget + isTarget: true + - name: ViewInspector + - name: SnapshotTesting + sourcesPath: Tests/Sources + resourcesPath: Resources +``` + +The dependencies file should contain the list of dependencies used by your package(s): + +```json +{ + "dependencies": [ + { + "name": "Alamofire", + "url": "https://github.com/Alamofire/Alamofire", + "version": "5.6.1" + }, + { + "name": "SnapshotTesting", + "url": "https://github.com/pointfreeco/swift-snapshot-testing", + "branch": "master" + }, + { + "name": "ViewInspector", + "url": "https://github.com/nalexn/ViewInspector", + "revision": "23d6fabc6e8f0115c94ad3af5935300c70e0b7fa" + } + ] +} +``` + +```yaml +dependencies: + - name: Alamofire + url: https://github.com/Alamofire/Alamofire + version: 5.6.1 + - name: SnapshotTesting + url: https://github.com/pointfreeco/swift-snapshot-testing + branch: master + - name: ViewInspector + url: https://github.com/nalexn/ViewInspector + revision: 23d6fabc6e8f0115c94ad3af5935300c70e0b7fa +``` + +> Note that `PackageGenerator` will automatically retrieve `url` & ( `version` || `branch` || `revision` ) values for the dependencies. If you need to override those values, you can set them in the package spec. + +We provide a default Stencil template we recommend using. -We provide a default Stencil template that `PackageGenerator` can work with. +Ideally, you want to use the `PackageGenerator` executable to automate tasks both locally and on CI. -The command `generate-packages` allows you to generate Package.swift files from a given folder of packages. -It takes the same arguments as `generate-package` along with `packages-folder-path`. `PackageGenerator` will loop though subfolders and generate Package.swift files from JSON specs. +You can download a build from the [release page](https://github.com/justeattakeaway/PackageGenerator/releases) or, alternatively, build it from the source code: + +```bash +swift build -c release --arch x86_64 --arch arm64 +``` -Ideally, you want to generate a `PackageGenerator` executable and automate tasks both locally and on CI. +The executable should be generated at `.build/apple/Products/Release/PackageGenerator`. ## Demo -In the `PackageGenerator` scheme, enable 'Use custom working directory' and set the value to the folder containing the `PackageGenerator` package. +In the `GeneratorPackage` scheme, enable 'Use custom working directory' and set the value to the folder containing the `PackageGenerator` package. The scheme has arguments set to showcase the creation of a `Package.swift` file using some provided files. When running the default scheme you should see a `Package.swift` file being generated in the `Packages/Example/` folder. diff --git a/Sources/Commands/GeneratePackage.swift b/Sources/Commands/GeneratePackage.swift index 76bd44b..041d777 100644 --- a/Sources/Commands/GeneratePackage.swift +++ b/Sources/Commands/GeneratePackage.swift @@ -1,34 +1,38 @@ // GeneratePackage.swift -import Foundation import ArgumentParser +import Foundation struct GeneratePackage: ParsableCommand { - static let configuration = CommandConfiguration(abstract: "Generate a Package.swift file from a JSON manifest.") - - @Option(name: .long, help: "Path to the folder containing the package.") - private var path: String - - @Option(name: .long, help: "Path to the Stencil template.") - private var templatePath: String - - @Option(name: .long, help: "Path to the RemoteDependencies.json file.") - private var dependenciesPath: String - + static let configuration = CommandConfiguration(abstract: "Generate a Package.swift file from a spec.") + + @Option(name: .long, help: "Path to a package spec file (supported formats: json, yaml)") + private var spec: String + + @Option(name: .long, help: "Path to s dependencies file (supported formats: json, yaml)") + private var dependencies: String + + @Option(name: .long, help: "Path to a template file (supported formats: stencil)") + private var template: String + func run() throws { - let packageFolderUrl = URL(fileURLWithPath: path, isDirectory: true) - let packageFolderName = packageFolderUrl.lastPathComponent - let dependenciesUrl = URL(fileURLWithPath: dependenciesPath, isDirectory: false) - let specGenerator = SpecGenerator(dependenciesUrl: dependenciesUrl, packagesFolder: packageFolderUrl) - let specUrl = packageFolderUrl.appendingPathComponent("\(packageFolderName).json") - let spec = try specGenerator.makeSpec(for: packageFolderName, specUrl: specUrl) - let path = try generatePackage(for: spec) + let content = try generatePackageContent() + let path = try write(content: content) print("✅ File successfully saved at \(path).") } - private func generatePackage(for spec: Spec) throws -> Path { - let templater = Templater(templatePath: templatePath) - let content = try templater.renderTemplate(context: spec.makeContext()) - return try Writer().writePackageFile(content: content, to: path) + private func generatePackageContent() throws -> Content { + let specUrl = URL(fileURLWithPath: spec, isDirectory: false) + let dependenciesUrl = URL(fileURLWithPath: dependencies, isDirectory: false) + let specGenerator = SpecGenerator(specUrl: specUrl, dependenciesUrl: dependenciesUrl) + let spec = try specGenerator.makeSpec() + let templater = Templater(templatePath: template) + return try templater.renderTemplate(context: spec.makeContext()) + } + + private func write(content: Content) throws -> String { + let specUrl = URL(fileURLWithPath: spec, isDirectory: false) + let packageFolder = specUrl.deletingLastPathComponent() + return try Writer().writePackageFile(content: content, to: packageFolder) } } diff --git a/Sources/Commands/GeneratePackages.swift b/Sources/Commands/GeneratePackages.swift deleted file mode 100644 index c44448f..0000000 --- a/Sources/Commands/GeneratePackages.swift +++ /dev/null @@ -1,42 +0,0 @@ -// GeneratePackages.swift - -import Foundation -import ArgumentParser - -struct GeneratePackages: ParsableCommand { - static let configuration = CommandConfiguration(abstract: "Generate Package.swift files from a folder of packages.") - - @Option(name: .long, help: "Path to the folder containing the packages.") - private var packagesFolderPath: String - - @Option(name: .long, help: "Path to the Stencil template.") - private var templatePath: String - - @Option(name: .long, help: "Path to the RemoteDependencies.json file.") - private var dependenciesPath: String - - func run() throws { - let packagesFolderUrl = URL(fileURLWithPath: packagesFolderPath, isDirectory: true) - let dependenciesUrl = URL(fileURLWithPath: dependenciesPath, isDirectory: false) - let specGenerator = SpecGenerator(dependenciesUrl: dependenciesUrl, packagesFolder: packagesFolderUrl) - let specs = try specGenerator.makeSpecs() - - let results: [String] = try specs.reduce(into: []) { partialResult, spec in - let path = try generatePackage(for: spec) - partialResult.append(path) - } - - for result in results { - print("✅ File successfully saved at \(result).") - } - } - - private func generatePackage(for spec: Spec) throws -> Path { - let templater = Templater(templatePath: templatePath) - let content = try templater.renderTemplate(context: spec.makeContext()) - let packagesFolderPath = URL(fileURLWithPath: packagesFolderPath, isDirectory: true) - .appendingPathComponent(spec.name) - .path - return try Writer().writePackageFile(content: content, to: packagesFolderPath) - } -} diff --git a/Sources/Core/SpecGenerator.swift b/Sources/Core/SpecGenerator.swift index 70f3d64..15a1ce1 100644 --- a/Sources/Core/SpecGenerator.swift +++ b/Sources/Core/SpecGenerator.swift @@ -1,90 +1,88 @@ // SpecGenerator.swift import Foundation +import Yams /// Class to generate Specs models that can be used to ultimately generate `Package.swift` files. final class SpecGenerator { - let decoder = JSONDecoder() + enum GeneratorError: Error { + case invalidFormat(String) + } + + let specUrl: URL let dependenciesUrl: URL - let packagesFolder: URL /// The default initializer. /// /// - Parameters: - /// - dependenciesUrl: The path to the RemoteDependencies.json file. - /// - packagesFolder: The path to the folder containing the packages. - init(dependenciesUrl: URL, packagesFolder: URL) { + /// - packagesFolder: Path to the package spec. + /// - dependenciesUrl: Path to the dependencies file. + init(specUrl: URL, dependenciesUrl: URL) { + self.specUrl = specUrl self.dependenciesUrl = dependenciesUrl - self.packagesFolder = packagesFolder } /// Generate a Spec model for a given package. /// - /// - Parameter packageName: The name of the package to generate a Spec for. /// - Returns: A Spec model. - func makeSpec(for packageName: PackageName, specUrl: URL) throws -> Spec { - try makeSpec(specUrl: specUrl) - } - - /// Generate Spec models for all packages. - /// - /// - Returns: An array of Spec models. - func makeSpecs() throws -> [Spec] { - try specURLs().map(makeSpec) - } - - private func makeSpec(specUrl: URL) throws -> Spec { - let dependenciesData = try Data(contentsOf: dependenciesUrl) - let specData = try Data(contentsOf: specUrl) + func makeSpec() throws -> Spec { + let spec: Spec = try decodeModel(from: specUrl) + let dependencies: Dependencies = try decodeModel(from: dependenciesUrl) - let partialSpec = try decoder.decode(Spec.self, from: specData) - let dependencies = try decoder.decode(Dependencies.self, from: dependenciesData).dependencies + let mappedDependencies: [Spec.RemoteDependency] = spec.remoteDependencies + .compactMap { remoteDependency -> Spec.RemoteDependency? in + guard let dependency = dependencies.dependencies.first(where: { + $0.name == remoteDependency.name + }) else { + return nil + } + return Spec.RemoteDependency( + name: dependency.name, + url: remoteDependency.url ?? dependency.url, + version: remoteDependency.version ?? dependency.version, + revision: remoteDependency.revision ?? dependency.revision, + branch: remoteDependency.branch ?? dependency.branch + ) + } - let mappedDependencies: [RemoteDependency] = partialSpec.remoteDependencies.compactMap { remoteDependency -> RemoteDependency? in - guard let dependency = dependencies.first(where: { $0.name == remoteDependency.name }) else { return nil } - return RemoteDependency(name: dependency.name, - url: remoteDependency.url ?? dependency.url, - version: remoteDependency.version ?? dependency.version, - revision: remoteDependency.revision ?? dependency.revision, - branch: remoteDependency.branch ?? dependency.branch) - } - - return Spec(name: partialSpec.name, - platforms: partialSpec.platforms, - localDependencies: partialSpec.localDependencies, - remoteDependencies: mappedDependencies, - products: partialSpec.products, - targets: partialSpec.targets, - localBinaryTargets: partialSpec.localBinaryTargets, - remoteBinaryTargets: partialSpec.remoteBinaryTargets, - swiftToolsVersion: partialSpec.swiftToolsVersion, - swiftLanguageVersions: partialSpec.swiftLanguageVersions) - } - - private func specURL(for packageName: PackageName) -> URL { - packagesFolder.appendingPathComponent("\(packageName)/\(packageName).json") + return Spec( + name: spec.name, + platforms: spec.platforms, + localDependencies: spec.localDependencies, + remoteDependencies: mappedDependencies, + products: spec.products, + targets: spec.targets, + localBinaryTargets: spec.localBinaryTargets, + remoteBinaryTargets: spec.remoteBinaryTargets, + swiftToolsVersion: spec.swiftToolsVersion, + swiftLanguageVersions: spec.swiftLanguageVersions + ) } - private func specURLs() throws -> [URL] { - let fileManager = FileManager.default - let contentURLs = try fileManager.contentsOfDirectory(at: packagesFolder, - includingPropertiesForKeys: [.nameKey], - options: .skipsHiddenFiles) - return contentURLs.map { item in - let packageName = item.lastPathComponent - return item.appendingPathComponent("\(packageName).json") - } - .filter { specUrl in - fileManager.fileExists(atPath: specUrl.path) + private func decodeModel(from url: URL) throws -> T { + let specData = try Data(contentsOf: url) + switch url.pathExtension { + case "json": + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: specData) + case "yaml", "yml": + let decoder = YAMLDecoder() + return try decoder.decode(T.self, from: specData) + default: + throw GeneratorError.invalidFormat(url.pathExtension) } - .sorted() } } +// move to other file + extension URL: Comparable { - public static func < (lhs: URL, rhs: URL) -> Bool { + public static func < ( + lhs: URL, + rhs: URL + ) -> Bool { lhs.path < rhs.path } } diff --git a/Sources/Core/Writer.swift b/Sources/Core/Writer.swift index ffed2d3..cb0c561 100644 --- a/Sources/Core/Writer.swift +++ b/Sources/Core/Writer.swift @@ -15,8 +15,8 @@ final class Writer { /// - packageFolder: The path to the folder containing the package. /// - Returns: The path of the saved `Package.swift` file. @discardableResult - func writePackageFile(content: String, to packageFolder: String) throws -> Path { - let url = URL(fileURLWithPath: packageFolder).appendingPathComponent("Package.swift") + func writePackageFile(content: String, to packageFolder: URL) throws -> Path { + let url = packageFolder.appendingPathComponent("Package.swift") try content.write(to: url, atomically: true, encoding: .utf8) try shellOut(to: "chmod 444", arguments: [url.path]) return url.path diff --git a/Sources/Models/Spec.swift b/Sources/Models/Spec.swift index 762aeee..f6f5485 100644 --- a/Sources/Models/Spec.swift +++ b/Sources/Models/Spec.swift @@ -5,6 +5,149 @@ import Foundation public typealias PackageName = String struct Spec: Decodable { + + struct Product: Decodable { + let name: String + let productType: String + let targets: [String] + + enum CodingKeys: CodingKey { + case name + case productType + case targets + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.productType = try container.decode(ProductType.self, forKey: .productType).rawValue + self.targets = try container.decode([String].self, forKey: .targets) + } + } + + enum ProductType: String, Decodable { + case library + case executable + case plugin + } + + struct LocalDependency: Decodable { + let name: String + let path: String + } + + struct RemoteDependency: Decodable { + let name: String + let url: String? + let version: String? + let revision: String? + let branch: String? + + enum CodingKeys: CodingKey { + case name + case url + case version + case revision + case branch + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.url = try container.decodeIfPresent(String.self, forKey: .url) + self.version = try container.decodeIfPresent(String.self, forKey: .version) + self.revision = try container.decodeIfPresent(String.self, forKey: .revision) + self.branch = try container.decodeIfPresent(String.self, forKey: .branch) + } + + init(name: String, url: String, version: String?, revision: String?, branch: String?) { + guard version != nil || revision != nil || branch != nil else { + fatalError("You need to provide at least one of the following: version, revision or branch") + } + + self.name = name + self.url = url + self.version = version + self.revision = revision + self.branch = branch + } + } + + enum TargetType: String, Decodable { + case target + case testTarget + case executableTarget + case plugin + } + + struct Target: Decodable { + let targetType: String + let name: String + let dependencies: [TargetDependency] + let sourcesPath: String + let resourcesPath: String? + let exclude: [String]? + let swiftSettings: [String]? + let cSettings: [String]? + let cxxSettings: [String]? + let linkerSettings: [String]? + let publicHeadersPath: String? + let plugins: [Plugin]? + + enum CodingKeys: CodingKey { + case targetType + case name + case dependencies + case sourcesPath + case resourcesPath + case exclude + case swiftSettings + case cSettings + case cxxSettings + case linkerSettings + case publicHeadersPath + case plugins + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.targetType = try container.decode(TargetType.self, forKey: .targetType).rawValue + self.name = try container.decode(String.self, forKey: .name) + self.dependencies = try container.decode([TargetDependency].self, forKey: .dependencies) + self.sourcesPath = try container.decode(String.self, forKey: .sourcesPath) + self.resourcesPath = try container.decodeIfPresent(String.self, forKey: .resourcesPath) + self.exclude = try container.decodeIfPresent([String].self, forKey: .exclude) + self.swiftSettings = try container.decodeIfPresent([String].self, forKey: .swiftSettings) + self.cSettings = try container.decodeIfPresent([String].self, forKey: .cSettings) + self.cxxSettings = try container.decodeIfPresent([String].self, forKey: .cxxSettings) + self.linkerSettings = try container.decodeIfPresent([String].self, forKey: .linkerSettings) + self.publicHeadersPath = try container.decodeIfPresent(String.self, forKey: .publicHeadersPath) + self.plugins = try container.decodeIfPresent([Plugin].self, forKey: .plugins) + } + } + + struct Plugin: Decodable { + let name: String + let package: String? + } + + struct TargetDependency: Decodable { + let name: String + let package: String? + let isTarget: Bool? + } + + struct LocalBinaryTarget: Decodable { + let name: String + let path: String + } + + struct RemoteBinaryTarget: Decodable { + let name: String + let url: String + let checksum: String + } + let name: PackageName let swiftToolsVersion: String? let platforms: [String]? @@ -56,145 +199,3 @@ extension Spec { return values.compactMapValues { $0 } } } - -struct Product: Decodable { - let name: String - let productType: String - let targets: [String] - - enum CodingKeys: CodingKey { - case name - case productType - case targets - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.name = try container.decode(String.self, forKey: .name) - self.productType = try container.decode(ProductType.self, forKey: .productType).rawValue - self.targets = try container.decode([String].self, forKey: .targets) - } -} - -enum ProductType: String, Decodable { - case library - case executable - case plugin -} - -struct LocalDependency: Decodable { - let name: String - let path: String -} - -struct RemoteDependency: Decodable { - let name: String - let url: String? - let version: String? - let revision: String? - let branch: String? - - enum CodingKeys: CodingKey { - case name - case url - case version - case revision - case branch - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.name = try container.decode(String.self, forKey: .name) - self.url = try container.decodeIfPresent(String.self, forKey: .url) - self.version = try container.decodeIfPresent(String.self, forKey: .version) - self.revision = try container.decodeIfPresent(String.self, forKey: .revision) - self.branch = try container.decodeIfPresent(String.self, forKey: .branch) - } - - init(name: String, url: String, version: String?, revision: String?, branch: String?) { - guard version != nil || revision != nil || branch != nil else { - fatalError("You need to provide at least one of the following: version, revision or branch") - } - - self.name = name - self.url = url - self.version = version - self.revision = revision - self.branch = branch - } -} - -enum TargetType: String, Decodable { - case target - case testTarget - case executableTarget - case plugin -} - -struct Target: Decodable { - let targetType: String - let name: String - let dependencies: [TargetDependency] - let sourcesPath: String - let resourcesPath: String? - let exclude: [String]? - let swiftSettings: [String]? - let cSettings: [String]? - let cxxSettings: [String]? - let linkerSettings: [String]? - let publicHeadersPath: String? - let plugins: [Plugin]? - - enum CodingKeys: CodingKey { - case targetType - case name - case dependencies - case sourcesPath - case resourcesPath - case exclude - case swiftSettings - case cSettings - case cxxSettings - case linkerSettings - case publicHeadersPath - case plugins - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.targetType = try container.decode(TargetType.self, forKey: .targetType).rawValue - self.name = try container.decode(String.self, forKey: .name) - self.dependencies = try container.decode([TargetDependency].self, forKey: .dependencies) - self.sourcesPath = try container.decode(String.self, forKey: .sourcesPath) - self.resourcesPath = try container.decodeIfPresent(String.self, forKey: .resourcesPath) - self.exclude = try container.decodeIfPresent([String].self, forKey: .exclude) - self.swiftSettings = try container.decodeIfPresent([String].self, forKey: .swiftSettings) - self.cSettings = try container.decodeIfPresent([String].self, forKey: .cSettings) - self.cxxSettings = try container.decodeIfPresent([String].self, forKey: .cxxSettings) - self.linkerSettings = try container.decodeIfPresent([String].self, forKey: .linkerSettings) - self.publicHeadersPath = try container.decodeIfPresent(String.self, forKey: .publicHeadersPath) - self.plugins = try container.decodeIfPresent([Plugin].self, forKey: .plugins) - } -} - -struct Plugin: Decodable { - let name: String - let package: String? -} - -struct TargetDependency: Decodable { - let name: String - let package: String? - let isTarget: Bool? -} - -struct LocalBinaryTarget: Decodable { - let name: String - let path: String -} - -struct RemoteBinaryTarget: Decodable { - let name: String - let url: String - let checksum: String -} diff --git a/Sources/PackageGenerator.swift b/Sources/PackageGenerator.swift index 402633b..aa91bef 100644 --- a/Sources/PackageGenerator.swift +++ b/Sources/PackageGenerator.swift @@ -7,7 +7,6 @@ import ArgumentParser struct PackageGenerator: AsyncParsableCommand { static let configuration = CommandConfiguration( subcommands: [ - GeneratePackage.self, - GeneratePackages.self + GeneratePackage.self ]) } diff --git a/Tests/PackageGeneratorTests.swift b/Tests/PackageGeneratorTests.swift index 62efdf9..c576da9 100644 --- a/Tests/PackageGeneratorTests.swift +++ b/Tests/PackageGeneratorTests.swift @@ -22,7 +22,7 @@ final class PackageGeneratorTests: XCTestCase { .appendingPathComponent("Resources") lazy var packagesFolderUrl = resourcesFolder.appendingPathComponent("Packages") - lazy var dependenciesUrl = resourcesFolder.appendingPathComponent("TestRemoteDependencies.json") + lazy var dependenciesFilename = "TestDependencies" lazy var templatePath = resourcesFolder.appendingPathComponent("Package.stencil") func test_SingleProduct() throws { @@ -62,25 +62,30 @@ final class PackageGeneratorTests: XCTestCase { } private func assertPackage(for packageType: PackageType) throws { - let packageSpecUrl = resourcesFolder - .appendingPathComponent("Packages") - .appendingPathComponent(packageType.rawValue) - .appendingPathComponent(packageType.rawValue) - .appendingPathExtension("json") - - let packageUrl = resourcesFolder - .appendingPathComponent("Packages") - .appendingPathComponent(packageType.rawValue) - .appendingPathComponent("\(packageType.rawValue)Package") - .appendingPathExtension("swift") - - let specGenerator = SpecGenerator(dependenciesUrl: dependenciesUrl, packagesFolder: packagesFolderUrl) - let spec = try specGenerator.makeSpec(for: packageType.rawValue, specUrl: packageSpecUrl) - let templater = Templater(templatePath: templatePath.absoluteString) - let packageContent = try templater.renderTemplate(context: spec.makeContext()) - - let expectedPackageContent = try String(contentsOf: packageUrl) - - XCTAssertEqual(packageContent, expectedPackageContent) + for `extension` in ["json", "yml"] { + let specUrl = resourcesFolder + .appendingPathComponent("Packages") + .appendingPathComponent(packageType.rawValue) + .appendingPathComponent(packageType.rawValue) + .appendingPathExtension(`extension`) + + let packageUrl = resourcesFolder + .appendingPathComponent("Packages") + .appendingPathComponent(packageType.rawValue) + .appendingPathComponent("Package") + .appendingPathExtension("swift") + + let dependenciesUrl = resourcesFolder + .appendingPathComponent(dependenciesFilename) + .appendingPathExtension("yml") + let specGenerator = SpecGenerator(specUrl: specUrl, dependenciesUrl: dependenciesUrl) + let spec = try specGenerator.makeSpec() + let templater = Templater(templatePath: templatePath.absoluteString) + let packageContent = try templater.renderTemplate(context: spec.makeContext()) + + let expectedPackageContent = try String(contentsOf: packageUrl) + + XCTAssertEqual(packageContent, expectedPackageContent) + } } } diff --git a/Tests/Resources/Packages/BranchProduct/BranchProduct.yml b/Tests/Resources/Packages/BranchProduct/BranchProduct.yml new file mode 100755 index 0000000..d78d6ba --- /dev/null +++ b/Tests/Resources/Packages/BranchProduct/BranchProduct.yml @@ -0,0 +1,31 @@ +name: BranchProduct +products: + - name: BranchProduct + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB + - name: RemoteDependencyD +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + - name: RemoteDependencyD + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/Packages/BranchProduct/BranchProductPackage.swift b/Tests/Resources/Packages/BranchProduct/Package.swift similarity index 100% rename from Tests/Resources/Packages/BranchProduct/BranchProductPackage.swift rename to Tests/Resources/Packages/BranchProduct/Package.swift diff --git a/Tests/Resources/Packages/ComplexTargets/ComplexTargets.yml b/Tests/Resources/Packages/ComplexTargets/ComplexTargets.yml new file mode 100755 index 0000000..cc37dfb --- /dev/null +++ b/Tests/Resources/Packages/ComplexTargets/ComplexTargets.yml @@ -0,0 +1,43 @@ +name: ComplexTarget +products: + - name: ComplexTarget + productType: library + targets: + - TargetA + - LocalBinaryTarget +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + exclude: + - /path1 + - /path1/path2 + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + swiftSettings: + - .define("setting") + - '.unsafeFlags(["flag"])' + sourcesPath: Tests/Sources + resourcesPath: Resources +localBinaryTargets: + - name: LocalBinaryTarget + path: path/LocalBinaryTarget +remoteBinaryTargets: + - name: RemoteBinaryTarget + url: 'https://github.com/RemoteBinaryTarget.zip' + checksum: '12345' \ No newline at end of file diff --git a/Tests/Resources/Packages/ComplexTargets/ComplexTargetsPackage.swift b/Tests/Resources/Packages/ComplexTargets/Package.swift similarity index 100% rename from Tests/Resources/Packages/ComplexTargets/ComplexTargetsPackage.swift rename to Tests/Resources/Packages/ComplexTargets/Package.swift diff --git a/Tests/Resources/Packages/CustomPlatforms/CustomPlatforms.yml b/Tests/Resources/Packages/CustomPlatforms/CustomPlatforms.yml new file mode 100755 index 0000000..2a7acb6 --- /dev/null +++ b/Tests/Resources/Packages/CustomPlatforms/CustomPlatforms.yml @@ -0,0 +1,32 @@ +name: CustomPlatforms +platforms: + - .iOS(.v16) + - .macOS(.v12) +products: + - name: CustomPlatforms + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/Packages/CustomPlatforms/CustomPlatformsPackage.swift b/Tests/Resources/Packages/CustomPlatforms/Package.swift similarity index 100% rename from Tests/Resources/Packages/CustomPlatforms/CustomPlatformsPackage.swift rename to Tests/Resources/Packages/CustomPlatforms/Package.swift diff --git a/Tests/Resources/Packages/DependencyOverride/DependencyOverride.yml b/Tests/Resources/Packages/DependencyOverride/DependencyOverride.yml new file mode 100755 index 0000000..2259378 --- /dev/null +++ b/Tests/Resources/Packages/DependencyOverride/DependencyOverride.yml @@ -0,0 +1,30 @@ +name: DependencyOverride +products: + - name: DependencyOverride + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + version: 3.0.0 + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/Packages/DependencyOverride/DependencyOverridePackage.swift b/Tests/Resources/Packages/DependencyOverride/Package.swift similarity index 100% rename from Tests/Resources/Packages/DependencyOverride/DependencyOverridePackage.swift rename to Tests/Resources/Packages/DependencyOverride/Package.swift diff --git a/Tests/Resources/Packages/ExecutableProduct/ExecutableProduct.yml b/Tests/Resources/Packages/ExecutableProduct/ExecutableProduct.yml new file mode 100755 index 0000000..039278f --- /dev/null +++ b/Tests/Resources/Packages/ExecutableProduct/ExecutableProduct.yml @@ -0,0 +1,29 @@ +name: ExcutableProduct +products: + - name: ExcutableProduct + productType: executable + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/Packages/ExecutableProduct/ExecutableProductPackage.swift b/Tests/Resources/Packages/ExecutableProduct/Package.swift similarity index 100% rename from Tests/Resources/Packages/ExecutableProduct/ExecutableProductPackage.swift rename to Tests/Resources/Packages/ExecutableProduct/Package.swift diff --git a/Tests/Resources/Packages/MultipleProducts/MultipleProducts.yml b/Tests/Resources/Packages/MultipleProducts/MultipleProducts.yml new file mode 100755 index 0000000..c50d988 --- /dev/null +++ b/Tests/Resources/Packages/MultipleProducts/MultipleProducts.yml @@ -0,0 +1,33 @@ +name: MultipleProducts +products: + - name: ProductA + productType: library + targets: + - TargetA + - name: ProductA + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/Packages/MultipleProducts/MultipleProductsPackage.swift b/Tests/Resources/Packages/MultipleProducts/Package.swift similarity index 100% rename from Tests/Resources/Packages/MultipleProducts/MultipleProductsPackage.swift rename to Tests/Resources/Packages/MultipleProducts/Package.swift diff --git a/Tests/Resources/Packages/PluginProduct/PluginProductPackage.swift b/Tests/Resources/Packages/PluginProduct/Package.swift similarity index 100% rename from Tests/Resources/Packages/PluginProduct/PluginProductPackage.swift rename to Tests/Resources/Packages/PluginProduct/Package.swift diff --git a/Tests/Resources/Packages/PluginProduct/PluginProduct.yml b/Tests/Resources/Packages/PluginProduct/PluginProduct.yml new file mode 100755 index 0000000..13d7dce --- /dev/null +++ b/Tests/Resources/Packages/PluginProduct/PluginProduct.yml @@ -0,0 +1,38 @@ +name: SingleProduct +products: + - name: Plugin + productType: plugin + targets: + - TargetA + - name: Library + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + sourcesPath: Framework/Sources + resourcesPath: Resources + plugins: + - name: Plugin + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources + plugins: + - name: Plugin + package: PluginTest \ No newline at end of file diff --git a/Tests/Resources/Packages/RevisionProduct/RevisionProductPackage.swift b/Tests/Resources/Packages/RevisionProduct/Package.swift similarity index 100% rename from Tests/Resources/Packages/RevisionProduct/RevisionProductPackage.swift rename to Tests/Resources/Packages/RevisionProduct/Package.swift diff --git a/Tests/Resources/Packages/RevisionProduct/RevisionProduct.yml b/Tests/Resources/Packages/RevisionProduct/RevisionProduct.yml new file mode 100755 index 0000000..57eb6d2 --- /dev/null +++ b/Tests/Resources/Packages/RevisionProduct/RevisionProduct.yml @@ -0,0 +1,31 @@ +name: RevisionProduct +products: + - name: RevisionProduct + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB + - name: RemoteDependencyC +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + - name: RemoteDependencyC + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/Packages/SingleProduct/SingleProductPackage.swift b/Tests/Resources/Packages/SingleProduct/Package.swift similarity index 100% rename from Tests/Resources/Packages/SingleProduct/SingleProductPackage.swift rename to Tests/Resources/Packages/SingleProduct/Package.swift diff --git a/Tests/Resources/Packages/SingleProduct/SingleProduct.yml b/Tests/Resources/Packages/SingleProduct/SingleProduct.yml new file mode 100755 index 0000000..6461548 --- /dev/null +++ b/Tests/Resources/Packages/SingleProduct/SingleProduct.yml @@ -0,0 +1,29 @@ +name: SingleProduct +products: + - name: SingleProduct + productType: library + targets: + - TargetA +localDependencies: + - name: LocalDependencyA + path: ../LocalDependencies +remoteDependencies: + - name: RemoteDependencyA + - name: RemoteDependencyB +targets: + - name: TargetA + targetType: target + dependencies: + - name: LocalDependencyA + - name: RemoteDependencyA + - name: RemoteDependencyB + sourcesPath: Framework/Sources + resourcesPath: Resources + - name: TargetATests + targetType: testTarget + dependencies: + - name: TargetA + isTarget: true + - name: RemoteDependencyB + sourcesPath: Tests/Sources + resourcesPath: Resources \ No newline at end of file diff --git a/Tests/Resources/TestRemoteDependencies.json b/Tests/Resources/TestDependencies.json similarity index 100% rename from Tests/Resources/TestRemoteDependencies.json rename to Tests/Resources/TestDependencies.json diff --git a/Tests/Resources/TestDependencies.yml b/Tests/Resources/TestDependencies.yml new file mode 100644 index 0000000..cf5a4b6 --- /dev/null +++ b/Tests/Resources/TestDependencies.yml @@ -0,0 +1,13 @@ +dependencies: + - name: RemoteDependencyA + url: https://github.com/DependencyA + version: 1.0.0 + - name: RemoteDependencyB + url: https://github.com/DependencyB + version: 2.0.0 + - name: RemoteDependencyC + url: https://github.com/DependencyC + revision: abcde1235kjh + - name: RemoteDependencyD + url: https://github.com/DependencyD + branch: master \ No newline at end of file