@tokamakdev/platform-ios-simulator-arm64 0.1.1-beta.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios-simulator-arm64/build/entrypoint +219 -0
- package/ios-simulator-arm64/frameworks/TokamakRuntime.framework/Headers/TokamakRuntime.h +45 -0
- package/ios-simulator-arm64/frameworks/TokamakRuntime.framework/Modules/module.modulemap +4 -0
- package/ios-simulator-arm64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/ios-simulator-arm64/native-shell/TokamakPlugins.swift +216 -0
- package/ios-simulator-arm64/native-shell/TokamakShell.swift +645 -0
- package/ios-simulator-arm64/platform-pack.json +21 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/darwin-arm64/README.md +3 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/darwin-arm64/bin/esbuild +0 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/darwin-arm64/package.json +20 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/darwin-x64/README.md +3 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/darwin-x64/bin/esbuild +0 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/darwin-x64/package.json +20 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/linux-x64/README.md +3 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/linux-x64/package.json +20 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/win32-x64/README.md +3 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/win32-x64/esbuild.exe +0 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/@esbuild/win32-x64/package.json +20 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/esbuild/LICENSE.md +21 -0
- package/ios-simulator-arm64/tools/runtime/node_modules/esbuild/bin/esbuild +24 -0
- package/ios-simulator-arm64/tools/tokamak-apple-signing +0 -0
- package/package.json +23 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ $# -ne 3 || $1 != build ]]; then
|
|
5
|
+
echo "usage: entrypoint build INPUT OUTPUT" >&2
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
input=$2
|
|
10
|
+
output=$3
|
|
11
|
+
metadata=$input/metadata
|
|
12
|
+
native_shell=$input/native-shell
|
|
13
|
+
framework_root=$input/runtime/frameworks
|
|
14
|
+
app_slug=$(cat "$metadata/app-slug")
|
|
15
|
+
identifier=$(cat "$metadata/identifier")
|
|
16
|
+
platform=$(cat "$metadata/platform")
|
|
17
|
+
target=$(cat "$metadata/target")
|
|
18
|
+
project_dir=$(cat "$metadata/project-dir")
|
|
19
|
+
output_parent=$(dirname "$output")
|
|
20
|
+
module_cache=$output_parent/.tokamak/module-cache
|
|
21
|
+
temporary=$(mktemp -d "${TMPDIR:-/tmp}/tokamak-apple.XXXXXX")
|
|
22
|
+
trap 'rm -rf "$temporary"' EXIT
|
|
23
|
+
apple_tool=$PWD/tools/tokamak-apple-signing
|
|
24
|
+
if [[ ! -x "$apple_tool" ]]; then
|
|
25
|
+
echo "Apple platform pack is missing its Apple helper: $apple_tool" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
case "$target" in
|
|
30
|
+
macos-arm64)
|
|
31
|
+
sdk=macosx
|
|
32
|
+
triple=arm64-apple-macos14.0
|
|
33
|
+
;;
|
|
34
|
+
macos-x64)
|
|
35
|
+
sdk=macosx
|
|
36
|
+
triple=x86_64-apple-macos14.0
|
|
37
|
+
;;
|
|
38
|
+
ios-arm64)
|
|
39
|
+
sdk=iphoneos
|
|
40
|
+
triple=arm64-apple-ios17.0
|
|
41
|
+
;;
|
|
42
|
+
ios-simulator-arm64)
|
|
43
|
+
sdk=iphonesimulator
|
|
44
|
+
triple=arm64-apple-ios17.0-simulator
|
|
45
|
+
;;
|
|
46
|
+
ios-simulator-x64)
|
|
47
|
+
sdk=iphonesimulator
|
|
48
|
+
triple=x86_64-apple-ios17.0-simulator
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "unsupported Apple target: $target" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
if [[ "$platform" == macos ]]; then
|
|
57
|
+
bundle=$output
|
|
58
|
+
contents=$bundle/Contents
|
|
59
|
+
executable_dir=$contents/MacOS
|
|
60
|
+
app_dir=$contents/Resources/app
|
|
61
|
+
else
|
|
62
|
+
bundle=$output
|
|
63
|
+
contents=$bundle
|
|
64
|
+
executable_dir=$bundle
|
|
65
|
+
app_dir=$bundle/app
|
|
66
|
+
fi
|
|
67
|
+
icons=$input/icons/$platform
|
|
68
|
+
icon_info_plist=$temporary/icon-info.plist
|
|
69
|
+
|
|
70
|
+
sign_bundle() {
|
|
71
|
+
if [[ "$platform" == ios ]]; then
|
|
72
|
+
signing_args=(
|
|
73
|
+
sign
|
|
74
|
+
--project "$project_dir"
|
|
75
|
+
--bundle "$bundle"
|
|
76
|
+
--bundle-id "$identifier"
|
|
77
|
+
)
|
|
78
|
+
if [[ -f "$metadata/device-id" ]]; then
|
|
79
|
+
signing_args+=(--device-id "$(cat "$metadata/device-id")")
|
|
80
|
+
fi
|
|
81
|
+
"$apple_tool" "${signing_args[@]}"
|
|
82
|
+
else
|
|
83
|
+
codesign --force --timestamp=none --sign - "$bundle"
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
bundle_key_file=$output_parent/.tokamak/$(basename "$output").input-key
|
|
88
|
+
if [[ -f "$metadata/input-key" ]]; then
|
|
89
|
+
plist_path=
|
|
90
|
+
case "$platform" in
|
|
91
|
+
ios|ios-simulator) plist_path=${TOKAMAK_IOS_PLIST:-} ;;
|
|
92
|
+
macos) plist_path=${TOKAMAK_MACOS_PLIST:-} ;;
|
|
93
|
+
esac
|
|
94
|
+
if [[ -n "$plist_path" && "$plist_path" != /* ]]; then
|
|
95
|
+
plist_path=$project_dir/$plist_path
|
|
96
|
+
fi
|
|
97
|
+
bundle_key=$(
|
|
98
|
+
{
|
|
99
|
+
cat "$metadata/input-key"
|
|
100
|
+
shasum -a 256 "$0" "$apple_tool"
|
|
101
|
+
if [[ -n "$plist_path" ]]; then
|
|
102
|
+
shasum -a 256 "$plist_path"
|
|
103
|
+
fi
|
|
104
|
+
env | LC_ALL=C sort | sed -n '/^TOKAMAK_/p' |
|
|
105
|
+
sed '/^TOKAMAK_IOS_TEAM_ID=/d; /^TOKAMAK_IOS_SIGNING_IDENTITY=/d; /^TOKAMAK_IOS_PROVISIONING_PROFILE=/d'
|
|
106
|
+
printf '%s\n' "${DEVELOPER_DIR:-}" "$PATH"
|
|
107
|
+
xcodebuild -version
|
|
108
|
+
} | shasum -a 256 | cut -d ' ' -f 1
|
|
109
|
+
)
|
|
110
|
+
if [[ -d "$bundle" && -f "$bundle_key_file" && $(cat "$bundle_key_file") == "$bundle_key" ]]; then
|
|
111
|
+
cp "$input/app/worker-environment.json" "$app_dir/worker-environment.json"
|
|
112
|
+
sign_bundle
|
|
113
|
+
exit 0
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
rm -f "$bundle_key_file"
|
|
118
|
+
rm -rf "$bundle"
|
|
119
|
+
mkdir -p "$executable_dir" "$app_dir" "$module_cache"
|
|
120
|
+
cp -R "$input/app/." "$app_dir/"
|
|
121
|
+
|
|
122
|
+
if [[ ( "$platform" == macos || "$platform" == ios || "$platform" == ios-simulator ) && -d "$icons/AppIcon.icon" ]]; then
|
|
123
|
+
if [[ "$platform" == macos ]]; then
|
|
124
|
+
icon_output=$contents/Resources
|
|
125
|
+
icon_minimum=14.0
|
|
126
|
+
icon_targets=(--target-device mac)
|
|
127
|
+
else
|
|
128
|
+
icon_output=$bundle
|
|
129
|
+
icon_minimum=17.0
|
|
130
|
+
icon_targets=(--target-device iphone --target-device ipad)
|
|
131
|
+
fi
|
|
132
|
+
xcrun --sdk "$sdk" actool \
|
|
133
|
+
--compile "$icon_output" \
|
|
134
|
+
--platform "$sdk" \
|
|
135
|
+
--minimum-deployment-target "$icon_minimum" \
|
|
136
|
+
--app-icon AppIcon \
|
|
137
|
+
"${icon_targets[@]}" \
|
|
138
|
+
--output-partial-info-plist "$icon_info_plist" \
|
|
139
|
+
--output-format human-readable-text \
|
|
140
|
+
"$icons/AppIcon.icon"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
shopt -s nullglob
|
|
144
|
+
swift_sources=()
|
|
145
|
+
for source in "$native_shell"/*.swift; do
|
|
146
|
+
[[ -f "$source" ]] || continue
|
|
147
|
+
swift_sources+=("$source")
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
if [[ ! -f "$native_shell/TokamakShell.swift" ]]; then
|
|
151
|
+
echo "Apple native shell source is missing: $native_shell/TokamakShell.swift" >&2
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
for plugin in "$input/plugins"/*; do
|
|
156
|
+
[[ -d "$plugin" ]] || continue
|
|
157
|
+
for source in "$plugin/sources"/*; do
|
|
158
|
+
[[ -f "$source" ]] || continue
|
|
159
|
+
case "$source" in
|
|
160
|
+
*.swift) swift_sources+=("$source") ;;
|
|
161
|
+
*)
|
|
162
|
+
echo "Apple plugin source must be Swift: $source" >&2
|
|
163
|
+
exit 1
|
|
164
|
+
;;
|
|
165
|
+
esac
|
|
166
|
+
done
|
|
167
|
+
done
|
|
168
|
+
|
|
169
|
+
registry=$temporary/TokamakPluginRegistry.swift
|
|
170
|
+
{
|
|
171
|
+
printf '%s\n\n' 'import Foundation'
|
|
172
|
+
printf '%s\n' 'func tokamakPlugins() -> [any TokamakPlugin] {'
|
|
173
|
+
printf '%s\n' ' ['
|
|
174
|
+
for plugin in "$input/plugins"/*; do
|
|
175
|
+
[[ -d "$plugin" ]] || continue
|
|
176
|
+
printf ' %s(),\n' "$(cat "$plugin/class")"
|
|
177
|
+
done
|
|
178
|
+
printf '%s\n' ' ]'
|
|
179
|
+
printf '%s\n' '}'
|
|
180
|
+
} > "$registry"
|
|
181
|
+
|
|
182
|
+
swift_args=(
|
|
183
|
+
"${swift_sources[@]}"
|
|
184
|
+
"$registry"
|
|
185
|
+
-target "$triple"
|
|
186
|
+
-swift-version 5
|
|
187
|
+
-Osize
|
|
188
|
+
-whole-module-optimization
|
|
189
|
+
-module-cache-path "$module_cache"
|
|
190
|
+
-F "$framework_root"
|
|
191
|
+
-framework TokamakRuntime
|
|
192
|
+
-framework CoreFoundation
|
|
193
|
+
-lc++
|
|
194
|
+
-lresolv
|
|
195
|
+
-Xlinker -dead_strip
|
|
196
|
+
-Xlinker -no_exported_symbols
|
|
197
|
+
)
|
|
198
|
+
for plugin in "$input/plugins"/*; do
|
|
199
|
+
[[ -d "$plugin" ]] || continue
|
|
200
|
+
for framework_file in "$plugin/frameworks"/*; do
|
|
201
|
+
[[ -f "$framework_file" ]] || continue
|
|
202
|
+
swift_args+=(-framework "$(cat "$framework_file")")
|
|
203
|
+
done
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
xcrun --sdk "$sdk" swiftc "${swift_args[@]}" -o "$executable_dir/$app_slug"
|
|
207
|
+
xcrun --sdk "$sdk" strip "$executable_dir/$app_slug"
|
|
208
|
+
|
|
209
|
+
plist_args=(plist --input "$input" --output "$contents/Info.plist")
|
|
210
|
+
if [[ -f "$icon_info_plist" ]]; then
|
|
211
|
+
plist_args+=(--icon-info-plist "$icon_info_plist")
|
|
212
|
+
fi
|
|
213
|
+
"$apple_tool" "${plist_args[@]}"
|
|
214
|
+
|
|
215
|
+
sign_bundle
|
|
216
|
+
if [[ -n "${bundle_key:-}" ]]; then
|
|
217
|
+
mkdir -p "$(dirname "$bundle_key_file")"
|
|
218
|
+
printf '%s' "$bundle_key" > "$bundle_key_file"
|
|
219
|
+
fi
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#ifndef TOKAMAK_RUNTIME_H
|
|
2
|
+
#define TOKAMAK_RUNTIME_H
|
|
3
|
+
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
#include <stddef.h>
|
|
6
|
+
#include <stdint.h>
|
|
7
|
+
|
|
8
|
+
enum {
|
|
9
|
+
TOKAMAK_DECISION_DEFAULT = 0,
|
|
10
|
+
TOKAMAK_DECISION_CANCEL = 1,
|
|
11
|
+
TOKAMAK_DECISION_USE = 2,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
typedef struct {
|
|
15
|
+
uint8_t *data;
|
|
16
|
+
size_t len;
|
|
17
|
+
} TokamakBytes;
|
|
18
|
+
|
|
19
|
+
typedef struct {
|
|
20
|
+
TokamakBytes certificate;
|
|
21
|
+
TokamakBytes private_key;
|
|
22
|
+
} TokamakIdentity;
|
|
23
|
+
|
|
24
|
+
void *tokamak_runtime_start(const char *packaged_dir, const char *state_dir,
|
|
25
|
+
const char *host, char *error, size_t error_len);
|
|
26
|
+
void *tokamak_runtime_start_development(const char *state_dir, const char *host,
|
|
27
|
+
const char *endpoint,
|
|
28
|
+
const char *session_token, char *error,
|
|
29
|
+
size_t error_len);
|
|
30
|
+
uint16_t tokamak_runtime_port(const void *runtime);
|
|
31
|
+
uint16_t tokamak_runtime_restore_gateway(const void *runtime, char *error,
|
|
32
|
+
size_t error_len);
|
|
33
|
+
bool tokamak_runtime_suspend(const void *runtime);
|
|
34
|
+
bool tokamak_runtime_resume(const void *runtime);
|
|
35
|
+
void tokamak_runtime_stop(void *runtime);
|
|
36
|
+
|
|
37
|
+
int32_t tokamak_runtime_server_authority(const void *runtime, const char *host,
|
|
38
|
+
TokamakBytes *authority);
|
|
39
|
+
int32_t tokamak_runtime_client_identity(const void *runtime, const char *host,
|
|
40
|
+
size_t previous_failures,
|
|
41
|
+
TokamakIdentity *identity);
|
|
42
|
+
void tokamak_bytes_free(TokamakBytes bytes);
|
|
43
|
+
void tokamak_identity_free(TokamakIdentity identity);
|
|
44
|
+
|
|
45
|
+
#endif
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
struct TokamakPluginError: Error {
|
|
5
|
+
let name: String
|
|
6
|
+
let message: String
|
|
7
|
+
|
|
8
|
+
static func notSupported(_ message: String) -> Self {
|
|
9
|
+
Self(name: "NotSupportedError", message: message)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
typealias TokamakPluginReply = (Result<Any?, TokamakPluginError>) -> Void
|
|
14
|
+
|
|
15
|
+
protocol TokamakPlugin: AnyObject {
|
|
16
|
+
var id: String { get }
|
|
17
|
+
|
|
18
|
+
func call(
|
|
19
|
+
method: String,
|
|
20
|
+
arguments: Any,
|
|
21
|
+
reply: @escaping TokamakPluginReply
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
func subscribe(
|
|
25
|
+
method: String,
|
|
26
|
+
arguments: Any,
|
|
27
|
+
reply: @escaping TokamakPluginReply
|
|
28
|
+
) -> (() -> Void)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
extension TokamakPlugin {
|
|
32
|
+
func call(
|
|
33
|
+
method: String,
|
|
34
|
+
arguments: Any,
|
|
35
|
+
reply: @escaping TokamakPluginReply
|
|
36
|
+
) {
|
|
37
|
+
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func subscribe(
|
|
41
|
+
method: String,
|
|
42
|
+
arguments: Any,
|
|
43
|
+
reply: @escaping TokamakPluginReply
|
|
44
|
+
) -> (() -> Void) {
|
|
45
|
+
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
|
|
51
|
+
private struct RequestKey: Hashable {
|
|
52
|
+
let session: String
|
|
53
|
+
let id: Int
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private let host: String
|
|
57
|
+
private let plugins: [String: any TokamakPlugin]
|
|
58
|
+
private var cancellations: [RequestKey: () -> Void] = [:]
|
|
59
|
+
weak var webView: WKWebView?
|
|
60
|
+
|
|
61
|
+
init(host: String, plugins: [any TokamakPlugin]) {
|
|
62
|
+
self.host = host
|
|
63
|
+
var pluginsById: [String: any TokamakPlugin] = [:]
|
|
64
|
+
for plugin in plugins {
|
|
65
|
+
precondition(pluginsById[plugin.id] == nil, "Duplicate plugin ID")
|
|
66
|
+
pluginsById[plugin.id] = plugin
|
|
67
|
+
}
|
|
68
|
+
self.plugins = pluginsById
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func install(in controller: WKUserContentController) {
|
|
72
|
+
controller.add(self, name: "tokamak")
|
|
73
|
+
controller.addUserScript(
|
|
74
|
+
WKUserScript(
|
|
75
|
+
source: Self.bootstrap(host: host),
|
|
76
|
+
injectionTime: .atDocumentStart,
|
|
77
|
+
forMainFrameOnly: true
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func close() {
|
|
83
|
+
let cancelAll = Array(cancellations.values)
|
|
84
|
+
cancellations.removeAll()
|
|
85
|
+
for cancel in cancelAll {
|
|
86
|
+
cancel()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func userContentController(
|
|
91
|
+
_ userContentController: WKUserContentController,
|
|
92
|
+
didReceive message: WKScriptMessage
|
|
93
|
+
) {
|
|
94
|
+
let origin = message.frameInfo.securityOrigin
|
|
95
|
+
guard
|
|
96
|
+
message.frameInfo.isMainFrame,
|
|
97
|
+
origin.protocol == "https",
|
|
98
|
+
origin.host == host,
|
|
99
|
+
origin.port == 0 || origin.port == 443,
|
|
100
|
+
let encoded = message.body as? String,
|
|
101
|
+
let data = encoded.data(using: .utf8),
|
|
102
|
+
let request = try? JSONSerialization.jsonObject(with: data)
|
|
103
|
+
as? [String: Any],
|
|
104
|
+
let type = request["type"] as? String,
|
|
105
|
+
let session = request["session"] as? String
|
|
106
|
+
else {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if type == "reset" {
|
|
111
|
+
close()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
guard let id = request["id"] as? Int else { return }
|
|
115
|
+
let key = RequestKey(session: session, id: id)
|
|
116
|
+
if type == "cancel" {
|
|
117
|
+
cancellations.removeValue(forKey: key)?()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
dispatch(type: type, key: key, request: request)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func dispatch(
|
|
124
|
+
type: String,
|
|
125
|
+
key: RequestKey,
|
|
126
|
+
request: [String: Any]
|
|
127
|
+
) {
|
|
128
|
+
guard
|
|
129
|
+
let pluginId = request["plugin"] as? String,
|
|
130
|
+
let method = request["method"] as? String,
|
|
131
|
+
let plugin = plugins[pluginId]
|
|
132
|
+
else {
|
|
133
|
+
send(
|
|
134
|
+
key: key,
|
|
135
|
+
result: .failure(.notSupported("Plugin is not supported")),
|
|
136
|
+
done: true
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
let arguments = request["arguments"] ?? NSNull()
|
|
141
|
+
let reply: TokamakPluginReply = { [weak self] result in
|
|
142
|
+
self?.send(key: key, result: result, done: type == "call")
|
|
143
|
+
}
|
|
144
|
+
switch type {
|
|
145
|
+
case "call":
|
|
146
|
+
plugin.call(method: method, arguments: arguments, reply: reply)
|
|
147
|
+
case "subscribe":
|
|
148
|
+
let cancellation = plugin.subscribe(
|
|
149
|
+
method: method,
|
|
150
|
+
arguments: arguments,
|
|
151
|
+
reply: reply
|
|
152
|
+
)
|
|
153
|
+
cancellations[key] = cancellation
|
|
154
|
+
default:
|
|
155
|
+
send(
|
|
156
|
+
key: key,
|
|
157
|
+
result: .failure(.notSupported("Plugin operation is not supported")),
|
|
158
|
+
done: true
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func send(
|
|
164
|
+
key: RequestKey,
|
|
165
|
+
result: Result<Any?, TokamakPluginError>,
|
|
166
|
+
done: Bool
|
|
167
|
+
) {
|
|
168
|
+
if done {
|
|
169
|
+
cancellations.removeValue(forKey: key)?()
|
|
170
|
+
}
|
|
171
|
+
var response: [String: Any] = [
|
|
172
|
+
"session": key.session,
|
|
173
|
+
"id": key.id,
|
|
174
|
+
"done": done,
|
|
175
|
+
]
|
|
176
|
+
switch result {
|
|
177
|
+
case .success(let value):
|
|
178
|
+
response["value"] = value ?? NSNull()
|
|
179
|
+
case .failure(let error):
|
|
180
|
+
response["error"] = ["name": error.name, "message": error.message]
|
|
181
|
+
}
|
|
182
|
+
guard
|
|
183
|
+
JSONSerialization.isValidJSONObject(response),
|
|
184
|
+
let data = try? JSONSerialization.data(withJSONObject: response),
|
|
185
|
+
let json = String(data: data, encoding: .utf8)
|
|
186
|
+
else {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
DispatchQueue.main.async { [weak self] in
|
|
190
|
+
guard
|
|
191
|
+
let self,
|
|
192
|
+
self.webView?.url?.scheme == "https",
|
|
193
|
+
self.webView?.url?.host == self.host,
|
|
194
|
+
self.webView?.url?.port == nil || self.webView?.url?.port == 443
|
|
195
|
+
else {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
self.webView?.evaluateJavaScript(
|
|
199
|
+
"globalThis.__tokamakReceive?.(\(json))"
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private static func bootstrap(host: String) -> String {
|
|
205
|
+
"""
|
|
206
|
+
if (globalThis.location.origin === "https://\(host)") {
|
|
207
|
+
globalThis.__tokamakNative = {
|
|
208
|
+
onmessage: null,
|
|
209
|
+
postMessage(message) {
|
|
210
|
+
globalThis.webkit.messageHandlers.tokamak.postMessage(message);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
"""
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
import Security
|
|
4
|
+
import TokamakRuntime
|
|
5
|
+
import WebKit
|
|
6
|
+
|
|
7
|
+
#if os(macOS)
|
|
8
|
+
import AppKit
|
|
9
|
+
#else
|
|
10
|
+
import UIKit
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
private let startupErrorFile = "startup-error.log"
|
|
14
|
+
private let failurePage =
|
|
15
|
+
"<h1>App failed to start</h1><p>Check startup-error.log for details.</p>"
|
|
16
|
+
|
|
17
|
+
private enum ShellError: Error, CustomStringConvertible {
|
|
18
|
+
case configuration(String)
|
|
19
|
+
case runtime(String)
|
|
20
|
+
|
|
21
|
+
var description: String {
|
|
22
|
+
switch self {
|
|
23
|
+
case .configuration(let message), .runtime(let message):
|
|
24
|
+
return message
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private enum AuthenticationMaterial<Value> {
|
|
30
|
+
case defaultHandling
|
|
31
|
+
case cancel
|
|
32
|
+
case use(Value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private final class RuntimeHost {
|
|
36
|
+
let host: String
|
|
37
|
+
private var handle: UnsafeMutableRawPointer?
|
|
38
|
+
|
|
39
|
+
init() throws {
|
|
40
|
+
guard
|
|
41
|
+
let host = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
|
|
42
|
+
!host.isEmpty
|
|
43
|
+
else {
|
|
44
|
+
throw ShellError.configuration("TokamakHost is required")
|
|
45
|
+
}
|
|
46
|
+
self.host = host
|
|
47
|
+
|
|
48
|
+
let state = try Self.stateDirectory()
|
|
49
|
+
var error = [CChar](repeating: 0, count: 512)
|
|
50
|
+
if let endpoint = Bundle.main.object(forInfoDictionaryKey: "TokamakDevEndpoint") as? String,
|
|
51
|
+
let sessionToken = Bundle.main.object(forInfoDictionaryKey: "TokamakDevSessionToken")
|
|
52
|
+
as? String
|
|
53
|
+
{
|
|
54
|
+
handle = state.path.withCString { statePath in
|
|
55
|
+
host.withCString { host in
|
|
56
|
+
endpoint.withCString { endpoint in
|
|
57
|
+
sessionToken.withCString { sessionToken in
|
|
58
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
59
|
+
tokamak_runtime_start_development(
|
|
60
|
+
statePath,
|
|
61
|
+
host,
|
|
62
|
+
endpoint,
|
|
63
|
+
sessionToken,
|
|
64
|
+
error.baseAddress,
|
|
65
|
+
error.count
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
let bundle = Bundle.main.resourceURL?.appendingPathComponent("app")
|
|
74
|
+
guard let bundle else {
|
|
75
|
+
throw ShellError.configuration("app bundle resources are unavailable")
|
|
76
|
+
}
|
|
77
|
+
handle = bundle.path.withCString { bundlePath in
|
|
78
|
+
state.path.withCString { statePath in
|
|
79
|
+
host.withCString { host in
|
|
80
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
81
|
+
tokamak_runtime_start(
|
|
82
|
+
bundlePath,
|
|
83
|
+
statePath,
|
|
84
|
+
host,
|
|
85
|
+
error.baseAddress,
|
|
86
|
+
error.count
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
guard handle != nil else {
|
|
94
|
+
throw ShellError.runtime(String(cString: error))
|
|
95
|
+
}
|
|
96
|
+
try? FileManager.default.removeItem(
|
|
97
|
+
at: state.appendingPathComponent(startupErrorFile)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
deinit {
|
|
102
|
+
if let handle {
|
|
103
|
+
tokamak_runtime_stop(handle)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var port: UInt16 {
|
|
108
|
+
tokamak_runtime_port(handle)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func restoreGateway() throws -> UInt16 {
|
|
112
|
+
var error = [CChar](repeating: 0, count: 512)
|
|
113
|
+
let port = error.withUnsafeMutableBufferPointer { error in
|
|
114
|
+
tokamak_runtime_restore_gateway(
|
|
115
|
+
handle,
|
|
116
|
+
error.baseAddress,
|
|
117
|
+
error.count
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
guard port != 0 else {
|
|
121
|
+
throw ShellError.runtime(String(cString: error))
|
|
122
|
+
}
|
|
123
|
+
return port
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func serverAuthority(
|
|
127
|
+
host: String
|
|
128
|
+
) -> AuthenticationMaterial<Data> {
|
|
129
|
+
var bytes = TokamakBytes(data: nil, len: 0)
|
|
130
|
+
let decision = host.withCString {
|
|
131
|
+
tokamak_runtime_server_authority(handle, $0, &bytes)
|
|
132
|
+
}
|
|
133
|
+
switch decision {
|
|
134
|
+
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
135
|
+
return .defaultHandling
|
|
136
|
+
case Int32(TOKAMAK_DECISION_USE):
|
|
137
|
+
defer { tokamak_bytes_free(bytes) }
|
|
138
|
+
guard let data = bytes.data else { return .cancel }
|
|
139
|
+
return .use(Data(bytes: data, count: bytes.len))
|
|
140
|
+
default:
|
|
141
|
+
return .cancel
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func clientIdentity(
|
|
146
|
+
host: String,
|
|
147
|
+
previousFailures: Int
|
|
148
|
+
) -> AuthenticationMaterial<(Data, Data)> {
|
|
149
|
+
var identity = TokamakIdentity(
|
|
150
|
+
certificate: TokamakBytes(data: nil, len: 0),
|
|
151
|
+
private_key: TokamakBytes(data: nil, len: 0)
|
|
152
|
+
)
|
|
153
|
+
let decision = host.withCString {
|
|
154
|
+
tokamak_runtime_client_identity(
|
|
155
|
+
handle,
|
|
156
|
+
$0,
|
|
157
|
+
max(previousFailures, 0),
|
|
158
|
+
&identity
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
switch decision {
|
|
162
|
+
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
163
|
+
return .defaultHandling
|
|
164
|
+
case Int32(TOKAMAK_DECISION_USE):
|
|
165
|
+
defer { tokamak_identity_free(identity) }
|
|
166
|
+
guard
|
|
167
|
+
let certificate = identity.certificate.data,
|
|
168
|
+
let privateKey = identity.private_key.data
|
|
169
|
+
else {
|
|
170
|
+
return .cancel
|
|
171
|
+
}
|
|
172
|
+
return .use(
|
|
173
|
+
(
|
|
174
|
+
Data(bytes: certificate, count: identity.certificate.len),
|
|
175
|
+
Data(bytes: privateKey, count: identity.private_key.len)
|
|
176
|
+
))
|
|
177
|
+
default:
|
|
178
|
+
return .cancel
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static func recordStartupFailure(_ error: Error) {
|
|
183
|
+
print("tokamak runtime startup failed: \(error)")
|
|
184
|
+
guard let state = try? stateDirectory() else { return }
|
|
185
|
+
try? String(describing: error).write(
|
|
186
|
+
to: state.appendingPathComponent(startupErrorFile),
|
|
187
|
+
atomically: true,
|
|
188
|
+
encoding: .utf8
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static func stateDirectory() throws -> URL {
|
|
193
|
+
guard let identifier = Bundle.main.bundleIdentifier else {
|
|
194
|
+
throw ShellError.configuration("CFBundleIdentifier is required")
|
|
195
|
+
}
|
|
196
|
+
let root = try FileManager.default.url(
|
|
197
|
+
for: .cachesDirectory,
|
|
198
|
+
in: .userDomainMask,
|
|
199
|
+
appropriateFor: nil,
|
|
200
|
+
create: true
|
|
201
|
+
)
|
|
202
|
+
let state = root.appendingPathComponent(identifier, isDirectory: true)
|
|
203
|
+
try FileManager.default.createDirectory(
|
|
204
|
+
at: state,
|
|
205
|
+
withIntermediateDirectories: true
|
|
206
|
+
)
|
|
207
|
+
return state
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDelegate {
|
|
212
|
+
private let runtime: RuntimeHost
|
|
213
|
+
private let pluginBridge: TokamakPluginBridge
|
|
214
|
+
|
|
215
|
+
init(runtime: RuntimeHost, pluginBridge: TokamakPluginBridge) {
|
|
216
|
+
self.runtime = runtime
|
|
217
|
+
self.pluginBridge = pluginBridge
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func webView(
|
|
221
|
+
_ webView: WKWebView,
|
|
222
|
+
didStartProvisionalNavigation navigation: WKNavigation?
|
|
223
|
+
) {
|
|
224
|
+
pluginBridge.close()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func webView(
|
|
228
|
+
_ webView: WKWebView,
|
|
229
|
+
decidePolicyFor navigationAction: WKNavigationAction,
|
|
230
|
+
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
|
231
|
+
) {
|
|
232
|
+
guard let url = navigationAction.request.url else {
|
|
233
|
+
decisionHandler(.cancel)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if navigationAction.targetFrame?.isMainFrame == false {
|
|
238
|
+
decisionHandler(.allow)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if isAppOrigin(url) {
|
|
243
|
+
if navigationAction.targetFrame == nil {
|
|
244
|
+
webView.load(navigationAction.request)
|
|
245
|
+
decisionHandler(.cancel)
|
|
246
|
+
} else {
|
|
247
|
+
decisionHandler(.allow)
|
|
248
|
+
}
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
openExternal(url)
|
|
253
|
+
decisionHandler(.cancel)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func webView(
|
|
257
|
+
_ webView: WKWebView,
|
|
258
|
+
decidePolicyFor navigationResponse: WKNavigationResponse,
|
|
259
|
+
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
|
|
260
|
+
) {
|
|
261
|
+
guard
|
|
262
|
+
navigationResponse.isForMainFrame,
|
|
263
|
+
let url = navigationResponse.response.url
|
|
264
|
+
else {
|
|
265
|
+
decisionHandler(.allow)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if isAppOrigin(url) {
|
|
270
|
+
decisionHandler(.allow)
|
|
271
|
+
} else {
|
|
272
|
+
openExternal(url)
|
|
273
|
+
decisionHandler(.cancel)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func webView(
|
|
278
|
+
_ webView: WKWebView,
|
|
279
|
+
createWebViewWith configuration: WKWebViewConfiguration,
|
|
280
|
+
for navigationAction: WKNavigationAction,
|
|
281
|
+
windowFeatures: WKWindowFeatures
|
|
282
|
+
) -> WKWebView? {
|
|
283
|
+
_ = (configuration, windowFeatures)
|
|
284
|
+
guard let url = navigationAction.request.url else { return nil }
|
|
285
|
+
if isAppOrigin(url) {
|
|
286
|
+
webView.load(navigationAction.request)
|
|
287
|
+
} else {
|
|
288
|
+
openExternal(url)
|
|
289
|
+
}
|
|
290
|
+
return nil
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func webView(
|
|
294
|
+
_ webView: WKWebView,
|
|
295
|
+
didReceive challenge: URLAuthenticationChallenge,
|
|
296
|
+
completionHandler:
|
|
297
|
+
@escaping (
|
|
298
|
+
URLSession.AuthChallengeDisposition,
|
|
299
|
+
URLCredential?
|
|
300
|
+
) -> Void
|
|
301
|
+
) {
|
|
302
|
+
let space = challenge.protectionSpace
|
|
303
|
+
switch space.authenticationMethod {
|
|
304
|
+
case NSURLAuthenticationMethodServerTrust:
|
|
305
|
+
answer(
|
|
306
|
+
runtime.serverAuthority(host: space.host),
|
|
307
|
+
trust: space.serverTrust,
|
|
308
|
+
completion: completionHandler
|
|
309
|
+
)
|
|
310
|
+
case NSURLAuthenticationMethodClientCertificate:
|
|
311
|
+
answer(
|
|
312
|
+
runtime.clientIdentity(
|
|
313
|
+
host: space.host,
|
|
314
|
+
previousFailures: challenge.previousFailureCount
|
|
315
|
+
),
|
|
316
|
+
completion: completionHandler
|
|
317
|
+
)
|
|
318
|
+
default:
|
|
319
|
+
completionHandler(.performDefaultHandling, nil)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
func webView(
|
|
324
|
+
_ webView: WKWebView,
|
|
325
|
+
didFailProvisionalNavigation navigation: WKNavigation?,
|
|
326
|
+
withError error: Error
|
|
327
|
+
) {
|
|
328
|
+
print("tokamak WebView navigation failed: \(error)")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private func isAppOrigin(_ url: URL) -> Bool {
|
|
332
|
+
guard
|
|
333
|
+
url.scheme?.caseInsensitiveCompare("https") == .orderedSame,
|
|
334
|
+
url.host?.caseInsensitiveCompare(runtime.host) == .orderedSame
|
|
335
|
+
else {
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
return url.port == nil || url.port == 443
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func openExternal(_ url: URL) {
|
|
342
|
+
#if os(iOS)
|
|
343
|
+
UIApplication.shared.open(url)
|
|
344
|
+
#else
|
|
345
|
+
NSWorkspace.shared.open(url)
|
|
346
|
+
#endif
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private func answer(
|
|
350
|
+
_ material: AuthenticationMaterial<Data>,
|
|
351
|
+
trust: SecTrust?,
|
|
352
|
+
completion:
|
|
353
|
+
@escaping (
|
|
354
|
+
URLSession.AuthChallengeDisposition,
|
|
355
|
+
URLCredential?
|
|
356
|
+
) -> Void
|
|
357
|
+
) {
|
|
358
|
+
switch material {
|
|
359
|
+
case .defaultHandling:
|
|
360
|
+
completion(.performDefaultHandling, nil)
|
|
361
|
+
case .cancel:
|
|
362
|
+
completion(.cancelAuthenticationChallenge, nil)
|
|
363
|
+
case .use(let authority):
|
|
364
|
+
guard
|
|
365
|
+
let trust,
|
|
366
|
+
let certificate = SecCertificateCreateWithData(
|
|
367
|
+
nil,
|
|
368
|
+
authority as CFData
|
|
369
|
+
),
|
|
370
|
+
SecTrustSetAnchorCertificates(
|
|
371
|
+
trust,
|
|
372
|
+
[certificate] as CFArray
|
|
373
|
+
) == errSecSuccess,
|
|
374
|
+
SecTrustSetAnchorCertificatesOnly(trust, true) == errSecSuccess
|
|
375
|
+
else {
|
|
376
|
+
completion(.cancelAuthenticationChallenge, nil)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
let queue = DispatchQueue.global(qos: .userInitiated)
|
|
380
|
+
queue.async {
|
|
381
|
+
SecTrustEvaluateAsyncWithError(trust, queue) { trust, trusted, _ in
|
|
382
|
+
DispatchQueue.main.async {
|
|
383
|
+
guard trusted else {
|
|
384
|
+
completion(.cancelAuthenticationChallenge, nil)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
completion(.useCredential, URLCredential(trust: trust))
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private func answer(
|
|
395
|
+
_ material: AuthenticationMaterial<(Data, Data)>,
|
|
396
|
+
completion: (
|
|
397
|
+
URLSession.AuthChallengeDisposition,
|
|
398
|
+
URLCredential?
|
|
399
|
+
) -> Void
|
|
400
|
+
) {
|
|
401
|
+
switch material {
|
|
402
|
+
case .defaultHandling:
|
|
403
|
+
completion(.performDefaultHandling, nil)
|
|
404
|
+
case .cancel:
|
|
405
|
+
completion(.cancelAuthenticationChallenge, nil)
|
|
406
|
+
case .use(let value):
|
|
407
|
+
guard
|
|
408
|
+
let identity = identity(
|
|
409
|
+
certificate: value.0,
|
|
410
|
+
privateKey: value.1
|
|
411
|
+
)
|
|
412
|
+
else {
|
|
413
|
+
completion(.cancelAuthenticationChallenge, nil)
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
completion(
|
|
417
|
+
.useCredential,
|
|
418
|
+
URLCredential(
|
|
419
|
+
identity: identity,
|
|
420
|
+
certificates: nil,
|
|
421
|
+
persistence: .none
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private func identity(
|
|
428
|
+
certificate: Data,
|
|
429
|
+
privateKey: Data
|
|
430
|
+
) -> SecIdentity? {
|
|
431
|
+
guard
|
|
432
|
+
let certificate = SecCertificateCreateWithData(
|
|
433
|
+
nil,
|
|
434
|
+
certificate as CFData
|
|
435
|
+
)
|
|
436
|
+
else {
|
|
437
|
+
return nil
|
|
438
|
+
}
|
|
439
|
+
let attributes: [CFString: Any] = [
|
|
440
|
+
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
|
441
|
+
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
|
442
|
+
kSecAttrKeySizeInBits: 256,
|
|
443
|
+
]
|
|
444
|
+
guard
|
|
445
|
+
let key = SecKeyCreateWithData(
|
|
446
|
+
privateKey as CFData,
|
|
447
|
+
attributes as CFDictionary,
|
|
448
|
+
nil
|
|
449
|
+
)
|
|
450
|
+
else {
|
|
451
|
+
return nil
|
|
452
|
+
}
|
|
453
|
+
return SecIdentityCreate(nil, certificate, key)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private final class TokamakController {
|
|
458
|
+
private var runtime: RuntimeHost?
|
|
459
|
+
private var navigation: NavigationDelegate?
|
|
460
|
+
private var pluginBridge: TokamakPluginBridge?
|
|
461
|
+
private weak var activeWebView: WKWebView?
|
|
462
|
+
private var dataStore: WKWebsiteDataStore?
|
|
463
|
+
private var proxyPort: UInt16?
|
|
464
|
+
|
|
465
|
+
func start(
|
|
466
|
+
frame: CGRect,
|
|
467
|
+
completion: @escaping (WKWebView) -> Void
|
|
468
|
+
) {
|
|
469
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
470
|
+
let result = Result { try RuntimeHost() }
|
|
471
|
+
DispatchQueue.main.async {
|
|
472
|
+
switch result {
|
|
473
|
+
case .success(let runtime):
|
|
474
|
+
self.runtime = runtime
|
|
475
|
+
completion(self.webView(frame: frame, runtime: runtime))
|
|
476
|
+
case .failure(let error):
|
|
477
|
+
RuntimeHost.recordStartupFailure(error)
|
|
478
|
+
completion(self.failureWebView(frame: frame))
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
func restoreGateway() {
|
|
485
|
+
guard let runtime, let dataStore else { return }
|
|
486
|
+
do {
|
|
487
|
+
let port = try runtime.restoreGateway()
|
|
488
|
+
guard port != proxyPort else { return }
|
|
489
|
+
setProxy(port: port, host: runtime.host, dataStore: dataStore)
|
|
490
|
+
proxyPort = port
|
|
491
|
+
activeWebView?.reload()
|
|
492
|
+
} catch {
|
|
493
|
+
print("tokamak gateway could not recover: \(error)")
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func webView(frame: CGRect, runtime: RuntimeHost) -> WKWebView {
|
|
498
|
+
let configuration = WKWebViewConfiguration()
|
|
499
|
+
let dataStore = WKWebsiteDataStore.default()
|
|
500
|
+
let port = runtime.port
|
|
501
|
+
setProxy(port: port, host: runtime.host, dataStore: dataStore)
|
|
502
|
+
self.dataStore = dataStore
|
|
503
|
+
self.proxyPort = port
|
|
504
|
+
configuration.websiteDataStore = dataStore
|
|
505
|
+
|
|
506
|
+
let pluginBridge = TokamakPluginBridge(
|
|
507
|
+
host: runtime.host,
|
|
508
|
+
plugins: tokamakPlugins()
|
|
509
|
+
)
|
|
510
|
+
pluginBridge.install(in: configuration.userContentController)
|
|
511
|
+
let navigation = NavigationDelegate(
|
|
512
|
+
runtime: runtime,
|
|
513
|
+
pluginBridge: pluginBridge
|
|
514
|
+
)
|
|
515
|
+
self.navigation = navigation
|
|
516
|
+
let webView = WKWebView(frame: frame, configuration: configuration)
|
|
517
|
+
#if os(iOS)
|
|
518
|
+
webView.scrollView.bounces = false
|
|
519
|
+
#endif
|
|
520
|
+
webView.allowsLinkPreview = false
|
|
521
|
+
activeWebView = webView
|
|
522
|
+
pluginBridge.webView = webView
|
|
523
|
+
self.pluginBridge = pluginBridge
|
|
524
|
+
webView.navigationDelegate = navigation
|
|
525
|
+
webView.uiDelegate = navigation
|
|
526
|
+
webView.load(URLRequest(url: URL(string: "https://\(runtime.host)/")!))
|
|
527
|
+
return webView
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private func setProxy(
|
|
531
|
+
port: UInt16,
|
|
532
|
+
host: String,
|
|
533
|
+
dataStore: WKWebsiteDataStore
|
|
534
|
+
) {
|
|
535
|
+
var proxy = ProxyConfiguration(
|
|
536
|
+
httpCONNECTProxy: .hostPort(
|
|
537
|
+
host: "127.0.0.1",
|
|
538
|
+
port: NWEndpoint.Port(rawValue: port)!
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
proxy.matchDomains = [host]
|
|
542
|
+
proxy.allowFailover = false
|
|
543
|
+
dataStore.proxyConfigurations = [proxy]
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private func failureWebView(frame: CGRect) -> WKWebView {
|
|
547
|
+
let webView = WKWebView(frame: frame)
|
|
548
|
+
webView.loadHTMLString(failurePage, baseURL: nil)
|
|
549
|
+
return webView
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#if os(macOS)
|
|
554
|
+
private final class TokamakMacApplicationDelegate: NSObject, NSApplicationDelegate {
|
|
555
|
+
private let controller = TokamakController()
|
|
556
|
+
private var window: NSWindow?
|
|
557
|
+
|
|
558
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
559
|
+
let window = NSWindow(
|
|
560
|
+
contentRect: NSRect(x: 0, y: 0, width: 1024, height: 768),
|
|
561
|
+
styleMask: [
|
|
562
|
+
.titled,
|
|
563
|
+
.closable,
|
|
564
|
+
.miniaturizable,
|
|
565
|
+
.resizable,
|
|
566
|
+
],
|
|
567
|
+
backing: .buffered,
|
|
568
|
+
defer: false
|
|
569
|
+
)
|
|
570
|
+
window.title =
|
|
571
|
+
Bundle.main.object(
|
|
572
|
+
forInfoDictionaryKey: "CFBundleName"
|
|
573
|
+
) as? String ?? "tokamak"
|
|
574
|
+
window.center()
|
|
575
|
+
window.makeKeyAndOrderFront(nil)
|
|
576
|
+
NSApplication.shared.activate(ignoringOtherApps: true)
|
|
577
|
+
self.window = window
|
|
578
|
+
|
|
579
|
+
guard let content = window.contentView else { return }
|
|
580
|
+
controller.start(frame: content.bounds) { webView in
|
|
581
|
+
webView.autoresizingMask = [.width, .height]
|
|
582
|
+
content.addSubview(webView)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
func applicationShouldTerminateAfterLastWindowClosed(
|
|
587
|
+
_ sender: NSApplication
|
|
588
|
+
) -> Bool {
|
|
589
|
+
true
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
@main
|
|
594
|
+
private enum TokamakApplication {
|
|
595
|
+
static func main() {
|
|
596
|
+
let application = NSApplication.shared
|
|
597
|
+
let delegate = TokamakMacApplicationDelegate()
|
|
598
|
+
application.setActivationPolicy(.regular)
|
|
599
|
+
application.delegate = delegate
|
|
600
|
+
application.run()
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
#else
|
|
604
|
+
private final class TokamakIOSApplicationDelegate: UIResponder, UIApplicationDelegate {
|
|
605
|
+
private let controller = TokamakController()
|
|
606
|
+
var window: UIWindow?
|
|
607
|
+
|
|
608
|
+
func application(
|
|
609
|
+
_ application: UIApplication,
|
|
610
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
611
|
+
) -> Bool {
|
|
612
|
+
let window = UIWindow(frame: UIScreen.main.bounds)
|
|
613
|
+
let viewController = UIViewController()
|
|
614
|
+
window.rootViewController = viewController
|
|
615
|
+
window.makeKeyAndVisible()
|
|
616
|
+
self.window = window
|
|
617
|
+
|
|
618
|
+
controller.start(frame: viewController.view.bounds) { webView in
|
|
619
|
+
webView.autoresizingMask = [
|
|
620
|
+
.flexibleWidth,
|
|
621
|
+
.flexibleHeight,
|
|
622
|
+
]
|
|
623
|
+
viewController.view.addSubview(webView)
|
|
624
|
+
}
|
|
625
|
+
return true
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
func applicationWillEnterForeground(_ application: UIApplication) {
|
|
629
|
+
controller.restoreGateway()
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
@main
|
|
635
|
+
private enum TokamakApplication {
|
|
636
|
+
static func main() {
|
|
637
|
+
UIApplicationMain(
|
|
638
|
+
CommandLine.argc,
|
|
639
|
+
CommandLine.unsafeArgv,
|
|
640
|
+
nil,
|
|
641
|
+
NSStringFromClass(TokamakIOSApplicationDelegate.self)
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
#endif
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tokamakVersion": "0.1.1",
|
|
3
|
+
"target": "ios-simulator-arm64",
|
|
4
|
+
"artifacts": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "runtimeLibrary",
|
|
7
|
+
"path": "frameworks/TokamakRuntime.framework"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"kind": "nativeShellDirectory",
|
|
11
|
+
"path": "native-shell"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"kind": "esbuildExecutable",
|
|
15
|
+
"path": "tools/runtime/node_modules/esbuild/bin/esbuild"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"requiredTools": [
|
|
19
|
+
"xcrun"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esbuild/darwin-arm64",
|
|
3
|
+
"version": "0.28.1",
|
|
4
|
+
"description": "The macOS ARM 64-bit binary for esbuild, a JavaScript bundler.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/evanw/esbuild.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"preferUnplugged": true,
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"arm64"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esbuild/darwin-x64",
|
|
3
|
+
"version": "0.28.1",
|
|
4
|
+
"description": "The macOS 64-bit binary for esbuild, a JavaScript bundler.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/evanw/esbuild.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"preferUnplugged": true,
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"x64"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esbuild/linux-x64",
|
|
3
|
+
"version": "0.28.1",
|
|
4
|
+
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/evanw/esbuild.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"preferUnplugged": true,
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"os": [
|
|
15
|
+
"linux"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"x64"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esbuild/win32-x64",
|
|
3
|
+
"version": "0.28.1",
|
|
4
|
+
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/evanw/esbuild.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"preferUnplugged": true,
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"os": [
|
|
15
|
+
"win32"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"x64"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Evan Wallace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { spawnSync } = require("node:child_process");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
function esbuildPath(platform, arch) {
|
|
5
|
+
const hosts = ["darwin-arm64", "darwin-x64", "linux-x64", "win32-x64"];
|
|
6
|
+
const host = `${platform}-${arch}`;
|
|
7
|
+
if (!hosts.includes(host)) {
|
|
8
|
+
throw new Error(`tokamak does not provide esbuild for ${host}`);
|
|
9
|
+
}
|
|
10
|
+
const executable = platform === "win32" ? "esbuild.exe" : "bin/esbuild";
|
|
11
|
+
return path.resolve(__dirname, "../..", "@esbuild", host, executable);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (require.main === module) {
|
|
15
|
+
const result = spawnSync(esbuildPath(process.platform, process.arch), process.argv.slice(2), {
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
});
|
|
18
|
+
if (result.error) {
|
|
19
|
+
throw result.error;
|
|
20
|
+
}
|
|
21
|
+
process.exit(result.status ?? 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { esbuildPath };
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokamakdev/platform-ios-simulator-arm64",
|
|
3
|
+
"version": "0.1.1-beta.38",
|
|
4
|
+
"description": "The tokamak platform pack for ios-simulator-arm64",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/mantty/tokamak.git",
|
|
8
|
+
"directory": "tokamak-cli/npm/platform-packs/ios-simulator-arm64"
|
|
9
|
+
},
|
|
10
|
+
"os": [
|
|
11
|
+
"darwin"
|
|
12
|
+
],
|
|
13
|
+
"cpu": [
|
|
14
|
+
"arm64"
|
|
15
|
+
],
|
|
16
|
+
"preferUnplugged": true,
|
|
17
|
+
"files": [
|
|
18
|
+
"ios-simulator-arm64"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
}
|
|
23
|
+
}
|