Skip to content

Commit 9b598bc

Browse files
Add implementation of tool to generate LibraryVersion
1 parent 7b401ff commit 9b598bc

5 files changed

Lines changed: 170 additions & 45 deletions

File tree

Package.resolved

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
// swift-tools-version: 5.9
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
3-
42
import PackageDescription
53

64
let package = Package(
75
name: "GitVersionPlugin",
6+
platforms: [
7+
.macOS(.v13),
8+
],
89
products: [
9-
// Products can be used to vend plugins, making them visible to other packages.
10-
.plugin(
11-
name: "GitVersionPlugin",
12-
targets: ["GitVersionPlugin"]),
10+
.plugin(name: "GitVersionPlugin", targets: ["GitVersionPlugin"]),
11+
],
12+
dependencies: [
13+
.package(url: "https://github.com/apple/swift-syntax.git", exact: "509.1.1"),
1314
],
1415
targets: [
15-
// Targets are the basic building blocks of a package, defining a module or a test suite.
16-
// Targets can depend on other targets in this package and products from dependencies.
17-
.plugin(
18-
name: "GitVersionPlugin",
19-
capability: .buildTool()
20-
),
16+
.plugin(name: "GitVersionPlugin", capability: .buildTool(), dependencies: [
17+
.target(name: "GitStatus"),
18+
]),
19+
.executableTarget(name: "GitStatus", dependencies: [
20+
.product(name: "SwiftSyntax", package: "swift-syntax"),
21+
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
22+
]),
2123
]
2224
)

Plugins/GitVersionPlugin.swift

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,44 @@ import PackagePlugin
33
@main
44
struct GitVersionPlugin: BuildToolPlugin {
55
/// Entry point for creating build commands for targets in Swift packages.
6-
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
7-
// This plugin only runs for package targets that can have source files.
8-
guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] }
9-
10-
// Find the code generator tool to run (replace this with the actual one).
11-
let generatorTool = try context.tool(named: "my-code-generator")
12-
13-
// Construct a build command for each source file with a particular suffix.
14-
return sourceFiles.map(\.path).compactMap {
15-
createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: generatorTool.path)
16-
}
6+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
7+
return try [
8+
createBuildCommand(
9+
in: context.package.directory,
10+
outputtingTo: context.pluginWorkDirectory,
11+
tool: context.tool(named: "GitStatus")
12+
),
13+
]
1714
}
1815
}
1916

2017
#if canImport(XcodeProjectPlugin)
2118
import XcodeProjectPlugin
2219

2320
extension GitVersionPlugin: XcodeBuildToolPlugin {
24-
// Entry point for creating build commands for targets in Xcode projects.
21+
/// Entry point for creating build commands for targets in Xcode projects.
2522
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
26-
// Find the code generator tool to run (replace this with the actual one).
27-
let generatorTool = try context.tool(named: "my-code-generator")
28-
29-
// Construct a build command for each source file with a particular suffix.
30-
return target.inputFiles.map(\.path).compactMap {
31-
createBuildCommand(for: $0, in: context.pluginWorkDirectory, with: generatorTool.path)
32-
}
23+
return try [
24+
createBuildCommand(
25+
in: context.xcodeProject.directory,
26+
outputtingTo: context.pluginWorkDirectory,
27+
tool: context.tool(named: "GitStatus")
28+
),
29+
]
3330
}
3431
}
3532

3633
#endif
3734

3835
extension GitVersionPlugin {
39-
/// Shared function that returns a configured build command if the input files is one that should be processed.
40-
func createBuildCommand(for inputPath: Path, in outputDirectoryPath: Path, with generatorToolPath: Path) -> Command? {
41-
// Skip any file that doesn't have the extension we're looking for (replace this with the actual one).
42-
guard inputPath.extension == "my-input-suffix" else { return .none }
43-
44-
// Return a command that will run during the build to generate the output file.
45-
let inputName = inputPath.lastComponent
46-
let outputName = inputPath.stem + ".swift"
47-
let outputPath = outputDirectoryPath.appending(outputName)
36+
func createBuildCommand(in rootPath: Path, outputtingTo outputPath: Path, tool: PluginContext.Tool) -> Command {
37+
let generatedSourceFile = outputPath.appending("LibraryVersion.swift")
38+
// Return a command that will run during the build to generate the LibraryVersion file.
4839
return .buildCommand(
49-
displayName: "Generating \(outputName) from \(inputName)",
50-
executable: generatorToolPath,
51-
arguments: ["\(inputPath)", "-o", "\(outputPath)"],
52-
inputFiles: [inputPath],
53-
outputFiles: [outputPath]
40+
displayName: "Getting package repository state",
41+
executable: tool.path,
42+
arguments: [rootPath, generatedSourceFile],
43+
outputFiles: [generatedSourceFile]
5444
)
5545
}
5646
}

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
# GitVersionPlugin
2+
23
A SwiftPM plugin that codegens a version number, as read by Git, for use in tooling
4+
5+
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.
6+
7+
The version is determined from:
8+
- If the repository is sitting at a tag with no uncommitted changes, use the tag.
9+
- Otherwise, use the commit hash (with a "there are changes" marker if needed.)
10+
- Finally, fall back to nil if nothing else is available.

Sources/GitStatus/main.swift

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// This file was sourced from:
3+
// https://github.com/apple/swift-testing/blob/7f39433a0a78ccc92b541597c542b70f68de75e6/Sources/GitStatus/main.swift
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
// See https://swift.org/LICENSE.txt for license information
8+
//
9+
10+
import Foundation
11+
import SwiftSyntax
12+
import SwiftSyntaxBuilder
13+
14+
// Resolve arguments to the tool.
15+
let repoPath = CommandLine.arguments[1]
16+
let generatedSourceURL = URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: false)
17+
18+
/// Run the `git` tool and process the output it writes to standard output.
19+
///
20+
/// - Parameters:
21+
/// - arguments: The arguments to pass to `git`.
22+
/// - maxOutputCount: The maximum amount of output to read and return.
23+
///
24+
/// - Returns: A string containing the `git` command's output, up to
25+
/// `maxOutputCount` UTF-8-encoded bytes, or `nil` if the command failed or
26+
/// the output could not be read.
27+
func _runGit(passing arguments: String..., readingUpToCount maxOutputCount: Int) -> String? {
28+
#if os(macOS) || os(Linux) || os(Windows)
29+
let path: String
30+
var arguments = ["-C", repoPath] + arguments
31+
#if os(Windows)
32+
path = "C:\\Program Files\\Git\\cmd\\git.exe"
33+
#else
34+
path = "/usr/bin/env"
35+
arguments = CollectionOfOne("git") + arguments
36+
#endif
37+
38+
let process = Process()
39+
process.executableURL = URL(fileURLWithPath: path, isDirectory: false)
40+
process.arguments = arguments
41+
42+
let stdoutPipe = Pipe()
43+
process.standardOutput = stdoutPipe
44+
process.standardError = nil
45+
do {
46+
try process.run()
47+
} catch {
48+
return nil
49+
}
50+
defer {
51+
process.terminate()
52+
}
53+
guard let output = try? stdoutPipe.fileHandleForReading.read(upToCount: maxOutputCount) else {
54+
return nil
55+
}
56+
return String(data: output, encoding: .utf8)
57+
#else
58+
return nil
59+
#endif
60+
}
61+
62+
// The current Git tag, if available.
63+
let currentGitTag = _runGit(passing: "describe", "--exact-match", "--tags", readingUpToCount: 40)?
64+
.split(whereSeparator: \.isNewline)
65+
.first
66+
.map(String.init)
67+
68+
// The current Git commit hash, if available.
69+
let currentGitCommitHash = _runGit(passing: "rev-parse", "HEAD", readingUpToCount: 40)?
70+
.split(whereSeparator: \.isNewline)
71+
.first
72+
.map(String.init)
73+
74+
// Whether or not the Git repository has uncommitted changes, if available.
75+
let gitHasUncommittedChanges = _runGit(passing: "status", "-s", readingUpToCount: 1)
76+
.map { !$0.isEmpty } ?? false
77+
78+
// Figure out what value to emit for the version:
79+
// - If the repository is sitting at a tag with no uncommitted changes, use the tag.
80+
// - Otherwise, use the commit hash (with a "there are changes" marker if needed.)
81+
// - Finally, fall back to nil if nothing else is available.
82+
let sourceCode: DeclSyntax = if !gitHasUncommittedChanges, let currentGitTag {
83+
"""
84+
var _toolVersion: String? {
85+
\(literal: currentGitTag)
86+
}
87+
"""
88+
} else if let currentGitCommitHash {
89+
if gitHasUncommittedChanges {
90+
"""
91+
var _toolVersion: String? {
92+
\(literal: currentGitCommitHash) + " (modified)"
93+
}
94+
"""
95+
} else {
96+
"""
97+
var _toolVersion: String? {
98+
\(literal: currentGitCommitHash)
99+
}
100+
"""
101+
}
102+
} else {
103+
"""
104+
var _toolVersion: String? {
105+
nil
106+
}
107+
"""
108+
}
109+
110+
// Write the generated Swift file to the specified destination path.
111+
try String(describing: sourceCode).write(to: generatedSourceURL, atomically: false, encoding: .utf8)

0 commit comments

Comments
 (0)