diff --git a/Empusa.xcodeproj/project.pbxproj b/Empusa.xcodeproj/project.pbxproj index ae0e4c9..c0fdda5 100644 --- a/Empusa.xcodeproj/project.pbxproj +++ b/Empusa.xcodeproj/project.pbxproj @@ -45,6 +45,8 @@ 3F3B176D2ADDE117000283AE /* DestinationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationView.swift; sourceTree = ""; }; 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesView.swift; sourceTree = ""; }; 3F3B17712ADDE25D000283AE /* DataProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProgressView.swift; sourceTree = ""; }; + 3F54EB962AE2553D008A45B0 /* Empusa.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Empusa.xctestplan; sourceTree = ""; }; + 3FC172272AE1D63A00DCBE28 /* EmpusaMacros */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EmpusaMacros; sourceTree = ""; }; 3FCE9D2E2AE0FE2E00A4E3F5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3FCE9D302AE17F5F00A4E3F5 /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -104,6 +106,7 @@ 3F21D31B2ADADC7400B6A5D9 /* Tests */ = { isa = PBXGroup; children = ( + 3F54EB962AE2553D008A45B0 /* Empusa.xctestplan */, 3F21D31C2ADADC7400B6A5D9 /* EmpusaTests.swift */, ); path = Tests; @@ -121,6 +124,7 @@ 3F21D3362ADADE8B00B6A5D9 /* Packages */ = { isa = PBXGroup; children = ( + 3FC172272AE1D63A00DCBE28 /* EmpusaMacros */, 3F21D3372ADADEBE00B6A5D9 /* EmpusaKit */, ); path = Packages; diff --git a/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cf7e7ee..deff244 100644 --- a/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "version" : "2.5.1" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } + }, { "identity" : "zip", "kind" : "remoteSourceControl", diff --git a/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme b/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme index 08f959f..730b674 100644 --- a/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme +++ b/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme @@ -29,7 +29,7 @@ shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/Packages/EmpusaKit/Package.swift b/Packages/EmpusaKit/Package.swift index e4bbabe..7147369 100644 --- a/Packages/EmpusaKit/Package.swift +++ b/Packages/EmpusaKit/Package.swift @@ -13,13 +13,15 @@ let package = Package( targets: ["EmpusaKit"]), ], dependencies: [ - .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2") + .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"), + .package(path: "../EmpusaMacros") ], targets: [ .target( name: "EmpusaKit", dependencies: [ - .product(name: "Zip", package: "Zip") + .product(name: "Zip", package: "Zip"), + .product(name: "EmpusaMacros", package: "EmpusaMacros") ] ), .testTarget( diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift index 735a139..f7f80d7 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import EmpusaMacros // MARK: - DisplayingSwitchResource @@ -67,77 +68,77 @@ public enum SwitchResource: String, Codable, CaseIterable { switch self { case .hekate: .github( - .init(string: "https://api.github.com/repos/CTCaer/hekate/releases/latest")!, + #URL("https://api.github.com/repos/CTCaer/hekate/releases/latest"), assetPrefix: "hekate_ctcaer_" ) case .hekateIPL: .link( - .init(string: "https://nh-server.github.io/switch-guide/files/emu/hekate_ipl.ini")!, + #URL("https://nh-server.github.io/switch-guide/files/emu/hekate_ipl.ini"), version: nil ) case .atmosphere: .github( - .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, + #URL("https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest"), assetPrefix: "atmosphere-" ) case .fusee: .github( - .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, + #URL("https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest"), assetPrefix: "fusee.bin" ) case .sigpatches: .link( - .init(string: "https://sigmapatches.coomer.party/sigpatches.zip")!, + #URL("https://sigmapatches.coomer.party/sigpatches.zip"), version: "16.1.0" ) case .tinfoil: .github( - .init(string: "https://api.github.com/repos/kkkkyue/Tinfoil/releases/latest")!, + #URL("https://api.github.com/repos/kkkkyue/Tinfoil/releases/latest"), assetPrefix: "Tinfoil.Self.Installer" ) case .bootLogos: .link( - .init(string: "https://nh-server.github.io/switch-guide/files/bootlogos.zip")!, + #URL("https://nh-server.github.io/switch-guide/files/bootlogos.zip"), version: nil ) case .emummc: .link( - .init(string: "https://nh-server.github.io/switch-guide/files/emummc.txt")!, + #URL("https://nh-server.github.io/switch-guide/files/emummc.txt"), version: nil ) case .lockpickRCM: .forgejo( - .init(string: "https://vps.suchmeme.nl/git/api/v1/repos/mudkip/Lockpick_RCM/releases/latest")!, + #URL("https://vps.suchmeme.nl/git/api/v1/repos/mudkip/Lockpick_RCM/releases/latest"), assetPrefix: "Lockpick_RCM.bin" ) case .hbAppStore: .github( - .init(string: "https://api.github.com/repos/fortheusers/hb-appstore/releases/latest")!, + #URL("https://api.github.com/repos/fortheusers/hb-appstore/releases/latest"), assetPrefix: "appstore.nro" ) case .jksv: .github( - .init(string: "https://api.github.com/repos/J-D-K/JKSV/releases/latest")!, + #URL("https://api.github.com/repos/J-D-K/JKSV/releases/latest"), assetPrefix: "JKSV.nro" ) case .ftpd: .github( - .init(string: "https://api.github.com/repos/mtheall/ftpd/releases/latest")!, + #URL("https://api.github.com/repos/mtheall/ftpd/releases/latest"), assetPrefix: "ftpd.nro" ) case .nxThemesInstaller: .github( - .init(string: "https://api.github.com/repos/exelix11/SwitchThemeInjector/releases/latest")!, + #URL("https://api.github.com/repos/exelix11/SwitchThemeInjector/releases/latest"), assetPrefix: "NXThemesInstaller.nro" ) case .nxShell: .github( - .init(string: "https://api.github.com/repos/joel16/NX-Shell/releases/latest")!, + #URL("https://api.github.com/repos/joel16/NX-Shell/releases/latest"), assetPrefix: "NX-Shell.nro" ) case .goldleaf: .github( - .init(string: "https://api.github.com/repos/XorTroll/Goldleaf/releases/latest")!, + #URL("https://api.github.com/repos/XorTroll/Goldleaf/releases/latest"), assetPrefix: "Goldleaf.nro" ) } diff --git a/Packages/EmpusaMacros/.gitignore b/Packages/EmpusaMacros/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Packages/EmpusaMacros/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Packages/EmpusaMacros/Package.swift b/Packages/EmpusaMacros/Package.swift new file mode 100644 index 0000000..106515b --- /dev/null +++ b/Packages/EmpusaMacros/Package.swift @@ -0,0 +1,45 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "EmpusaMacros", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "EmpusaMacros", + targets: ["EmpusaMacros"] + ), + ], + dependencies: [ + // Depend on the Swift 5.9 release of SwiftSyntax + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Macro implementation that performs the source transformation of a macro. + .macro( + name: "EmpusaMacrosImpl", + 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: "EmpusaMacros", dependencies: ["EmpusaMacrosImpl"]), + + // A test target used to develop the macro implementation. + .testTarget( + name: "EmpusaMacrosTests", + dependencies: [ + "EmpusaMacrosImpl", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] +) diff --git a/Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift b/Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift new file mode 100644 index 0000000..9c54b90 --- /dev/null +++ b/Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift @@ -0,0 +1,4 @@ +import Foundation + +@freestanding(expression) +public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "EmpusaMacrosImpl", type: "URLMacro") diff --git a/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift new file mode 100644 index 0000000..9ccd485 --- /dev/null +++ b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift @@ -0,0 +1,11 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +@main +struct EmpusaMacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + URLMacro.self + ] +} diff --git a/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift new file mode 100644 index 0000000..6542ef5 --- /dev/null +++ b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +enum URLMacroError: Error, CustomStringConvertible { + case requiresStaticStringLiteral + case malformedURL(urlString: String) + + var description: String { + switch self { + case .requiresStaticStringLiteral: + return "#URL requires a static string literal" + case .malformedURL(let urlString): + return "The input URL is malformed: \(urlString)" + } + } +} + +public struct URLMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + guard + let argument = node.argumentList.first?.expression, + let segments = argument.as(StringLiteralExprSyntax.self)?.segments, + segments.count == 1, + case .stringSegment(let literalSegment)? = segments.first + else { + throw URLMacroError.requiresStaticStringLiteral + } + + guard let _ = URL(string: literalSegment.content.text) else { + throw URLMacroError.malformedURL(urlString: "\(argument)") + } + + return "URL(string: \(argument))!" + } +} diff --git a/Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift b/Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift new file mode 100644 index 0000000..eb72fce --- /dev/null +++ b/Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift @@ -0,0 +1,46 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. +#if canImport(EmpusaMacrosMacros) +import EmpusaMacrosMacros + +let testMacros: [String: Macro.Type] = [ + "stringify": StringifyMacro.self, +] +#endif + +final class EmpusaMacrosTests: XCTestCase { + func testMacro() throws { + #if canImport(EmpusaMacrosMacros) + assertMacroExpansion( + """ + #stringify(a + b) + """, + expandedSource: """ + (a + b, "a + b") + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroWithStringLiteral() throws { + #if canImport(EmpusaMacrosMacros) + assertMacroExpansion( + #""" + #stringify("Hello, \(name)") + """#, + expandedSource: #""" + ("Hello, \(name)", #""Hello, \(name)""#) + """#, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} diff --git a/Packages/EmpusaKit/Tests/Empusa.xctestplan b/Tests/Empusa.xctestplan similarity index 100% rename from Packages/EmpusaKit/Tests/Empusa.xctestplan rename to Tests/Empusa.xctestplan