From 23fbc8aae12ee1a6b41e391d9af4af2f90c0858e Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Thu, 15 Jan 2026 16:08:44 -0800 Subject: [PATCH] Add --version flag to SafeDITool --- .github/workflows/ci.yml | 11 +++ Plugins/Shared.swift | 2 +- SafeDI.podspec | 2 +- Scripts/check-version-consistency.sh | 51 ++++++++++++++ Sources/SafeDITool/SafeDITool.swift | 11 +++ .../Helpers/SafeDIToolTestExecution.swift | 1 + .../SafeDIToolCodeGenerationErrorTests.swift | 2 + .../SafeDIToolVersionTests.swift | 69 +++++++++++++++++++ 8 files changed, 147 insertions(+), 2 deletions(-) create mode 100755 Scripts/check-version-consistency.sh create mode 100644 Tests/SafeDIToolTests/SafeDIToolVersionTests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82a2dbd..3cb5840 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,17 @@ on: pull_request: jobs: + version-check: + name: Version Consistency Check + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout Repo + uses: actions/checkout@v5 + - name: Check version consistency + run: ./Scripts/check-version-consistency.sh + xcodebuild: name: Build with xcodebuild on Xcode 16 runs-on: macos-15 diff --git a/Plugins/Shared.swift b/Plugins/Shared.swift index 9880cdb..7be3589 100644 --- a/Plugins/Shared.swift +++ b/Plugins/Shared.swift @@ -29,7 +29,7 @@ import PackagePlugin // As of Xcode 15.0, Xcode command plugins have no way to read the package manifest, therefore we must hardcode the version number. // It is okay for this number to be behind the most current release if the inputs and outputs to SafeDITool have not changed. // Unlike SPM plugins, Xcode plugins can not determine the current version number, so we must hardcode it. - "1.4.3" + "1.5.0" } var safeDIOrigin: URL { diff --git a/SafeDI.podspec b/SafeDI.podspec index d6122ee..aa39bbc 100644 --- a/SafeDI.podspec +++ b/SafeDI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SafeDI' - s.version = '1.4.3' + s.version = '1.5.0' s.summary = 'Compile-time-safe dependency injection' s.homepage = 'https://github.com/dfed/SafeDI' s.license = 'MIT' diff --git a/Scripts/check-version-consistency.sh b/Scripts/check-version-consistency.sh new file mode 100755 index 0000000..cff334a --- /dev/null +++ b/Scripts/check-version-consistency.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# This script checks that the version string is consistent across all files +# that contain it. Run this in CI to catch version mismatches. + +set -e + +# Helper function to extract and validate a version +extract_version() { + local file="$1" + local pattern="$2" + local context_lines="$3" + local quote_char="$4" + + local version + version=$(grep -A"$context_lines" "$pattern" "$file" | grep -o "${quote_char}[0-9]*\.[0-9]*\.[0-9]*${quote_char}" | tr -d "$quote_char") + + local count + count=$(echo "$version" | grep -c . || true) + + if [ -z "$version" ]; then + echo "ERROR: Could not find version in $file" >&2 + return 1 + elif [ "$count" -gt 1 ]; then + echo "ERROR: Found multiple versions in $file: $version" >&2 + return 1 + fi + + echo "$version" +} + +echo "Checking version consistency..." + +# Extract version from SafeDITool.swift (looks for the line after "static var currentVersion") +TOOL_VERSION=$(extract_version "Sources/SafeDITool/SafeDITool.swift" "static var currentVersion" 1 '"') +echo " SafeDITool.swift: $TOOL_VERSION" + +# Extract version from Plugins/Shared.swift (the safeDIVersion property in XcodePluginContext) +PLUGIN_VERSION=$(extract_version "Plugins/Shared.swift" "var safeDIVersion: String" 4 '"') +echo " Plugins/Shared.swift: $PLUGIN_VERSION" + +# Extract version from SafeDI.podspec +PODSPEC_VERSION=$(extract_version "SafeDI.podspec" "s.version" 0 "'") +echo " SafeDI.podspec: $PODSPEC_VERSION" + +if [ "$TOOL_VERSION" != "$PLUGIN_VERSION" ] || [ "$TOOL_VERSION" != "$PODSPEC_VERSION" ]; then + echo "ERROR: Version mismatch detected!" + exit 1 +fi + +echo "All versions match: $TOOL_VERSION" diff --git a/Sources/SafeDITool/SafeDITool.swift b/Sources/SafeDITool/SafeDITool.swift index be7c19e..06dc543 100644 --- a/Sources/SafeDITool/SafeDITool.swift +++ b/Sources/SafeDITool/SafeDITool.swift @@ -29,6 +29,8 @@ struct SafeDITool: AsyncParsableCommand, Sendable { @Argument(help: "A path to a CSV file containing paths of Swift files to parse.") var swiftSourcesFilePath: String? + @Flag(name: .customLong("version"), help: "Print the SafeDITool version and exit.") var showVersion = false + @Option(parsing: .upToNextOption, help: "Directories containing Swift files to include, relative to the executing directory.") var include: [String] = [] @Option(help: "A path to a CSV file comprising directories containing Swift files to include, relative to the executing directory.") var includeFilePath: String? @@ -47,7 +49,16 @@ struct SafeDITool: AsyncParsableCommand, Sendable { // MARK: Internal + static var currentVersion: String { + "1.5.0" + } + func run() async throws { + guard !showVersion else { + print(Self.currentVersion) + return + } + if swiftSourcesFilePath == nil, include.isEmpty, includeFilePath == nil { throw ValidationError("Must provide 'swift-sources-file-path', '--include', or '--include-file-path'.") } diff --git a/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift b/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift index 3b4aff3..ada6f6f 100644 --- a/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift +++ b/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift @@ -62,6 +62,7 @@ func executeSafeDIToolTest( return try await SafeDITool.$fileFinder.withValue(StubFileFinder(files: swiftFiles)) { // Successfully execute the file finder code path. var tool = SafeDITool() tool.swiftSourcesFilePath = swiftFileCSV.relativePath + tool.showVersion = false tool.include = [] tool.includeFilePath = !includeFolders.isEmpty ? includeFile.relativePath : nil tool.additionalImportedModules = [] diff --git a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift index 6ace254..dac4ac1 100644 --- a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift @@ -1933,6 +1933,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { await SafeDITool.$fileFinder.withValue(FailingFileFinder()) { var tool = SafeDITool() tool.swiftSourcesFilePath = nil + tool.showVersion = false tool.include = ["Fake"] tool.includeFilePath = nil tool.additionalImportedModules = [] @@ -1951,6 +1952,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { func include_throwsErrorWhenNoSwiftSourcesFilePathAndNoInclude() async { var tool = SafeDITool() tool.swiftSourcesFilePath = nil + tool.showVersion = false tool.include = [] tool.includeFilePath = nil tool.additionalImportedModules = [] diff --git a/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift b/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift new file mode 100644 index 0000000..7428356 --- /dev/null +++ b/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift @@ -0,0 +1,69 @@ +// Distributed under the MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import Testing + +#if os(Linux) + import Glibc +#else + import Darwin +#endif + +@testable import SafeDITool + +@MainActor // serialized due to changes to stdout +struct SafeDIToolVersionTests { + @Test + func run_withVersionFlag_printsCurrentVersion() async throws { + var tool = SafeDITool() + tool.swiftSourcesFilePath = nil + tool.showVersion = true + tool.include = [] + tool.includeFilePath = nil + tool.additionalImportedModules = [] + tool.additionalImportedModulesFilePath = nil + tool.moduleInfoOutput = nil + tool.dependentModuleInfoFilePath = nil + tool.dependencyTreeOutput = nil + tool.dotFileOutput = nil + + let output = try await captureStandardOutput { + try await tool.run() + } + + let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) + #expect(trimmedOutput == SafeDITool.currentVersion) + } + + private func captureStandardOutput(_ block: () async throws -> Void) async throws -> String { + let pipe = Pipe() + let originalStdout = dup(STDOUT_FILENO) + dup2(pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) + + try await block() + + pipe.fileHandleForWriting.closeFile() + dup2(originalStdout, STDOUT_FILENO) + close(originalStdout) + + return String(data: pipe.fileHandleForReading.availableData, encoding: .utf8) ?? "" + } +}