diff --git a/Plugins/BuildWithVersionPlugin/BuildWithVersionPlugin.swift b/Plugins/BuildWithVersionPlugin/BuildWithVersionPlugin.swift index 6a3e596..a817422 100644 --- a/Plugins/BuildWithVersionPlugin/BuildWithVersionPlugin.swift +++ b/Plugins/BuildWithVersionPlugin/BuildWithVersionPlugin.swift @@ -22,7 +22,7 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin { .buildCommand( displayName: "Build With Version Plugin", executable: tool.path, - arguments: ["build", "--verbose", "--git-directory", gitDirectoryPath.string, outputPath.string], + arguments: ["build", "--verbose", "--git-directory", gitDirectoryPath.string, "--target", outputPath.string], environment: [:], inputFiles: target.sourceFiles.map(\.path), outputFiles: [outputFile] diff --git a/Plugins/GenerateVersionPlugin/GenerateVersionPlugin.swift b/Plugins/GenerateVersionPlugin/GenerateVersionPlugin.swift index fd89cd4..1ff4d2d 100644 --- a/Plugins/GenerateVersionPlugin/GenerateVersionPlugin.swift +++ b/Plugins/GenerateVersionPlugin/GenerateVersionPlugin.swift @@ -1,5 +1,5 @@ -import PackagePlugin import Foundation +import PackagePlugin @main struct GenerateVersionPlugin: CommandPlugin { @@ -27,4 +27,3 @@ struct GenerateVersionPlugin: CommandPlugin { } } } - diff --git a/Sources/CliVersion/CliClient.swift b/Sources/CliVersion/CliClient.swift index 37d1844..895c88a 100644 --- a/Sources/CliVersion/CliClient.swift +++ b/Sources/CliVersion/CliClient.swift @@ -34,26 +34,26 @@ public struct CliClient: Sendable { case major, minor, patch } - // TODO: Use Int for `verbose`. public struct SharedOptions: Equatable, Sendable { - let gitDirectory: String? + let dryRun: Bool let fileName: String + let gitDirectory: String? + let logLevel: Logger.Level let target: String - let verbose: Bool public init( gitDirectory: String? = nil, dryRun: Bool = false, fileName: String = "Version.swift", target: String, - verbose: Bool = true + logLevel: Logger.Level = .debug ) { self.gitDirectory = gitDirectory self.dryRun = dryRun self.fileName = fileName self.target = target - self.verbose = verbose + self.logLevel = logLevel } } @@ -101,7 +101,7 @@ public extension CliClient.SharedOptions { _ operation: () async throws -> T ) async rethrows -> T { try await withDependencies { - $0.logger.logLevel = .init(verbose: verbose) + $0.logger.logLevel = logLevel } operation: { try await operation() } @@ -114,6 +114,7 @@ public extension CliClient.SharedOptions { try await fileClient.write(string: string, to: url) } else { logger.debug("Skipping, due to dry-run being passed.") + logger.debug("\(string)") } } } @@ -147,7 +148,7 @@ private extension CliClient.SharedOptions { } } - private func getVersionString() async throws -> (String, Bool) { + private func getVersionString() async throws -> (version: String, usesOptionalType: Bool) { @Dependency(\.fileClient) var fileClient @Dependency(\.gitVersionClient) var gitVersionClient @Dependency(\.logger) var logger @@ -290,6 +291,10 @@ public struct Template: Sendable { } public static func build(_ version: String? = nil) -> String { + nonOptional(version) + } + + public static func nonOptional(_ version: String? = nil) -> String { Self(type: .string, version: version).value } diff --git a/Sources/CliVersion/Helpers.swift b/Sources/CliVersion/Helpers.swift index bf922d7..b8566c0 100644 --- a/Sources/CliVersion/Helpers.swift +++ b/Sources/CliVersion/Helpers.swift @@ -7,11 +7,12 @@ import Logging @_spi(Internal) public extension Logger.Level { - init(verbose: Bool) { - if verbose { - self = .debug - } else { - self = .info + init(verbose: Int) { + switch verbose { + case 1: self = .warning + case 2: self = .debug + case 3...: self = .trace + default: self = .info } } } diff --git a/Sources/cli-version/BuildCommand.swift b/Sources/cli-version/BuildCommand.swift index 6ad0842..1c89c0e 100644 --- a/Sources/cli-version/BuildCommand.swift +++ b/Sources/cli-version/BuildCommand.swift @@ -10,40 +10,13 @@ extension CliVersionCommand { discussion: "This should generally not be interacted with directly, outside of the build plugin." ) - @OptionGroup var shared: SharedOptions + @OptionGroup var globals: GlobalOptions - @Option( - name: .customLong("git-directory"), - help: "The git directory for the version." - ) - var gitDirectory: String - - // TODO: Use CliClient func run() async throws { - try await withDependencies { - $0.logger.logLevel = .debug - $0.fileClient = .liveValue - $0.gitVersionClient = .liveValue - } operation: { - @Dependency(\.gitVersionClient) var gitVersion - @Dependency(\.fileClient) var fileClient - @Dependency(\.logger) var logger: Logger - - logger.info("Building with git-directory: \(gitDirectory)") - - let fileUrl = URL(fileURLWithPath: shared.target) - .appendingPathComponent(shared.fileName) - - let fileString = fileUrl.fileString() - logger.info("File Url: \(fileString)") - - let currentVersion = try await gitVersion.currentVersion(in: gitDirectory) - - let fileContents = buildTemplate - .replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"") - - try await fileClient.write(string: fileContents, to: fileUrl) - logger.info("Updated version file: \(fileString)") + try await globals.run { + @Dependency(\.cliClient) var cliClient + let output = try await cliClient.build(globals.shared) + print(output) } } } diff --git a/Sources/cli-version/BumpCommand.swift b/Sources/cli-version/BumpCommand.swift new file mode 100644 index 0000000..f8dde53 --- /dev/null +++ b/Sources/cli-version/BumpCommand.swift @@ -0,0 +1,28 @@ +import ArgumentParser +import CliVersion +import Dependencies + +extension CliVersionCommand { + struct Bump: AsyncParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "bump", + abstract: "Bump version of a command-line tool." + ) + + @OptionGroup var globals: GlobalOptions + + @Flag + var bumpOption: CliClient.BumpOption = .patch + + func run() async throws { + try await globals.run { + @Dependency(\.cliClient) var cliClient + let output = try await cliClient.bump(bumpOption, globals.shared) + print(output) + } + } + } +} + +extension CliClient.BumpOption: EnumerableFlag {} diff --git a/Sources/cli-version/CliVersionCommand.swift b/Sources/cli-version/CliVersionCommand.swift index 5e180b6..1a7cac5 100644 --- a/Sources/cli-version/CliVersionCommand.swift +++ b/Sources/cli-version/CliVersionCommand.swift @@ -8,6 +8,7 @@ struct CliVersionCommand: AsyncParsableCommand { version: VERSION ?? "0.0.0", subcommands: [ Build.self, + Bump.self, Generate.self, Update.self ] diff --git a/Sources/cli-version/GenerateCommand.swift b/Sources/cli-version/GenerateCommand.swift index 61cbcb8..671eb8b 100644 --- a/Sources/cli-version/GenerateCommand.swift +++ b/Sources/cli-version/GenerateCommand.swift @@ -12,28 +12,13 @@ extension CliVersionCommand { version: VERSION ?? "0.0.0" ) - @OptionGroup var shared: SharedOptions + @OptionGroup var globals: GlobalOptions - // TODO: Use CliClient func run() async throws { - @Dependency(\.logger) var logger: Logger - @Dependency(\.fileClient) var fileClient - - let targetUrl = parseTarget(shared.target) - let fileUrl = targetUrl.appendingPathComponent(shared.fileName) - - let fileString = fileUrl.fileString() - - guard !FileManager.default.fileExists(atPath: fileUrl.absoluteString) else { - logger.info("File already exists at path.") - throw GenerationError.fileExists(path: fileString) - } - - if !shared.dryRun { - try await fileClient.write(string: optionalTemplate, to: fileUrl) - logger.info("Generated file at: \(fileString)") - } else { - logger.info("Would generate file at: \(fileString)") + try await globals.run { + @Dependency(\.cliClient) var cliClient + let output = try await cliClient.generate(globals.shared) + print(output) } } } diff --git a/Sources/cli-version/GlobalOptions.swift b/Sources/cli-version/GlobalOptions.swift new file mode 100644 index 0000000..b0e438e --- /dev/null +++ b/Sources/cli-version/GlobalOptions.swift @@ -0,0 +1,88 @@ +import ArgumentParser +@_spi(Internal) import CliVersion +import Dependencies +import Foundation + +func parseTarget(_ target: String) -> URL { + let url = URL(fileURLWithPath: target) + let urlTest = url + .deletingLastPathComponent() + + guard urlTest.lastPathComponent == "Sources" else { + return URL(fileURLWithPath: "Sources") + .appendingPathComponent(target) + } + return url +} + +extension URL { + func fileString() -> String { + absoluteString + .replacingOccurrences(of: "file://", with: "") + } +} + +let optionalTemplate = """ +// Do not set this variable, it is set during the build process. +let VERSION: String? = nil + +""" + +let buildTemplate = """ +// Do not set this variable, it is set during the build process. +let VERSION: String = nil + +""" + +struct GlobalOptions: ParsableArguments { + + @Option( + name: .customLong("git-directory"), + help: "The git directory for the version (default: current directory)" + ) + var gitDirectory: String? + + @Option( + name: .shortAndLong, + help: "The target for the version file." + ) + var target: String + + @Option( + name: .customLong("filename"), + help: "Specify the file name for the version file in the target." + ) + var fileName: String = "Version.swift" + + @Flag(name: .customLong("dry-run")) + var dryRun: Bool = false + + @Flag( + name: .shortAndLong, + help: "Increase logging level, can be passed multiple times (example: -vvv)." + ) + var verbose: Int +} + +extension GlobalOptions { + + var shared: CliClient.SharedOptions { + .init( + gitDirectory: gitDirectory, + dryRun: dryRun, + fileName: fileName, + target: target, + logLevel: .init(verbose: verbose) + ) + } + + func run(_ operation: () async throws -> Void) async throws { + try await withDependencies { + $0.fileClient = .liveValue + $0.gitVersionClient = .liveValue + $0.cliClient = .liveValue + } operation: { + try await operation() + } + } +} diff --git a/Sources/cli-version/Helpers.swift b/Sources/cli-version/Helpers.swift deleted file mode 100644 index 2f795c4..0000000 --- a/Sources/cli-version/Helpers.swift +++ /dev/null @@ -1,52 +0,0 @@ -import ArgumentParser -import Foundation - -func parseTarget(_ target: String) -> URL { - let url = URL(fileURLWithPath: target) - let urlTest = url - .deletingLastPathComponent() - - guard urlTest.lastPathComponent == "Sources" else { - return URL(fileURLWithPath: "Sources") - .appendingPathComponent(target) - } - return url -} - -extension URL { - func fileString() -> String { - absoluteString - .replacingOccurrences(of: "file://", with: "") - } -} - -let optionalTemplate = """ -// Do not set this variable, it is set during the build process. -let VERSION: String? = nil - -""" - -let buildTemplate = """ -// Do not set this variable, it is set during the build process. -let VERSION: String = nil - -""" - -// TODO: Use Int for `verbose`. -struct SharedOptions: ParsableArguments { - - @Argument(help: "The target for the version file.") - var target: String - - @Option( - name: .customLong("filename"), - help: "Specify the file name for the version file." - ) - var fileName: String = "Version.swift" - - @Flag(name: .customLong("dry-run")) - var dryRun: Bool = false - - @Flag(name: .long, help: "Increase logging level.") - var verbose: Bool = false -} diff --git a/Sources/cli-version/UpdateCommand.swift b/Sources/cli-version/UpdateCommand.swift index ef94dd7..17cc14c 100644 --- a/Sources/cli-version/UpdateCommand.swift +++ b/Sources/cli-version/UpdateCommand.swift @@ -12,45 +12,13 @@ extension CliVersionCommand { discussion: "This command can be interacted with directly outside of the plugin context." ) - @OptionGroup var shared: SharedOptions + @OptionGroup var globals: GlobalOptions - @Option( - name: .customLong("git-directory"), - help: "The git directory for the version." - ) - var gitDirectory: String? - - // TODO: Use CliClient func run() async throws { - try await withDependencies { - $0.logger.logLevel = shared.verbose ? .debug : .info - $0.fileClient = .liveValue - $0.gitVersionClient = .liveValue - $0.asyncShellClient = .liveValue - } operation: { - @Dependency(\.gitVersionClient) var gitVersion - @Dependency(\.fileClient) var fileClient - @Dependency(\.logger) var logger - @Dependency(\.asyncShellClient) var shell - - let targetUrl = parseTarget(shared.target) - let fileUrl = targetUrl - .appendingPathComponent(shared.fileName) - - let fileString = fileUrl.fileString() - - let currentVersion = try await gitVersion.currentVersion(in: gitDirectory) - - let fileContents = optionalTemplate - .replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"") - - if !shared.dryRun { - try await fileClient.write(string: fileContents, to: fileUrl) - logger.info("Updated version file: \(fileString)") - } else { - logger.info("Would update file contents to:") - logger.info("\(fileContents)") - } + try await globals.run { + @Dependency(\.cliClient) var cliClient + let output = try await cliClient.update(globals.shared) + print(output) } } } diff --git a/Sources/cli-version/Version.swift b/Sources/cli-version/Version.swift index 9203956..847b6de 100644 --- a/Sources/cli-version/Version.swift +++ b/Sources/cli-version/Version.swift @@ -1,2 +1,2 @@ // Do not set this variable, it is set during the build process. -let VERSION: String? = "0.1.0" +let VERSION: String? = "0.1.1" diff --git a/Tests/CliVersionTests/CliClientTests.swift b/Tests/CliVersionTests/CliClientTests.swift index 68454fb..26bc49a 100644 --- a/Tests/CliVersionTests/CliClientTests.swift +++ b/Tests/CliVersionTests/CliClientTests.swift @@ -1,6 +1,7 @@ @_spi(Internal) import CliVersion import Dependencies import Foundation +import Logging import Testing import TestSupport @@ -12,16 +13,8 @@ struct CliClientTests { ) func testBuild(target: String) async throws { try await run { - let client = CliClient.liveValue - - let output = try await client.build(.init( - gitDirectory: "/baz", - dryRun: false, - fileName: "foo", - target: target, - verbose: true - )) - + @Dependency(\.cliClient) var client + let output = try await client.build(.testOptions(target: target)) #expect(output == "/baz/Sources/bar/foo") } } @@ -35,27 +28,21 @@ struct CliClientTests { $0.fileClient.fileExists = { _ in true } $0.fileClient.read = { @Sendable _ in template } } operation: { - let client = CliClient.liveValue - let output = try await client.bump( - type, - .init( - gitDirectory: "/baz", - dryRun: false, - fileName: "foo", - target: "bar", - verbose: true - ) - ) + @Dependency(\.cliClient) var client + let output = try await client.bump(type, .testOptions()) #expect(output == "/baz/Sources/bar/foo") } assert: { string, _ in + + #expect(string != nil) let typeString = optional ? "String?" : "String" + switch type { case .major: - #expect(string.contains("let VERSION: \(typeString) = \"2.0.0\"")) + #expect(string!.contains("let VERSION: \(typeString) = \"2.0.0\"")) case .minor: - #expect(string.contains("let VERSION: \(typeString) = \"1.1.0\"")) + #expect(string!.contains("let VERSION: \(typeString) = \"1.1.0\"")) case .patch: - #expect(string.contains("let VERSION: \(typeString) = \"1.0.1\"")) + #expect(string!.contains("let VERSION: \(typeString) = \"1.0.1\"")) } } } @@ -64,42 +51,34 @@ struct CliClientTests { arguments: TestArguments.testCases ) func generate(target: String) async throws { - // let (stream, continuation) = AsyncStream.makeStream() try await run { - let client = CliClient.liveValue - let output = try await client.generate(.init( - gitDirectory: "/baz", - dryRun: false, - fileName: "foo", - target: target, - verbose: true - )) + @Dependency(\.cliClient) var client + let output = try await client.generate(.testOptions(target: target)) #expect(output == "/baz/Sources/bar/foo") } } @Test( - arguments: TestArguments.testCases + arguments: TestArguments.updateCases ) - func update(target: String) async throws { - // let (stream, continuation) = AsyncStream.makeStream() + func update(target: String, dryRun: Bool) async throws { try await run { - let client = CliClient.liveValue - let output = try await client.update(.init( - gitDirectory: "/baz", - dryRun: false, - fileName: "foo", - target: target, - verbose: true - )) + $0.fileClient.fileExists = { _ in false } + } operation: { + @Dependency(\.cliClient) var client + let output = try await client.update(.testOptions(dryRun: dryRun, target: target)) #expect(output == "/baz/Sources/bar/foo") + } assert: { string, _ in + if dryRun { + #expect(string == nil) + } } } func run( setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in }, operation: @Sendable @escaping () async throws -> Void, - assert: @escaping (String, URL) -> Void = { _, _ in } + assert: @escaping (String?, URL?) -> Void = { _, _ in } ) async throws { let captured = CapturingWrite() @@ -108,18 +87,17 @@ struct CliClientTests { $0.fileClient = .capturing(captured) $0.fileClient.fileExists = { _ in false } $0.gitVersionClient = .init { _, _ in "1.0.0" } + $0.cliClient = .liveValue setupDependencies(&$0) } operation: { try await operation() } let data = await captured.data let url = await captured.url + var string: String? - guard let data, - let string = String(bytes: data, encoding: .utf8), - let url - else { - throw TestError() + if let data { + string = String(bytes: data, encoding: .utf8) } assert(string, url) @@ -132,6 +110,26 @@ enum TestArguments { $0.append(($1, true)) $0.append(($1, false)) } + + static let updateCases = testCases.map { ($0, Bool.random()) } } struct TestError: Error {} + +extension CliClient.SharedOptions { + static func testOptions( + gitDirectory: String? = "/baz", + dryRun: Bool = false, + fileName: String = "foo", + target: String = "bar", + logLevel: Logger.Level = .trace + ) -> Self { + .init( + gitDirectory: gitDirectory, + dryRun: dryRun, + fileName: fileName, + target: target, + logLevel: logLevel + ) + } +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..7bee14b --- /dev/null +++ b/justfile @@ -0,0 +1,9 @@ + +build configuration="release": + @swift build --configuration {{configuration}} + +run *ARGS: + @swift run cli-version {{ARGS}} + +clean: + rm -rf .build