Skip to content
Draft
168 changes: 168 additions & 0 deletions .github/scripts/report-swift-extraction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import Foundation

enum InvocationSummaryError: Error {
case invalidLog(String)
case ioFailure(String, NSError)
}

struct SwiftInvocationSummary: Encodable {
let module: String
let target: String
let sdk: String
let sourceArgumentCount: Int
let moduleEmission: Bool
let objcHeaderEmission: Bool
let executableEmission: Bool
let accessibilityAmbiguityCount: Int
let otherCompilerErrorCount: Int
}

struct SwiftLogSummary: Encodable {
let examinedInvocationLogCount: Int
let invocations: [SwiftInvocationSummary]
}

func capturedValues(_ pattern: String, _ text: String) throws -> [String] {
let regex: NSRegularExpression = try NSRegularExpression(pattern: pattern)
let range: NSRange = NSRange(text.startIndex..<text.endIndex, in: text)
return try regex.matches(in: text, range: range).map { match in
guard let valueRange: Range<String.Index> = Range(match.range(at: 1), in: text) else {
throw InvocationSummaryError.invalidLog("Extractor metadata capture lacks its required field")
}
return String(text[valueRange])
}
}

func singleValue(_ pattern: String, _ text: String, _ field: String) throws -> String {
let values: [String] = try capturedValues(pattern, text)
guard values.count == 1 else {
throw InvocationSummaryError.invalidLog("Expected one extractor " + field + " value")
}
return values[0]
}

/// Only closed module names, validated public toolchain identifiers and counts leave the log boundary.
func summarizeInvocation(_ text: String) throws -> SwiftInvocationSummary {
let lines: [String] = text.components(separatedBy: "\n")
guard lines.count <= 100_000, lines.allSatisfy({ $0.utf8.count <= 256 * 1024 }) else {
throw InvocationSummaryError.invalidLog("Extractor log line bounds exceeded")
}
let commands: [String] = lines.filter { $0.contains("calling extractor with arguments \"") }
guard commands.count == 1 else {
throw InvocationSummaryError.invalidLog("Expected one command record per extractor invocation log")
}
let command: String = commands[0]
let module: String = try singleValue(#"-module-name\s+([^\s\"]+)"#, command, "module")
let modules: Set<String> = ["SystemProfilerExplorer", "SystemProfilerExplorerTests", "SwiftExtractionSummary"]
guard modules.contains(module) else {
throw InvocationSummaryError.invalidLog("Extractor module is outside the closed diagnostic contract")
}
let target: String = try singleValue(#"-target\s+((?:arm64|x86_64)-apple-macosx?[0-9]{1,2}(?:\.[0-9]{1,2}){0,2})(?=\s|\")"#, command, "target")
let sdk: String = try singleValue(#"-sdk\s+[^\s\"]*/(MacOSX[0-9]{1,2}(?:\.[0-9]{1,2}){0,2}\.sdk)(?=\s|\")"#, command, "SDK")
let sources: [String] = try capturedValues(#"(?:\s|^)([^\s\"]+\.swift)(?=\s|\")"#, command)
let compilerErrors: [String] = lines.filter { $0.contains("ERRO [extractor/compiler]") }
let ambiguities: Int = compilerErrors.filter { $0.contains("reference to 'NSAccessibilityElement' is ambiguous") }.count
return SwiftInvocationSummary(
module: module, target: target, sdk: sdk, sourceArgumentCount: sources.count,
moduleEmission: !(try capturedValues(#"(?:\s|")(-emit-module)(?=\s|")"#, command)).isEmpty,
objcHeaderEmission: !(try capturedValues(#"(?:\s|")(-emit-objc-header)(?=\s|")"#, command)).isEmpty,
executableEmission: !(try capturedValues(#"(?:\s|")(-emit-executable)(?=\s|")"#, command)).isEmpty,
accessibilityAmbiguityCount: ambiguities,
otherCompilerErrorCount: compilerErrors.count - ambiguities
)
}

func checkedDirectory(_ url: URL) throws -> URL {
let values: URLResourceValues
do {
values = try url.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey])
} catch let error as NSError {
throw InvocationSummaryError.ioFailure("inspect extractor log directory", error)
}
guard values.isDirectory == true, values.isSymbolicLink == false else {
throw InvocationSummaryError.invalidLog("Extractor log directory is missing or is a symbolic link")
}
return url
}

func readInvocationLogs(_ temporaryDirectory: URL) throws -> [String] {
var extractorDirectory: URL = try checkedDirectory(temporaryDirectory.resolvingSymlinksInPath())
for component in ["codeql_databases", "swift", "log", "swift", "extractor"] {
extractorDirectory = try checkedDirectory(extractorDirectory.appendingPathComponent(component))
}
let files: [URL]
do {
files = try FileManager.default.contentsOfDirectory(at: extractorDirectory, includingPropertiesForKeys: [.isRegularFileKey, .isSymbolicLinkKey, .fileSizeKey])
} catch let error as NSError {
throw InvocationSummaryError.ioFailure("list Swift extractor invocation logs", error)
}
let logs: [URL] = files.filter { $0.pathExtension == "log" }.sorted { $0.lastPathComponent < $1.lastPathComponent }
guard !logs.isEmpty, logs.count <= 500 else {
throw InvocationSummaryError.invalidLog("Swift invocation log count must be between 1 and 500")
}
var totalBytes: Int = 0
return try logs.map { url in
let values: URLResourceValues
do {
values = try url.resourceValues(forKeys: [.isRegularFileKey, .isSymbolicLinkKey, .fileSizeKey])
} catch let error as NSError {
throw InvocationSummaryError.ioFailure("inspect Swift extractor invocation log", error)
}
guard values.isRegularFile == true, values.isSymbolicLink == false,
let size: Int = values.fileSize, size >= 0, size <= 20 * 1024 * 1024 else {
throw InvocationSummaryError.invalidLog("Invocation log must be a bounded regular file without a symbolic link")
}
let data: Data
do {
data = try Data(contentsOf: url, options: .mappedIfSafe)
} catch let error as NSError {
throw InvocationSummaryError.ioFailure("read Swift extractor invocation log", error)
}
guard data.count <= 20 * 1024 * 1024 else {
throw InvocationSummaryError.invalidLog("Swift invocation log changed beyond the byte limit during reading")
}
totalBytes += data.count
guard totalBytes <= 80 * 1024 * 1024 else {
throw InvocationSummaryError.invalidLog("Swift invocation logs exceed the total byte limit")
}
guard let text: String = String(data: data, encoding: .utf8) else {
throw InvocationSummaryError.invalidLog("Swift invocation log must contain valid UTF-8")
}
return text
}
}

func writeSummary(_ arguments: [String]) throws {
guard arguments.count == 2 else {
throw InvocationSummaryError.invalidLog("Expected the runner temporary directory and summary output path")
}
let temporaryDirectory: URL = URL(fileURLWithPath: arguments[0])
let output: URL = URL(fileURLWithPath: arguments[1])
guard output.standardizedFileURL.path == temporaryDirectory.appendingPathComponent("swift-invocation-summary.json").standardizedFileURL.path else {
throw InvocationSummaryError.invalidLog("Summary output must use the fixed task-owned temporary file")
}
let logs: [String] = try readInvocationLogs(temporaryDirectory)
let summary: SwiftLogSummary = SwiftLogSummary(examinedInvocationLogCount: logs.count, invocations: try logs.map(summarizeInvocation))
let encoder: JSONEncoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data: Data = try encoder.encode(summary)
guard data.count <= 256 * 1024 else {
throw InvocationSummaryError.invalidLog("Bounded invocation metadata exceeds the output byte limit")
}
do {
try data.write(to: output, options: .withoutOverwriting)
} catch let error as NSError {
throw InvocationSummaryError.ioFailure("write bounded Swift invocation summary", error)
}
FileHandle.standardOutput.write(data + Data("\n".utf8))
}

do {
try writeSummary(Array(CommandLine.arguments.dropFirst()))
} catch InvocationSummaryError.invalidLog(let reason) {
FileHandle.standardError.write(Data("Swift invocation summary rejected: \(reason).\n".utf8))
exit(1)
} catch InvocationSummaryError.ioFailure(let operation, let error) {
FileHandle.standardError.write(Data("Swift invocation summary I/O failed: operation=\(operation) domain=\(error.domain) code=\(error.code).\n".utf8))
exit(1)
}
63 changes: 61 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
os: ubuntu-latest
build-mode: none
env:
DEVELOPER_DIR: ${{ matrix.language == 'swift' && '/Applications/Xcode_26.4.1.app/Contents/Developer' || '' }}
DEVELOPER_DIR: ${{ matrix.language == 'swift' && '/Applications/Xcode_26.6.app/Contents/Developer' || '' }}
steps:
- name: Check out source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand All @@ -48,17 +48,76 @@ jobs:
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Report Swift extraction toolchain
if: matrix.language == 'swift'
run: |
xcodebuild -version
xcrun swiftc --version
xcrun --sdk macosx --show-sdk-version
xcrun --sdk macosx --show-sdk-build-version
- name: Build Swift source for extraction
if: matrix.language == 'swift'
run: xcodebuild -project SystemProfilerExplorer.xcodeproj -scheme SystemProfilerExplorer -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build test
run: |
xcrun swiftc -swift-version 6 -module-name SwiftExtractionSummary \
-target x86_64-apple-macos13.0 -sdk "$(xcrun --sdk macosx --show-sdk-path)" \
.github/scripts/report-swift-extraction.swift -o "$RUNNER_TEMP/report-swift-extraction"
xcodebuild -project SystemProfilerExplorer.xcodeproj \
-scheme SystemProfilerExplorer \
-destination 'platform=macOS,arch=x86_64' \
-derivedDataPath "$RUNNER_TEMP/codeql-build" \
ARCHS=x86_64 ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
COMPILATION_CACHE_ENABLE_CACHING=NO SWIFT_ENABLE_COMPILE_CACHE=NO \
SWIFT_USE_INTEGRATED_DRIVER=NO SWIFT_ENABLE_EXPLICIT_MODULES=NO \
SWIFT_COMPILATION_MODE=wholemodule \
build test
- name: Analyze source without upload permissions
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
with:
category: /language:${{ matrix.language }}
output: results
upload: never
upload-database: false
- name: Reject Swift extraction errors
if: matrix.language == 'swift'
env:
SARIF_FILE: results/swift.sarif
run: |
if [[ ! -s "$SARIF_FILE" ]]; then
printf 'Swift SARIF is missing or empty.\n' >&2
exit 1
fi
error_count="$(jq -er '
if type != "object" or (.runs | type) != "array" or (.runs | length) == 0 then
error("Invalid Swift SARIF runs")
elif any(.runs[]; type != "object" or (.invocations | type) != "array" or (.invocations | length) == 0) then
error("Invalid Swift SARIF invocations")
elif any(.runs[].invocations[]; type != "object" or (.toolExecutionNotifications | type) != "array") then
error("Invalid Swift SARIF notifications")
elif any(.runs[].invocations[].toolExecutionNotifications[]; type != "object" or (.descriptor | type) != "object" or (.descriptor.id | type) != "string") then
error("Invalid Swift SARIF notification descriptor")
else
[.runs[].invocations[].toolExecutionNotifications[] | select(.descriptor.id == "swift/diagnostics/extraction-errors")] | length
end
' "$SARIF_FILE")"
if [[ "$error_count" != "0" ]]; then
printf 'Swift extraction reported %s compiler diagnostics; inspect the analysis diagnostics before publishing SARIF.\n' "$error_count" >&2
exit 1
fi
printf 'Swift extraction compiler diagnostics: 0\n'
- name: Summarize bounded Swift invocation diagnostics
if: matrix.language == 'swift' && (success() || failure())
run: '"$RUNNER_TEMP/report-swift-extraction" "$RUNNER_TEMP" "$RUNNER_TEMP/swift-invocation-summary.json"'
- name: Retain bounded Swift context after failure
if: matrix.language == 'swift' && failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: swift-invocation-context
path: ${{ runner.temp }}/swift-invocation-summary.json
if-no-files-found: error
retention-days: 1
- name: Retain analysis results for the upload job
if: success() || failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: codeql-results-${{ matrix.language }}
Expand Down
Loading