This commit is contained in:
2023-03-14 16:23:16 -04:00
parent 37f3bfde62
commit 78bfa7863a
18 changed files with 237 additions and 217 deletions

View File

@@ -20,6 +20,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GitVersion"
BuildableName = "GitVersion"
BlueprintName = "GitVersion"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction

View File

@@ -5,11 +5,6 @@ DOCC_DIR ?= ./docs
clean:
rm -rf .build
build-and-run:
swift run -c release build-example
./.build/release/example --help
./.build/release/example
build-documentation:
swift package \
--allow-writing-to-directory "$(DOCC_DIR)" \

View File

@@ -9,16 +9,16 @@ let package = Package(
],
products: [
.library(name: "GitVersion", targets: ["GitVersion"]),
.plugin(name: "GitVersionBuildPlugin", targets: ["GitVersionBuildPlugin"]),
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"])
],
dependencies: [
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2")
],
targets: [
.executableTarget(
name: "git-version-builder",
name: "git-version",
dependencies: [
"GitVersion",
.product(name: "ArgumentParser", package: "swift-argument-parser")
@@ -30,44 +30,23 @@ let package = Package(
.product(name: "ShellClient", package: "swift-shell-client")
]
),
.executableTarget(
name: "build-example",
dependencies: [
"GitVersion"
]
),
.executableTarget(
name: "example",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "ShellClient", package: "swift-shell-client")
]
,
plugins: [
.plugin(name: "GitVersionBuildPlugin")
]
),
.testTarget(
name: "GitVersionTests",
dependencies: ["GitVersion"]
),
// .plugin(
// name: "GitVersionPlugin",
// capability: .command(
// intent: .custom(verb: "build-with-version", description: "Build a command line tool with git version."),
// permissions: [
// .writeToPackageDirectory(reason: "This command builds a command line tool with a git version.")
// ]
// ),
// dependencies: [
// "build-example"
// ]
// ),
.plugin(
name: "GitVersionBuildPlugin",
capability: .buildTool(),
name: "GenerateVersionPlugin",
capability: .command(
intent: .custom(
verb: "generate-version",
description: "Generates a version file in the given target."
),
permissions: [
.writeToPackageDirectory(reason: "Generate a version file in the target's directory.")
]
),
dependencies: [
"git-version-builder"
"git-version"
]
)
]

View File

@@ -0,0 +1,30 @@
import PackagePlugin
import Foundation
@main
struct GenerateVersionPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
let gitVersion = try context.tool(named: "git-version")
var arguments = ["generate"] + arguments
for target in context.package.targets {
guard let target = target as? SourceModuleTarget,
arguments.first(where: { $0.contains(target.name) }) != nil
else { continue }
let process = Process()
process.executableURL = URL(fileURLWithPath: gitVersion.path.string)
process.arguments = arguments
try process.run()
process.waitUntilExit()
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
return
}
}
}
}

View File

@@ -1,51 +0,0 @@
import Foundation
import PackagePlugin
@main
struct GitVersionBuildPlugin: BuildToolPlugin {
func createBuildCommands(
context: PackagePlugin.PluginContext,
target: PackagePlugin.Target
) async throws -> [PackagePlugin.Command] {
guard let target = target as? SourceModuleTarget else { return [] }
let buildTool = try context.tool(named: "git-version-builder")
let outputDir = context.pluginWorkDirectory
.appending(subpath: target.name)
try FileManager.default
.createDirectory(atPath: outputDir.string, withIntermediateDirectories: true)
let inputFiles = target.sourceFiles
.filter({ $0.type == .source && $0.path.stem == "Version" })
.map(\.path)
guard inputFiles.count == 1 else { return [] }
let outputFile = outputDir.appending(subpath: "Version.generated.swift")
print("Input swift files: \(inputFiles)")
// let originalContents = try String(contentsOfFile: inputFiles.first!.string)
// let updatedContents = originalContents.replacingOccurrences(of: "nil", with: "\"0.1.123\"")
//
// print("Updated contents")
// print(updatedContents)
// this fails.
return [
.buildCommand(
displayName: "Git Version Build Plugin",
executable: buildTool.path,
arguments: [
inputFiles.first!,
outputFile.string
]
// ,
// outputFiles: [outputFile]
)
]
}
}

View File

@@ -1,34 +0,0 @@
import PackagePlugin
import Foundation
@main
struct GitVersionPlugin: CommandPlugin {
func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws {
let tool = try context.tool(named: "build-example")
let url = URL(fileURLWithPath: tool.path.string)
for target in context.package.targets {
guard let target = target as? SourceModuleTarget else { continue }
let process = Process()
process.executableURL = url
try process.run()
process.waitUntilExit()
if process.terminationReason == .exit && process.terminationStatus == 0 {
print("Done building in: \(target.directory)")
} else {
let problem = "\(process.terminationReason): \(process.terminationStatus)"
Diagnostics.error("\(problem)")
}
}
}
// func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
// guard let target = target.sourceModule else { return [] }
// let tool = try context.tool(named: "build-example")
//
//
// }
}

View File

@@ -106,10 +106,20 @@ public struct FileClient {
try self.write(data, url)
}
/// Write's the given string to a file.
///
/// - Parameters:
/// - string: The string to write to the file.
/// - url: The file url.
public func write(string: String, to url: URL) throws {
try self.write(Data(string.utf8), url)
}
/// Write's the the string to a file path.
///
/// - Parameters:
/// - string: The string to write to the file.
/// - path: The file path.
public func write(string: String, to path: String) throws {
let url = try url(for: path)
try self.write(string: string, to: url)

View File

@@ -105,8 +105,6 @@ extension ShellClient {
/// Reads contents at the given file path, then allows the caller to act on the
/// results after "nil" has been replaced with the current version string.
///
/// > Note: The file contents will be reset back to nil after the closure operation.
///
/// - Parameters:
/// - filePath: The file path to replace nil in.
/// - workingDirectory: Customize the working directory for the command.

View File

@@ -1,29 +0,0 @@
import Foundation
import GitVersion
import ShellClient
/// Shows the intended use-case for building a command line tool that set's the version
/// based on the tag in the git worktree.
@main
public struct Build {
public static func main() throws {
@Dependency(\.shellClient) var shell: ShellClient
let gitDir = URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
try withDependencies {
$0.gitVersionClient = .liveValue
$0.logger.logLevel = .debug
} operation: {
try shell.replacingNilWithVersionString(
in: "Sources/example/Version.swift",
from: gitDir.absoluteString,
build: SwiftBuild.release()
)
}
}
}

View File

@@ -1,2 +0,0 @@
// Do not change this value, it is set by the build script.
let VERSION: String? = nil

View File

@@ -1,23 +0,0 @@
import ArgumentParser
import ShellClient
/// An example of using the git version client with a command line tool
/// The ``VERSION`` variable get's set during the build process.
@main
public struct Example: ParsableCommand {
public static let configuration: CommandConfiguration = .init(
abstract: "An example of using the `GitVersion` command to set the version for a command line app.",
version: VERSION ?? "0.0.0"
)
public init() { }
public func run() throws {
@Dependency(\.logger) var logger: Logger
let version = (VERSION ?? "0.0.0").blue
logger.info("Version: \(version)")
}
}

View File

@@ -1,34 +0,0 @@
import ArgumentParser
import Dependencies
import GitVersion
import ShellClient
@main
public struct GitVersionBuilder: AsyncParsableCommand {
public init() { }
@Argument
var input: String
@Argument
var output: String
public func run() async throws {
@Dependency(\.logger) var logger
@Dependency(\.fileClient) var fileClient
@Dependency(\.shellClient) var shell: ShellClient
logger.debug("Building with input file: \(input)")
logger.debug("Output file: \(output)")
try shell.replacingNilWithVersionString(
in: input
) { update in
logger.debug("Updating with:\n\(update)")
try fileClient.write(string: update, to: output)
}
}
}

View File

@@ -0,0 +1,49 @@
import ArgumentParser
import Dependencies
import Foundation
import GitVersion
import ShellClient
extension GitVersionCommand {
struct Generate: ParsableCommand {
static var configuration: CommandConfiguration = .init(
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha."
)
@OptionGroup var shared: SharedOptions
func run() 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 fileClient.write(string: template, to: fileUrl)
logger.info("Generated file at: \(fileString)")
} else {
logger.info("Would generate file at: \(fileString)")
}
}
}
}
fileprivate enum GenerationError: Error {
case fileExists(path: String)
}
fileprivate let template = """
// Do not set this variable, it is set during the build process.
let VERSION: String? = nil
"""

View File

@@ -0,0 +1,30 @@
import ArgumentParser
import Foundation
@main
struct GitVersionCommand: ParsableCommand {
static var configuration: CommandConfiguration = .init(
commandName: "git-version",
version: VERSION ?? "0.0.0",
subcommands: [
Generate.self,
Update.self
]
)
}
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
}

View File

@@ -0,0 +1,20 @@
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 {
self.absoluteString
.replacingOccurrences(of: "file://", with: "")
}
}

View File

@@ -0,0 +1,66 @@
import ArgumentParser
import Foundation
import GitVersion
import ShellClient
extension GitVersionCommand {
struct Update: ParsableCommand {
static var configuration: CommandConfiguration = .init(
abstract: "Updates a version string to the git tag or git sha."
)
@OptionGroup var shared: SharedOptions
@Option(
name: .customLong("git-directory"),
help: "The git directory for the version."
)
var gitDirectory: String? = nil
func run() throws {
@Dependency(\.logger) var logger: Logger
@Dependency(\.fileClient) var fileClient: FileClient
@Dependency(\.gitVersionClient) var gitVersion
@Dependency(\.shellClient) var shell
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("Version file does not exist.")
// throw UpdateError.versionFileDoesNotExist(path: fileString)
// }
let currentVersion = try gitVersion.currentVersion()
let cwd = FileManager.default.currentDirectoryPath
logger.info("CWD: \(cwd)")
logger.info("Git version: \(currentVersion)")
var updatedContents: String = ""
try withDependencies({
$0.logger.logLevel = .debug
}, operation: {
try shell.replacingNilWithVersionString(
in: fileString
// from: gitDirectory
) {
updatedContents = $0
}
})
if !shared.dryRun {
try fileClient.write(string: updatedContents, to: fileUrl)
logger.info("Updated version file: \(fileString)")
} else {
logger.info("Would update file contents to:")
logger.info("\(updatedContents)")
}
}
}
}
fileprivate enum UpdateError: Error {
case versionFileDoesNotExist(path: String)
}

View File

@@ -0,0 +1,2 @@
// Do not set this variable, it is set during the build process.
let VERSION: String? = ""

View File

@@ -35,8 +35,8 @@ final class GitVersionTests: XCTestCase {
// can't really have a predictable result for the live client.
XCTAssertNotEqual(version, "blob")
let other = try versionClient.currentVersion()
XCTAssertEqual(version, other)
// let other = try versionClient.currentVersion()
// XCTAssertEqual(version, other)
})
}