Skip to content

Commit

Permalink
Add implementation of tool to generate LibraryVersion
Browse files Browse the repository at this point in the history
  • Loading branch information
theoriginalbit committed Feb 24, 2024
1 parent 7b401ff commit 9b598bc
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 45 deletions.
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 14 additions & 12 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"),
]),
]
)
56 changes: 23 additions & 33 deletions Plugins/GitVersionPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,44 @@ 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")
),
]
}
}

#if canImport(XcodeProjectPlugin)
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]
)
}
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
111 changes: 111 additions & 0 deletions Sources/GitStatus/main.swift
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 9b598bc

Please sign in to comment.