From 4d48c5c7f837f23c7acb737881b72aa2a411d529 Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Tue, 4 Aug 2026 10:09:30 +0200 Subject: [PATCH 1/3] Add Swift interfaces for agent tooling --- .agents/interfaces/.gitkeep | 0 .agents/interfaces/Snippets.swiftinterface | 873 +++++++++++++++++++++ Makefile | 5 + scripts/generate-swiftinterfaces.sh | 237 ++++++ scripts/list-swift-targets.swift | 82 ++ 5 files changed, 1197 insertions(+) create mode 100644 .agents/interfaces/.gitkeep create mode 100644 .agents/interfaces/Snippets.swiftinterface create mode 100644 Makefile create mode 100755 scripts/generate-swiftinterfaces.sh create mode 100755 scripts/list-swift-targets.swift diff --git a/.agents/interfaces/.gitkeep b/.agents/interfaces/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.agents/interfaces/Snippets.swiftinterface b/.agents/interfaces/Snippets.swiftinterface new file mode 100644 index 0000000..4cbf570 --- /dev/null +++ b/.agents/interfaces/Snippets.swiftinterface @@ -0,0 +1,873 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 6.4 (swiftlang-6.4.0.25.4 clang-2100.3.25.1) +// swift-module-flags: -target arm64-apple-macos12.0 -enable-objc-interop -language-mode 6 -O -enable-experimental-feature DebugDescriptionMacro -module-name Snippets -package-name swift_snippets +// swift-module-flags-ignorable: -formal-cxx-interoperability-mode=off -interface-compiler-version 6.4 +import Dependencies +import Foundation +import KeyPathsExtensions +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims +extension Snippets::SnippetRepresentableLiteral { + public static func const(_ value: Snippets::Snippets.Snippets::Const) -> Self +} +extension Snippets::Snippets.Snippets::Const { + @inlinable public init(_ other: Snippets::Snippets.Snippets::Const) { + self = other + } +} +extension Snippets::Snippets.Snippets::Const where Output : Snippets::SnippetRepresentableString { + @inlinable public static var newline: Snippets::Snippets.Snippets::Const { + get { + return .newlines(1) + } + } + @inlinable public static func newlines(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating("\n", count: count) + } + @inlinable public static var whitespace: Snippets::Snippets.Snippets::Const { + get { + return .whitespaces(1) + } + } + @inlinable public static func whitespaces(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating(" ", count: count) + } + @inlinable public static var tab: Snippets::Snippets.Snippets::Const { + get { + return .tabs(1) + } + } + @inlinable public static func tabs(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating("\t", count: count) + } + @inlinable public static var quote: Snippets::Snippets.Snippets::Const { + get { + return .quotes(1) + } + } + @inlinable public static func quotes(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating("\"", count: count) + } + @inlinable public static var backtick: Snippets::Snippets.Snippets::Const { + get { + return .backticks(1) + } + } + @inlinable public static func backticks(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating("`", count: count) + } + @inlinable public static var colon: Snippets::Snippets.Snippets::Const { + get { + return .colons(1) + } + } + @inlinable public static func colons(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating(":", count: count) + } + @inlinable public static var dot: Snippets::Snippets.Snippets::Const { + get { + return .dots(1) + } + } + @inlinable public static func dots(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating(".", count: count) + } + @inlinable public static var comma: Snippets::Snippets.Snippets::Const { + get { + return .commas(1) + } + } + @inlinable public static func commas(_ count: Swift::Int) -> Snippets::Snippets.Snippets::Const { + .repeating(",", count: count) + } + @inlinable public static var leftBracket: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: "[") + } + } + @inlinable public static var rightBracket: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: "]") + } + } + @inlinable public static var leftBrace: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: "{") + } + } + @inlinable public static var rightBrace: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: "}") + } + } + @inlinable public static var leftParenthesis: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: "(") + } + } + @inlinable public static var rightParenthesis: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: ")") + } + } + @inlinable public static var lessThan: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: "<") + } + } + @inlinable public static var greaterThan: Snippets::Snippets.Snippets::Const { + get { + return .init(snippetLiteral: ">") + } + } + @inlinable public static var equal: Snippets::Snippets.Snippets::Const { + get { + return .equals(count: 1) + } + } + @inlinable public static func equals(count: Swift::Int = 2) -> Snippets::Snippets.Snippets::Const { + return .repeating("=", count: count) + } +} +extension Swift::Array : Snippets::SnippetRepresentable where Element : Snippets::SnippetRepresentableLiteral { + public struct SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Snippets::_StaticSnippetOutputCollector { + @usableFromInline + internal let literal: [Element.SnippetRepresentation] + public init(snippetLiteral: [Element]) + @inlinable public init() { + self.init(snippetLiteral: []) + } + @inlinable public func render() -> [Element] { literal.map { $0.render() } } + @inlinable public static func _collect(_ elements: [Swift::Array.Snippets::SnippetRepresentation.Snippets::Output]) -> [Element] { + .init(elements.joined(separator: environment(\.separator).render())) + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = [Element] + public typealias SnippetLiteralType = [Element] + public typealias SnippetRepresentation = Swift::Array.Snippets::SnippetRepresentation + } +} +extension Swift::Array : Snippets::SnippetRepresentableLiteral where Element : Snippets::SnippetRepresentableLiteral { +} +extension Foundation::Data : Snippets::SnippetRepresentableLiteral { + public struct SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Snippets::_StaticSnippetOutputCollector { + @usableFromInline + internal let literal: Foundation::Data + public init(snippetLiteral: Foundation::Data) + @inlinable public init() { + self.init(snippetLiteral: .init()) + } + @inlinable public func render() -> Foundation::Data { literal } + @inlinable public static func _collect(_ elements: [Foundation::Data.Snippets::SnippetRepresentation.Snippets::Output]) -> Foundation::Data { + .init(elements.joined(separator: environment(\.separator).render())) + } + @available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + @available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) + public typealias Output = Foundation::Data + @available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) + public typealias SnippetLiteralType = Foundation::Data + @available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) + public typealias SnippetRepresentation = Foundation::Data.Snippets::SnippetRepresentation + } +} +extension Swift::Optional : Snippets::SnippetRepresentable where Wrapped : Snippets::SnippetRepresentableLiteral { + public struct SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Snippets::_StaticSnippetOutputCollector { + @usableFromInline + internal let literal: Wrapped.SnippetRepresentation? + public init(snippetLiteral: Wrapped?) + @inlinable public init() { + self.init(snippetLiteral: nil) + } + @inlinable public func render() -> Wrapped? { literal?.render() } + @inlinable public static func _collect(_ elements: [Swift::Optional.Snippets::SnippetRepresentation.Snippets::Output]) -> Wrapped? { + Wrapped.SnippetRepresentation._collect(elements.compactMap { $0 }) + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Wrapped? + public typealias SnippetLiteralType = Wrapped? + public typealias SnippetRepresentation = Swift::Optional.Snippets::SnippetRepresentation + } +} +extension Swift::Optional : Snippets::SnippetRepresentableLiteral where Wrapped : Snippets::SnippetRepresentableLiteral { +} +extension Swift::String : Snippets::SnippetRepresentableString { + public struct SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Snippets::_StaticSnippetOutputCollector { + @usableFromInline + internal let literal: Swift::String + public init(snippetLiteral: Swift::String) + @inlinable public init() { + self.init(snippetLiteral: "") + } + @inlinable public func render() -> Swift::String { literal } + @inlinable public static func _collect(_ elements: [Swift::String.Snippets::SnippetRepresentation.Snippets::Output]) -> Swift::String.Snippets::SnippetRepresentation.Snippets::Output { + elements.joined(separator: environment(\.separator).render()) + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Swift::String + public typealias SnippetLiteralType = Swift::String + public typealias SnippetRepresentation = Swift::String.Snippets::SnippetRepresentation + } +} +extension Swift::Substring : Snippets::SnippetRepresentableString { + public struct SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Snippets::_StaticSnippetOutputCollector { + @usableFromInline + internal let literal: Swift::Substring + public init(snippetLiteral: Swift::Substring) + @inlinable public init() { + self.init(snippetLiteral: "") + } + @inlinable public func render() -> Swift::Substring { literal } + @inlinable public static func _collect(_ elements: [Swift::Substring.Snippets::SnippetRepresentation.Snippets::Output]) -> Swift::Substring { + .init(elements.joined(separator: environment(\.separator).render())) + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Swift::Substring + public typealias SnippetLiteralType = Swift::Substring + public typealias SnippetRepresentation = Swift::Substring.Snippets::SnippetRepresentation + } +} +extension Snippets::SnippetEnvironmentValues { + public var collectPreprocessor: @Sendable ([Output]) -> [Output] { + get + } + public var nonprocessingOutputCollector: @Sendable ([Output]) -> Output { + get + } + public var outputCollector: @Sendable ([Output]) -> Output { + get + } +} +extension Snippets::Snippet { + public func withNextOutputPreprocessor(_ preprocessor: @escaping @Sendable ([Self.Output]) -> [Self.Output], perform operation: () throws -> Self.Output) rethrows -> Self.Output + public func withOutputCollector(fileID: Swift::StaticString = #fileID, filePath: Swift::StaticString = #filePath, line: Swift::UInt = #line, column: Swift::UInt = #column, perform operation: (@Sendable ([Self.Output]) -> Self.Output) throws -> Self.Output) rethrows -> Self.Output +} +extension Snippets::SnippetEnvironmentValues { + public var emptySeparator: some Snippets::Snippet { + get + } + public var separator: some Snippets::Snippet { + get + } +} +public protocol SnippetEnvironmentKey : Dependencies::DependencyKey { + associatedtype Output : Snippets::SnippetRepresentableLiteral + static var defaultValue: Self.Value { get } +} +extension Snippets::SnippetEnvironmentKey { + public static var liveValue: Self.Value { + get + } + public static var testValue: Self.Value { + get + } + public static var previewValue: Self.Value { + get + } +} +extension Snippets::Snippet { + @inlinable public static func environment(fileID: Swift::StaticString = #fileID, filePath: Swift::StaticString = #filePath, line: Swift::UInt = #line, column: Swift::UInt = #column) -> Snippets::SnippetEnvironmentValues { + SnippetEnvironmentValues( + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } + @inlinable public static func environment(_ keyPath: Swift::KeyPath, Value>, fileID: Swift::StaticString = #fileID, filePath: Swift::StaticString = #filePath, line: Swift::UInt = #line, column: Swift::UInt = #column) -> Value { + environment()[keyPath: keyPath] + } +} +public struct SnippetEnvironmentValues : Swift::Sendable where Output : Snippets::SnippetRepresentableLiteral { + @usableFromInline + internal let fileID: Swift::StaticString + @usableFromInline + internal let filePath: Swift::StaticString + @usableFromInline + internal let line: Swift::UInt + @usableFromInline + internal let column: Swift::UInt + @usableFromInline + internal init(fileID: Swift::StaticString = #fileID, filePath: Swift::StaticString = #filePath, line: Swift::UInt = #line, column: Swift::UInt = #column) + public func dependency(_ keyPath: Swift::KeyPath) -> Value +} +extension Snippets::SnippetExpressibleByLiteral where Self.Output : Swift::ExpressibleByStringLiteral { + public typealias StringLiteralType = Self.Output.StringLiteralType + public typealias UnicodeScalarLiteralType = Self.Output.UnicodeScalarLiteralType + public typealias ExtendedGraphemeClusterLiteralType = Self.Output.ExtendedGraphemeClusterLiteralType + public init(stringLiteral value: Self.Output.StringLiteralType) + public init(unicodeScalarLiteral value: Self.Output.UnicodeScalarLiteralType) + public init(extendedGraphemeClusterLiteral value: Self.Output.ExtendedGraphemeClusterLiteralType) +} +public protocol SnippetExpressibleByLiteral : Snippets::Snippet { + associatedtype SnippetLiteralType where Self.Output == Self.SnippetLiteralType + init(snippetLiteral: Self.SnippetLiteralType) + @inlinable init() +} +extension Snippets::SnippetExpressibleByLiteral where Self.Output : Swift::StringProtocol { + public init(_ snippet: some Snippet) +} +@usableFromInline +internal func uncheckedSendableAction(_ action: @escaping (Input) -> Output) -> @Sendable (Input) -> Output +public protocol Snippet : Snippets::SnippetRepresentable, Swift::Sendable where Self == Self.SnippetRepresentation { + associatedtype Output : Snippets::SnippetRepresentableLiteral + associatedtype Content : Snippets::Snippet + func render() -> Self.Output + @Snippets::SnippetBuilder var content: Self.Content { get } +} +extension Snippets::Snippet { + public func makeSnippet() -> Self.SnippetRepresentation +} +extension Snippets::Snippet where Self.Output == Self.Content.Output { + @inlinable @_optimize(none) public func render() -> Self.Output { + self.content.render() + } +} +extension Snippets::Snippet where Self.Content == Swift::Never.Snippets::SnippetRepresentation { + @_transparent public var content: Swift::Never.Snippets::SnippetRepresentation { + @_transparent get { + ._fatalError("'\(Self.self)' has no body.") + } + } +} +extension Swift::Never : Snippets::SnippetRepresentableLiteral { + public struct SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Snippets::_StaticSnippetOutputCollector { + public init(snippetLiteral: Swift::Never) + public init() + public var content: some Snippets::Snippet { + get + } + @_transparent public func render() -> Swift::Never { + fatalError("Unreachable") + } + @_transparent public func makeSnippet() -> Swift::Never.Snippets::SnippetRepresentation { + fatalError("Unreachable") + } + @_transparent public static func _fatalError(_ message: Swift::String) -> Swift::Never.Snippets::SnippetRepresentation { + fatalError(message) + } + public static func _collect(_ elements: [Swift::Never.Snippets::SnippetRepresentation.Snippets::Output]) -> Swift::Never.Snippets::SnippetRepresentation.Snippets::Output + public typealias Content = @_opaqueReturnTypeOf("$ss5NeverO8SnippetsE21SnippetRepresentationV7contentQrvp", 0) __ + public typealias Output = Swift::Never + public typealias SnippetLiteralType = Swift::Never + public typealias SnippetRepresentation = Swift::Never.Snippets::SnippetRepresentation + } +} +@_functionBuilder public enum SnippetBuilder where Output : Snippets::SnippetRepresentableLiteral { + @inlinable public static func buildBlock() -> some Snippets::Snippet { + Output.SnippetRepresentation() + } + + @inlinable public static func buildPartialBlock(first: Component) -> some Snippets::Snippet where Output == Component.Output, Component : Snippets::Snippet { + first + } + + @inlinable public static func buildPartialBlock(accumulated: Accumulated, next: Next) -> some Snippets::Snippet where Output == Accumulated.Output, Accumulated : Snippets::Snippet, Next : Snippets::Snippet, Accumulated.Output == Next.Output { + _Sequence(accumulated, next) + } + + @inlinable public static func buildEither(first component: S0) -> Snippets::SnippetBuilder.Snippets::_Conditional where Output == S0.Output, S0 : Snippets::Snippet, S1 : Snippets::Snippet, S0.Output == S1.Output { + .first(component) + } + @inlinable public static func buildEither(second component: S1) -> Snippets::SnippetBuilder.Snippets::_Conditional where Output == S0.Output, S0 : Snippets::Snippet, S1 : Snippets::Snippet, S0.Output == S1.Output { + .second(component) + } + @inlinable public static func buildIf(_ content: Content?) -> Content? where Output == Content.Output, Content : Snippets::Snippet { + content + } + @inlinable public static func buildOptional(_ component: Component?) -> some Snippets::Snippet where Output == Component.Output, Component : Snippets::Snippet { + _Optional(component) + } + + @inlinable public static func buildArray(_ components: [Component]) -> some Snippets::Snippet where Output == Component.Output, Component : Snippets::Snippet { + _SequenceMany(components) + } + + @inlinable public static func buildExpression(_ component: any Snippets::Snippet) -> some Snippets::Snippet { + AnySnippet(component) + } + + @inlinable public static func buildExpression(_ component: Component?) -> some Snippets::Snippet where Output == Component.Output, Component : Snippets::Snippet { + _Optional(component) + } + + @inlinable public static func buildExpression(_ expression: Output) -> some Snippets::Snippet { + expression.makeSnippet() + } + + @inlinable public static func buildExpression(_ expression: some Snippet) -> some Snippets::Snippet { + expression + } + + @inlinable public static func buildExpression(_ expression: [some Snippet]) -> some Snippets::Snippet { + buildArray(expression) + } + + @inlinable public static func buildExpression(_ expression: [Output.SnippetRepresentation]) -> some Snippets::Snippet where Output == S.SnippetRepresentation.Output, S : Snippets::SnippetRepresentable { + buildArray(expression) + } + + @inlinable public static func buildExpression(_ expression: S) -> some Snippets::Snippet where Output == S.SnippetRepresentation.Output, S : Snippets::SnippetRepresentable { + expression.makeSnippet() + } + + @inlinable public static func buildFinalResult(_ component: Component) -> some Snippets::Snippet where Output == Component.Output, Component : Snippets::Snippet { + component + } + + public enum _Conditional : Snippets::Snippet where Output == First.Output, First : Snippets::Snippet, Second : Snippets::Snippet, First.Output == Second.Output { + case first(First) + case second(Second) + @inlinable public func render() -> Output { + switch self { + case let .first(first): + return first.render() + + case let .second(second): + return second.render() + } + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Output + public typealias SnippetRepresentation = Snippets::SnippetBuilder.Snippets::_Conditional + } + public enum _Optional : Snippets::Snippet where Output == Contents.Output, Contents : Snippets::Snippet { + case some(Contents) + case none + public init(_ content: Contents?) + @inlinable public func render() -> Output { + switch self { + case let .some(contents): + return contents.render() + + case .none: + return Snippets.Const.empty.render() + } + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Output + public typealias SnippetRepresentation = Snippets::SnippetBuilder.Snippets::_Optional + } + public struct _Sequence : Snippets::Snippet where Output == A.Output, A : Snippets::Snippet, B : Snippets::Snippet, A.Output == B.Output { + @usableFromInline + internal let a: A + @usableFromInline + internal let b: B + public init(_ a: A, _ b: B) + @inlinable public func render() -> Output { + withOutputCollector { collect in + collect(renderOutputs(for: self)) + } + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Output + public typealias SnippetRepresentation = Snippets::SnippetBuilder.Snippets::_Sequence + } + public struct _SequenceMany : Snippets::Snippet where Output == Element.Output, Element : Snippets::Snippet { + @usableFromInline + internal let elements: [Element] + public init(_ elements: [Element]) + @inlinable public func render() -> Output { + withOutputCollector { collect in + collect(renderOutputs(for: self)) + } + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias Output = Output + public typealias SnippetRepresentation = Snippets::SnippetBuilder.Snippets::_SequenceMany + } +} +@usableFromInline +internal func renderOutputs(for snippet: some Snippet) -> [Output] +public struct AnySnippet : Snippets::Snippet where Output : Snippets::SnippetRepresentableLiteral { + @usableFromInline + internal let underlying: any Snippets::Snippet + public init(_ underlying: any Snippets::Snippet) + @inlinable public init(@Snippets::SnippetBuilder content: () -> some Snippet) { + self.init(content()) + } + @_disfavoredOverload public init(_ representable: R) where Output == R.SnippetRepresentation.Output, R : Snippets::SnippetRepresentable + @inlinable public func render() -> Output { + underlying.render() + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::AnySnippet +} +extension Snippets::Snippet { + @inlinable public func eraseToAnySnippet() -> Snippets::AnySnippet { + AnySnippet(self) + } +} +extension Snippets::Snippets { + public struct Append : Snippets::Snippet where Output == Source.Output, Source : Snippets::Snippet, Contents : Snippets::Snippet, Source.Output == Contents.Output { + @usableFromInline + internal let source: Source + @usableFromInline + internal let contents: Contents + public init(to source: Source, @Snippets::SnippetBuilder content: () -> Contents) + public func render() -> Output + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Append + } +} +extension Snippets::Snippet { + @inlinable public func withPrefix(_ prefix: some Snippet) -> some Snippets::Snippet { + Snippets.Append(to: prefix) { self } + } + + @inlinable public func withSuffix(_ suffix: some Snippet) -> some Snippets::Snippet { + Snippets.Append(to: self) { suffix } + } + +} +extension Snippets::SnippetExpressibleByLiteral { + @inlinable public func prefixed(with prefix: Self) -> Self { + .init(snippetLiteral: Snippets.Append(to: prefix) { self }.render()) + } + @inlinable public func suffixed(with suffix: Self) -> Self { + .init(snippetLiteral: Snippets.Append(to: self) { suffix }.render()) + } +} +extension Snippets::Snippets { + public struct Bracket : Snippets::Snippet where Output == Target.Output, Target : Snippets::Snippet { + public struct Brackets : Swift::Sendable { + @usableFromInline + internal let leading: Snippets::Snippets.Snippets::Const + @usableFromInline + internal let trailing: Snippets::Snippets.Snippets::Const + public init(leading: Snippets::Snippets.Snippets::Const, trailing: Snippets::Snippets.Snippets::Const) + public init(_ bracket: Snippets::Snippets.Snippets::Const) + } + @usableFromInline + internal let target: Target + @usableFromInline + internal let brackets: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets + public init(in brackets: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets, @Snippets::SnippetBuilder target: () -> Target) + public init(_ target: Target, in brackets: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets) + @inlinable public var content: some Snippets::Snippet { + get { + Join { + brackets.leading + target + brackets.trailing + } + } + } + public typealias Content = @_opaqueReturnTypeOf("$s8SnippetsAAO7BracketV7contentQrvp", 0) __ + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Bracket + } +} +extension Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + public func withInner(_ brackets: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets) -> Snippets::Snippets.Snippets::Bracket.Snippets::Brackets + public func withOuter(_ brackets: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets) -> Snippets::Snippets.Snippets::Bracket.Snippets::Brackets +} +extension Snippets::Snippets.Snippets::Bracket.Snippets::Brackets where Output : Snippets::SnippetRepresentableString { + @inlinable public static func backticks(_ count: Swift::Int = 1) -> Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + .init(.backticks(count)) + } + @inlinable public static func quotes(_ count: Swift::Int = 1) -> Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + .init(.quotes(count)) + } + @inlinable public static var brackets: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + get { + return .init( + leading: .leftBracket, + trailing: .rightBracket + ) + } + } + @inlinable public static var braces: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + get { + return .init( + leading: .leftBrace, + trailing: .rightBrace + ) + } + } + @inlinable public static var parenthesis: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + get { + return .init( + leading: .leftParenthesis, + trailing: .rightParenthesis + ) + } + } + @inlinable public static var diamond: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + get { + return .init( + leading: .lessThan, + trailing: .greaterThan + ) + } + } + @inlinable public static var newlines: Snippets::Snippets.Snippets::Bracket.Snippets::Brackets { + get { + return .init( + leading: .newline, + trailing: .newline + ) + } + } +} +extension Snippets::Snippets { + public struct Const : Snippets::SnippetExpressibleByLiteral where Output : Snippets::SnippetRepresentableLiteral { + @usableFromInline + internal let literal: Output.SnippetRepresentation + public init(_ literal: Output.SnippetRepresentation) + @inlinable public init(_ snippetLiteral: Output) { + self.literal = snippetLiteral.makeSnippet() + } + @inlinable public init(snippetLiteral: Output) { + self.init(snippetLiteral) + } + @inlinable public init() { + self.init(Output.SnippetRepresentation().render()) + } + @inlinable public func render() -> Output { literal.render() } + public static func repeating(_ output: Output, count: Swift::Int) -> Snippets::Snippets.Snippets::Const + public static func repeating(_ literal: Snippets::Snippets.Snippets::Const, count: Swift::Int) -> Snippets::Snippets.Snippets::Const + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetLiteralType = Output + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Const + } +} +extension Snippets::Snippets.Snippets::Const { + @inlinable public static var empty: Snippets::Snippets.Snippets::Const { + get { .init() } + } +} +extension Snippets::Snippets.Snippets::Const : Swift::ExpressibleByStringLiteral, Swift::ExpressibleByUnicodeScalarLiteral, Swift::ExpressibleByExtendedGraphemeClusterLiteral where Output : Swift::ExpressibleByStringLiteral { +} +extension Snippets::Snippets.Snippets::Const : Swift::Encodable where Output : Swift::Encodable { + public func encode(to encoder: any Swift::Encoder) throws +} +extension Snippets::Snippets.Snippets::Const : Swift::Decodable where Output : Swift::Decodable { + public init(from decoder: any Swift::Decoder) throws +} +extension Snippets::Snippets { + public struct ForEach : Snippets::Snippet where Output == ElementSnippet.Output, Element : Swift::Sendable, ElementSnippet : Snippets::Snippet { + @usableFromInline + internal let elements: [ElementSnippet] + public init(_ sequence: S, @Snippets::SnippetBuilder content: (Element) -> ElementSnippet) where Element == S.Element, S : Swift::Sequence + @inlinable public var content: some Snippets::Snippet { + get { + elements + } + } + public typealias Content = @_opaqueReturnTypeOf("$s8SnippetsAAO7ForEachV7contentQrvp", 0) __ + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::ForEach + } +} +extension Snippets::Snippets { + public struct Group : Snippets::Snippet where Output == Contents.Output, Contents : Snippets::Snippet { + @usableFromInline + internal let contents: Contents + public init(@Snippets::SnippetBuilder content: () -> Contents) + public var content: some Snippets::Snippet { + get + } + public typealias Content = @_opaqueReturnTypeOf("$s8SnippetsAAO5GroupV7contentQrvp", 0) __ + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Group + } +} +extension Snippets::Snippets { + public struct Inline : Snippets::Snippet where Output : Snippets::SnippetRepresentableLiteral { + @usableFromInline + internal let output: @Sendable () -> Output + public init(render: @escaping @Sendable () -> Output) + @inlinable public func render() -> Output { output() } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Inline + } +} +extension Snippets::Snippets { + public struct Join : Snippets::Snippet where Output == Separator.Output, Separator : Snippets::Snippet, Contents : Snippets::Snippet, Separator.Output == Contents.Output { + @usableFromInline + internal let separator: Separator + @usableFromInline + internal let contents: Contents + public init(_ separator: Separator, @Snippets::SnippetBuilder content: () -> Contents) + public func render() -> Output + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Join + } +} +extension Snippets::Snippets.Snippets::Join where Separator == Output.SnippetRepresentation { + @inlinable public init(_ separator: Output, @Snippets::SnippetBuilder content: () -> Contents) { + self.init( + separator.makeSnippet(), + content: content + ) + } + @_disfavoredOverload @inlinable public init(_ separator: Separator = .init(), @Snippets::SnippetBuilder content: () -> Contents) { + self.init( + separator, + content: content + ) + } +} +extension Snippets::Snippets { + public struct Map : Snippets::Snippet where Output : Snippets::SnippetRepresentableLiteral, Input : Snippets::Snippet { + @usableFromInline + internal let input: Input + @usableFromInline + internal let transform: @Sendable (Input.Output) -> Output + public init(_ input: Input, transform: @escaping @Sendable (Input.Output) -> Output) + @inlinable public init(@Snippets::SnippetBuilder input: () -> Input, transform: @escaping @Sendable (Input.Output) -> Output) { + self.init( + input(), + transform: transform + ) + } + @inlinable public func render() -> Output { + transform(input.render()) + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::Map + } +} +extension Snippets::Snippet { + @inlinable public func map(_ transform: @escaping @Sendable (Self.Output) -> T) -> some Snippets::Snippet where T : Snippets::SnippetRepresentableLiteral { + Snippets.Map(self, transform: transform) + } + +} +extension Snippets::Snippets { + public struct FlatMap : Snippets::Snippet where Output == Result.Output, Result : Snippets::Snippet, Input : Snippets::Snippet { + @usableFromInline + internal let input: Input + @usableFromInline + internal let transform: @Sendable (Input.Output) -> Result + public init(_ input: Input, @Snippets::SnippetBuilder transform: @escaping @Sendable (Input.Output) -> Result) + @inlinable public init(@Snippets::SnippetBuilder input: () -> Input, @Snippets::SnippetBuilder transform: @escaping @Sendable (Input.Output) -> Result) { + self.init( + input(), + transform: transform + ) + } + @inlinable public func render() -> Output { + transform(input.render()).render() + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::FlatMap + } +} +extension Snippets::Snippet { + @inlinable public func flatMap(@Snippets::SnippetBuilder _ transform: @escaping @Sendable (Self.Output) -> T) -> some Snippets::Snippet where T : Snippets::Snippet { + Snippets.FlatMap(self, transform: transform) + } + +} +extension Snippets::Snippets { + public struct SkipEmpty : Snippets::Snippet where Output : Swift::Collection, Output == Contents.Output, Contents : Snippets::Snippet { + @usableFromInline + internal let contents: Contents + public init(_ contents: Contents) + public init(@Snippets::SnippetBuilder content: () -> Contents) + @inlinable public func render() -> Output { + withNextOutputPreprocessor( + uncheckedSendableAction { $0.filter { !$0.isEmpty } } + ) { + contents.render() + } + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::SkipEmpty + } +} +extension Snippets::Snippets.Snippets::SkipEmpty : Snippets::SnippetExpressibleByLiteral where Contents == Output.SnippetRepresentation { + public init(snippetLiteral: Output) + public init() + public typealias SnippetLiteralType = Output +} +extension Snippets::Snippet where Self.Output : Swift::Collection { + @inlinable public func skipEmpty() -> some Snippets::Snippet { + Snippets.SkipEmpty(self) + } + +} +extension Snippets::Snippets { + public struct UnsafeSendable : Snippets::SnippetExpressibleByLiteral where Output : Snippets::SnippetRepresentableLiteral { + @usableFromInline + internal let underlying: Snippets::Snippets.Snippets::UnsafeSendable.Snippets::Underlying + public init(_ literal: Output) + @inlinable public init(snippetLiteral: Output) { + self.init(snippetLiteral) + } + @inlinable public init() { + self.init(snippetLiteral: Output.SnippetRepresentation().render()) + } + public init(render: @escaping () -> Output) + @inlinable public func render() -> Output { underlying.render() } + @usableFromInline + internal enum Underlying : Snippets::Snippet { + case const(Snippets::Snippets.Snippets::UnsafeSendable.Snippets::Const) + case inline(Snippets::Snippets.Snippets::UnsafeSendable.Snippets::Inline) + @usableFromInline + internal func render() -> Output + @usableFromInline + internal typealias Content = Swift::Never.Snippets::SnippetRepresentation + @usableFromInline + internal typealias SnippetRepresentation = Snippets::Snippets.Snippets::UnsafeSendable.Snippets::Underlying + } + @usableFromInline + internal struct Const : Snippets::SnippetExpressibleByLiteral { + @usableFromInline + nonisolated(unsafe) internal let output: Output + @usableFromInline + internal init(snippetLiteral: Output) + @usableFromInline + internal init() + @usableFromInline + internal func render() -> Output + @usableFromInline + internal typealias Content = Swift::Never.Snippets::SnippetRepresentation + @usableFromInline + internal typealias SnippetLiteralType = Output + @usableFromInline + internal typealias SnippetRepresentation = Snippets::Snippets.Snippets::UnsafeSendable.Snippets::Const + } + @usableFromInline + internal struct Inline : Snippets::Snippet { + @usableFromInline + nonisolated(unsafe) internal let output: () -> Output + @usableFromInline + internal init(render: @escaping () -> Output) + @usableFromInline + internal func render() -> Output + @usableFromInline + internal typealias Content = Swift::Never.Snippets::SnippetRepresentation + @usableFromInline + internal typealias SnippetRepresentation = Snippets::Snippets.Snippets::UnsafeSendable.Snippets::Inline + } + public typealias Content = Swift::Never.Snippets::SnippetRepresentation + public typealias SnippetLiteralType = Output + public typealias SnippetRepresentation = Snippets::Snippets.Snippets::UnsafeSendable + } +} +public protocol SnippetRepresentable { + associatedtype SnippetRepresentation : Snippets::Snippet + func makeSnippet() -> Self.SnippetRepresentation +} +extension Snippets::SnippetRepresentable where Self : Snippets::SnippetRepresentableLiteral { + public func makeSnippet() -> Self.SnippetRepresentation +} +public protocol _StaticSnippetOutputCollector : Snippets::Snippet { + static func _collect(_ elements: [Self.Output]) -> Self.Output +} +public protocol SnippetRepresentableLiteral : Snippets::SnippetRepresentable where Self == Self.SnippetRepresentation.Output, Self.SnippetRepresentation : Snippets::SnippetExpressibleByLiteral, Self.SnippetRepresentation : Snippets::_StaticSnippetOutputCollector { +} +public protocol SnippetRepresentableString : Snippets::SnippetRepresentableLiteral, Swift::StringProtocol { +} +extension Snippets::SnippetRepresentable { + @inlinable public static func snippet(@Snippets::SnippetBuilder content: () -> some Snippet) -> Self { + return .snippet(content()) + } + @inlinable public static func snippet(_ snippet: some Snippet) -> Self { + snippet.render() + } +} +public enum Snippets { +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab93b4e --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +output ?= .agents/interfaces + +.PHONY: swiftinterface +swiftinterface: + @./scripts/generate-swiftinterfaces.sh --package-path . --output "$(output)" diff --git a/scripts/generate-swiftinterfaces.sh b/scripts/generate-swiftinterfaces.sh new file mode 100755 index 0000000..2011ead --- /dev/null +++ b/scripts/generate-swiftinterfaces.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +package_path="." +output_path=".agents/interfaces" +configuration="release" +include_tests=false +triple="" +sdk="" +swift_sdk="" +scratch_path="" +declare -a selected_targets=() +declare -a build_arguments=() + +usage() { + sed -n '1,$p' <<'USAGE' +Generate public Swift interfaces for SwiftPM targets. + +Usage: + generate-swiftinterfaces.sh [options] + +Options: + --package-path Package root (default: .) + --output Destination, relative to the package root unless absolute + (default: .agents/interfaces) + --configuration debug or release (default: release) + --include-tests Include Swift test targets + --target Generate one target; repeat to select several + --triple Forward a target triple to SwiftPM + --sdk Forward an SDK path to SwiftPM + --swift-sdk Forward an installed Swift SDK name to SwiftPM + --scratch-path Use a specific SwiftPM scratch directory + --build-arg Forward one additional argument to `swift build`; repeatable + -h, --help Show this help +USAGE +} + +fail() { + printf 'swiftinterfaces: %s\n' "$*" >&2 + exit 1 +} + +require_value() { + [[ $# -ge 2 ]] || fail "$1 requires a value" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --package-path) + require_value "$@" + package_path="$2" + shift 2 + ;; + --output) + require_value "$@" + output_path="$2" + shift 2 + ;; + --configuration) + require_value "$@" + configuration="$2" + shift 2 + ;; + --include-tests) + include_tests=true + shift + ;; + --target) + require_value "$@" + selected_targets+=("$2") + shift 2 + ;; + --triple) + require_value "$@" + triple="$2" + shift 2 + ;; + --sdk) + require_value "$@" + sdk="$2" + shift 2 + ;; + --swift-sdk) + require_value "$@" + swift_sdk="$2" + shift 2 + ;; + --scratch-path) + require_value "$@" + scratch_path="$2" + shift 2 + ;; + --build-arg) + require_value "$@" + build_arguments+=("$2") + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "unknown option: $1" + ;; + esac +done + +case "$configuration" in + debug|release) ;; + *) fail "--configuration must be debug or release" ;; +esac + +command -v swift >/dev/null 2>&1 || fail "Swift is required" +[[ -f "$script_dir/list-swift-targets.swift" ]] || fail "missing target parser beside generator" +[[ -d "$package_path" ]] || fail "package path does not exist: $package_path" + +package_root="$(cd "$package_path" && pwd -P)" +[[ -f "$package_root/Package.swift" ]] || fail "no Package.swift at $package_root" +package_identity="$(basename "$package_root" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g')" + +if [[ "$output_path" = /* ]]; then + output_root="$output_path" +else + output_root="$package_root/$output_path" +fi + +if [[ -z "$scratch_path" ]]; then + scratch_root="$package_root/.build/swiftinterfaces/$configuration" +elif [[ "$scratch_path" = /* ]]; then + scratch_root="$scratch_path" +else + scratch_root="$package_root/$scratch_path" +fi + +mkdir -p "$output_root" "$scratch_root/module-cache" + +description_file="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-description.XXXXXX")" +targets_file="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-targets.XXXXXX")" +build_log="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-build.XXXXXX")" +cleanup() { + rm -f "$description_file" "$targets_file" "$build_log" +} +trap cleanup EXIT INT TERM + +( + cd "$package_root" + swift package describe --type json >"$description_file" +) + +declare -a parser_arguments=("$description_file") +if [[ "$include_tests" == true ]]; then + parser_arguments+=(--include-tests) +fi +if (( ${#selected_targets[@]} > 0 )); then + for target in "${selected_targets[@]}"; do + parser_arguments+=(--target "$target") + done +fi + +CLANG_MODULE_CACHE_PATH="$scratch_root/module-cache" \ +SWIFT_MODULECACHE_PATH="$scratch_root/module-cache" \ + swift "$script_dir/list-swift-targets.swift" "${parser_arguments[@]}" >"$targets_file" + +generated_count=0 +while IFS=$'\t' read -r target_name module_name target_type package_name; do + [[ -n "$target_name" && -n "$module_name" ]] || continue + + printf 'Generating %s (%s)...\n' "$module_name" "$target_name" + + declare -a common_command=( + swift build + --package-path "$package_root" + --scratch-path "$scratch_root" + --configuration "$configuration" + --enable-parseable-module-interfaces + ) + + [[ -z "$triple" ]] || common_command+=(--triple "$triple") + [[ -z "$sdk" ]] || common_command+=(--sdk "$sdk") + [[ -z "$swift_sdk" ]] || common_command+=(--swift-sdk "$swift_sdk") + if (( ${#build_arguments[@]} > 0 )); then + common_command+=("${build_arguments[@]}") + fi + + declare -a command=("${common_command[@]}") + if [[ "$target_type" == "test" ]]; then + command+=(--build-tests -Xswiftc -enable-testing) + else + command+=(--target "$target_name") + fi + + : >"$build_log" + if ! CLANG_MODULE_CACHE_PATH="$scratch_root/module-cache" \ + SWIFT_MODULECACHE_PATH="$scratch_root/module-cache" \ + "${command[@]}" 2>&1 | tee "$build_log"; then + if [[ "$target_type" != "test" ]] && \ + grep -Eiq 'multiple targets.*(name|named)' "$build_log"; then + printf 'Target name is ambiguous; building root package products instead...\n' + : >"$build_log" + CLANG_MODULE_CACHE_PATH="$scratch_root/module-cache" \ + SWIFT_MODULECACHE_PATH="$scratch_root/module-cache" \ + "${common_command[@]}" 2>&1 | tee "$build_log" + else + fail "SwiftPM could not build target $target_name" + fi + fi + + interface_path="$( + find "$scratch_root" -type f -name "$module_name.swiftinterface" \ + -path "*/$package_name.build/*" -exec ls -t {} + 2>/dev/null | head -n 1 || true + )" + if [[ -z "$interface_path" ]]; then + interface_path="$( + find "$scratch_root" -type f -name "$module_name.swiftinterface" \ + -exec grep -F -l -- "-package-name $package_identity" {} + 2>/dev/null | head -n 1 || true + )" + fi + if [[ -z "$interface_path" ]]; then + interface_path="$( + find "$scratch_root" -type f -name "$module_name.swiftinterface" \ + -exec ls -t {} + 2>/dev/null | head -n 1 || true + )" + fi + [[ -n "$interface_path" && -f "$interface_path" ]] || \ + fail "build succeeded but no public interface was found for $target_name ($module_name)" + + temporary_output="$output_root/.$module_name.swiftinterface.$$" + cp "$interface_path" "$temporary_output" + chmod 0644 "$temporary_output" + mv "$temporary_output" "$output_root/$module_name.swiftinterface" + generated_count=$((generated_count + 1)) +done <"$targets_file" + +[[ $generated_count -gt 0 ]] || fail "no interfaces were generated" +printf 'Generated %d interface(s) in %s\n' "$generated_count" "$output_root" diff --git a/scripts/list-swift-targets.swift b/scripts/list-swift-targets.swift new file mode 100755 index 0000000..4bac0c3 --- /dev/null +++ b/scripts/list-swift-targets.swift @@ -0,0 +1,82 @@ +#!/usr/bin/env swift + +import Foundation + +private struct PackageDescription: Decodable { + let name: String + let targets: [Target] +} + +private struct Target: Decodable { + let c99Name: String? + let moduleType: String? + let name: String + let type: String + + enum CodingKeys: String, CodingKey { + case c99Name = "c99name" + case moduleType = "module_type" + case name + case type + } +} + +private struct Options { + var includeTests = false + var selectedTargets = Set() + var jsonPath: String? +} + +private func fail(_ message: String) -> Never { + FileHandle.standardError.write(Data("swiftinterfaces: \(message)\n".utf8)) + exit(1) +} + +private var options = Options() +private var arguments = Array(CommandLine.arguments.dropFirst()) + +while !arguments.isEmpty { + let argument = arguments.removeFirst() + switch argument { + case "--include-tests": + options.includeTests = true + case "--target": + guard !arguments.isEmpty else { fail("--target requires a value") } + options.selectedTargets.insert(arguments.removeFirst()) + default: + guard !argument.hasPrefix("-") else { fail("unknown parser option: \(argument)") } + guard options.jsonPath == nil else { fail("expected exactly one package-description JSON file") } + options.jsonPath = argument + } +} + +guard let jsonPath = options.jsonPath else { fail("missing package-description JSON file") } + +do { + let data = try Data(contentsOf: URL(fileURLWithPath: jsonPath)) + let package = try JSONDecoder().decode(PackageDescription.self, from: data) + let swiftTargets = package.targets.filter { target in + target.moduleType == "SwiftTarget" && (options.includeTests || target.type != "test") + } + let availableNames = Set(swiftTargets.map(\.name)) + let unknownTargets = options.selectedTargets.subtracting(availableNames).sorted() + + guard unknownTargets.isEmpty else { + fail("unknown or excluded Swift target(s): \(unknownTargets.joined(separator: ", "))") + } + + let selected = swiftTargets + .filter { options.selectedTargets.isEmpty || options.selectedTargets.contains($0.name) } + .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } + + guard !selected.isEmpty else { fail("the package has no matching Swift targets") } + + for target in selected { + guard let moduleName = target.c99Name, !moduleName.isEmpty else { + fail("target \(target.name) does not have a Swift module name") + } + print("\(target.name)\t\(moduleName)\t\(target.type)\t\(package.name)") + } +} catch { + fail("could not decode package description: \(error)") +} From 29b21ad90ef88381e9a0cbdfde31f87aa5b06c1a Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Tue, 4 Aug 2026 11:19:01 +0200 Subject: [PATCH 2/3] Generate Swift interfaces per platform --- .../{ => macos}/Snippets.swiftinterface | 0 .github/workflows/generate-swiftinterface.yml | 143 ++++++++++++++ .github/workflows/update-swiftinterfaces.yml | 102 ++++++++++ Makefile | 4 +- scripts/generate-swiftinterfaces.sh | 179 +++++++++++++++--- scripts/list-swift-targets.swift | 18 ++ 6 files changed, 419 insertions(+), 27 deletions(-) rename .agents/interfaces/{ => macos}/Snippets.swiftinterface (100%) create mode 100644 .github/workflows/generate-swiftinterface.yml create mode 100644 .github/workflows/update-swiftinterfaces.yml diff --git a/.agents/interfaces/Snippets.swiftinterface b/.agents/interfaces/macos/Snippets.swiftinterface similarity index 100% rename from .agents/interfaces/Snippets.swiftinterface rename to .agents/interfaces/macos/Snippets.swiftinterface diff --git a/.github/workflows/generate-swiftinterface.yml b/.github/workflows/generate-swiftinterface.yml new file mode 100644 index 0000000..3c4628a --- /dev/null +++ b/.github/workflows/generate-swiftinterface.yml @@ -0,0 +1,143 @@ +name: Generate Swift interface +run-name: Generate ${{ inputs.platform }} interfaces + +on: + workflow_dispatch: + inputs: + platform: + description: Normalized platform name (macos, ios, linux, windows, wasi, ...) + required: true + type: string + base_branch: + description: Branch that receives the generated-interface PR (defaults to the dispatched branch) + required: false + default: "" + type: string + request_id: + description: Optional stable ID shared by an umbrella run + required: false + default: "" + type: string + swift_sdk: + description: Optional installed Swift SDK ID (auto-detected for WASI) + required: false + default: "" + type: string + workflow_call: + inputs: + platform: + required: true + type: string + base_branch: + required: true + type: string + request_id: + required: true + type: string + swift_sdk: + required: false + default: "" + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + generate: + name: Generate ${{ inputs.platform }} + runs-on: ${{ (inputs.platform == 'linux' || inputs.platform == 'wasi') && 'ubuntu-latest' || inputs.platform == 'windows' && 'windows-latest' || 'macos-latest' }} + env: + GH_TOKEN: ${{ github.token }} + INPUT_BASE_BRANCH: ${{ inputs.base_branch }} + INPUT_REQUEST_ID: ${{ inputs.request_id }} + INPUT_SWIFT_SDK: ${{ inputs.swift_sdk }} + PLATFORM: ${{ inputs.platform }} + steps: + - name: Validate request + shell: bash + run: | + if [[ ! "$PLATFORM" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then + echo "Platform must be a normalized lowercase directory name: $PLATFORM" >&2 + exit 1 + fi + + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.base_branch != '' && inputs.base_branch || github.ref_name }} + + - name: Set up Swift outside Xcode + if: runner.os != 'macOS' + uses: SwiftyLab/setup-swift@v1 + with: + swift-version: "6" + sdks: ${{ inputs.platform == 'wasi' && 'wasm' || '' }} + + - name: Prepare generation branch + shell: bash + run: | + base_branch="${INPUT_BASE_BRANCH:-$GITHUB_REF_NAME}" + request_id="${INPUT_REQUEST_ID:-$GITHUB_RUN_ID}" + interface_branch="automation/swiftinterfaces/$request_id/$PLATFORM" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -C "$interface_branch" + echo "BASE_BRANCH=$base_branch" >> "$GITHUB_ENV" + echo "INTERFACE_BRANCH=$interface_branch" >> "$GITHUB_ENV" + + - name: Select WASI SDK + if: inputs.platform == 'wasi' + shell: bash + run: | + swift_sdk="$INPUT_SWIFT_SDK" + if [[ -z "$swift_sdk" ]]; then + swift_sdk="$(swift sdk list | sed -nE '/(_wasm|wasi)/ { s/^[[:space:]]*([^[:space:]]+).*/\1/p; q; }')" + fi + if [[ -z "$swift_sdk" ]]; then + echo "No installed WASI Swift SDK was found" >&2 + swift sdk list >&2 || true + exit 1 + fi + echo "INPUT_SWIFT_SDK=$swift_sdk" >> "$GITHUB_ENV" + + - name: Generate interface + shell: bash + run: | + interface_dir=".agents/interfaces/$PLATFORM" + if [[ -d "$interface_dir" ]]; then + find "$interface_dir" -maxdepth 1 -type f -name '*.swiftinterface' -delete + fi + if [[ -n "$INPUT_SWIFT_SDK" ]]; then + make swiftinterface platform="$PLATFORM" swift_sdk="$INPUT_SWIFT_SDK" + else + make swiftinterface platform="$PLATFORM" + fi + + - name: Commit and push interface + id: commit + shell: bash + run: | + git add --all -- ".agents/interfaces/$PLATFORM" + if git diff --cached --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No $PLATFORM interface changes" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + git commit -m "Update $PLATFORM Swift interfaces" + git push --force-with-lease --set-upstream origin "$INTERFACE_BRANCH" + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Open platform PR + if: steps.commit.outputs.changed == 'true' + shell: bash + run: | + pr_url="$(gh pr list --state open --base "$BASE_BRANCH" --head "$INTERFACE_BRANCH" --json url --jq '.[0].url')" + if [[ -z "$pr_url" ]]; then + pr_url="$(gh pr create \ + --base "$BASE_BRANCH" \ + --head "$INTERFACE_BRANCH" \ + --title "Update $PLATFORM Swift interfaces" \ + --body "Generated from \`$BASE_BRANCH\` by workflow run $GITHUB_RUN_ID. Merge this PR into the aggregation branch after reviewing the platform-specific API snapshot.")" + fi + echo "$pr_url" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/update-swiftinterfaces.yml b/.github/workflows/update-swiftinterfaces.yml new file mode 100644 index 0000000..151464e --- /dev/null +++ b/.github/workflows/update-swiftinterfaces.yml @@ -0,0 +1,102 @@ +name: Update Swift interfaces +run-name: Update Swift interfaces from ${{ inputs.source_branch || github.ref_name }} + +on: + workflow_dispatch: + inputs: + source_branch: + description: Source branch to aggregate into (defaults to the dispatched branch) + required: false + default: "" + type: string + platforms: + description: JSON array of independently generated platforms + required: true + default: '["macos","ios","tvos","watchos","maccatalyst"]' + type: string + wasi_swift_sdk: + description: Optional installed Swift SDK ID for WASI + required: false + default: "" + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + prepare: + name: Create aggregation branch + runs-on: ubuntu-latest + outputs: + source_branch: ${{ steps.branch.outputs.source_branch }} + update_branch: ${{ steps.branch.outputs.update_branch }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ inputs.source_branch != '' && inputs.source_branch || github.ref_name }} + + - name: Push aggregation branch + id: branch + shell: bash + env: + SOURCE_BRANCH_INPUT: ${{ inputs.source_branch != '' && inputs.source_branch || github.ref_name }} + run: | + source_branch="$SOURCE_BRANCH_INPUT" + update_branch="automation/swiftinterfaces/$GITHUB_RUN_ID" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -C "$update_branch" + git commit --allow-empty -m "Prepare Swift interface update" + git push --force-with-lease --set-upstream origin "$update_branch" + echo "source_branch=$source_branch" >> "$GITHUB_OUTPUT" + echo "update_branch=$update_branch" >> "$GITHUB_OUTPUT" + + generate: + name: Generate ${{ matrix.platform }} + needs: prepare + strategy: + fail-fast: false + matrix: + platform: ${{ fromJSON(inputs.platforms) }} + permissions: + contents: write + pull-requests: write + uses: ./.github/workflows/generate-swiftinterface.yml + with: + platform: ${{ matrix.platform }} + base_branch: ${{ needs.prepare.outputs.update_branch }} + request_id: ${{ format('{0}', github.run_id) }} + swift_sdk: ${{ matrix.platform == 'wasi' && inputs.wasi_swift_sdk || '' }} + + finalize: + name: Open draft aggregation PR + if: always() && needs.prepare.result == 'success' + needs: + - prepare + - generate + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + SOURCE_BRANCH: ${{ needs.prepare.outputs.source_branch }} + UPDATE_BRANCH: ${{ needs.prepare.outputs.update_branch }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.prepare.outputs.source_branch }} + + - name: Open draft PR + shell: bash + run: | + pr_url="$(gh pr list --state open --base "$SOURCE_BRANCH" --head "$UPDATE_BRANCH" --json url --jq '.[0].url')" + if [[ -z "$pr_url" ]]; then + pr_url="$(gh pr create \ + --draft \ + --base "$SOURCE_BRANCH" \ + --head "$UPDATE_BRANCH" \ + --title "Update platform Swift interfaces" \ + --body "Aggregation PR for workflow run $GITHUB_RUN_ID. Merge the successful platform PRs into \`$UPDATE_BRANCH\`; failed platform jobs remain visible in the workflow and can be fixed or intentionally ignored. Matrix result: \`${{ needs.generate.result }}\`.")" + fi + echo "$pr_url" >> "$GITHUB_STEP_SUMMARY" diff --git a/Makefile b/Makefile index ab93b4e..c9eab41 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ output ?= .agents/interfaces +platform ?= host +swift_sdk ?= .PHONY: swiftinterface swiftinterface: - @./scripts/generate-swiftinterfaces.sh --package-path . --output "$(output)" + @./scripts/generate-swiftinterfaces.sh --package-path . --output "$(output)" --platform "$(platform)" $(if $(swift_sdk),--swift-sdk "$(swift_sdk)") diff --git a/scripts/generate-swiftinterfaces.sh b/scripts/generate-swiftinterfaces.sh index 2011ead..b028e55 100755 --- a/scripts/generate-swiftinterfaces.sh +++ b/scripts/generate-swiftinterfaces.sh @@ -7,6 +7,8 @@ package_path="." output_path=".agents/interfaces" configuration="release" include_tests=false +list_declared_platforms=false +platform="" triple="" sdk="" swift_sdk="" @@ -16,24 +18,32 @@ declare -a build_arguments=() usage() { sed -n '1,$p' <<'USAGE' -Generate public Swift interfaces for SwiftPM targets. +Generate public Swift interfaces for one SwiftPM platform. Usage: - generate-swiftinterfaces.sh [options] + generate-swiftinterfaces.sh --platform [options] + generate-swiftinterfaces.sh --list-declared-platforms [options] Options: - --package-path Package root (default: .) - --output Destination, relative to the package root unless absolute - (default: .agents/interfaces) - --configuration debug or release (default: release) - --include-tests Include Swift test targets - --target Generate one target; repeat to select several - --triple Forward a target triple to SwiftPM - --sdk Forward an SDK path to SwiftPM - --swift-sdk Forward an installed Swift SDK name to SwiftPM - --scratch-path Use a specific SwiftPM scratch directory - --build-arg Forward one additional argument to `swift build`; repeatable - -h, --help Show this help + --package-path Package root (default: .) + --output Platform-parent destination, relative to the package root + unless absolute (default: .agents/interfaces) + --platform Output/build platform, for example macos, ios, linux, + windows, or wasi; host resolves to the current platform + --list-declared-platforms Print the Apple/custom platforms declared by Package.swift + --configuration debug or release (default: release) + --include-tests Include Swift test targets + --target Generate one target; repeat to select several + --triple Forward a target triple to SwiftPM + --sdk Forward an SDK path to SwiftPM + --swift-sdk Forward an installed Swift SDK name to SwiftPM + --scratch-path Use a specific SwiftPM scratch directory + --build-arg Forward one additional argument to `swift build`; repeatable + -h, --help Show this help + +The platform name becomes an output directory. For example, --platform ios writes +/ios/.swiftinterface. Generate platforms in separate invocations so +one unsupported platform does not suppress successful platform snapshots. USAGE } @@ -46,6 +56,32 @@ require_value() { [[ $# -ge 2 ]] || fail "$1 requires a value" } +host_platform() { + case "$(uname -s)" in + Darwin) printf 'macos\n' ;; + Linux) printf 'linux\n' ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) printf 'windows\n' ;; + *) fail "cannot infer a platform from host $(uname -s); pass --platform explicitly" ;; + esac +} + +normalize_platform() { + local value + value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + case "$value" in + osx|macosx) value="macos" ;; + iphoneos|iphonesimulator) value="ios" ;; + appletvos|appletvsimulator) value="tvos" ;; + watchsimulator) value="watchos" ;; + xros|xrsimulator) value="visionos" ;; + catalyst|mac-catalyst|mac_catalyst) value="maccatalyst" ;; + wasm|webassembly) value="wasi" ;; + host) value="$(host_platform)" ;; + esac + [[ "$value" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || fail "invalid platform name: $1" + printf '%s\n' "$value" +} + while [[ $# -gt 0 ]]; do case "$1" in --package-path) @@ -58,6 +94,16 @@ while [[ $# -gt 0 ]]; do output_path="$2" shift 2 ;; + --platform) + require_value "$@" + [[ -z "$platform" ]] || fail "--platform may be passed only once" + platform="$2" + shift 2 + ;; + --list-declared-platforms) + list_declared_platforms=true + shift + ;; --configuration) require_value "$@" configuration="$2" @@ -112,35 +158,39 @@ case "$configuration" in *) fail "--configuration must be debug or release" ;; esac +[[ "$list_declared_platforms" == false || -z "$platform" ]] || \ + fail "--platform and --list-declared-platforms are separate operations" +[[ "$list_declared_platforms" == true || -n "$platform" ]] || \ + fail "--platform is required; use --list-declared-platforms to inspect Package.swift" + command -v swift >/dev/null 2>&1 || fail "Swift is required" -[[ -f "$script_dir/list-swift-targets.swift" ]] || fail "missing target parser beside generator" +[[ -f "$script_dir/list-swift-targets.swift" ]] || fail "missing package parser beside generator" [[ -d "$package_path" ]] || fail "package path does not exist: $package_path" package_root="$(cd "$package_path" && pwd -P)" [[ -f "$package_root/Package.swift" ]] || fail "no Package.swift at $package_root" package_identity="$(basename "$package_root" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g')" -if [[ "$output_path" = /* ]]; then - output_root="$output_path" -else - output_root="$package_root/$output_path" +if [[ -n "$platform" ]]; then + platform="$(normalize_platform "$platform")" fi if [[ -z "$scratch_path" ]]; then - scratch_root="$package_root/.build/swiftinterfaces/$configuration" + scratch_root="$package_root/.build/swiftinterfaces/${platform:-metadata}/$configuration" elif [[ "$scratch_path" = /* ]]; then scratch_root="$scratch_path" else scratch_root="$package_root/$scratch_path" fi -mkdir -p "$output_root" "$scratch_root/module-cache" +mkdir -p "$scratch_root/module-cache" description_file="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-description.XXXXXX")" targets_file="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-targets.XXXXXX")" +platforms_file="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-platforms.XXXXXX")" build_log="$(mktemp "${TMPDIR:-/tmp}/swiftinterfaces-build.XXXXXX")" cleanup() { - rm -f "$description_file" "$targets_file" "$build_log" + rm -f "$description_file" "$targets_file" "$platforms_file" "$build_log" } trap cleanup EXIT INT TERM @@ -149,6 +199,83 @@ trap cleanup EXIT INT TERM swift package describe --type json >"$description_file" ) +CLANG_MODULE_CACHE_PATH="$scratch_root/module-cache" \ +SWIFT_MODULECACHE_PATH="$scratch_root/module-cache" \ + swift "$script_dir/list-swift-targets.swift" \ + --list-platforms "$description_file" >"$platforms_file" + +if [[ "$list_declared_platforms" == true ]]; then + if [[ -s "$platforms_file" ]]; then + cat "$platforms_file" + else + printf 'swiftinterfaces: Package.swift declares no platforms; support must be proven by a platform build\n' >&2 + fi + exit 0 +fi + +host="$(host_platform)" +if [[ -z "$swift_sdk" ]]; then + sdk_name="" + default_triple="" + case "$platform" in + macos) + [[ "$host" == "macos" || -n "$triple" || -n "$sdk" ]] || \ + fail "macos generation requires a macOS host or explicit cross-compilation options" + ;; + ios) + sdk_name="iphonesimulator" + default_triple="arm64-apple-ios-simulator" + ;; + tvos) + sdk_name="appletvsimulator" + default_triple="arm64-apple-tvos-simulator" + ;; + watchos) + sdk_name="watchsimulator" + default_triple="arm64-apple-watchos-simulator" + ;; + visionos) + sdk_name="xrsimulator" + default_triple="arm64-apple-xros-simulator" + ;; + maccatalyst) + sdk_name="macosx" + default_triple="arm64-apple-ios-macabi" + ;; + driverkit) + sdk_name="driverkit" + default_triple="arm64-apple-driverkit" + ;; + linux|windows) + [[ "$host" == "$platform" || -n "$triple" || -n "$sdk" ]] || \ + fail "$platform generation requires a $platform host or explicit cross-compilation options" + ;; + wasi) + [[ -n "$triple" || -n "$sdk" ]] || \ + fail "wasi generation requires --swift-sdk (recommended) or explicit --triple/--sdk options" + ;; + *) + [[ -n "$triple" || -n "$sdk" ]] || \ + fail "custom platform $platform requires --swift-sdk or explicit --triple/--sdk options" + ;; + esac + + if [[ -n "$sdk_name" ]]; then + [[ "$host" == "macos" ]] || fail "$platform generation requires Xcode on macOS" + command -v xcrun >/dev/null 2>&1 || fail "xcrun is required for $platform generation" + [[ -n "$sdk" ]] || sdk="$(xcrun --sdk "$sdk_name" --show-sdk-path)" + [[ -n "$triple" ]] || triple="$default_triple" + fi +fi + +if [[ "$output_path" = /* ]]; then + output_parent="$output_path" +else + output_parent="$package_root/$output_path" +fi +output_root="$output_parent/$platform" +mkdir -p "$output_root" + declare -a parser_arguments=("$description_file") if [[ "$include_tests" == true ]]; then parser_arguments+=(--include-tests) @@ -167,7 +294,7 @@ generated_count=0 while IFS=$'\t' read -r target_name module_name target_type package_name; do [[ -n "$target_name" && -n "$module_name" ]] || continue - printf 'Generating %s (%s)...\n' "$module_name" "$target_name" + printf 'Generating %s (%s) for %s...\n' "$module_name" "$target_name" "$platform" declare -a common_command=( swift build @@ -203,7 +330,7 @@ while IFS=$'\t' read -r target_name module_name target_type package_name; do SWIFT_MODULECACHE_PATH="$scratch_root/module-cache" \ "${common_command[@]}" 2>&1 | tee "$build_log" else - fail "SwiftPM could not build target $target_name" + fail "SwiftPM could not build target $target_name for $platform" fi fi @@ -224,7 +351,7 @@ while IFS=$'\t' read -r target_name module_name target_type package_name; do )" fi [[ -n "$interface_path" && -f "$interface_path" ]] || \ - fail "build succeeded but no public interface was found for $target_name ($module_name)" + fail "build succeeded but no public interface was found for $target_name ($module_name) on $platform" temporary_output="$output_root/.$module_name.swiftinterface.$$" cp "$interface_path" "$temporary_output" @@ -233,5 +360,5 @@ while IFS=$'\t' read -r target_name module_name target_type package_name; do generated_count=$((generated_count + 1)) done <"$targets_file" -[[ $generated_count -gt 0 ]] || fail "no interfaces were generated" +[[ $generated_count -gt 0 ]] || fail "no interfaces were generated for $platform" printf 'Generated %d interface(s) in %s\n' "$generated_count" "$output_root" diff --git a/scripts/list-swift-targets.swift b/scripts/list-swift-targets.swift index 4bac0c3..b66371d 100755 --- a/scripts/list-swift-targets.swift +++ b/scripts/list-swift-targets.swift @@ -4,7 +4,13 @@ import Foundation private struct PackageDescription: Decodable { let name: String + let platforms: [Platform] let targets: [Target] + + struct Platform: Decodable { + let name: String + let version: String + } } private struct Target: Decodable { @@ -23,6 +29,7 @@ private struct Target: Decodable { private struct Options { var includeTests = false + var listPlatforms = false var selectedTargets = Set() var jsonPath: String? } @@ -40,6 +47,8 @@ while !arguments.isEmpty { switch argument { case "--include-tests": options.includeTests = true + case "--list-platforms": + options.listPlatforms = true case "--target": guard !arguments.isEmpty else { fail("--target requires a value") } options.selectedTargets.insert(arguments.removeFirst()) @@ -55,6 +64,15 @@ guard let jsonPath = options.jsonPath else { fail("missing package-description J do { let data = try Data(contentsOf: URL(fileURLWithPath: jsonPath)) let package = try JSONDecoder().decode(PackageDescription.self, from: data) + if options.listPlatforms { + guard options.selectedTargets.isEmpty, !options.includeTests else { + fail("--list-platforms cannot be combined with target-selection options") + } + for platform in package.platforms.sorted(by: { $0.name < $1.name }) { + print("\(platform.name)\t\(platform.version)") + } + exit(0) + } let swiftTargets = package.targets.filter { target in target.moduleType == "SwiftTarget" && (options.includeTests || target.type != "test") } From 50476e18cd6bc3fb15fa264184f3f9a0b9c562f0 Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Tue, 4 Aug 2026 12:12:33 +0200 Subject: [PATCH 3/3] Refine Swift interface workflows --- .github/workflows/generate-swiftinterface.yml | 53 ++++++------------- .github/workflows/update-swiftinterfaces.yml | 33 +++--------- 2 files changed, 23 insertions(+), 63 deletions(-) diff --git a/.github/workflows/generate-swiftinterface.yml b/.github/workflows/generate-swiftinterface.yml index 3c4628a..a7a4239 100644 --- a/.github/workflows/generate-swiftinterface.yml +++ b/.github/workflows/generate-swiftinterface.yml @@ -8,36 +8,19 @@ on: description: Normalized platform name (macos, ios, linux, windows, wasi, ...) required: true type: string - base_branch: + target_branch: description: Branch that receives the generated-interface PR (defaults to the dispatched branch) required: false default: "" type: string - request_id: - description: Optional stable ID shared by an umbrella run - required: false - default: "" - type: string - swift_sdk: - description: Optional installed Swift SDK ID (auto-detected for WASI) - required: false - default: "" - type: string workflow_call: inputs: platform: required: true type: string - base_branch: + target_branch: required: true type: string - request_id: - required: true - type: string - swift_sdk: - required: false - default: "" - type: string permissions: contents: write @@ -49,9 +32,7 @@ jobs: runs-on: ${{ (inputs.platform == 'linux' || inputs.platform == 'wasi') && 'ubuntu-latest' || inputs.platform == 'windows' && 'windows-latest' || 'macos-latest' }} env: GH_TOKEN: ${{ github.token }} - INPUT_BASE_BRANCH: ${{ inputs.base_branch }} - INPUT_REQUEST_ID: ${{ inputs.request_id }} - INPUT_SWIFT_SDK: ${{ inputs.swift_sdk }} + INPUT_TARGET_BRANCH: ${{ inputs.target_branch }} PLATFORM: ${{ inputs.platform }} steps: - name: Validate request @@ -65,7 +46,7 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - ref: ${{ inputs.base_branch != '' && inputs.base_branch || github.ref_name }} + ref: ${{ inputs.target_branch != '' && inputs.target_branch || github.ref_name }} - name: Set up Swift outside Xcode if: runner.os != 'macOS' @@ -77,29 +58,25 @@ jobs: - name: Prepare generation branch shell: bash run: | - base_branch="${INPUT_BASE_BRANCH:-$GITHUB_REF_NAME}" - request_id="${INPUT_REQUEST_ID:-$GITHUB_RUN_ID}" - interface_branch="automation/swiftinterfaces/$request_id/$PLATFORM" + target_branch="${INPUT_TARGET_BRANCH:-$GITHUB_REF_NAME}" + interface_branch="automation/swiftinterfaces/$GITHUB_RUN_ID/$PLATFORM" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git switch -C "$interface_branch" - echo "BASE_BRANCH=$base_branch" >> "$GITHUB_ENV" + echo "TARGET_BRANCH=$target_branch" >> "$GITHUB_ENV" echo "INTERFACE_BRANCH=$interface_branch" >> "$GITHUB_ENV" - name: Select WASI SDK if: inputs.platform == 'wasi' shell: bash run: | - swift_sdk="$INPUT_SWIFT_SDK" - if [[ -z "$swift_sdk" ]]; then - swift_sdk="$(swift sdk list | sed -nE '/(_wasm|wasi)/ { s/^[[:space:]]*([^[:space:]]+).*/\1/p; q; }')" - fi + swift_sdk="$(swift sdk list | sed -nE '/(_wasm|wasi)/ { s/^[[:space:]]*([^[:space:]]+).*/\1/p; q; }')" if [[ -z "$swift_sdk" ]]; then echo "No installed WASI Swift SDK was found" >&2 swift sdk list >&2 || true exit 1 fi - echo "INPUT_SWIFT_SDK=$swift_sdk" >> "$GITHUB_ENV" + echo "SWIFT_SDK=$swift_sdk" >> "$GITHUB_ENV" - name: Generate interface shell: bash @@ -108,8 +85,8 @@ jobs: if [[ -d "$interface_dir" ]]; then find "$interface_dir" -maxdepth 1 -type f -name '*.swiftinterface' -delete fi - if [[ -n "$INPUT_SWIFT_SDK" ]]; then - make swiftinterface platform="$PLATFORM" swift_sdk="$INPUT_SWIFT_SDK" + if [[ -n "${SWIFT_SDK:-}" ]]; then + make swiftinterface platform="$PLATFORM" swift_sdk="$SWIFT_SDK" else make swiftinterface platform="$PLATFORM" fi @@ -132,12 +109,12 @@ jobs: if: steps.commit.outputs.changed == 'true' shell: bash run: | - pr_url="$(gh pr list --state open --base "$BASE_BRANCH" --head "$INTERFACE_BRANCH" --json url --jq '.[0].url')" + pr_url="$(gh pr list --state open --base "$TARGET_BRANCH" --head "$INTERFACE_BRANCH" --json url --jq '.[0].url')" if [[ -z "$pr_url" ]]; then pr_url="$(gh pr create \ - --base "$BASE_BRANCH" \ + --base "$TARGET_BRANCH" \ --head "$INTERFACE_BRANCH" \ - --title "Update $PLATFORM Swift interfaces" \ - --body "Generated from \`$BASE_BRANCH\` by workflow run $GITHUB_RUN_ID. Merge this PR into the aggregation branch after reviewing the platform-specific API snapshot.")" + --title "chore: update $PLATFORM Swift interfaces" \ + --body "Generated from \`$TARGET_BRANCH\` by workflow run $GITHUB_RUN_ID. Merge this PR into the aggregation branch after reviewing the platform-specific API snapshot.")" fi echo "$pr_url" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/update-swiftinterfaces.yml b/.github/workflows/update-swiftinterfaces.yml index 151464e..27fc7c9 100644 --- a/.github/workflows/update-swiftinterfaces.yml +++ b/.github/workflows/update-swiftinterfaces.yml @@ -1,24 +1,14 @@ name: Update Swift interfaces -run-name: Update Swift interfaces from ${{ inputs.source_branch || github.ref_name }} +run-name: Update Swift interfaces from ${{ github.ref_name }} on: workflow_dispatch: inputs: - source_branch: - description: Source branch to aggregate into (defaults to the dispatched branch) - required: false - default: "" - type: string platforms: description: JSON array of independently generated platforms required: true default: '["macos","ios","tvos","watchos","maccatalyst"]' type: string - wasi_swift_sdk: - description: Optional installed Swift SDK ID for WASI - required: false - default: "" - type: string permissions: contents: write @@ -29,28 +19,23 @@ jobs: name: Create aggregation branch runs-on: ubuntu-latest outputs: - source_branch: ${{ steps.branch.outputs.source_branch }} update_branch: ${{ steps.branch.outputs.update_branch }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - ref: ${{ inputs.source_branch != '' && inputs.source_branch || github.ref_name }} + ref: ${{ github.ref_name }} - name: Push aggregation branch id: branch shell: bash - env: - SOURCE_BRANCH_INPUT: ${{ inputs.source_branch != '' && inputs.source_branch || github.ref_name }} run: | - source_branch="$SOURCE_BRANCH_INPUT" update_branch="automation/swiftinterfaces/$GITHUB_RUN_ID" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git switch -C "$update_branch" git commit --allow-empty -m "Prepare Swift interface update" git push --force-with-lease --set-upstream origin "$update_branch" - echo "source_branch=$source_branch" >> "$GITHUB_OUTPUT" echo "update_branch=$update_branch" >> "$GITHUB_OUTPUT" generate: @@ -66,9 +51,7 @@ jobs: uses: ./.github/workflows/generate-swiftinterface.yml with: platform: ${{ matrix.platform }} - base_branch: ${{ needs.prepare.outputs.update_branch }} - request_id: ${{ format('{0}', github.run_id) }} - swift_sdk: ${{ matrix.platform == 'wasi' && inputs.wasi_swift_sdk || '' }} + target_branch: ${{ needs.prepare.outputs.update_branch }} finalize: name: Open draft aggregation PR @@ -79,24 +62,24 @@ jobs: runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} - SOURCE_BRANCH: ${{ needs.prepare.outputs.source_branch }} + TARGET_BRANCH: ${{ github.ref_name }} UPDATE_BRANCH: ${{ needs.prepare.outputs.update_branch }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - ref: ${{ needs.prepare.outputs.source_branch }} + ref: ${{ github.ref_name }} - name: Open draft PR shell: bash run: | - pr_url="$(gh pr list --state open --base "$SOURCE_BRANCH" --head "$UPDATE_BRANCH" --json url --jq '.[0].url')" + pr_url="$(gh pr list --state open --base "$TARGET_BRANCH" --head "$UPDATE_BRANCH" --json url --jq '.[0].url')" if [[ -z "$pr_url" ]]; then pr_url="$(gh pr create \ --draft \ - --base "$SOURCE_BRANCH" \ + --base "$TARGET_BRANCH" \ --head "$UPDATE_BRANCH" \ - --title "Update platform Swift interfaces" \ + --title "chore: update platform Swift interfaces" \ --body "Aggregation PR for workflow run $GITHUB_RUN_ID. Merge the successful platform PRs into \`$UPDATE_BRANCH\`; failed platform jobs remain visible in the workflow and can be fixed or intentionally ignored. Matrix result: \`${{ needs.generate.result }}\`.")" fi echo "$pr_url" >> "$GITHUB_STEP_SUMMARY"