From 9b598bcfe36b3e0b6c983a2ce2813ba9d68c7bf0 Mon Sep 17 00:00:00 2001 From: theoriginalbit Date: Sat, 24 Feb 2024 10:54:59 +1100 Subject: [PATCH] Add implementation of tool to generate LibraryVersion --- Package.resolved | 14 +++++ Package.swift | 26 ++++---- Plugins/GitVersionPlugin.swift | 56 +++++++---------- README.md | 8 +++ Sources/GitStatus/main.swift | 111 +++++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/GitStatus/main.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..fa0a833 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index 7d2064e..ca0a096 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,24 @@ // swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - import PackageDescription let package = Package( name: "GitVersionPlugin", + platforms: [ + .macOS(.v13), + ], products: [ - // Products can be used to vend plugins, making them visible to other packages. - .plugin( - name: "GitVersionPlugin", - targets: ["GitVersionPlugin"]), + .plugin(name: "GitVersionPlugin", targets: ["GitVersionPlugin"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-syntax.git", exact: "509.1.1"), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .plugin( - name: "GitVersionPlugin", - capability: .buildTool() - ), + .plugin(name: "GitVersionPlugin", capability: .buildTool(), dependencies: [ + .target(name: "GitStatus"), + ]), + .executableTarget(name: "GitStatus", dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + ]), ] ) diff --git a/Plugins/GitVersionPlugin.swift b/Plugins/GitVersionPlugin.swift index 8b1a565..317bd4b 100644 --- a/Plugins/GitVersionPlugin.swift +++ b/Plugins/GitVersionPlugin.swift @@ -3,17 +3,14 @@ import PackagePlugin @main struct GitVersionPlugin: BuildToolPlugin { /// Entry point for creating build commands for targets in Swift packages. - func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { - // This plugin only runs for package targets that can have source files. - guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] } - - // Find the code generator tool to run (replace this with the actual one). - let generatorTool = try context.tool(named: "my-code-generator") - - // Construct a build command for each source file with a particular suffix. - return sourceFiles.map(\.path).compactMap { - createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: generatorTool.path) - } + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + return try [ + createBuildCommand( + in: context.package.directory, + outputtingTo: context.pluginWorkDirectory, + tool: context.tool(named: "GitStatus") + ), + ] } } @@ -21,36 +18,29 @@ struct GitVersionPlugin: BuildToolPlugin { import XcodeProjectPlugin extension GitVersionPlugin: XcodeBuildToolPlugin { - // Entry point for creating build commands for targets in Xcode projects. + /// Entry point for creating build commands for targets in Xcode projects. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { - // Find the code generator tool to run (replace this with the actual one). - let generatorTool = try context.tool(named: "my-code-generator") - - // Construct a build command for each source file with a particular suffix. - return target.inputFiles.map(\.path).compactMap { - createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: generatorTool.path) - } + return try [ + createBuildCommand( + in: context.xcodeProject.directory, + outputtingTo: context.pluginWorkDirectory, + tool: context.tool(named: "GitStatus") + ), + ] } } #endif extension GitVersionPlugin { - /// Shared function that returns a configured build command if the input files is one that should be processed. - func createBuildCommand(for inputPath: Path, in outputDirectoryPath: Path, with generatorToolPath: Path) -> Command? { - // Skip any file that doesn't have the extension we're looking for (replace this with the actual one). - guard inputPath.extension == "my-input-suffix" else { return .none } - - // Return a command that will run during the build to generate the output file. - let inputName = inputPath.lastComponent - let outputName = inputPath.stem + ".swift" - let outputPath = outputDirectoryPath.appending(outputName) + func createBuildCommand(in rootPath: Path, outputtingTo outputPath: Path, tool: PluginContext.Tool) -> Command { + let generatedSourceFile = outputPath.appending("LibraryVersion.swift") + // Return a command that will run during the build to generate the LibraryVersion file. return .buildCommand( - displayName: "Generating \(outputName) from \(inputName)", - executable: generatorToolPath, - arguments: ["\(inputPath)", "-o", "\(outputPath)"], - inputFiles: [inputPath], - outputFiles: [outputPath] + displayName: "Getting package repository state", + executable: tool.path, + arguments: [rootPath, generatedSourceFile], + outputFiles: [generatedSourceFile] ) } } diff --git a/README.md b/README.md index f084656..4d4d17d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # GitVersionPlugin + A SwiftPM plugin that codegens a version number, as read by Git, for use in tooling + +Split from [Apple's swift-testing repo](https://github.com/apple/swift-testing/blob/7f39433a0a78ccc92b541597c542b70f68de75e6/Sources/GitStatus/main.swift) and Xcode plugin added to the interface. + +The version is determined from: +- If the repository is sitting at a tag with no uncommitted changes, use the tag. +- Otherwise, use the commit hash (with a "there are changes" marker if needed.) +- Finally, fall back to nil if nothing else is available. diff --git a/Sources/GitStatus/main.swift b/Sources/GitStatus/main.swift new file mode 100644 index 0000000..9675d47 --- /dev/null +++ b/Sources/GitStatus/main.swift @@ -0,0 +1,111 @@ +// +// This file was sourced from: +// https://github.com/apple/swift-testing/blob/7f39433a0a78ccc92b541597c542b70f68de75e6/Sources/GitStatus/main.swift +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// See https://swift.org/LICENSE.txt for license information +// + +import Foundation +import SwiftSyntax +import SwiftSyntaxBuilder + +// Resolve arguments to the tool. +let repoPath = CommandLine.arguments[1] +let generatedSourceURL = URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: false) + +/// Run the `git` tool and process the output it writes to standard output. +/// +/// - Parameters: +/// - arguments: The arguments to pass to `git`. +/// - maxOutputCount: The maximum amount of output to read and return. +/// +/// - Returns: A string containing the `git` command's output, up to +/// `maxOutputCount` UTF-8-encoded bytes, or `nil` if the command failed or +/// the output could not be read. +func _runGit(passing arguments: String..., readingUpToCount maxOutputCount: Int) -> String? { + #if os(macOS) || os(Linux) || os(Windows) + let path: String + var arguments = ["-C", repoPath] + arguments + #if os(Windows) + path = "C:\\Program Files\\Git\\cmd\\git.exe" + #else + path = "/usr/bin/env" + arguments = CollectionOfOne("git") + arguments + #endif + + let process = Process() + process.executableURL = URL(fileURLWithPath: path, isDirectory: false) + process.arguments = arguments + + let stdoutPipe = Pipe() + process.standardOutput = stdoutPipe + process.standardError = nil + do { + try process.run() + } catch { + return nil + } + defer { + process.terminate() + } + guard let output = try? stdoutPipe.fileHandleForReading.read(upToCount: maxOutputCount) else { + return nil + } + return String(data: output, encoding: .utf8) + #else + return nil + #endif +} + +// The current Git tag, if available. +let currentGitTag = _runGit(passing: "describe", "--exact-match", "--tags", readingUpToCount: 40)? + .split(whereSeparator: \.isNewline) + .first + .map(String.init) + +// The current Git commit hash, if available. +let currentGitCommitHash = _runGit(passing: "rev-parse", "HEAD", readingUpToCount: 40)? + .split(whereSeparator: \.isNewline) + .first + .map(String.init) + +// Whether or not the Git repository has uncommitted changes, if available. +let gitHasUncommittedChanges = _runGit(passing: "status", "-s", readingUpToCount: 1) + .map { !$0.isEmpty } ?? false + +// Figure out what value to emit for the version: +// - If the repository is sitting at a tag with no uncommitted changes, use the tag. +// - Otherwise, use the commit hash (with a "there are changes" marker if needed.) +// - Finally, fall back to nil if nothing else is available. +let sourceCode: DeclSyntax = if !gitHasUncommittedChanges, let currentGitTag { + """ + var _toolVersion: String? { + \(literal: currentGitTag) + } + """ +} else if let currentGitCommitHash { + if gitHasUncommittedChanges { + """ + var _toolVersion: String? { + \(literal: currentGitCommitHash) + " (modified)" + } + """ + } else { + """ + var _toolVersion: String? { + \(literal: currentGitCommitHash) + } + """ + } +} else { + """ + var _toolVersion: String? { + nil + } + """ +} + +// Write the generated Swift file to the specified destination path. +try String(describing: sourceCode).write(to: generatedSourceURL, atomically: false, encoding: .utf8)