diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 01529bb..6e42c59 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,19 +8,26 @@ on:
jobs:
build:
+ strategy:
+ matrix:
+ configuration: ['debug', 'release']
runs-on: macOS-13
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '^15.0.1'
- - uses: actions/checkout@v2
- - run: swift build
+ - uses: actions/checkout@v4
+ - run: swift build -c ${{ matrix.configuration }} -Xswiftc -enable-testing
podspec:
+ strategy:
+ matrix:
+ configuration: ['Debug', 'Release']
+ platform: ['ios', 'macos']
runs-on: macOS-13
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '^15.0.1'
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- run: bundle install
- - run: bundle exec pod lib lint
+ - run: bundle exec pod lib lint --platforms=${{ matrix.platform }} --configuration=${{ matrix.configuration }}
diff --git a/BuildHelper/Assets.xcassets/AccentColor.colorset/Contents.json b/BuildHelper/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/BuildHelper/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/BuildHelper/Assets.xcassets/AppIcon.appiconset/Contents.json b/BuildHelper/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..3f00db4
--- /dev/null
+++ b/BuildHelper/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "images" : [
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/BuildHelper/Assets.xcassets/Contents.json b/BuildHelper/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/BuildHelper/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/BuildHelper/BuildHelper.entitlements b/BuildHelper/BuildHelper.entitlements
new file mode 100644
index 0000000..997a18c
--- /dev/null
+++ b/BuildHelper/BuildHelper.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.files.user-selected.read-only
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+
+
diff --git a/BuildHelper/BuildHelper.xcodeproj/project.pbxproj b/BuildHelper/BuildHelper.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..2fab050
--- /dev/null
+++ b/BuildHelper/BuildHelper.xcodeproj/project.pbxproj
@@ -0,0 +1,384 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 63599A3E2B03A121009186F4 /* BuildHelperApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63599A3B2B03A121009186F4 /* BuildHelperApp.swift */; };
+ 63599A3F2B03A121009186F4 /* ProxyBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63599A3C2B03A121009186F4 /* ProxyBrowser.swift */; };
+ 63599A402B03A121009186F4 /* BuildHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63599A3D2B03A121009186F4 /* BuildHelper.swift */; };
+ 638083822B04D5FC00A39A64 /* MCBrowserViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638083812B04D5FC00A39A64 /* MCBrowserViewControllerView.swift */; };
+ 63A7469E2AFDD748003FA3AC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63A7469D2AFDD748003FA3AC /* Assets.xcassets */; };
+ 63A746A12AFDD748003FA3AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63A746A02AFDD748003FA3AC /* Preview Assets.xcassets */; };
+ 63A746B12AFDD8F4003FA3AC /* SwiftHotReload.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63A746B02AFDD8F4003FA3AC /* SwiftHotReload.framework */; };
+ 63A746B22AFDD8F4003FA3AC /* SwiftHotReload.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 63A746B02AFDD8F4003FA3AC /* SwiftHotReload.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 63A746B32AFDD8F4003FA3AC /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 63A746B22AFDD8F4003FA3AC /* SwiftHotReload.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 63599A3B2B03A121009186F4 /* BuildHelperApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildHelperApp.swift; sourceTree = ""; };
+ 63599A3C2B03A121009186F4 /* ProxyBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyBrowser.swift; sourceTree = ""; };
+ 63599A3D2B03A121009186F4 /* BuildHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildHelper.swift; sourceTree = ""; };
+ 638083812B04D5FC00A39A64 /* MCBrowserViewControllerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCBrowserViewControllerView.swift; sourceTree = ""; };
+ 63A746962AFDD742003FA3AC /* BuildHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BuildHelper.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 63A7469D2AFDD748003FA3AC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 63A746A02AFDD748003FA3AC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 63A746A22AFDD748003FA3AC /* BuildHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BuildHelper.entitlements; sourceTree = ""; };
+ 63A746AE2AFDD7DC003FA3AC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 63A746B02AFDD8F4003FA3AC /* SwiftHotReload.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftHotReload.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 63A746932AFDD742003FA3AC /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 63A746B12AFDD8F4003FA3AC /* SwiftHotReload.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 63599A3A2B03A121009186F4 /* Sources */ = {
+ isa = PBXGroup;
+ children = (
+ 63599A3B2B03A121009186F4 /* BuildHelperApp.swift */,
+ 63599A3C2B03A121009186F4 /* ProxyBrowser.swift */,
+ 638083812B04D5FC00A39A64 /* MCBrowserViewControllerView.swift */,
+ 63599A3D2B03A121009186F4 /* BuildHelper.swift */,
+ );
+ path = Sources;
+ sourceTree = "";
+ };
+ 63A7468D2AFDD742003FA3AC = {
+ isa = PBXGroup;
+ children = (
+ 63A746AE2AFDD7DC003FA3AC /* Info.plist */,
+ 63599A3A2B03A121009186F4 /* Sources */,
+ 63A7469D2AFDD748003FA3AC /* Assets.xcassets */,
+ 63A746A22AFDD748003FA3AC /* BuildHelper.entitlements */,
+ 63A7469F2AFDD748003FA3AC /* Preview Content */,
+ 63A746972AFDD742003FA3AC /* Products */,
+ 63A746AF2AFDD8F4003FA3AC /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 63A746972AFDD742003FA3AC /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 63A746962AFDD742003FA3AC /* BuildHelper.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 63A7469F2AFDD748003FA3AC /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 63A746A02AFDD748003FA3AC /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 63A746AF2AFDD8F4003FA3AC /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 63A746B02AFDD8F4003FA3AC /* SwiftHotReload.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 63A746952AFDD742003FA3AC /* BuildHelper */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 63A746A52AFDD748003FA3AC /* Build configuration list for PBXNativeTarget "BuildHelper" */;
+ buildPhases = (
+ 63A746922AFDD742003FA3AC /* Sources */,
+ 63A746932AFDD742003FA3AC /* Frameworks */,
+ 63A746942AFDD742003FA3AC /* Resources */,
+ 63A746B32AFDD8F4003FA3AC /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = BuildHelper;
+ productName = BuildHelper;
+ productReference = 63A746962AFDD742003FA3AC /* BuildHelper.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 63A7468E2AFDD742003FA3AC /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ 63A746952AFDD742003FA3AC = {
+ CreatedOnToolsVersion = 15.0.1;
+ };
+ };
+ };
+ buildConfigurationList = 63A746912AFDD742003FA3AC /* Build configuration list for PBXProject "BuildHelper" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 63A7468D2AFDD742003FA3AC;
+ productRefGroup = 63A746972AFDD742003FA3AC /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 63A746952AFDD742003FA3AC /* BuildHelper */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 63A746942AFDD742003FA3AC /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 63A746A12AFDD748003FA3AC /* Preview Assets.xcassets in Resources */,
+ 63A7469E2AFDD748003FA3AC /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 63A746922AFDD742003FA3AC /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 63599A3E2B03A121009186F4 /* BuildHelperApp.swift in Sources */,
+ 63599A402B03A121009186F4 /* BuildHelper.swift in Sources */,
+ 638083822B04D5FC00A39A64 /* MCBrowserViewControllerView.swift in Sources */,
+ 63599A3F2B03A121009186F4 /* ProxyBrowser.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 63A746A32AFDD748003FA3AC /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 63A746A42AFDD748003FA3AC /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 13.5;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ };
+ name = Release;
+ };
+ 63A746A62AFDD748003FA3AC /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = BuildHelper.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Info.plist;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Build and send executables for nearby devices";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = jp.banjun.SwiftHotReload.BuildHelper;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 63A746A72AFDD748003FA3AC /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = BuildHelper.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Info.plist;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Build and send executables for nearby devices";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = jp.banjun.SwiftHotReload.BuildHelper;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 63A746912AFDD742003FA3AC /* Build configuration list for PBXProject "BuildHelper" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 63A746A32AFDD748003FA3AC /* Debug */,
+ 63A746A42AFDD748003FA3AC /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 63A746A52AFDD748003FA3AC /* Build configuration list for PBXNativeTarget "BuildHelper" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 63A746A62AFDD748003FA3AC /* Debug */,
+ 63A746A72AFDD748003FA3AC /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 63A7468E2AFDD742003FA3AC /* Project object */;
+}
diff --git a/BuildHelper/Info.plist b/BuildHelper/Info.plist
new file mode 100644
index 0000000..4ada98b
--- /dev/null
+++ b/BuildHelper/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSBonjourServices
+
+ _swifthotreload._tcp
+ _swifthotreload._udp
+
+
+
diff --git a/BuildHelper/Preview Content/Preview Assets.xcassets/Contents.json b/BuildHelper/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/BuildHelper/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/BuildHelper/Sources/BuildHelper.swift b/BuildHelper/Sources/BuildHelper.swift
new file mode 100644
index 0000000..1e6a0b8
--- /dev/null
+++ b/BuildHelper/Sources/BuildHelper.swift
@@ -0,0 +1,96 @@
+#if os(macOS)
+@testable import SwiftHotReload // NOTE: use internal methods. SPM does not allow overlapping sources for a single Package.swift
+// NOTE: should not be submitted for App Store Review
+// 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
+
+public final class BuildHelper: ObservableObject {
+ let proxyBrowser = ProxyBrowser()
+
+ @Published public private(set) var monitoredFile: URL? {
+ didSet {
+ fileMonitor = monitoredFile.map { FileMonitor(file: $0) }
+ }
+ }
+ private var fileMonitor: FileMonitor? {
+ didSet {
+ Task {
+ fileMonitorCancellable = await fileMonitor?.$fileChanges.compactMap {$0}.sink { [weak self] _ in
+ self?.reload()
+ }
+ }
+ }
+ }
+ private var fileMonitorCancellable: AnyCancellable?
+ private let core = Core()
+
+ @Published public private(set) var dateReloaded: Date?
+ private var cancellables: Set = []
+
+ public init() {
+ Task { @MainActor in
+ await proxyBrowser.$runtimePeers.receive(on: DispatchQueue.main).sink { [weak self] runtimePeers in
+ guard let self else { return }
+ // TODO: support multiple peers
+ let runtimePeer = runtimePeers.first
+ NSLog("%@", "🍓 TODO: support multiple peers: \(runtimePeers.count) peers connected. currently using only first peer \(String(describing: runtimePeer))")
+ monitoredFile = runtimePeer?.builderParams?.targetSwiftFile
+ Task { await self.core.setRuntimePeer(runtimePeer) }
+ }.store(in: &cancellables)
+ }
+ }
+
+ private actor Core {
+ private var counter: Int = 0
+
+ private var builder: Builder?
+ private var runtimePeer: RuntimePeer? {
+ didSet {
+ self.builder = runtimePeer?.builderParams.map(Builder.init)
+ }
+ }
+
+ enum Error: Swift.Error {
+ case builderUninitialized
+ }
+
+ init() {}
+
+ func setRuntimePeer(_ runtimePeer: RuntimePeer?) {
+ var runtimePeer = runtimePeer
+ if let p = runtimePeer?.builderParams {
+ let identity = p.env.estimatedProductBundlePath.filter { FileManager.default.fileExists(atPath: $0.path) }.lazy.compactMap {
+ let stderr = try? NSTaskCommand(launchPath: "/usr/bin/codesign", args: ["-dvvvvv", $0.path]).run().stderr
+ // extract `Apple Development: xxxxxx@xxxxxx (XXXXXXXXXX)`
+ return stderr?.components(separatedBy: "\n").first { $0.hasPrefix("Authority=") }?.split(separator: "=", maxSplits: 2).last
+ }.map(String.init).first
+ runtimePeer?.builderParams?.codesignIdentity = identity
+ }
+ self.runtimePeer = runtimePeer
+ }
+
+ func reload() async throws {
+ guard let builder else { throw Error.builderUninitialized }
+ counter += 1
+
+ let dylibPath = try await builder.build(dylibFilename: "HotReload\(counter).dylib")
+ guard let session = runtimePeer?.session, let server = runtimePeer?.peerID else { return }
+ try await withCheckedThrowingContinuation { (c: CheckedContinuation) in
+ session.sendResource(at: dylibPath, withName: dylibPath.lastPathComponent, toPeer: server) { error in
+ if let error { c.resume(throwing: error) }
+ else { c.resume() }
+ }
+ }
+ }
+ }
+
+ public func reload() {
+ Task { @MainActor in
+ try await core.reload()
+ dateReloaded = Date()
+ }
+ }
+}
+#endif
diff --git a/BuildHelper/Sources/BuildHelperApp.swift b/BuildHelper/Sources/BuildHelperApp.swift
new file mode 100644
index 0000000..6ca346a
--- /dev/null
+++ b/BuildHelper/Sources/BuildHelperApp.swift
@@ -0,0 +1,47 @@
+#if os(macOS)
+import SwiftUI
+
+/// to use: `swift run BuildHelper`
+/// to debug: Build & Run BuildHelper target on SwiftHotReload.xcworkspace
+/// NOTE: when run as an app, the app sandbox should be disabled to:
+/// - monitor any file changes (to trigger build a swift file)
+/// - run swiftc
+@main
+struct BuildHelperApp: SwiftUI.App {
+ @ObservedObject private(set) var buildHelper = BuildHelper()
+
+ var body: some Scene {
+ Window("BuildHelper (\(String(ProcessInfo().processIdentifier)))", id: "Main") {
+ ContentView()
+ .environmentObject(buildHelper)
+ }
+ MenuBarExtra("BuildHelper", systemImage: "hammer.circle.fill") {
+ Button("Show All") {
+ // needs workaround: not works nicely when launched via `swift run BuildHelper`
+ NSApp.unhide(nil)
+ }
+ Divider()
+ Button("Quit") {
+ NSApp.terminate(nil)
+ }
+ }
+ }
+
+ struct ContentView: View {
+ @EnvironmentObject var buildHelper: BuildHelper
+
+ var body: some View {
+ VStack(spacing: 20) {
+ Text("Monitored File" + "\n" + (buildHelper.monitoredFile?.path ?? "Nothing"))
+ .multilineTextAlignment(.center)
+
+ Text("Date Reloaded" + "\n" + (buildHelper.dateReloaded?.formatted(date: .numeric, time: .complete) ?? "Never"))
+ .multilineTextAlignment(.center)
+
+ buildHelper.proxyBrowser.browserView
+ }
+ .padding()
+ }
+ }
+}
+#endif
diff --git a/BuildHelper/Sources/MCBrowserViewControllerView.swift b/BuildHelper/Sources/MCBrowserViewControllerView.swift
new file mode 100644
index 0000000..1662d34
--- /dev/null
+++ b/BuildHelper/Sources/MCBrowserViewControllerView.swift
@@ -0,0 +1,44 @@
+#if os(macOS)
+import Foundation
+import MultipeerConnectivity
+import SwiftUI
+@testable import SwiftHotReload
+
+struct MCBrowserViewControllerView: NSViewControllerRepresentable {
+ var browser: MCNearbyServiceBrowser
+ var session: MCSession
+
+ func makeNSViewController(context: Context) -> MCBrowserViewController {
+ let vc = MCBrowserViewController(browser: browser, session: session)
+ vc.delegate = context.coordinator
+ vc.maximumNumberOfPeers = 1
+ return vc
+ }
+
+ func makeCoordinator() -> Coordinator {
+ .init()
+ }
+
+ final class Coordinator: NSObject, MCBrowserViewControllerDelegate {
+ func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
+ NSLog("%@", "🍓 \(#function) Done pressed. ignored. continue searching...")
+ }
+ func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
+ NSLog("%@", "🍓 \(#function) Cancel pressed. ignored. continue searching...")
+ }
+
+ func browserViewController(_ browserViewController: MCBrowserViewController, shouldPresentNearbyPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) -> Bool {
+ NSLog("%@", "🍓 \(#function) peerID = \(peerID), info = \(String(describing: info))")
+ guard info == MultipeerConnectivityConstants.serverDiscoveryInfo else {
+ NSLog("%@", "🍓 \(#function) ignore peer \(peerID) as it's not a server")
+ return false
+ }
+ return true
+ }
+ }
+
+ func updateNSViewController(_ vc: MCBrowserViewController, context: Context) {
+ vc.delegate = context.coordinator
+ }
+}
+#endif
diff --git a/BuildHelper/Sources/ProxyBrowser.swift b/BuildHelper/Sources/ProxyBrowser.swift
new file mode 100644
index 0000000..60d1994
--- /dev/null
+++ b/BuildHelper/Sources/ProxyBrowser.swift
@@ -0,0 +1,97 @@
+#if os(macOS)
+// NOTE: should not be submitted for App Store Review
+// 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
+@testable import SwiftHotReload // NOTE: use internal methods. SPM does not allow overlapping sources for a single Package.swift
+
+final actor ProxyBrowser {
+ @Published private(set) var runtimePeers: [RuntimePeer] = []
+ private let peerID: MCPeerID
+ private let session: MCSession
+ private let browser: MCNearbyServiceBrowser
+ private let sessionDelegate: SessionDelegate = .init()
+ let browserView: MCBrowserViewControllerView
+
+ init(hostName: String = ProcessInfo().hostName, bundleID: String? = Env.shared.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)
+
+// Task {
+ session.delegate = sessionDelegate
+ sessionDelegate.owner = self
+// }
+ }
+
+ func start() {
+ NSLog("%@", "🍓 ProxyBrowser.\(#function)")
+ browser.startBrowsingForPeers()
+ }
+
+ func stop() {
+ NSLog("%@", "🍓 ProxyBrowser.\(#function)")
+ browser.stopBrowsingForPeers()
+ }
+
+ // MARK: - MCSessionDelegate
+
+ func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
+ switch state {
+ case .notConnected:
+ NSLog("%@", "🍓 \(#function) .notConnected: peerID = \(peerID)")
+ runtimePeers = runtimePeers.filter { $0.peerID != peerID }
+ case .connecting:
+ NSLog("%@", "🍓 \(#function) .connecting: peerID = \(peerID)")
+ case .connected:
+ NSLog("%@", "🍓 \(#function) .connected: peerID = \(peerID)")
+ // NOTE: it is a good idea doing some auth to refrain from sending secret dylib to the unidentified server
+ runtimePeers.append(.init(session: session, peerID: peerID, builderParams: nil))
+ @unknown default:
+ NSLog("%@", "🍓 \(#function) @unknown default: peerID = \(peerID)")
+ }
+ }
+
+ func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
+ NSLog("%@", "🍓 \(#function) data = \(data.count) bytes, peerID = \(peerID)")
+ do {
+ let builderParams = try JSONDecoder().decode(Builder.InputParameters.self, from: data)
+ NSLog("%@", "🍓 \(#function) using received build parameters when build for the session: \(builderParams)")
+ guard let index = (runtimePeers.firstIndex { $0.peerID == peerID }) else { return }
+ runtimePeers[index].builderParams = builderParams
+ } catch {
+ NSLog("%@", "🍓 \(#function) error = \(error)")
+ }
+ }
+}
+
+private extension ProxyBrowser {
+ private final class SessionDelegate: NSObject, MCSessionDelegate {
+ unowned 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) }
+ }
+
+ func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
+ Task { await owner?.session(session, didReceive: data, fromPeer: peerID) }
+ }
+
+ func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
+ NSLog("%@", "🍓 \(#function) stream = \(stream), streamName = \(streamName), peerID = \(peerID)")
+ }
+
+ func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
+ NSLog("%@", "🍓 \(#function) resourceName = \(resourceName), peerID = \(peerID), progress = \(progress)")
+ }
+
+ 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))")
+ }
+ }
+}
+#endif
diff --git a/Example/App.swift b/Example/App.swift
index dd5d474..6052a73 100644
--- a/Example/App.swift
+++ b/Example/App.swift
@@ -1,10 +1,3 @@
-//
-// SwiftHotReloadExampleApp.swift
-// SwiftHotReloadExample
-//
-// Created by BAN Jun on 2023/10/26.
-//
-
import SwiftUI
import SwiftHotReload
@@ -12,15 +5,28 @@ import SwiftHotReload
@main
struct App: SwiftUI.App {
-
#if DEBUG
- // see also ReplaceView.swift
- static let reloader = StandaloneReloader(monitoredSwiftFile: Env.shared.estimatedHomeDir!
- .appendingPathComponent("projects/github/SwiftHotReload")
- .appendingPathComponent("Example/ReplaceView.swift")
+ // For Simulators and macOS apps:
+ // just use StandaloneReloader
+ //
+ // For iPhone devices:
+ // use ProxyReloader while running BuildHelper.app on the host Mac
+ //
+ // See also `ReplaceView.swift`
+ //
+ // ↓ Change true/false to switch StandaloneReloader or ProxyReloader
+#if true
+ // StandaloneReloader
+ static let reloader = StandaloneReloader(monitoredSwiftFile: URL(fileURLWithPath: #filePath).deletingLastPathComponent()
+ .appendingPathComponent("ReplaceView.swift")
)
+#else
+ // ProxyReloader
+ static let reloader = ProxyReloader(.init(targetSwiftFile: URL(fileURLWithPath: #filePath).deletingLastPathComponent()
+ .appendingPathComponent("ReplaceView.swift")
+ ))
+#endif
#endif
-
var body: some Scene {
WindowGroup {
ContentView()
diff --git a/Example/ContentView.swift b/Example/ContentView.swift
index bd843ad..e5d2f93 100644
--- a/Example/ContentView.swift
+++ b/Example/ContentView.swift
@@ -22,7 +22,7 @@ struct ContentView: View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
- .foregroundStyle(.tint)
+// .foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
diff --git a/Example/SwiftHotReloadExample-Info.plist b/Example/SwiftHotReloadExample-Info.plist
new file mode 100644
index 0000000..4ada98b
--- /dev/null
+++ b/Example/SwiftHotReloadExample-Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSBonjourServices
+
+ _swifthotreload._tcp
+ _swifthotreload._udp
+
+
+
diff --git a/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj b/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj
index c5fbc62..e546dca 100644
--- a/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj
+++ b/Example/SwiftHotReloadExample.xcodeproj/project.pbxproj
@@ -30,6 +30,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 6355B1112AFB8BE9008C4C50 /* SwiftHotReloadExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "SwiftHotReloadExample-Info.plist"; sourceTree = ""; };
63980F432AEA8DA50099B122 /* SwiftHotReloadExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftHotReloadExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
63980F462AEA8DA50099B122 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
63980F482AEA8DA50099B122 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
@@ -55,6 +56,7 @@
63980F3A2AEA8DA50099B122 = {
isa = PBXGroup;
children = (
+ 6355B1112AFB8BE9008C4C50 /* SwiftHotReloadExample-Info.plist */,
63980F462AEA8DA50099B122 /* App.swift */,
63980F482AEA8DA50099B122 /* ContentView.swift */,
63980F872AEA95F10099B122 /* ReplaceView.swift */,
@@ -296,8 +298,11 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
+ DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "SwiftHotReloadExample-Info.plist";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "SwiftHotReload Proxy via MultipeerConnectivity";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -308,7 +313,7 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.5;
@@ -333,8 +338,11 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
+ DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "SwiftHotReloadExample-Info.plist";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "SwiftHotReload Proxy via MultipeerConnectivity";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -345,7 +353,7 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.5;
diff --git a/FrameworkTarget/SwiftHotReload.xcodeproj/project.pbxproj b/FrameworkTarget/SwiftHotReload.xcodeproj/project.pbxproj
index cced618..4bc45e9 100644
--- a/FrameworkTarget/SwiftHotReload.xcodeproj/project.pbxproj
+++ b/FrameworkTarget/SwiftHotReload.xcodeproj/project.pbxproj
@@ -9,10 +9,16 @@
/* Begin PBXBuildFile section */
630C245E2AEBD4E10012C490 /* TargetSwiftFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630C245B2AEBD4E10012C490 /* TargetSwiftFile.swift */; };
630C245F2AEBD4E10012C490 /* Env.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630C245C2AEBD4E10012C490 /* Env.swift */; };
+ 6323BB722AFCC1DA005E80DF /* NSTaskCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6323BB712AFCC1DA005E80DF /* NSTaskCommand.swift */; };
63458ABD2AFA622E001A5630 /* StandaloneReloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63458ABC2AFA622E001A5630 /* StandaloneReloader.swift */; };
63458ABF2AFA6232001A5630 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63458ABE2AFA6232001A5630 /* Loader.swift */; };
63458AC32AFA6247001A5630 /* Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63458AC22AFA6247001A5630 /* Builder.swift */; };
63458AC42AFA6247001A5630 /* FileMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63458AC12AFA6247001A5630 /* FileMonitor.swift */; };
+ 6355B0F52AFB6899008C4C50 /* ProxyReloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6355B0F42AFB6899008C4C50 /* ProxyReloader.swift */; };
+ 63A746BA2AFE0286003FA3AC /* RuntimePeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A746B92AFE0286003FA3AC /* RuntimePeer.swift */; };
+ 63A746BC2AFE02EA003FA3AC /* Proxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A746BB2AFE02EA003FA3AC /* Proxy.swift */; };
+ 63A746C12AFE0700003FA3AC /* MultipeerConnectivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A746C02AFE0700003FA3AC /* MultipeerConnectivityConstants.swift */; };
+ 63FE42F92B107425000A950E /* UserConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FE42F82B107425000A950E /* UserConsent.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -20,11 +26,17 @@
630C24522AEBD4780012C490 /* SwiftHotReloadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftHotReloadTests.swift; sourceTree = ""; };
630C245B2AEBD4E10012C490 /* TargetSwiftFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TargetSwiftFile.swift; sourceTree = ""; };
630C245C2AEBD4E10012C490 /* Env.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Env.swift; sourceTree = ""; };
+ 6323BB712AFCC1DA005E80DF /* NSTaskCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSTaskCommand.swift; sourceTree = ""; };
63458ABC2AFA622E001A5630 /* StandaloneReloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandaloneReloader.swift; sourceTree = ""; };
63458ABE2AFA6232001A5630 /* Loader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; };
63458AC12AFA6247001A5630 /* FileMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMonitor.swift; sourceTree = ""; };
63458AC22AFA6247001A5630 /* Builder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Builder.swift; sourceTree = ""; };
+ 6355B0F42AFB6899008C4C50 /* ProxyReloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyReloader.swift; sourceTree = ""; };
63980F712AEA93310099B122 /* SwiftHotReload.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftHotReload.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 63A746B92AFE0286003FA3AC /* RuntimePeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimePeer.swift; sourceTree = ""; };
+ 63A746BB2AFE02EA003FA3AC /* Proxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Proxy.swift; sourceTree = ""; };
+ 63A746C02AFE0700003FA3AC /* MultipeerConnectivityConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipeerConnectivityConstants.swift; sourceTree = ""; };
+ 63FE42F82B107425000A950E /* UserConsent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConsent.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -41,12 +53,9 @@
630C244A2AEBD45B0012C490 /* Sources */ = {
isa = PBXGroup;
children = (
- 630C245C2AEBD4E10012C490 /* Env.swift */,
- 63458ABC2AFA622E001A5630 /* StandaloneReloader.swift */,
- 63458AC12AFA6247001A5630 /* FileMonitor.swift */,
- 63458AC22AFA6247001A5630 /* Builder.swift */,
- 63458ABE2AFA6232001A5630 /* Loader.swift */,
- 630C245B2AEBD4E10012C490 /* TargetSwiftFile.swift */,
+ 63A746BD2AFE031C003FA3AC /* Core */,
+ 63A746BF2AFE034F003FA3AC /* StandaloneReloader */,
+ 63A746BE2AFE033F003FA3AC /* ProxyReloader */,
);
name = Sources;
path = ../Sources;
@@ -79,6 +88,39 @@
name = Products;
sourceTree = "";
};
+ 63A746BD2AFE031C003FA3AC /* Core */ = {
+ isa = PBXGroup;
+ children = (
+ 630C245C2AEBD4E10012C490 /* Env.swift */,
+ 63458AC12AFA6247001A5630 /* FileMonitor.swift */,
+ 63458AC22AFA6247001A5630 /* Builder.swift */,
+ 63458ABE2AFA6232001A5630 /* Loader.swift */,
+ 630C245B2AEBD4E10012C490 /* TargetSwiftFile.swift */,
+ 6323BB712AFCC1DA005E80DF /* NSTaskCommand.swift */,
+ );
+ path = Core;
+ sourceTree = "";
+ };
+ 63A746BE2AFE033F003FA3AC /* ProxyReloader */ = {
+ isa = PBXGroup;
+ children = (
+ 6355B0F42AFB6899008C4C50 /* ProxyReloader.swift */,
+ 63FE42F82B107425000A950E /* UserConsent.swift */,
+ 63A746C02AFE0700003FA3AC /* MultipeerConnectivityConstants.swift */,
+ 63A746BB2AFE02EA003FA3AC /* Proxy.swift */,
+ 63A746B92AFE0286003FA3AC /* RuntimePeer.swift */,
+ );
+ path = ProxyReloader;
+ sourceTree = "";
+ };
+ 63A746BF2AFE034F003FA3AC /* StandaloneReloader */ = {
+ isa = PBXGroup;
+ children = (
+ 63458ABC2AFA622E001A5630 /* StandaloneReloader.swift */,
+ );
+ path = StandaloneReloader;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -160,9 +202,15 @@
buildActionMask = 2147483647;
files = (
630C245E2AEBD4E10012C490 /* TargetSwiftFile.swift in Sources */,
+ 6323BB722AFCC1DA005E80DF /* NSTaskCommand.swift in Sources */,
63458ABD2AFA622E001A5630 /* StandaloneReloader.swift in Sources */,
63458AC42AFA6247001A5630 /* FileMonitor.swift in Sources */,
+ 63A746BC2AFE02EA003FA3AC /* Proxy.swift in Sources */,
+ 63A746BA2AFE0286003FA3AC /* RuntimePeer.swift in Sources */,
+ 6355B0F52AFB6899008C4C50 /* ProxyReloader.swift in Sources */,
+ 63A746C12AFE0700003FA3AC /* MultipeerConnectivityConstants.swift in Sources */,
630C245F2AEBD4E10012C490 /* Env.swift in Sources */,
+ 63FE42F92B107425000A950E /* UserConsent.swift in Sources */,
63458ABF2AFA6232001A5630 /* Loader.swift in Sources */,
63458AC32AFA6247001A5630 /* Builder.swift in Sources */,
);
@@ -305,7 +353,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"@executable_path/Frameworks",
"@loader_path/Frameworks",
@@ -345,7 +393,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"@executable_path/Frameworks",
"@loader_path/Frameworks",
diff --git a/Package.swift b/Package.swift
index 74e81e0..8a4a702 100644
--- a/Package.swift
+++ b/Package.swift
@@ -10,7 +10,16 @@ let package = Package(
.library(name: "SwiftHotReload", targets: ["SwiftHotReload"]),
],
targets: [
- .target(name: "SwiftHotReload"),
+ .target(
+ name: "SwiftHotReload",
+ path: "Sources",
+ swiftSettings: [.define("DEBUG", .when(configuration: .debug))]
+ ),
+ .executableTarget(
+ name: "BuildHelper",
+ dependencies: ["SwiftHotReload"],
+ path: "BuildHelper/Sources"
+ ),
.testTarget(name: "SwiftHotReloadTests", dependencies: ["SwiftHotReload"]),
]
)
diff --git a/README.md b/README.md
index abc6645..fb69fe6 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,17 @@ SwiftHotReload is an experimental project. We investigate a real world applicati
* Xcode 15.x
* Host macOS 13.x, 14.x
-* Runtime macOS app
-* Runtime simulators for iOS, iPadOS, and possibly visionOS
+
+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.
+
+| Runtime Target App | Standalone | Proxy & BuildHelper |
+|-------------------------------|------------|---------------------|
+| iOS app on Simulator | ✅ | ✅ |
+| iOS app on Device | ❌ | ✅ (codesign with Individual, Company or Enterprise ADP) |
+| macOS app (App Sandbox = NO) | ✅ | ✅ |
+| macOS app (App Sandbox = YES) | ❌ | ❌ (codesign cannot be trusted to load) |
+| macOS app (Designed for iPad) | ❌ | ❌ (codesign cannot be trusted to load) |
+
## Features
@@ -24,11 +33,10 @@ SwiftHotReload is an experimental project. We investigate a real world applicati
* SPM project structures
* CocoaPods project structures
* Update trigger for SwiftUI views
+* Helper app on host & Reload on devices
### TODOs (not yet implemented, nice to have)
-* Helper app on host
-* Reload on devices
* Less invasive: be easy to adopt & compatible for App Store submission
* Build settings (-Xfrontend ...)
* Sandbox restrictions for macOS app
@@ -38,7 +46,6 @@ SwiftHotReload is an experimental project. We investigate a real world applicati
## How to use the Example app
* Open `SwiftHotReload.xcworkspace`
-* Modify `targetSwiftFile:` file path to along with your path in `App.swift`
* Run `SwiftHotReloadExample` on Mac or any simulators
* Edit `ReplaceView.swift` and save
@@ -66,17 +73,28 @@ Set up app as described below and build & run on a supported platform.
```swift
extension App {
- static let reloader = StandaloneReloader(
+ static let reloader = StandaloneReloader(monitoredSwiftFile: URL(fileURLWithPath: #filePath).deletingLastPathComponent()
// file path to be monitored
- monitoredSwiftFile: Env.shared.estimatedHomeDir!
- .appendingPathComponent("path_to_project/RuntimeOverrides.swift")
- )
+ .appendingPathComponent("RuntimeOverrides.swift")
:
_ = App.reloader // use to load the lazy static property above and start a file monitor
}
```
-### Disable sandbox (only required for macOS app target):
+### (on iOS Device) Use ProxyReloader & BuildHelper
+
+If the app is for iOS Device, use `ProxyReloader` instead of `StandaloneReloader`. Run BuildHelper separately on the host Mac:
+
+```
+git clone https://github.com/banjun/SwiftHotReload.git
+cd SwiftHotReload
+
+swift run BuildHelper -c debug
+```
+
+Alternatively to `swift run`, we can run BuildHelper as an app (not CLI) using BuildHelper target on SwiftHotReload.xcworkspace.
+
+### (only required for macOS app target) Disable App Sandbox:
Modify the app entitlements file:
@@ -84,7 +102,7 @@ Modify the app entitlements file:
App Sandbox = NO
```
-### (Optionally but recommended) set build settings:
+### (optional but recommended) Set build settings:
* Add to `OTHER_SWIFT_FLAGS` of the app target
* `-Xfrontend` `-enable-implicit-dynamic`
@@ -92,6 +110,13 @@ App Sandbox = NO
* `-Xfrontend` `-enable-private-imports`
* use the flag instead of making related `func`s or `var`s visible by removing `private`
+
+### (optional) Insert hooks to update SwiftUI view after reloadings:
+
+```swift
+@ObservedObject private var reloader = App.reloader
+```
+
### Create `path_to_project/RuntimeOverrides.swift`:
Any funcs/vars can be replaced (not only for SwiftUI).
@@ -106,11 +131,3 @@ extension ContentView { // <- typically use extension for a type containing func
}
}
```
-
-### (Optionally) to update SwiftUI view after reloadings:
-
-```swift
-@ObservedObject private var reloader = App.reloader
-```
-
-
diff --git a/Sources/Builder.swift b/Sources/Builder.swift
deleted file mode 100644
index faaecb1..0000000
--- a/Sources/Builder.swift
+++ /dev/null
@@ -1,113 +0,0 @@
-#if DEBUG
-import Foundation
-
-final actor Builder {
- private let targetSwiftFile: URL
- private let derivedData: URL
- private let moduleCachePath: URL
- private let confBuildDir: URL
- private let headerSearchPaths: [URL]
- private let headerMaps: [URL]
- private let buildDir: URL
- private let targetTriple: String
- private let sdk: URL
- private let arch: String
- private let platformName: String
-
- enum Error: Swift.Error {
- case cannotBuildOnRuntime(String?)
- case noSuchFile(URL)
- case swiftcFailure(Int?)
- }
-
- 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) {
- self.targetSwiftFile = targetSwiftFile
- let derivedData = derivedData ?? env.estimataedDerivedData!
- self.derivedData = derivedData
- self.moduleCachePath = derivedData.appendingPathComponent("ModuleCache.noindex")
- let confBuildDirAppRandomString = confBuildDirAppRandomString ?? env.estimatedConfigurationBuildRandomString!
- let mainModule = mainModule ?? env.estimatedMainModule!
- let intermediatesDir = derivedData
- .appendingPathComponent(confBuildDirAppRandomString)
- .appendingPathComponent("Build/Intermediates.noindex")
- let configurationPlatform = configurationPlatform ?? env.estimatedConfigurationPlatform!
- let confBuildDir = intermediatesDir
- .appendingPathComponent(mainModule + ".build")
- .appendingPathComponent(configurationPlatform)
- self.confBuildDir = confBuildDir
- let arch = arch ?? env.estimatedArch
- self.arch = arch
- self.headerSearchPaths = ([mainModule] + modules).map {
- confBuildDir
- .appendingPathComponent($0 + ".build")
- .appendingPathComponent("Objects-normal")
- .appendingPathComponent(arch)
- }
- self.headerMaps = [confBuildDir
- .appendingPathComponent(mainModule + ".build")
- .appendingPathComponent("\(mainModule)-project-headers.hmap")
- ] + [intermediatesDir
- .appendingPathComponent("Pods" + ".build")
- .appendingPathComponent(configurationPlatform)
- .appendingPathComponent("Pods-\(mainModule)" + ".build")
- .appendingPathComponent("Pods_\(mainModule)-project-headers.hmap")
- ]
- self.buildDir = headerSearchPaths.first!
- self.targetTriple = targetTriple ?? env.estimatedTargetTriple!
- self.sdk = sdk ?? env.estimatedSDK!
- self.platformName = platformName ?? env.DTPlatformName!
- }
-
- func build(dylibFilename: String) throws -> URL {
- let dylibPath = buildDir.appendingPathComponent(dylibFilename)
- try build(dylibPath: dylibPath)
- return dylibPath
- }
-
- func build(dylibPath: URL) throws {
- guard platformName != "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)
- }
-
- guard let file = try? TargetSwiftFile(targetSwiftFile) else { throw Error.noSuchFile(targetSwiftFile) }
- let importedModuleSearchPaths = file.importedModules.map {
- confBuildDir
- .appendingPathComponent($0 + ".build")
- .appendingPathComponent("Objects-normal")
- .appendingPathComponent(arch)
- }
-
- let NSTask: AnyClass = NSClassFromString("NSTask")!
- // NSLog("%@", "🍓 NSTask = \(NSTask)")
- let task = NSTask.value(forKey: "new")! as! NSObject
- // NSLog("%@", "🍓 task = \(task)")
- task.setValue([:], forKey: "environment")
- let launchPath = "/usr/bin/swiftc"
- task.setValue(launchPath, forKey: "launchPath")
- let args: [String] = [
- ["-emit-library"], // generates dylib
- [targetSwiftFile.path],
- ["-o", dylibPath.path],
- ["-sdk", sdk.path],
- ["-target", targetTriple],
- ["-module-cache-path", moduleCachePath.path], // required in some cases
- ["-Xlinker", "-undefined", "-Xlinker", "suppress"], // avoid fatal error on the linker
- ["-Xfrontend", "-disable-access-control"], // with this, internal symbols can be used
- ["-Xlinker", "-flat_namespace"], // for Xcode 14 (unneeded for Xcode 15)
- (headerSearchPaths + importedModuleSearchPaths).flatMap { ["-I", $0.path] },
- headerMaps.flatMap { ["-Xcc", "-I", "-Xcc", $0.path] }
- ].flatMap { $0 }
- task.setValue(args, forKey: "arguments")
- NSLog("%@", "🍓 exec and args = ")
- print("\(launchPath) \(args.joined(separator: " "))")
- task.value(forKey: "launch")
- task.value(forKey: "waitUntilExit")
-
- let terminationStatus = task.value(forKey: "terminationStatus") as? Int
- // NSLog("%@", "🍓 terminationStatus = \(String(describing: terminationStatus))")
- guard terminationStatus == 0 else { throw Error.swiftcFailure(terminationStatus) }
- }
-}
-
-#endif
diff --git a/Sources/Core/Builder.swift b/Sources/Core/Builder.swift
new file mode 100644
index 0000000..9df7052
--- /dev/null
+++ b/Sources/Core/Builder.swift
@@ -0,0 +1,157 @@
+#if DEBUG || os(macOS)
+import Foundation
+
+public final actor Builder {
+ private let targetSwiftFile: URL
+ private let derivedData: URL
+ private let moduleCachePath: URL
+ private let confBuildDir: URL
+ private let headerSearchPaths: [URL]
+ private let headerMaps: [URL]
+ private let buildDir: URL
+ private let targetTriple: String
+ private let sdk: URL?
+ private let arch: String
+ private let platformName: String
+ private let codesignIdentity: String?
+
+ public struct InputParameters: Codable {
+ public var targetSwiftFile: URL
+ public var env: Env
+ public var derivedData: URL?
+ public var confBuildDirAppRandomString: String?
+ public var mainModule: String?
+ public var modules: [String] = []
+ public var configurationPlatform: String?
+ public var arch: String?
+ public var targetTriple: String?
+ public var sdk: URL?
+ 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) {
+ self.targetSwiftFile = targetSwiftFile
+ self.env = env
+ self.derivedData = derivedData
+ self.confBuildDirAppRandomString = confBuildDirAppRandomString
+ self.mainModule = mainModule
+ self.modules = modules
+ self.configurationPlatform = configurationPlatform
+ self.arch = arch
+ self.targetTriple = targetTriple
+ self.sdk = sdk
+ self.platformName = platformName
+ self.codesignIdentity = codesignIdentity
+ }
+ }
+
+ enum Error: Swift.Error {
+ case cannotBuildOnRuntime(String?)
+ case noSuchFile(URL)
+ case swiftcFailure(Int?)
+ }
+
+ init(_ p: InputParameters) {
+ self.targetSwiftFile = p.targetSwiftFile
+ let derivedData = p.derivedData ?? p.env.estimataedDerivedData!
+ self.derivedData = derivedData
+ self.moduleCachePath = derivedData.appendingPathComponent("ModuleCache.noindex")
+ let confBuildDirAppRandomString = p.confBuildDirAppRandomString ?? p.env.estimatedConfigurationBuildRandomString!
+ let mainModule = p.mainModule ?? p.env.estimatedMainModule!
+ let intermediatesDir = derivedData
+ .appendingPathComponent(confBuildDirAppRandomString)
+ .appendingPathComponent("Build/Intermediates.noindex")
+ let configurationPlatform = p.configurationPlatform ?? p.env.estimatedConfigurationPlatform!
+ let confBuildDir = intermediatesDir
+ .appendingPathComponent(mainModule + ".build")
+ .appendingPathComponent(configurationPlatform)
+ self.confBuildDir = confBuildDir
+ let arch = p.arch ?? p.env.estimatedArch
+ self.arch = arch
+ self.headerSearchPaths = ([mainModule] + p.modules).map {
+ confBuildDir
+ .appendingPathComponent($0 + ".build")
+ .appendingPathComponent("Objects-normal")
+ .appendingPathComponent(arch)
+ }
+ self.headerMaps = [confBuildDir
+ .appendingPathComponent(mainModule + ".build")
+ .appendingPathComponent("\(mainModule)-project-headers.hmap")
+ ] + [intermediatesDir
+ .appendingPathComponent("Pods" + ".build")
+ .appendingPathComponent(configurationPlatform)
+ .appendingPathComponent("Pods-\(mainModule)" + ".build")
+ .appendingPathComponent("Pods_\(mainModule)-project-headers.hmap")
+ ]
+ self.buildDir = headerSearchPaths.first!
+ self.targetTriple = p.targetTriple ?? p.env.estimatedTargetTriple!
+ self.sdk = p.sdk ?? p.env.estimatedSDK ?? Env.shared.estimatedSDK
+ self.platformName = p.platformName ?? p.env.DTPlatformName!
+ self.codesignIdentity = p.codesignIdentity
+ }
+
+ func build(dylibFilename: String) throws -> URL {
+ let dylibPath = buildDir.appendingPathComponent(dylibFilename)
+ try build(dylibPath: dylibPath)
+ if let codesignIdentity {
+ try codesign(dylibPath: dylibPath, codesignIdentity: codesignIdentity)
+ }
+ return dylibPath
+ }
+
+ func build(dylibPath: URL) throws {
+ guard Env.shared.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)
+ }
+
+ guard let file = try? TargetSwiftFile(targetSwiftFile) else { throw Error.noSuchFile(targetSwiftFile) }
+ let importedModuleSearchPaths = file.importedModules.map {
+ confBuildDir
+ .appendingPathComponent($0 + ".build")
+ .appendingPathComponent("Objects-normal")
+ .appendingPathComponent(arch)
+ }
+
+ let command = NSTaskCommand(
+ launchPath: "/usr/bin/xcrun",
+ args: [
+ ["--sdk", platformName], // `xcrun --sdk iphoneos swiftc ...` to suppress `clang: warning: using sysroot for 'MacOSX' but targeting 'iPhone' [-Wincompatible-sysroot]` and to set correct VersionSDK for codesign
+ ["/usr/bin/swiftc"],
+ ["-emit-library"], // generates dylib
+ [targetSwiftFile.path],
+ ["-o", dylibPath.path],
+ sdk.flatMap { ["-sdk", $0.path] } ?? [],
+ ["-target", targetTriple],
+ ["-module-cache-path", moduleCachePath.path], // required in some cases
+ ["-Xlinker", "-undefined", "-Xlinker", "suppress"], // avoid fatal error on the linker
+ ["-Xfrontend", "-disable-access-control"], // with this, internal symbols can be used
+ ["-Xlinker", "-flat_namespace"], // for Xcode 14 (unneeded for Xcode 15)
+ (headerSearchPaths + importedModuleSearchPaths).flatMap { ["-I", $0.path] },
+ headerMaps.flatMap { ["-Xcc", "-I", "-Xcc", $0.path] }
+ ].flatMap { $0 })
+
+ NSLog("%@", "🍓 build: exec and args = ")
+ print("\(command.launchPath) \(command.args.joined(separator: " "))")
+
+ try command.run()
+ }
+
+ /// codesign the dylib
+ ///
+ /// in case error message on a runtime device on dlopen:
+ /// > .dylib' not valid for use in process: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.
+ ///
+ /// ad-hoc sign is not valid for devices
+ func codesign(dylibPath: URL, codesignIdentity: String) throws {
+ let command = NSTaskCommand(launchPath: "/usr/bin/codesign", args: [
+ "-f", "-s", codesignIdentity, dylibPath.path
+ ])
+ NSLog("%@", "🍓 codesign: exec and args = ")
+ print("\(command.launchPath) \(command.args.joined(separator: " "))")
+ let outputs = try command.run()
+ print(outputs)
+ }
+}
+
+#endif
diff --git a/Sources/Env.swift b/Sources/Core/Env.swift
similarity index 88%
rename from Sources/Env.swift
rename to Sources/Core/Env.swift
index c34628c..953ab00 100644
--- a/Sources/Env.swift
+++ b/Sources/Core/Env.swift
@@ -1,7 +1,7 @@
-#if DEBUG
+#if DEBUG || os(macOS)
import Foundation
-public struct Env {
+public struct Env: Codable, Equatable {
public static let shared: Env = .init()
/// /Users/username
@@ -10,7 +10,7 @@ public struct Env {
}
/// /Users/username/Library/Developer/Xcode/DerivedData/app-abcdefg0123456789/Build/Products/Debug-iphonesimulator
var estimatedBuilProductsDir: [URL] {
- DYLD_FRAMEWORK_PATH.map(URL.init(fileURLWithPath:))
+ DYLD_FRAMEWORK_PATH.filter {!$0.isEmpty}.map(URL.init(fileURLWithPath:))
+ [(__XPC_DYLD_FRAMEWORK_PATH ?? __XPC_DYLD_LIBRARY_PATH ?? __XCODE_BUILT_PRODUCTS_DIR_PATHS ?? __XPC_DYLD_LIBRARY_PATH ?? PWD).map(URL.init(fileURLWithPath:))].compactMap {$0}
}
/// /Users/username/Library/Developer/Xcode/DerivedData
@@ -60,6 +60,7 @@ public struct Env {
.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
}
/// /Applications/Xcode1501.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator17.0.sdk
public var estimatedSDK: URL? {
@@ -94,6 +95,11 @@ public struct Env {
"x86_64"
#endif
}
+ /// Product app bundle on host
+ public var estimatedProductBundlePath: [URL] {
+ guard let CFBundleName else { return [] }
+ return estimatedBuilProductsDir.map { $0.appendingPathComponent(CFBundleName).appendingPathExtension("app") }
+ }
// Environment Variables
var SIMULATOR_HOST_HOME: String?
@@ -116,6 +122,8 @@ public struct Env {
var MinimumOSVersion: String?
var LSMinimumSystemVersion: String?
var CFBundleExecutable: String?
+ var CFBundleIdentifier: String?
+ var CFBundleName: String?
private init() {
let env = ProcessInfo().environment
@@ -139,6 +147,8 @@ public struct Env {
MinimumOSVersion = info["MinimumOSVersion"] as? String
LSMinimumSystemVersion = info["LSMinimumSystemVersion"] as? String
CFBundleExecutable = info["CFBundleExecutable"] as? String
+ CFBundleIdentifier = info["CFBundleIdentifier"] as? String
+ CFBundleName = info["CFBundleName"] as? String
}
}
#endif
diff --git a/Sources/FileMonitor.swift b/Sources/Core/FileMonitor.swift
similarity index 87%
rename from Sources/FileMonitor.swift
rename to Sources/Core/FileMonitor.swift
index b7f8c37..ea366fa 100644
--- a/Sources/FileMonitor.swift
+++ b/Sources/Core/FileMonitor.swift
@@ -1,4 +1,4 @@
-#if DEBUG
+#if DEBUG || os(macOS)
import Foundation
final actor FileMonitor {
@@ -16,10 +16,10 @@ final actor FileMonitor {
private var lastTargetFileContent: String?
- init(file: URL, platformName: String) {
+ init(file: URL) {
self.file = file
- guard platformName != "iphoneos" else {
+ guard Env.shared.DTPlatformName != "iphoneos" else {
NSLog("%@", "🍓 ⚠️ To do hot reloads, the process host should be able to execute swiftc. cancelled installing the file monitor. ⚠️")
return
}
@@ -30,6 +30,7 @@ final actor FileMonitor {
}
private func install() {
+ 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
diff --git a/Sources/Loader.swift b/Sources/Core/Loader.swift
similarity index 55%
rename from Sources/Loader.swift
rename to Sources/Core/Loader.swift
index 099ec9d..dee27dd 100644
--- a/Sources/Loader.swift
+++ b/Sources/Core/Loader.swift
@@ -1,9 +1,11 @@
-#if DEBUG
+#if DEBUG || os(macOS)
import Foundation
final actor Loader {
enum Error: Swift.Error {
case symbol_not_found_in_flat_namespace(String)
+ case code_signature_invalid(String)
+ case system_policy(String)
case unknown(String)
}
@@ -17,6 +19,14 @@ final actor Loader {
NSLog("%@", "🍓 possible workarounds: remove `private` from the func, or add `-Xfrontend -enable-private-imports` to OTHER_SWIFT_FLAGS of the module to be overridden")
throw Error.symbol_not_found_in_flat_namespace(error)
}
+ if error.contains("code signature invalid") {
+ NSLog("%@", "🍓 code signature invalid: on device dylibs needs to be signed by Individual, Company or Enterprise identity (it cannot be verified by Personal identity. see `amfid` process message on the device console)")
+ throw Error.code_signature_invalid(error)
+ }
+ if error.contains("library load disallowed by system policy") {
+ NSLog("%@", "🍓 possible workarounds: turn off App Sandbox")
+ throw Error.system_policy(error)
+ }
throw Error.unknown(error)
}
}
diff --git a/Sources/Core/NSTaskCommand.swift b/Sources/Core/NSTaskCommand.swift
new file mode 100644
index 0000000..a07d4fc
--- /dev/null
+++ b/Sources/Core/NSTaskCommand.swift
@@ -0,0 +1,44 @@
+#if DEBUG || os(macOS)
+import Foundation
+
+struct NSTaskCommand {
+ var launchPath: String
+ var args: [String]
+
+ enum Error: Swift.Error {
+ case nsTaskUnavailable
+ case failureStatus(status: Int?, stdout: String?, stderr: String?)
+ }
+
+ @discardableResult
+ func run(clearEnvironments: Bool = true) throws -> (stdout: String?, stderr: String?) {
+ let NSTask: AnyClass? = NSClassFromString("NSTask")
+ let task = NSTask?.value(forKey: "new") as? NSObject
+ guard let task else { throw Error.nsTaskUnavailable }
+
+ if clearEnvironments {
+ task.setValue([:], forKey: "environment")
+ }
+
+ task.setValue(launchPath, forKey: "launchPath")
+ task.setValue(args, forKey: "arguments")
+
+ let stdout = Pipe()
+ let stderr = Pipe()
+ task.setValue(stdout, forKey: "standardOutput")
+ task.setValue(stderr, forKey: "standardError")
+
+ task.value(forKey: "launch")
+ task.value(forKey: "waitUntilExit")
+
+ let outputs = (
+ stdout: (try? stdout.fileHandleForReading.readToEnd()).flatMap { String(data: $0, encoding: .utf8) },
+ stderr: (try? stderr.fileHandleForReading.readToEnd()).flatMap { String(data: $0, encoding: .utf8) })
+
+ let terminationStatus = task.value(forKey: "terminationStatus") as? Int
+ guard terminationStatus == 0 else { throw Error.failureStatus(status: terminationStatus, stdout: outputs.stdout, stderr: outputs.stderr) }
+
+ return outputs
+ }
+}
+#endif
diff --git a/Sources/TargetSwiftFile.swift b/Sources/Core/TargetSwiftFile.swift
similarity index 95%
rename from Sources/TargetSwiftFile.swift
rename to Sources/Core/TargetSwiftFile.swift
index cb6ee7f..4db44e7 100644
--- a/Sources/TargetSwiftFile.swift
+++ b/Sources/Core/TargetSwiftFile.swift
@@ -1,4 +1,4 @@
-#if DEBUG
+#if DEBUG || os(macOS)
import Foundation
struct TargetSwiftFile {
diff --git a/Sources/ProxyReloader/MultipeerConnectivityConstants.swift b/Sources/ProxyReloader/MultipeerConnectivityConstants.swift
new file mode 100644
index 0000000..2fcdf77
--- /dev/null
+++ b/Sources/ProxyReloader/MultipeerConnectivityConstants.swift
@@ -0,0 +1,15 @@
+#if DEBUG || os(macOS)
+import Foundation
+
+enum MultipeerConnectivityConstants {
+ /// MultipeerConnectivity service type
+ /// The type of service to advertise. This should be a short text string that describes the app's networking protocol, in the same format as a Bonjour service type (without the transport protocol) and meeting the restrictions of RFC 6335 (section 5.1) governing Service Name Syntax. In particular, the string:
+ /// * Must be 1–15 characters long
+ /// * Can contain only ASCII lowercase letters, numbers, and hyphens
+ /// * Must contain at least one ASCII letter
+ /// * Must not begin or end with a hyphen
+ /// * Must not contain hyphens adjacent to other hyphens.
+ static let serviceType = "swifthotreload"
+ static let serverDiscoveryInfo: [String: String] = ["SwiftHotReloadServer": "1"]
+}
+#endif
diff --git a/Sources/ProxyReloader/Proxy.swift b/Sources/ProxyReloader/Proxy.swift
new file mode 100644
index 0000000..d4d27d8
--- /dev/null
+++ b/Sources/ProxyReloader/Proxy.swift
@@ -0,0 +1,169 @@
+#if DEBUG || os(macOS)
+import Foundation
+import MultipeerConnectivity
+
+final actor Proxy {
+ private let loader: Loader = .init()
+ @Published private(set) var receivedDylibFiles: [URL] = []
+ private var shouldConnectToBuilder: (_ title: String, _ message: String) async -> Bool
+ func setShouldConnectToBuilder(_ shouldConnectToBuilder: @escaping (String, String) async -> Bool) { self.shouldConnectToBuilder = shouldConnectToBuilder }
+
+ private let builderParams: Builder.InputParameters
+
+ private let peerID: MCPeerID
+ private let advertiser: MCNearbyServiceAdvertiser
+ private var session: MCSession? {
+ didSet {
+ oldValue?.disconnect()
+ session?.delegate = sessionDelegate
+ }
+ }
+ private let advertiserDelegate: AdvertiserDelegate
+ private let sessionDelegate: SessionDelegate
+
+ enum Error: Swift.Error {
+ case invalidFilePath(String)
+ 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) {
+ 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.
+ let displayName = String("\(hostName) \(bundleID)(\(processID))".utf8.prefix(63))!
+ self.peerID = MCPeerID(displayName: displayName)
+ self.advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: MultipeerConnectivityConstants.serverDiscoveryInfo, serviceType: MultipeerConnectivityConstants.serviceType)
+ self.advertiserDelegate = AdvertiserDelegate()
+ self.sessionDelegate = SessionDelegate()
+
+ self.advertiser.delegate = self.advertiserDelegate
+
+ Task {
+ advertiserDelegate.proxy = self
+ sessionDelegate.proxy = self
+ await start()
+ }
+ }
+
+ func start() {
+ advertiser.startAdvertisingPeer()
+ }
+
+ func stop() {
+ advertiser.stopAdvertisingPeer()
+ session = nil
+ }
+
+ // MARK: - MCNearbyServiceAdvertiserDelegate
+
+ private final class AdvertiserDelegate: NSObject, MCNearbyServiceAdvertiserDelegate {
+ unowned var proxy: Proxy?
+ override init() { super.init() }
+ func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @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) }
+ }
+ func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Swift.Error) {
+ NSLog("%@", "🍓 \(#function) advertiser = \(advertiser), error = \(error)")
+ Task { await proxy?.advertiser(advertiser, didNotStartAdvertisingPeer: error) }
+ }
+ }
+
+ private func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
+ NSLog("%@", "🍓 \(#function) advertiser = \(advertiser), peerID = \(peerID), context = \(context?.count ?? 0) bytes")
+ guard session == nil else {
+ NSLog("%@", "🍓 \(#function) ignored additional session")
+ return
+ }
+
+ Task {
+ let trusted = await shouldConnectToBuilder("⚠️ Connect to a Builder \(peerID)?", "SwiftHotReload loads any code from the Builder")
+ if trusted {
+ self.session = MCSession(peer: self.peerID, securityIdentity: nil, encryptionPreference: .required)
+ }
+ invitationHandler(trusted, self.session)
+ }
+ }
+
+ private func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Swift.Error) {
+ NSLog("%@", "🍓 \(#function) advertiser = \(advertiser), error = \(error)")
+ }
+
+ // MARK: - MCSessionDelegate
+
+ private final class SessionDelegate: NSObject, MCSessionDelegate {
+ unowned 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) }
+ }
+
+ func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
+ NSLog("%@", "🍓 \(#function) data = \(data.count) bytes, peerID = \(peerID)")
+ }
+
+ func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
+ NSLog("%@", "🍓 \(#function) stream = \(stream), streamName = \(streamName), peerID = \(peerID)")
+ }
+
+ func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
+ NSLog("%@", "🍓 \(#function) resourceName = \(resourceName), peerID = \(peerID), progress = \(progress)")
+ }
+
+ 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) }
+ }
+ }
+
+ private func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
+ switch state {
+ case .notConnected:
+ self.session = nil
+ case .connecting: break
+ case .connected:
+ do {
+ NSLog("%@", "🍓 \(#function) connected: sending builderParams = \(builderParams)")
+ let payload = try JSONEncoder().encode(builderParams)
+ try self.session?.send(payload, toPeers: [peerID], with: .reliable)
+ } catch {
+ NSLog("%@", "🍓 \(#function) error = \(error)")
+ }
+ @unknown default: break
+ }
+ }
+
+ private func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Swift.Error?) {
+ guard let localURL else { return }
+ let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
+ .appendingPathComponent("SwiftHotReload")
+ .appendingPathComponent("dylibs")
+ guard let filename = resourceName.components(separatedBy: "/").last else { return } // { throw Error.invalidFilePath(resourceName) }
+ let tmpDylibPath = tmpDir.appendingPathComponent(filename)
+
+ do {
+ try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)
+ // guard !FileManager.default.fileExists(atPath: tmpDylibPath.path) else { return }
+
+ if FileManager.default.fileExists(atPath: tmpDylibPath.path) {
+ try FileManager.default.removeItem(atPath: tmpDylibPath.path)
+ }
+
+ try FileManager.default.copyItem(at: localURL, to: tmpDylibPath)
+ try FileManager.default.setAttributes([.posixPermissions: 0o644], ofItemAtPath: tmpDylibPath.path)
+ } catch {
+ NSLog("%@", "🍓 \(#function) line \(#line) error = \(error)")
+ }
+ Task {
+ do {
+ try await loader.load(dylibPath: tmpDylibPath)
+ receivedDylibFiles.append(tmpDylibPath)
+ } catch {
+ NSLog("%@", "🍓 \(#function) line \(#line) error = \(error)")
+ }
+ }
+ }
+}
+#endif
diff --git a/Sources/ProxyReloader/ProxyReloader.swift b/Sources/ProxyReloader/ProxyReloader.swift
new file mode 100644
index 0000000..a830f98
--- /dev/null
+++ b/Sources/ProxyReloader/ProxyReloader.swift
@@ -0,0 +1,23 @@
+#if DEBUG || os(macOS)
+import Foundation
+import MultipeerConnectivity
+
+public final class ProxyReloader: ObservableObject {
+ private let proxy: Proxy
+
+ @Published public private(set) var dateReloaded: Date?
+
+ public init(_ builderParams: Builder.InputParameters) {
+ self.proxy = Proxy(builderParams: builderParams, shouldConnectToBuilder: UserConsent.alert)
+
+ Task {
+ await proxy.$receivedDylibFiles.map {_ in Date() }.receive(on: DispatchQueue.main).assign(to: &$dateReloaded)
+ await proxy.start()
+ }
+ }
+
+ public func setShouldConnectToBuilder(_ shouldConnectToBuilder: @escaping (String, String) async -> Bool) {
+ Task { await proxy.setShouldConnectToBuilder(shouldConnectToBuilder) }
+ }
+}
+#endif
diff --git a/Sources/ProxyReloader/RuntimePeer.swift b/Sources/ProxyReloader/RuntimePeer.swift
new file mode 100644
index 0000000..297f650
--- /dev/null
+++ b/Sources/ProxyReloader/RuntimePeer.swift
@@ -0,0 +1,13 @@
+#if DEBUG || os(macOS)
+import Foundation
+import MultipeerConnectivity
+
+struct RuntimePeer {
+ /// route for sending dylib
+ var session: MCSession
+ /// the destination peerID that will load the dylib on runtime
+ var peerID: MCPeerID
+ /// build environments for the destination
+ var builderParams: Builder.InputParameters?
+}
+#endif
diff --git a/Sources/ProxyReloader/UserConsent.swift b/Sources/ProxyReloader/UserConsent.swift
new file mode 100644
index 0000000..674b907
--- /dev/null
+++ b/Sources/ProxyReloader/UserConsent.swift
@@ -0,0 +1,47 @@
+import Foundation
+
+enum UserConsent {
+}
+
+#if canImport(AppKit)
+import AppKit
+extension UserConsent {
+ @MainActor
+ static func alert(_ title: String, _ message: String) async -> Bool {
+ await withCheckedContinuation { continuation in
+ let alert = NSAlert()
+ alert.alertStyle = .critical
+ alert.messageText = title
+ alert.informativeText = message
+ alert.addButton(withTitle: "Trust").hasDestructiveAction = true
+ alert.addButton(withTitle: "Cancel")
+
+ continuation.resume(returning: alert.runModal() == .alertFirstButtonReturn)
+ }
+ }
+}
+
+#elseif canImport(UIKit)
+import UIKit
+extension UserConsent {
+ @MainActor
+ static func alert(_ title: String, _ message: String) async -> Bool {
+ guard let window = (UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.compactMap { $0.windows.first { $0.isKeyWindow } }.first) else {
+ NSLog("%@", "⚠️ cannot get keyWindow from scenes = \(UIApplication.shared.connectedScenes)")
+ return false
+ }
+
+ var vc = window.rootViewController
+ while let pvc = vc?.presentedViewController {
+ vc = pvc
+ }
+
+ return await withCheckedContinuation { continuation in
+ let ac = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ ac.addAction(.init(title: "Trust", style: .destructive) {_ in continuation.resume(returning: true) })
+ ac.addAction(.init(title: "Cancel", style: .cancel) {_ in continuation.resume(returning: false) })
+ vc?.present(ac, animated: true)
+ }
+ }
+}
+#endif
diff --git a/Sources/StandaloneReloader.swift b/Sources/StandaloneReloader/StandaloneReloader.swift
similarity index 77%
rename from Sources/StandaloneReloader.swift
rename to Sources/StandaloneReloader/StandaloneReloader.swift
index 5809650..d1ee789 100644
--- a/Sources/StandaloneReloader.swift
+++ b/Sources/StandaloneReloader/StandaloneReloader.swift
@@ -1,4 +1,4 @@
-#if DEBUG
+#if DEBUG || os(macOS)
import Foundation
import Combine
@@ -14,8 +14,8 @@ public final class StandaloneReloader: ObservableObject {
NSLog("%@", "🍓 ⚠️ To do hot reloads standalone, the process host should be able to execute swiftc. ⚠️")
}
- fileMonitor = .init(file: monitoredSwiftFile, platformName: platformName ?? env.DTPlatformName!)
- core = .init(builder: .init(targetSwiftFile: monitoredSwiftFile, env: env, derivedData: derivedData, confBuildDirAppRandomString: confBuildDirAppRandomString, mainModule: mainModule, modules: modules, configurationPlatform: configurationPlatform, arch: arch, targetTriple: targetTriple, sdk: sdk, platformName: platformName), loader: .init())
+ fileMonitor = .init(file: monitoredSwiftFile)
+ core = .init(builder: .init(.init(targetSwiftFile: monitoredSwiftFile, env: env, derivedData: derivedData, confBuildDirAppRandomString: confBuildDirAppRandomString, mainModule: mainModule, modules: modules, configurationPlatform: configurationPlatform, arch: arch, targetTriple: targetTriple, sdk: sdk, platformName: platformName)), loader: .init())
Task {
await fileMonitor.$fileChanges.compactMap {$0}.sink { [weak self] _ in
diff --git a/SwiftHotReload.podspec b/SwiftHotReload.podspec
index fc86ee4..c3e2aef 100644
--- a/SwiftHotReload.podspec
+++ b/SwiftHotReload.podspec
@@ -16,4 +16,10 @@ Pod::Spec.new do |spec|
spec.source = { :git => "https://github.com/banjun/SwiftHotReload.git", :tag => "#{spec.version}" }
spec.source_files = "Sources/**/*.swift"
spec.swift_version = "5.1"
+
+ spec.ios.deployment_target = "14.0"
+ spec.osx.deployment_target = "11.0"
+ # spec.watchos.deployment_target = "2.0"
+ # spec.tvos.deployment_target = "9.0"
+ # spec.visionos.deployment_target = "1.0"
end
diff --git a/SwiftHotReload.xcworkspace/contents.xcworkspacedata b/SwiftHotReload.xcworkspace/contents.xcworkspacedata
index 84efcd4..10ee0b9 100644
--- a/SwiftHotReload.xcworkspace/contents.xcworkspacedata
+++ b/SwiftHotReload.xcworkspace/contents.xcworkspacedata
@@ -4,6 +4,13 @@
+
+
+
+
@@ -19,4 +26,7 @@
+
+