diff --git a/Package.resolved b/Package.resolved index 91c72c22..f888930c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -22,7 +22,7 @@ { "identity" : "dns", "kind" : "remoteSourceControl", - "location" : "https://github.com/Bouke/DNS", + "location" : "https://github.com/Bouke/DNS.git", "state" : { "revision" : "78bbd1589890a90b202d11d5f9e1297050cf0eb2", "version" : "1.2.0" @@ -31,7 +31,7 @@ { "identity" : "dnsclient", "kind" : "remoteSourceControl", - "location" : "https://github.com/orlandos-nl/DNSClient", + "location" : "https://github.com/orlandos-nl/DNSClient.git", "state" : { "revision" : "551fbddbf4fa728d4cd86f6a5208fe4f925f0549", "version" : "2.4.4" @@ -235,6 +235,15 @@ "version" : "2.8.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 1b37d842..ddb51e1f 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. +import CompilerPluginSupport import Foundation import PackageDescription @@ -55,6 +56,7 @@ let package = Package( .package(url: "https://github.com/orlandos-nl/DNSClient.git", from: "2.4.1"), .package(url: "https://github.com/Bouke/DNS.git", from: "1.2.0"), .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0"), ], targets: [ .executableTarget( @@ -259,6 +261,7 @@ let package = Package( "ContainerPlugin", "ContainerXPC", "TerminalProgress", + "HelperMacros", ] ), .testTarget( @@ -373,5 +376,18 @@ let package = Package( .define("BUILDER_SHIM_VERSION", to: "\"\(builderShimVersion)\""), ] ), + .macro( + name: "HelperMacrosMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + ] + ), + + // Library that exposes a macro as part of its API, which is used in client programs. + .target( + name: "HelperMacros", + dependencies: ["HelperMacrosMacros"] + ), ] ) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index a218deb2..473db18f 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -17,8 +17,10 @@ import ArgumentParser import ContainerizationError import Foundation +import HelperMacros public struct Flags { + @OptionGroupPassthrough public struct Global: ParsableArguments { public init() {} @@ -26,6 +28,7 @@ public struct Flags { public var debug = false } + @OptionGroupPassthrough public struct Process: ParsableArguments { public init() {} @@ -63,6 +66,7 @@ public struct Flags { public var cwd: String? } + @OptionGroupPassthrough public struct Resource: ParsableArguments { public init() {} @@ -76,6 +80,7 @@ public struct Flags { public var memory: String? } + @OptionGroupPassthrough public struct Registry: ParsableArguments { public init() {} @@ -87,6 +92,7 @@ public struct Flags { public var scheme: String = "auto" } + @OptionGroupPassthrough public struct Management: ParsableArguments { public init() {} @@ -201,6 +207,7 @@ public struct Flags { public var virtualization: Bool = false } + @OptionGroupPassthrough public struct Progress: ParsableArguments { public init() {} diff --git a/Sources/HelperMacros/HelperMacros.swift b/Sources/HelperMacros/HelperMacros.swift new file mode 100644 index 00000000..e73e653b --- /dev/null +++ b/Sources/HelperMacros/HelperMacros.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// +// HelperMacros.swift +// container +// +// Created by Morris Richman on 10/3/25. +// + +import Foundation + +/// Creates a function in OptionGroups called `passThroughCommands` to return an array of strings to be appended and passed down for Plugin support. +@attached(member, names: named(passThroughCommands)) +public macro OptionGroupPassthrough() = #externalMacro(module: "HelperMacrosMacros", type: "OptionGroupPassthrough") diff --git a/Sources/HelperMacrosMacros/HelperMacrosMacros.swift b/Sources/HelperMacrosMacros/HelperMacrosMacros.swift new file mode 100644 index 00000000..7ab94578 --- /dev/null +++ b/Sources/HelperMacrosMacros/HelperMacrosMacros.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// +// HelperMacrosMacros.swift +// container +// +// Created by Morris Richman on 10/3/25. +// + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntaxMacros + +@main +struct SwiftMacrosAndMePlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + OptionGroupPassthrough.self + ] +} + +extension String: @retroactive Error { +} + +enum MacroExpansionError: Error { + case unsupportedDeclaration + + var localizedDescription: String { + switch self { + case .unsupportedDeclaration: + return "Unsupported declaration for macro expansion." + } + } +} diff --git a/Sources/HelperMacrosMacros/OptionGroupPassthrough.swift b/Sources/HelperMacrosMacros/OptionGroupPassthrough.swift new file mode 100644 index 00000000..6bc52a65 --- /dev/null +++ b/Sources/HelperMacrosMacros/OptionGroupPassthrough.swift @@ -0,0 +1,213 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// +// OptionGroupPassthrough.swift +// container +// +// Created by Morris Richman on 10/3/25. +// + +import Foundation +import SwiftSyntax +import SwiftSyntaxMacros + +/// Creates a function in OptionGroups called `passThroughCommands` to return an array of strings to be appended and passed down for Plugin support. +public struct OptionGroupPassthrough: MemberMacro { + public static func expansion( + of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard let structDecl = declaration.as(StructDeclSyntax.self) else { + throw MacroExpansionError.unsupportedDeclaration + } + let members = structDecl.memberBlock.members.filter({ $0.decl.is(VariableDeclSyntax.self) }) + var commands: [CommandOutline] = [] + + // Append comman outlines for each member + for member in members { + guard let decl = member.decl.as(VariableDeclSyntax.self) else { + continue + } + if let option = decl.attributes.first(where: { $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "Option" }), + let option = option.as(AttributeSyntax.self) + { + commands.append(try getOptionPropertyCommands(option, decl: decl)) + } else if let option = decl.attributes.first(where: { $0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "Flag" }), + let option = option.as(AttributeSyntax.self) + { + commands.append(try getFlagPropertyCommands(option, decl: decl)) + } + } + + // Create begining of function + var function = """ + /// Autogenerated by ``OptionGroupPassthrough``. This function returns the ``OptionGroup`` as an array of commands that can be passed down to a ``ContainerCommands`` command. + public func passThroughCommands() -> [String] { + var commands: [String] = [] + + """ + + // Append the code for each command + for command in commands { + function.append(command.code) + function.append("") + } + + // Close function + function.append("return commands\n}") + + return [.init(stringLiteral: function)] + } + + private static func getFlagPropertyCommands(_ option: AttributeSyntax, decl: VariableDeclSyntax) throws -> CommandOutline { + let (optionType, customName) = try getOptionNameType(option) + guard let identifierBinding = decl.bindings.first(where: { $0.pattern.is(IdentifierPatternSyntax.self) }), + let parameter = identifierBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier + else { + throw "Could Not Determine Variable" + } + + // Get string command tack + let optionName = customName ?? parameter.text + let nameCommand = optionType.tacks + optionName + + return CommandOutline(type: .flag, flag: nameCommand, variable: parameter.text) + } + + private static func getOptionPropertyCommands(_ option: AttributeSyntax, decl: VariableDeclSyntax) throws -> CommandOutline { + let (optionType, customName) = try getOptionNameType(option) + guard let identifierBinding = decl.bindings.first(where: { $0.pattern.is(IdentifierPatternSyntax.self) }), + let parameter = identifierBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier + else { + throw "Could Not Determine Variable" + } + + // Get string command tack + let optionName = customName ?? parameter.text + let nameCommand = optionType.tacks + optionName + + return CommandOutline(type: .option, flag: nameCommand, variable: parameter.text) + } + + private static func getOptionNameType(_ option: AttributeSyntax) throws -> (OptionNameType, String?) { + guard let attribute = option.arguments?.as(LabeledExprListSyntax.self)?.first(where: { $0.label?.text == "name" }) else { + // Default to long if not described in PropertyWrapper + return (.long, nil) + } + let expression: MemberAccessExprSyntax = try _getOptionNameTypeExpressionFromExpression(attribute.expression) + guard let optionType = OptionNameType(baseName: expression.declName.baseName) else { + throw "Error Parsing Option Name" + } + + // Get the name of the custom short/long if needed + var customString: String? + if [OptionNameType.customLong, .customShort].contains(optionType) { + if let arrayExpression = attribute.expression.as(ArrayExprSyntax.self), + let last = arrayExpression.elements.last + { + customString = try _getCustomOptionNameFromExpression(last.expression) + } else { + customString = try _getCustomOptionNameFromExpression(attribute.expression) + } + } + + return (optionType, customString) + } + + private static func _getOptionNameTypeExpressionFromExpression(_ expression: ExprSyntax) throws -> MemberAccessExprSyntax { + if let expr = expression.as(MemberAccessExprSyntax.self) { + return expr + } else if let function = expression.as(FunctionCallExprSyntax.self), + let expr = function.calledExpression.as(MemberAccessExprSyntax.self) + { + return expr + } else if let array = expression.as(ArrayExprSyntax.self), + let last = array.elements.last + { + return try _getOptionNameTypeExpressionFromExpression(last.expression) + } else { + throw "Error Parsing Option Name Expression: \(expression)" + } + } + private static func _getCustomOptionNameFromExpression(_ expression: ExprSyntax) throws -> String? { + let customNameArguments = expression.as(FunctionCallExprSyntax.self)?.arguments + guard let customNameArg = customNameArguments?.first, + let segment = customNameArg.expression.as(StringLiteralExprSyntax.self)?.segments.first + else { + throw "Error Parsing Custom Option Name" + } + return segment.as(StringSegmentSyntax.self)?.content.text + } + + private enum OptionNameType: String { + case short, long, customLong, customShort + + init?(baseName: TokenSyntax) { + guard let result = OptionNameType(baseName: baseName.text) else { + return nil + } + + self = result + } + + init?(baseName: String) { + switch baseName { + case "shortAndLong": self = .long + case "customLong": self = .customLong + case "long": self = .long + case "customShort": self = .customShort + case "short": self = .short + default: return nil + } + } + + var tacks: String { + switch self { + case .short, .customShort: + "-" + case .long, .customLong: + "--" + } + } + } +} + +private struct CommandOutline { + let type: `Type` + let flag: String + let variable: String + + enum `Type` { + case flag, option + } + + var code: String { + switch type { + case .flag: + """ + if \(variable) { + commands.append("\(flag)") + } + """ + case .option: + """ + if "\\(\(variable), default: "%absolute-nil%")" != "%absolute-nil%" { + commands.append(contentsOf: ["\(flag)", "\\(\(variable), default: "%absolute-nil%")"]) + } + """ + } + } +}