diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b7595e1..2691ba0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,9 +15,9 @@ jobs: steps: - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '^15.2.0' + xcode-version: '^16.0.0' - uses: actions/checkout@v4 - - run: swift build -c ${{ matrix.configuration }} -Xswiftc -enable-testing + - run: swift build -c ${{ matrix.configuration }} -Xswiftc -enable-testing -Xswiftc -swift-version -Xswiftc 6 podspec: strategy: matrix: @@ -27,7 +27,7 @@ jobs: steps: - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '^15.2.0' + xcode-version: '^16.0.0' - uses: actions/checkout@v4 - run: bundle install - run: bundle exec pod lib lint --platforms=${{ matrix.platform }} --configuration=${{ matrix.configuration }} diff --git a/BuildHelper/BuildHelper.xcodeproj/project.pbxproj b/BuildHelper/BuildHelper.xcodeproj/project.pbxproj index 2fab050..981bc17 100644 --- a/BuildHelper/BuildHelper.xcodeproj/project.pbxproj +++ b/BuildHelper/BuildHelper.xcodeproj/project.pbxproj @@ -133,7 +133,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1600; TargetAttributes = { 63A746952AFDD742003FA3AC = { CreatedOnToolsVersion = 15.0.1; @@ -219,6 +219,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -282,6 +283,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -312,6 +314,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -326,7 +329,7 @@ PRODUCT_BUNDLE_IDENTIFIER = jp.banjun.SwiftHotReload.BuildHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -339,6 +342,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -353,7 +357,7 @@ PRODUCT_BUNDLE_IDENTIFIER = jp.banjun.SwiftHotReload.BuildHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; diff --git a/BuildHelper/Sources/BuildHelper.swift b/BuildHelper/Sources/BuildHelper.swift index 4735e49..4136279 100644 --- a/BuildHelper/Sources/BuildHelper.swift +++ b/BuildHelper/Sources/BuildHelper.swift @@ -4,8 +4,9 @@ // Release build is not disabled as BuildHelper.app is to be buildable for generating a mac helper app. // TODO: BuildHelper may be separated into sub- spec/package import Foundation -import Combine +@preconcurrency import Combine +@MainActor public final class BuildHelper: ObservableObject { let proxyBrowser = ProxyBrowser() @@ -17,7 +18,7 @@ public final class BuildHelper: ObservableObject { private var fileMonitor: FileMonitor? { didSet { Task { - fileMonitorCancellable = await fileMonitor?.$fileChanges.compactMap {$0}.sink { [weak self] _ in + fileMonitorCancellable = await fileMonitor?.fileChanges.compactMap {$0}.sink { [weak self] _ in self?.reload() } } diff --git a/BuildHelper/Sources/BuildHelperApp.swift b/BuildHelper/Sources/BuildHelperApp.swift index 6ca346a..d3f742b 100644 --- a/BuildHelper/Sources/BuildHelperApp.swift +++ b/BuildHelper/Sources/BuildHelperApp.swift @@ -29,6 +29,7 @@ struct BuildHelperApp: SwiftUI.App { struct ContentView: View { @EnvironmentObject var buildHelper: BuildHelper + @State private var browserView: MCBrowserViewControllerView? var body: some View { VStack(spacing: 20) { @@ -38,7 +39,9 @@ struct BuildHelperApp: SwiftUI.App { Text("Date Reloaded" + "\n" + (buildHelper.dateReloaded?.formatted(date: .numeric, time: .complete) ?? "Never")) .multilineTextAlignment(.center) - buildHelper.proxyBrowser.browserView + if let browserView { browserView } else { ProgressView().task { + browserView = await buildHelper.proxyBrowser.browserView() + }} } .padding() } diff --git a/BuildHelper/Sources/ProxyBrowser.swift b/BuildHelper/Sources/ProxyBrowser.swift index 60d1994..ad46e1a 100644 --- a/BuildHelper/Sources/ProxyBrowser.swift +++ b/BuildHelper/Sources/ProxyBrowser.swift @@ -3,8 +3,9 @@ // Release build is not disabled as BuildHelper.app is to be buildable for generating a mac helper app. // TODO: BuildHelper may be separated into sub- spec/package import Foundation -import MultipeerConnectivity +@preconcurrency import MultipeerConnectivity @testable import SwiftHotReload // NOTE: use internal methods. SPM does not allow overlapping sources for a single Package.swift +import SwiftUI final actor ProxyBrowser { @Published private(set) var runtimePeers: [RuntimePeer] = [] @@ -12,14 +13,16 @@ final actor ProxyBrowser { private let session: MCSession private let browser: MCNearbyServiceBrowser private let sessionDelegate: SessionDelegate = .init() - let browserView: MCBrowserViewControllerView + private(set) var browserView: @Sendable () async -> MCBrowserViewControllerView - init(hostName: String = ProcessInfo().hostName, bundleID: String? = Env.shared.CFBundleIdentifier, processID: Int32 = ProcessInfo().processIdentifier) { + init(hostName: String = ProcessInfo().hostName, bundleID: String? = Env.host.CFBundleIdentifier, processID: Int32 = ProcessInfo().processIdentifier) { let displayName = String("Client[\(hostName)] \(bundleID ?? "cli")(\(processID))".utf8.prefix(63))! self.peerID = MCPeerID(displayName: displayName) - self.session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required) - self.browser = MCNearbyServiceBrowser(peer: peerID, serviceType: MultipeerConnectivityConstants.serviceType) - self.browserView = MCBrowserViewControllerView(browser: browser, session: session) + let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required) + self.session = session + let browser = MCNearbyServiceBrowser(peer: peerID, serviceType: MultipeerConnectivityConstants.serviceType) + self.browser = browser + self.browserView = { await MCBrowserViewControllerView(browser: browser, session: session) } // Task { session.delegate = sessionDelegate @@ -69,16 +72,16 @@ final actor ProxyBrowser { } private extension ProxyBrowser { - private final class SessionDelegate: NSObject, MCSessionDelegate { - unowned var owner: ProxyBrowser? + private final class SessionDelegate: NSObject, MCSessionDelegate, Sendable { + unowned nonisolated(unsafe) var owner: ProxyBrowser? override init() { super.init() } func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { - Task { await owner?.session(session, peer: peerID, didChange: state) } + Task { @Sendable in await self.owner?.session(session, peer: peerID, didChange: state) } } func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - Task { await owner?.session(session, didReceive: data, fromPeer: peerID) } + Task { @Sendable in await self.owner?.session(session, didReceive: data, fromPeer: peerID) } } func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { diff --git a/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj b/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj index 966290f..334dc1c 100644 --- a/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj +++ b/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj @@ -123,7 +123,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1600; TargetAttributes = { 63980F422AEA8DA50099B122 = { CreatedOnToolsVersion = 15.0.1; @@ -209,6 +209,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -270,6 +271,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -297,6 +299,7 @@ CODE_SIGN_ENTITLEMENTS = SwiftHotReloadExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -325,7 +328,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; @@ -338,6 +341,7 @@ CODE_SIGN_ENTITLEMENTS = SwiftHotReloadExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -366,7 +370,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; diff --git a/Example/SwiftHotReloadExample.xcodeproj/xcshareddata/xcschemes/SwiftHotReloadExample.xcscheme b/Example/SwiftHotReloadExample.xcodeproj/xcshareddata/xcschemes/SwiftHotReloadExample.xcscheme index 796167d..c24c1fc 100644 --- a/Example/SwiftHotReloadExample.xcodeproj/xcshareddata/xcschemes/SwiftHotReloadExample.xcscheme +++ b/Example/SwiftHotReloadExample.xcodeproj/xcshareddata/xcschemes/SwiftHotReloadExample.xcscheme @@ -1,6 +1,6 @@ 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) + base64 (0.2.0) + bigdecimal (3.1.8) claide (1.1.0) - cocoapods (1.13.0) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.13.0) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -34,7 +42,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.13.0) + cocoapods-core (1.15.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -45,7 +53,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -54,40 +62,44 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + drb (2.2.1) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.16.3) + ffi (1.17.0-arm64-darwin) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - json (2.6.3) - minitest (5.20.0) + json (2.7.2) + minitest (5.25.1) molinillo (0.8.0) + mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) + nkf (0.2.0) public_suffix (4.0.7) - rexml (3.2.6) + rexml (3.3.7) ruby-macho (2.5.1) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.23.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - zeitwerk (2.6.12) + rexml (>= 3.3.2, < 4.0) PLATFORMS + arm64-darwin-21 arm64-darwin-22 DEPENDENCIES diff --git a/Package.swift b/Package.swift index 485d6ea..88d342e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index 162439d..1b86182 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ SwiftHotReload is an experimental project. We investigate a real world applicati ## Supported Platforms -* Xcode 15.x -* Host macOS 13.x, 14.x +* Xcode 16.x +* Host macOS 14.x, 15.x We can use either Standalone Reloader or Proxy Reloader. Standalone Reloader runs all required tasks on the runtime target process. Proxy Reloader runs on the runtime target process and receives dylibs from BuildHelper via network. BuildHelper runs on the host Mac and monitors file changes to build the file and send dylibs to Proxy on the target. diff --git a/Sources/Core/Builder.swift b/Sources/Core/Builder.swift index fb90938..419a5fb 100644 --- a/Sources/Core/Builder.swift +++ b/Sources/Core/Builder.swift @@ -15,7 +15,7 @@ public final actor Builder { private let platformName: String private let codesignIdentity: String? - public struct InputParameters: Codable { + public struct InputParameters: Codable, Sendable { public var targetSwiftFile: URL public var env: Env public var derivedData: URL? @@ -29,7 +29,7 @@ public final actor Builder { public var platformName: String? public var codesignIdentity: String? - public init(targetSwiftFile: URL, env: Env = .shared, derivedData: URL? = nil, confBuildDirAppRandomString: String? = nil, mainModule: String? = nil, modules: [String] = [], configurationPlatform: String? = nil, arch: String? = nil, targetTriple: String? = nil, sdk: URL? = nil, platformName: String? = nil, codesignIdentity: String? = nil) { + public init(targetSwiftFile: URL, env: Env = .host, derivedData: URL? = nil, confBuildDirAppRandomString: String? = nil, mainModule: String? = nil, modules: [String] = [], configurationPlatform: String? = nil, arch: String? = nil, targetTriple: String? = nil, sdk: URL? = nil, platformName: String? = nil, codesignIdentity: String? = nil) { self.targetSwiftFile = targetSwiftFile self.env = env self.derivedData = derivedData @@ -97,7 +97,7 @@ public final actor Builder { } self.buildDir = buildDir self.targetTriple = p.targetTriple ?? p.env.estimatedTargetTriple! - self.sdk = p.sdk ?? p.env.estimatedSDK ?? Env.shared.estimatedSDK + self.sdk = p.sdk ?? p.env.estimatedSDK ?? Env.host.estimatedSDK self.platformName = p.platformName ?? p.env.DTPlatformName! self.codesignIdentity = p.codesignIdentity } @@ -112,7 +112,7 @@ public final actor Builder { } func build(dylibPath: URL) throws { - guard Env.shared.DTPlatformName != "iphoneos" else { + guard Env.host.DTPlatformName != "iphoneos" else { NSLog("%@", "🍓 ⚠️ To do hot reloads, the process host should be able to execute swiftc. cancelled building the target swift file. ⚠️") throw Error.cannotBuildOnRuntime(platformName) } diff --git a/Sources/Core/Env.swift b/Sources/Core/Env.swift index bcd767b..a282496 100644 --- a/Sources/Core/Env.swift +++ b/Sources/Core/Env.swift @@ -1,9 +1,16 @@ #if DEBUG || os(macOS) import Foundation import MachO +import struct os.OSAllocatedUnfairLock -public struct Env: Codable, Equatable { - public static let shared: Env = .init() +public struct Env: Codable, Equatable, Sendable { + public static let host: Env = _host.withLock { _host in + if let _host { return _host } + let env = Env() + _host = env + return env + } + private static let _host: OSAllocatedUnfairLock = .init(uncheckedState: nil) /// /Users/username public var estimatedHomeDir: URL? { @@ -64,8 +71,9 @@ public struct Env: Codable, Equatable { .reversed().drop {$0 != "Platforms"}.dropFirst().reversed() .joined(separator: "/") }).map(URL.init(fileURLWithPath:)) - ?? (self != .shared ? Env.shared.estimatedDeveloperDir : nil) // on iphoneos, developer dir is not available in env. use host env typically on macOS build helper + ?? (self != .host ? Env.host.estimatedDeveloperDir : nil) // on iphoneos, developer dir is not available in env. use host env typically on macOS build helper } + /// /Applications/Xcode1501.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator17.0.sdk public var estimatedSDK: URL? { estimatedDeveloperDir?.appendingPathComponent("Platforms") diff --git a/Sources/Core/FileMonitor.swift b/Sources/Core/FileMonitor.swift index ea366fa..3ae672c 100644 --- a/Sources/Core/FileMonitor.swift +++ b/Sources/Core/FileMonitor.swift @@ -1,9 +1,11 @@ #if DEBUG || os(macOS) import Foundation +@preconcurrency import Combine final actor FileMonitor { private let file: URL - @Published private(set) var fileChanges: Date? + private let fileChangesSubject: CurrentValueSubject = .init(nil) + var fileChanges: AnyPublisher { fileChangesSubject.eraseToAnyPublisher() } private var monitor: DispatchSourceFileSystemObject? { didSet { @@ -19,7 +21,7 @@ final actor FileMonitor { init(file: URL) { self.file = file - guard Env.shared.DTPlatformName != "iphoneos" else { + guard Env.host.DTPlatformName != "iphoneos" else { NSLog("%@", "🍓 ⚠️ To do hot reloads, the process host should be able to execute swiftc. cancelled installing the file monitor. ⚠️") return } @@ -33,20 +35,27 @@ final actor FileMonitor { NSLog("%@", "🍓 \(#function) starting file monitor for file at \(file.path)") let handle = FileHandle(forReadingAtPath: file.path) monitor = handle.map { DispatchSource.makeFileSystemObjectSource(fileDescriptor: $0.fileDescriptor, eventMask: .all) } - monitor?.setEventHandler { [unowned self] in - let content = try? TargetSwiftFile(file).content - guard content != lastTargetFileContent else { - // NSLog("%@", "🍓 target file change detected but same content. ignored.") - return - } - NSLog("%@", "🍓 target file change detected") - lastTargetFileContent = content - fileChanges = Date() + monitor?.setEventHandler { [weak self] in + guard let self else { return } + Task { await targetFileChangeDetected(handle) } + } + } - self.monitor = nil - handle?.closeFile() - self.install() + private func targetFileChangeDetected(_ handle: FileHandle?) { + let content = try? TargetSwiftFile(file).content + guard content != lastTargetFileContent else { + // NSLog("%@", "🍓 target file change detected but same content. ignored.") + return + } + NSLog("%@", "🍓 target file change detected") + lastTargetFileContent = content + Task { @MainActor in + await fileChangesSubject.send(Date()) } + + monitor = nil + handle?.closeFile() + install() } } diff --git a/Sources/ProxyReloader/Proxy.swift b/Sources/ProxyReloader/Proxy.swift index d620bdd..5906e03 100644 --- a/Sources/ProxyReloader/Proxy.swift +++ b/Sources/ProxyReloader/Proxy.swift @@ -1,10 +1,12 @@ #if DEBUG || os(macOS) import Foundation -import MultipeerConnectivity +@preconcurrency import MultipeerConnectivity +@preconcurrency import Combine final actor Proxy { private let loader: Loader = .init() - @Published private(set) var receivedDylibFiles: [URL] = [] + private let receivedDylibFilesSubject: CurrentValueSubject<[URL], Never> = .init([]) + var receivedDylibFiles: AnyPublisher<[URL], Never> { receivedDylibFilesSubject.eraseToAnyPublisher() } private var shouldConnectToBuilder: (_ title: String, _ message: String) async -> Bool func setShouldConnectToBuilder(_ shouldConnectToBuilder: @escaping (String, String) async -> Bool) { self.shouldConnectToBuilder = shouldConnectToBuilder } @@ -26,7 +28,7 @@ final actor Proxy { case fileAlreadyExists(String) } - init(hostName: String = ProcessInfo().hostName, bundleID: String = Env.shared.CFBundleIdentifier!, processID: Int32 = ProcessInfo().processIdentifier, builderParams: Builder.InputParameters, shouldConnectToBuilder: @escaping (_ title: String, _ message: String) async -> Bool) { + init(hostName: String = ProcessInfo().hostName, bundleID: String = Env.host.CFBundleIdentifier!, processID: Int32 = ProcessInfo().processIdentifier, builderParams: Builder.InputParameters, shouldConnectToBuilder: @Sendable @escaping (_ title: String, _ message: String) async -> Bool) { self.builderParams = builderParams self.shouldConnectToBuilder = shouldConnectToBuilder // the doc: The display name is intended for use in UI elements, and should be short and descriptive of the local peer. The maximum allowable length is 63 bytes in UTF-8 encoding. The displayName parameter may not be nil or an empty string. @@ -54,18 +56,23 @@ final actor Proxy { session = nil } + + func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws { + try session?.send(data, toPeers: peerIDs, with: mode) + } + // MARK: - MCNearbyServiceAdvertiserDelegate - private final class AdvertiserDelegate: NSObject, MCNearbyServiceAdvertiserDelegate { - unowned var proxy: Proxy? + private final class AdvertiserDelegate: NSObject, MCNearbyServiceAdvertiserDelegate, Sendable { + unowned nonisolated(unsafe) var proxy: Proxy? override init() { super.init() } - func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { + func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @Sendable @escaping (Bool, MCSession?) -> Void) { NSLog("%@", "🍓 \(#function) advertiser = \(advertiser), peerID = \(peerID), context = \(context?.count ?? 0) bytes") - Task { await proxy?.advertiser(advertiser, didReceiveInvitationFromPeer: peerID, withContext: context, invitationHandler: invitationHandler) } + Task { @Sendable in await proxy?.advertiser(advertiser, didReceiveInvitationFromPeer: peerID, withContext: context, invitationHandler: invitationHandler) } } func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Swift.Error) { NSLog("%@", "🍓 \(#function) advertiser = \(advertiser), error = \(error)") - Task { await proxy?.advertiser(advertiser, didNotStartAdvertisingPeer: error) } + Task { @Sendable in await proxy?.advertiser(advertiser, didNotStartAdvertisingPeer: error) } } } @@ -91,13 +98,13 @@ final actor Proxy { // MARK: - MCSessionDelegate - private final class SessionDelegate: NSObject, MCSessionDelegate { - unowned var proxy: Proxy? + private final class SessionDelegate: NSObject, MCSessionDelegate, Sendable { + unowned nonisolated(unsafe) var proxy: Proxy? override init() { super.init() } func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { NSLog("%@", "🍓 \(#function) peerID = \(peerID), state = \(state)") - Task { await proxy?.session(session, peer: peerID, didChange: state) } + Task { @Sendable in await self.proxy?.session(session, peer: peerID, didChange: state) } } func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { @@ -114,7 +121,7 @@ final actor Proxy { func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Swift.Error?) { NSLog("%@", "🍓 \(#function) resourceName = \(resourceName), peerID = \(peerID), localURL = \(String(describing: localURL)), error = \(String(describing: error))") - Task { await proxy?.session(session, didFinishReceivingResourceWithName: resourceName, fromPeer: peerID, at: localURL, withError: error) } + Task { @Sendable in await proxy?.session(session, didFinishReceivingResourceWithName: resourceName, fromPeer: peerID, at: localURL, withError: error) } } } @@ -124,11 +131,11 @@ final actor Proxy { self.session = nil case .connecting: break case .connected: - Task.detached { @MainActor in + Task.detached { @Sendable in do { NSLog("%@", "🍓 \(#function) connected: sending builderParams = \(self.builderParams)") let payload = try JSONEncoder().encode(self.builderParams) - try await self.session?.send(payload, toPeers: [peerID], with: .reliable) + try await self.send(payload, toPeers: [peerID], with: .reliable) } catch { NSLog("%@", "🍓 \(#function) error = \(error)") } @@ -161,7 +168,10 @@ final actor Proxy { Task { do { try await loader.load(dylibPath: tmpDylibPath) - receivedDylibFiles.append(tmpDylibPath) + let urls: [URL] = receivedDylibFilesSubject.value + [tmpDylibPath] + Task { @MainActor in + await receivedDylibFilesSubject.send(urls) + } } catch { NSLog("%@", "🍓 \(#function) line \(#line) error = \(error)") } diff --git a/Sources/ProxyReloader/ProxyReloader.swift b/Sources/ProxyReloader/ProxyReloader.swift index c972ae1..72305b7 100644 --- a/Sources/ProxyReloader/ProxyReloader.swift +++ b/Sources/ProxyReloader/ProxyReloader.swift @@ -1,18 +1,19 @@ #if DEBUG || os(macOS) import Foundation import MultipeerConnectivity +@preconcurrency import Combine +@MainActor public final class ProxyReloader: ObservableObject { private let proxy: Proxy @Published public private(set) var dateReloaded: Date? public init(_ builderParams: Builder.InputParameters) { - print(Env.shared) + print(Env.host) self.proxy = Proxy(builderParams: builderParams, shouldConnectToBuilder: UserConsent.alert) - Task { - await proxy.$receivedDylibFiles.map {_ in Date() }.receive(on: DispatchQueue.main).assign(to: &$dateReloaded) + await proxy.receivedDylibFiles.map {_ in Date()}.receive(on: DispatchQueue.main).assign(to: &self.$dateReloaded) await proxy.start() } } diff --git a/Sources/StandaloneReloader/StandaloneReloader.swift b/Sources/StandaloneReloader/StandaloneReloader.swift index 626e379..8f8c687 100644 --- a/Sources/StandaloneReloader/StandaloneReloader.swift +++ b/Sources/StandaloneReloader/StandaloneReloader.swift @@ -1,7 +1,8 @@ #if DEBUG || os(macOS) import Foundation -import Combine +@preconcurrency import Combine +@MainActor public final class StandaloneReloader: ObservableObject { private let fileMonitor: FileMonitor private let core: Core? @@ -9,7 +10,7 @@ public final class StandaloneReloader: ObservableObject { @Published public private(set) var dateReloaded: Date? private var cancellables: Set = [] - public init(monitoredSwiftFile: URL, env: Env = .shared, derivedData: URL? = nil, confBuildDirAppRandomString: String? = nil, mainModule: String? = nil, modules: [String] = [], configurationPlatform: String? = nil, arch: String? = nil, targetTriple: String? = nil, sdk: URL? = nil, platformName: String? = nil) { + @MainActor public init(monitoredSwiftFile: URL, env: Env = .host, derivedData: URL? = nil, confBuildDirAppRandomString: String? = nil, mainModule: String? = nil, modules: [String] = [], configurationPlatform: String? = nil, arch: String? = nil, targetTriple: String? = nil, sdk: URL? = nil, platformName: String? = nil) { if env.DTPlatformName == "iphoneos" { NSLog("%@", "🍓 ⚠️ To do hot reloads standalone, the process host should be able to execute swiftc. ⚠️") } @@ -24,7 +25,7 @@ public final class StandaloneReloader: ObservableObject { } Task { - await fileMonitor.$fileChanges.compactMap {$0}.sink { [weak self] _ in + await fileMonitor.fileChanges.compactMap {$0}.sink { [weak self] _ in self?.reload() }.store(in: &cancellables) } diff --git a/SwiftHotReload.podspec b/SwiftHotReload.podspec index 736db75..28ad705 100644 --- a/SwiftHotReload.podspec +++ b/SwiftHotReload.podspec @@ -15,11 +15,11 @@ Pod::Spec.new do |spec| spec.visionos.deployment_target = "1.0" spec.source = { :git => "https://github.com/banjun/SwiftHotReload.git", :tag => "#{spec.version}" } spec.source_files = "Sources/**/*.swift" - spec.swift_version = "5.1" + spec.swift_version = "6.0" - spec.ios.deployment_target = "14.0" - spec.osx.deployment_target = "11.0" + spec.ios.deployment_target = "16.0" + spec.osx.deployment_target = "13.0" # spec.watchos.deployment_target = "2.0" # spec.tvos.deployment_target = "9.0" - # spec.visionos.deployment_target = "1.0" + spec.visionos.deployment_target = "1.0" end