@driftengine/package 3.61.0 → 3.61.2
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/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/9.2.0/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/app/build.gradle.kts +91 -0
- package/android/app/src/main/AndroidManifest.xml +33 -0
- package/android/app/src/main/java/dev/driftengine/host/HostBridge.java +334 -0
- package/android/app/src/main/java/dev/driftengine/host/MainActivity.java +347 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/values/strings.xml +11 -0
- package/android/build/reports/problems/problems-report.html +659 -0
- package/android/build.gradle.kts +3 -0
- package/android/gradle.properties +4 -0
- package/android/settings.gradle.kts +24 -0
- package/assets/icon.png +0 -0
- package/assets/lockup.svg +45 -0
- package/assets/mark-plain.svg +23 -0
- package/assets/mark-small.svg +22 -0
- package/assets/mark.svg +24 -0
- package/assets/originProbe.html +117 -0
- package/assets/splash.html +119 -0
- package/ios/DriftHost/AppDelegate.swift +23 -0
- package/ios/DriftHost/HostBridge.swift +159 -0
- package/ios/DriftHost/SchemeHandler.swift +130 -0
- package/ios/DriftHost/ViewController.swift +126 -0
- package/ios/project.yml +57 -0
- package/package.json +7 -2
- package/scripts/build-macos.sh +71 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// Serves the game out of the app bundle over a scheme that is an origin.
|
|
5
|
+
///
|
|
6
|
+
/// **`file://` is not an origin**, and a game loaded from one fails to import its own modules —
|
|
7
|
+
/// which reads as a broken build rather than as a missing origin. A custom scheme handled here is
|
|
8
|
+
/// one, and it costs no listening socket inside a game.
|
|
9
|
+
///
|
|
10
|
+
/// **The host is `localhost`, and that is the whole of why this is a secure context.**
|
|
11
|
+
///
|
|
12
|
+
/// A custom scheme is *not* trustworthy to WebKit on its own. What rescues this one is the host:
|
|
13
|
+
/// W3C's "is origin potentially trustworthy" decides on the **host** before it considers any
|
|
14
|
+
/// scheme beyond https — step 4 returns trustworthy for a host in `127.0.0.0/8` or `::1/128`, and
|
|
15
|
+
/// step 5 for a host of `localhost`, and **neither step mentions the scheme**. So
|
|
16
|
+
/// `drift://localhost/…` is a secure context and `drift://app/…` is not, and the two differ by six
|
|
17
|
+
/// characters.
|
|
18
|
+
///
|
|
19
|
+
/// **Do not confuse this with the other well-known custom-scheme problem, which is real and is not
|
|
20
|
+
/// this one.** An `https://` page cannot fetch a subresource from a custom scheme — WebKit blocks
|
|
21
|
+
/// it as mixed content — and the workaround written up for that is `_registerURLSchemeAsSecure:`,
|
|
22
|
+
/// a private selector on `WKProcessPool` that risks a review rejection. That is a question about a
|
|
23
|
+
/// custom scheme used as a *subresource origin underneath an https document*. This is a question
|
|
24
|
+
/// about the *document's own* origin, and the answers are different. Nothing private is used here,
|
|
25
|
+
/// and nothing needs to be.
|
|
26
|
+
///
|
|
27
|
+
/// This is what Capacitor has shipped on iOS for years — `capacitor://localhost`, with their own
|
|
28
|
+
/// documentation recommending the hostname stay `localhost` precisely because it "allows the use of
|
|
29
|
+
/// Web APIs that would otherwise require a secure context". Millions of installed applications are
|
|
30
|
+
/// the evidence; no device here was needed to read it.
|
|
31
|
+
///
|
|
32
|
+
/// **So there is no loopback server**, which was the alternative this was going to cost: a
|
|
33
|
+
/// listening socket inside a game, a port that can be taken, and a permission prompt on some
|
|
34
|
+
/// platforms.
|
|
35
|
+
///
|
|
36
|
+
/// **The containment check is not optional.** A scheme handler that resolves a path and serves it
|
|
37
|
+
/// is a filesystem the page can walk, so every request is resolved and then checked to be inside
|
|
38
|
+
/// the bundle root — the same rule the desktop shell's `resolveWithinRoot` enforces.
|
|
39
|
+
final class SchemeHandler: NSObject, WKURLSchemeHandler {
|
|
40
|
+
static let scheme = "drift"
|
|
41
|
+
|
|
42
|
+
/// The reserved prefix the packager's own assets are served under.
|
|
43
|
+
///
|
|
44
|
+
/// One host rather than two, unlike the desktop shell's `app` and `shell`: a second host would
|
|
45
|
+
/// not be `localhost`, and would therefore not be a secure context. A path prefix costs
|
|
46
|
+
/// nothing and keeps the whole application on the one origin that is trustworthy.
|
|
47
|
+
static let shellPrefix = "/__drift/"
|
|
48
|
+
|
|
49
|
+
private let root: URL
|
|
50
|
+
private let shell: URL
|
|
51
|
+
|
|
52
|
+
init(root: URL, shell: URL) {
|
|
53
|
+
self.root = root.standardizedFileURL
|
|
54
|
+
self.shell = shell.standardizedFileURL
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func webView(_ webView: WKWebView, start task: WKURLSchemeTask) {
|
|
58
|
+
guard let url = task.request.url else {
|
|
59
|
+
task.didFailWithError(URLError(.badURL))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let path = url.path.isEmpty || url.path == "/" ? "/index.html" : url.path
|
|
64
|
+
let shellAsset = path.hasPrefix(Self.shellPrefix)
|
|
65
|
+
let base = shellAsset ? shell : root
|
|
66
|
+
let relative = shellAsset ? String(path.dropFirst(Self.shellPrefix.count - 1)) : path
|
|
67
|
+
let resolved = base.appendingPathComponent(relative).standardizedFileURL
|
|
68
|
+
|
|
69
|
+
// A prefix check on the string alone accepts a sibling directory whose name starts with
|
|
70
|
+
// the root's; the separator is what makes this a directory check.
|
|
71
|
+
guard resolved.path == base.path || resolved.path.hasPrefix(base.path + "/") else {
|
|
72
|
+
task.didFailWithError(URLError(.noPermissionsToReadFile))
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
guard let data = try? Data(contentsOf: resolved) else {
|
|
77
|
+
task.didFailWithError(URLError(.fileDoesNotExist))
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let response = HTTPURLResponse(
|
|
82
|
+
url: url,
|
|
83
|
+
statusCode: 200,
|
|
84
|
+
httpVersion: "HTTP/1.1",
|
|
85
|
+
headerFields: [
|
|
86
|
+
"Content-Type": Self.contentType(for: resolved.pathExtension.lowercased()),
|
|
87
|
+
"Content-Length": String(data.count),
|
|
88
|
+
// The same policy the desktop shell serves: everything a game does is allowed and
|
|
89
|
+
// a script from somewhere else is not.
|
|
90
|
+
"Content-Security-Policy":
|
|
91
|
+
"default-src 'self' drift: blob: data:; "
|
|
92
|
+
+ "script-src 'self' drift: blob: 'unsafe-inline' 'wasm-unsafe-eval'; "
|
|
93
|
+
+ "style-src 'self' drift: 'unsafe-inline'; "
|
|
94
|
+
+ "img-src 'self' drift: blob: data: https:; "
|
|
95
|
+
+ "media-src 'self' drift: blob: data: https:; "
|
|
96
|
+
+ "connect-src 'self' drift: blob: data: https: wss:",
|
|
97
|
+
]
|
|
98
|
+
)!
|
|
99
|
+
|
|
100
|
+
task.didReceive(response)
|
|
101
|
+
task.didReceive(data)
|
|
102
|
+
task.didFinish()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {
|
|
106
|
+
// Nothing is held between start and finish, so there is nothing to cancel.
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// **A wrong type is a silent load failure**, not a wrong picture: a module served as anything
|
|
110
|
+
/// but JavaScript is refused by the loader, and an SVG served as bytes renders as a broken
|
|
111
|
+
/// image — which is exactly how the desktop shell's splash failed once.
|
|
112
|
+
static func contentType(for ext: String) -> String {
|
|
113
|
+
switch ext {
|
|
114
|
+
case "html": return "text/html; charset=utf-8"
|
|
115
|
+
case "js", "mjs": return "text/javascript; charset=utf-8"
|
|
116
|
+
case "css": return "text/css; charset=utf-8"
|
|
117
|
+
case "json": return "application/json; charset=utf-8"
|
|
118
|
+
case "wasm": return "application/wasm"
|
|
119
|
+
case "svg": return "image/svg+xml"
|
|
120
|
+
case "png": return "image/png"
|
|
121
|
+
case "jpg", "jpeg": return "image/jpeg"
|
|
122
|
+
case "webp": return "image/webp"
|
|
123
|
+
case "ktx2": return "image/ktx2"
|
|
124
|
+
case "ogg": return "audio/ogg"
|
|
125
|
+
case "mp3": return "audio/mpeg"
|
|
126
|
+
case "wav": return "audio/wav"
|
|
127
|
+
default: return "application/octet-stream"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// One view controller, one `WKWebView`, and nothing else.
|
|
5
|
+
///
|
|
6
|
+
/// **The renderer here is WebKit and there is no choice about that.** Apple requires it for
|
|
7
|
+
/// anything that browses the web, so a bundled Chromium is not an option on this platform — which
|
|
8
|
+
/// is also why the engine's WebGL2 baseline matters: what the device offers is what the game gets,
|
|
9
|
+
/// and the acceptance probe decides whether WebGPU is among it.
|
|
10
|
+
///
|
|
11
|
+
/// **The bridge is injected at document start**, before the game's own scripts, the same timing
|
|
12
|
+
/// the desktop preload and Android's document-start script have. Everything readable synchronously
|
|
13
|
+
/// goes in as a literal; see `HostBridge` for why that is not a shortcut but the only shape this
|
|
14
|
+
/// platform allows.
|
|
15
|
+
final class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
|
|
16
|
+
private var web: WKWebView!
|
|
17
|
+
private var bridge: HostBridge!
|
|
18
|
+
|
|
19
|
+
override var prefersStatusBarHidden: Bool { true }
|
|
20
|
+
override var prefersHomeIndicatorAutoHidden: Bool { true }
|
|
21
|
+
|
|
22
|
+
override func viewDidLoad() {
|
|
23
|
+
super.viewDidLoad()
|
|
24
|
+
|
|
25
|
+
guard let root = Bundle.main.url(forResource: "www", withExtension: nil) else {
|
|
26
|
+
assertionFailure("the game was not copied into the bundle; see ios.ts")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
let shell = Bundle.main.url(forResource: "shell", withExtension: nil) ?? root
|
|
30
|
+
|
|
31
|
+
bridge = HostBridge(controller: self)
|
|
32
|
+
|
|
33
|
+
let configuration = WKWebViewConfiguration()
|
|
34
|
+
configuration.setURLSchemeHandler(
|
|
35
|
+
SchemeHandler(root: root, shell: shell), forURLScheme: SchemeHandler.scheme)
|
|
36
|
+
// A game's own audio is not an autoplaying advert, and the engine's mixer starts on the
|
|
37
|
+
// first input anyway — so the gesture requirement only ever delays the first sound.
|
|
38
|
+
configuration.allowsInlineMediaPlayback = true
|
|
39
|
+
configuration.mediaTypesRequiringUserActionForPlayback = []
|
|
40
|
+
|
|
41
|
+
let content = configuration.userContentController
|
|
42
|
+
content.addScriptMessageHandler(bridge, contentWorld: .page, name: HostBridge.name)
|
|
43
|
+
content.addUserScript(WKUserScript(
|
|
44
|
+
source: "globalThis.__driftHostState = \(bridge.stateJson());",
|
|
45
|
+
injectionTime: .atDocumentStart,
|
|
46
|
+
forMainFrameOnly: true
|
|
47
|
+
))
|
|
48
|
+
if let shim = Bundle.main.url(forResource: "drift-shim", withExtension: "js"),
|
|
49
|
+
let source = try? String(contentsOf: shim, encoding: .utf8) {
|
|
50
|
+
content.addUserScript(WKUserScript(
|
|
51
|
+
source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true))
|
|
52
|
+
} else {
|
|
53
|
+
// Loud rather than silent: the game runs and believes it is in a plain browser, which
|
|
54
|
+
// is a defined state and a surprising one to debug from the outside.
|
|
55
|
+
NSLog("[drift] the bridge shim is missing from the bundle; the game has no host")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
web = WKWebView(frame: view.bounds, configuration: configuration)
|
|
59
|
+
web.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
60
|
+
web.navigationDelegate = self
|
|
61
|
+
web.uiDelegate = self
|
|
62
|
+
// A game is not a document: rubber-banding at its edges reads as the whole screen coming
|
|
63
|
+
// loose, and the scroll view is the only thing that does it.
|
|
64
|
+
web.scrollView.bounces = false
|
|
65
|
+
web.scrollView.isScrollEnabled = false
|
|
66
|
+
web.isOpaque = false
|
|
67
|
+
web.backgroundColor = .black
|
|
68
|
+
view.backgroundColor = .black
|
|
69
|
+
view.addSubview(web)
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* **`localhost`, not `app`.** The host is what makes this a secure context — see the note
|
|
73
|
+
* in `SchemeHandler` — and the difference between the two is `crossOriginIsolated`,
|
|
74
|
+
* `SharedArrayBuffer`, the wake lock, and everything else gated on one.
|
|
75
|
+
*/
|
|
76
|
+
web.load(URLRequest(url: URL(string: "\(SchemeHandler.scheme)://localhost/index.html")!))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
80
|
+
super.viewWillTransition(to: size, with: coordinator)
|
|
81
|
+
// A rotation changes what the injected state says, and a bridge reading a stale object
|
|
82
|
+
// would report the previous orientation for the rest of the session.
|
|
83
|
+
coordinator.animate(alongsideTransition: nil) { [weak self] _ in
|
|
84
|
+
guard let self else { return }
|
|
85
|
+
self.web.evaluateJavaScript(
|
|
86
|
+
"Object.assign(globalThis.__driftHostState ?? {}, \(self.bridge.stateJson()))")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// A link to the outside opens in Safari, never inside the game.
|
|
91
|
+
///
|
|
92
|
+
/// The same rule the desktop shell follows: their browser has their session, their bookmarks
|
|
93
|
+
/// and a way back, and an in-app view is a browser nobody is maintaining. Only `http`, `https`
|
|
94
|
+
/// and `mailto` leave at all.
|
|
95
|
+
func webView(
|
|
96
|
+
_ webView: WKWebView,
|
|
97
|
+
decidePolicyFor action: WKNavigationAction,
|
|
98
|
+
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
|
99
|
+
) {
|
|
100
|
+
guard let url = action.request.url else {
|
|
101
|
+
decisionHandler(.cancel)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if url.scheme == SchemeHandler.scheme {
|
|
105
|
+
decisionHandler(.allow)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if ["http", "https", "mailto"].contains(url.scheme ?? "") {
|
|
109
|
+
UIApplication.shared.open(url)
|
|
110
|
+
}
|
|
111
|
+
decisionHandler(.cancel)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// `target="_blank"` arrives here rather than as a navigation, and goes the same way out.
|
|
115
|
+
func webView(
|
|
116
|
+
_ webView: WKWebView,
|
|
117
|
+
createWebViewWith configuration: WKWebViewConfiguration,
|
|
118
|
+
for action: WKNavigationAction,
|
|
119
|
+
windowFeatures: WKWindowFeatures
|
|
120
|
+
) -> WKWebView? {
|
|
121
|
+
if let url = action.request.url, ["http", "https", "mailto"].contains(url.scheme ?? "") {
|
|
122
|
+
UIApplication.shared.open(url)
|
|
123
|
+
}
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
}
|
package/ios/project.yml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The Xcode project, described rather than committed.
|
|
2
|
+
#
|
|
3
|
+
# The `__DRIFT_*__` placeholders are substituted by `src/ios.ts` when it stages a copy — plain text
|
|
4
|
+
# replacement rather than Xcode build settings, so what XcodeGen reads is a finished spec and there
|
|
5
|
+
# is one place a wrong value could come from.
|
|
6
|
+
#
|
|
7
|
+
# A `.xcodeproj` is a generated artefact with absolute paths and a merge conflict in every pull
|
|
8
|
+
# request; XcodeGen turns this file into one at build time, and `drift-package bootstrap
|
|
9
|
+
# --target=ios` fetches XcodeGen itself. Every value the packager substitutes is a placeholder
|
|
10
|
+
# here, and the defaults are visibly placeholders rather than plausible.
|
|
11
|
+
name: DriftHost
|
|
12
|
+
options:
|
|
13
|
+
bundleIdPrefix: dev.driftengine
|
|
14
|
+
deploymentTarget:
|
|
15
|
+
# 15 is where WKWebView's modern JavaScript and WebGL2 are dependable. WebGPU arrives at 26
|
|
16
|
+
# and the engine's acceptance probe decides whether it is usable, per target.
|
|
17
|
+
iOS: '15.0'
|
|
18
|
+
createIntermediateGroups: true
|
|
19
|
+
|
|
20
|
+
settings:
|
|
21
|
+
base:
|
|
22
|
+
PRODUCT_NAME: __DRIFT_APP_NAME__
|
|
23
|
+
MARKETING_VERSION: __DRIFT_VERSION_NAME__
|
|
24
|
+
CURRENT_PROJECT_VERSION: __DRIFT_VERSION_CODE__
|
|
25
|
+
# Signing is decided by the build, not by this file: an unsigned archive is what a sideload
|
|
26
|
+
# tool re-signs, and a team id turns automatic signing back on.
|
|
27
|
+
CODE_SIGN_STYLE: Automatic
|
|
28
|
+
DEVELOPMENT_TEAM: __DRIFT_TEAM_ID__
|
|
29
|
+
TARGETED_DEVICE_FAMILY: '1,2'
|
|
30
|
+
|
|
31
|
+
targets:
|
|
32
|
+
DriftHost:
|
|
33
|
+
type: application
|
|
34
|
+
platform: iOS
|
|
35
|
+
sources:
|
|
36
|
+
- path: DriftHost
|
|
37
|
+
# Written into the staging copy by the packager: the game, and the bridge shim beside it.
|
|
38
|
+
- path: www
|
|
39
|
+
type: folder
|
|
40
|
+
optional: true
|
|
41
|
+
- path: drift-shim.js
|
|
42
|
+
optional: true
|
|
43
|
+
info:
|
|
44
|
+
path: DriftHost/Info.plist
|
|
45
|
+
properties:
|
|
46
|
+
CFBundleDisplayName: __DRIFT_APP_NAME__
|
|
47
|
+
CFBundleIdentifier: __DRIFT_APP_ID__
|
|
48
|
+
UILaunchScreen: {}
|
|
49
|
+
# A game is landscape or portrait as it likes; the host locks nothing, and
|
|
50
|
+
# `ScreenPresentation.lockOrientation` reports iOS's refusal rather than hiding it.
|
|
51
|
+
UISupportedInterfaceOrientations:
|
|
52
|
+
- UIInterfaceOrientationPortrait
|
|
53
|
+
- UIInterfaceOrientationLandscapeLeft
|
|
54
|
+
- UIInterfaceOrientationLandscapeRight
|
|
55
|
+
UIRequiresFullScreen: true
|
|
56
|
+
UIStatusBarHidden: true
|
|
57
|
+
UIViewControllerBasedStatusBarAppearance: false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/package",
|
|
3
|
-
"version": "3.61.
|
|
3
|
+
"version": "3.61.2",
|
|
4
4
|
"description": "Builds an installable application from a game built on the engine",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"!src/**/*.test.ts",
|
|
22
22
|
"!src/**/*.test.mjs",
|
|
23
23
|
"!src/**/__snapshots__",
|
|
24
|
+
"bin",
|
|
25
|
+
"android",
|
|
26
|
+
"ios",
|
|
27
|
+
"assets",
|
|
28
|
+
"scripts",
|
|
24
29
|
"README.md",
|
|
25
30
|
"LICENSE",
|
|
26
31
|
"NOTICE"
|
|
@@ -30,7 +35,7 @@
|
|
|
30
35
|
"drift-package": "bin/drift-package.mjs"
|
|
31
36
|
},
|
|
32
37
|
"peerDependencies": {
|
|
33
|
-
"@driftengine/core": "3.61.
|
|
38
|
+
"@driftengine/core": "3.61.2",
|
|
34
39
|
"steamworks.js": ">=0.4.0"
|
|
35
40
|
},
|
|
36
41
|
"dependencies": {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Build the macOS artifacts on a Mac.
|
|
4
|
+
#
|
|
5
|
+
# **This is not the only route any more.** A Mac target cross-builds from Linux, where the packager
|
|
6
|
+
# ad-hoc signs it with rcodesign and archives it with `zip -y`. What that route cannot produce is a
|
|
7
|
+
# .dmg — hdiutil is macOS only — so this script is what makes one, and what signs with a real
|
|
8
|
+
# Developer ID. `drift-package doctor` prints which of the two a machine is on.
|
|
9
|
+
#
|
|
10
|
+
# The signing mode is decided by `planSigning` from the environment and printed by the build. With
|
|
11
|
+
# no certificate this is an **ad-hoc** signature, which is the minimum that runs: Apple Silicon's
|
|
12
|
+
# loader refuses an unsigned binary outright. A .dmg built that way still carries the quarantine
|
|
13
|
+
# flag when downloaded, and the first launch is refused until `xattr -dr com.apple.quarantine`
|
|
14
|
+
# clears it — which is the reason to buy a Developer ID eventually, and not a reason to wait.
|
|
15
|
+
#
|
|
16
|
+
# ./build-macos.sh --project ~/work/game
|
|
17
|
+
# CSC_LINK=~/certs/developer-id.p12 CSC_KEY_PASSWORD=... ./build-macos.sh --project .
|
|
18
|
+
# APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=... ./build-macos.sh --project .
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
PROJECT=""
|
|
23
|
+
TARGET="mac-arm64"
|
|
24
|
+
|
|
25
|
+
while [ $# -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--project) PROJECT="$2"; shift 2 ;;
|
|
28
|
+
--target) TARGET="$2"; shift 2 ;;
|
|
29
|
+
-h|--help) sed -n '2,16p' "$0"; exit 0 ;;
|
|
30
|
+
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
[ -n "$PROJECT" ] || { echo "--project is required" >&2; exit 2; }
|
|
35
|
+
[ -f "$PROJECT/drift.package.json" ] || {
|
|
36
|
+
echo "no drift.package.json in $PROJECT; that file is what a build is configured from" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
command -v node >/dev/null || { echo "node is not on PATH" >&2; exit 1; }
|
|
41
|
+
command -v codesign >/dev/null || {
|
|
42
|
+
echo "codesign is missing: install the Xcode command line tools with xcode-select --install" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# See the note in build-windows.ps1: an Electron-based terminal exports this, and it turns the
|
|
47
|
+
# artifact into one that cannot find Electron.
|
|
48
|
+
unset ELECTRON_RUN_AS_NODE
|
|
49
|
+
|
|
50
|
+
cd "$PROJECT"
|
|
51
|
+
npm ci
|
|
52
|
+
npx drift-package doctor
|
|
53
|
+
npx drift-package build --target="$TARGET"
|
|
54
|
+
|
|
55
|
+
echo
|
|
56
|
+
echo "Artifacts:"
|
|
57
|
+
find "out/$TARGET" -maxdepth 1 -type f -print
|
|
58
|
+
|
|
59
|
+
# Said again at the end, because the interesting line scrolled past twenty minutes ago.
|
|
60
|
+
if [ -z "${CSC_LINK:-}${CSC_NAME:-}" ]; then
|
|
61
|
+
cat <<'EOF'
|
|
62
|
+
|
|
63
|
+
This build is ad-hoc signed. It runs here. A copy downloaded through a browser will be refused by
|
|
64
|
+
Gatekeeper until the person clears the quarantine flag:
|
|
65
|
+
|
|
66
|
+
xattr -dr com.apple.quarantine "/Applications/<the app>.app"
|
|
67
|
+
|
|
68
|
+
A Developer ID certificate in CSC_LINK removes that, and notarisation credentials remove it for
|
|
69
|
+
everybody rather than for the people willing to run a command.
|
|
70
|
+
EOF
|
|
71
|
+
fi
|