Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Plugins/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion SafeDI.podspec
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
51 changes: 51 additions & 0 deletions Scripts/check-version-consistency.sh
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 11 additions & 0 deletions Sources/SafeDITool/SafeDITool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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'.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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 = []
Expand Down
69 changes: 69 additions & 0 deletions Tests/SafeDIToolTests/SafeDIToolVersionTests.swift
Original file line number Diff line number Diff line change
@@ -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) ?? ""
}
}