ruby_everywhere 0.1.15 → 0.2.0
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.
- checksums.yaml +4 -4
- data/bridge/README.md +57 -2
- data/bridge/everywhere/bridge.js +803 -9
- data/bridge/package.json +1 -1
- data/lib/everywhere/builders/ios.rb +364 -0
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/commands/build.rb +17 -1
- data/lib/everywhere/commands/clean.rb +19 -10
- data/lib/everywhere/commands/dev.rb +182 -19
- data/lib/everywhere/commands/doctor.rb +45 -3
- data/lib/everywhere/commands/icon.rb +14 -0
- data/lib/everywhere/commands/install.rb +37 -6
- data/lib/everywhere/commands/logs.rb +56 -0
- data/lib/everywhere/commands/platform/build.rb +3 -3
- data/lib/everywhere/commands/platform/runner.rb +1 -1
- data/lib/everywhere/commands/release.rb +1 -1
- data/lib/everywhere/commands/shell_dir.rb +11 -4
- data/lib/everywhere/config.rb +366 -1
- data/lib/everywhere/engine.rb +33 -1
- data/lib/everywhere/icon.rb +50 -0
- data/lib/everywhere/log_filter.rb +28 -2
- data/lib/everywhere/mobile_config_endpoint.rb +75 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/native_helper.rb +156 -0
- data/lib/everywhere/paths.rb +41 -0
- data/lib/everywhere/raster.rb +17 -0
- data/lib/everywhere/shellout.rb +3 -1
- data/lib/everywhere/simulator.rb +74 -0
- data/lib/everywhere/ui.rb +18 -0
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/ios/App/App.xcconfig +6 -0
- data/support/mobile/ios/App/AppDelegate.swift +163 -0
- data/support/mobile/ios/App/Assets.xcassets/AccentColor.colorset/Contents.json +20 -0
- data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- data/support/mobile/ios/App/Assets.xcassets/Contents.json +6 -0
- data/support/mobile/ios/App/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
- data/support/mobile/ios/App/Base.lproj/LaunchScreen.storyboard +32 -0
- data/support/mobile/ios/App/Bridge/BiometricsComponent.swift +276 -0
- data/support/mobile/ios/App/Bridge/HapticsComponent.swift +47 -0
- data/support/mobile/ios/App/Bridge/NotificationComponent.swift +56 -0
- data/support/mobile/ios/App/Bridge/PermissionsComponent.swift +142 -0
- data/support/mobile/ios/App/Bridge/StorageComponent.swift +63 -0
- data/support/mobile/ios/App/ErrorViewController.swift +64 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +268 -0
- data/support/mobile/ios/App/EverywhereHost.swift +34 -0
- data/support/mobile/ios/App/Extensions/EverywhereExtensions.swift +32 -0
- data/support/mobile/ios/App/Info.plist +28 -0
- data/support/mobile/ios/App/Resources/everywhere.json +8 -0
- data/support/mobile/ios/App/Resources/path-configuration.json +19 -0
- data/support/mobile/ios/App/SceneDelegate.swift +443 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +454 -0
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +14 -0
- data/support/mobile/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +77 -0
- data/support/mobile/ios/NativeExtensions/Package.swift +26 -0
- data/support/mobile/ios/NativeExtensions/Sources/NativeExtensions/Exports.swift +5 -0
- data/support/mobile/ios/README.md +66 -0
- metadata +35 -1
data/bridge/package.json
CHANGED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "../paths"
|
|
5
|
+
require_relative "../shellout"
|
|
6
|
+
require_relative "../log_filter"
|
|
7
|
+
require_relative "../icon"
|
|
8
|
+
require_relative "../ui"
|
|
9
|
+
|
|
10
|
+
module Everywhere
|
|
11
|
+
module Builders
|
|
12
|
+
# Assembles a Hotwire Native iOS app around an already-deployed (remote
|
|
13
|
+
# mode) app. No Tebako on iOS — the "build" is: copy the gem's frozen
|
|
14
|
+
# Xcode template, stamp it (xcconfig + everywhere.json + AppIcon; never
|
|
15
|
+
# the pbxproj), and hand it to xcodebuild.
|
|
16
|
+
#
|
|
17
|
+
# MVP scope is the Simulator (ad-hoc signed, no certs). Device signing,
|
|
18
|
+
# archive/.ipa export and store distribution hang off this same seam
|
|
19
|
+
# later — see platform/docs/build-engine.md §5.
|
|
20
|
+
class Ios
|
|
21
|
+
# Fixed by the template contract: the target/product is always "App"
|
|
22
|
+
# (App.app), so build paths never depend on per-app values.
|
|
23
|
+
SCHEME = "App"
|
|
24
|
+
|
|
25
|
+
def initialize(config:, root:, target:, template_dir: nil)
|
|
26
|
+
@config = config
|
|
27
|
+
@root = root
|
|
28
|
+
@target = target
|
|
29
|
+
@template_dir = template_dir
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Build the Simulator .app and copy it into dist_dir. `dev` builds Debug
|
|
33
|
+
# and relaxes the remote-mode requirement (the dev URL arrives at launch
|
|
34
|
+
# via SIMCTL_CHILD_EVERYWHERE_DEV_URL, not baked into the bundle).
|
|
35
|
+
# Returns the path of the built .app.
|
|
36
|
+
def build!(dist_dir:, dev: false)
|
|
37
|
+
preflight!(dev: dev)
|
|
38
|
+
template = @template_dir ? File.expand_path(@template_dir) : Paths.ios_dir!
|
|
39
|
+
work = stage(template)
|
|
40
|
+
stamp!(work)
|
|
41
|
+
configuration = dev ? "Debug" : "Release"
|
|
42
|
+
xcodebuild!(work, configuration, dist_dir)
|
|
43
|
+
collect(configuration, dist_dir)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def name = @config.name(target: @target)
|
|
49
|
+
def bundle_id = @config.bundle_id(target: @target)
|
|
50
|
+
|
|
51
|
+
def preflight!(dev:)
|
|
52
|
+
unless dev || @config.remote?
|
|
53
|
+
UI.die!("iOS apps are remote mode only for now — set remote.url in config/everywhere.yml " \
|
|
54
|
+
"(the shell wraps your deployed app; nothing is packaged locally)")
|
|
55
|
+
end
|
|
56
|
+
UI.die!("iOS builds need macOS with Xcode installed") unless RUBY_PLATFORM.include?("darwin")
|
|
57
|
+
return if Shellout.run?("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path", quiet: true)
|
|
58
|
+
|
|
59
|
+
UI.die!("no iOS Simulator SDK — install full Xcode (not just the Command Line Tools) " \
|
|
60
|
+
"and run: xcode-select --switch /Applications/Xcode.app")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Copy the template into the app's persistent work dir. Persistent (not
|
|
64
|
+
# a tmpdir) so the project path is stable — Xcode's incremental state
|
|
65
|
+
# stays valid and users can open the stamped project directly.
|
|
66
|
+
def stage(template)
|
|
67
|
+
work = Paths.ios_work_dir(bundle_id)
|
|
68
|
+
FileUtils.mkdir_p(File.dirname(work))
|
|
69
|
+
FileUtils.rm_rf(work)
|
|
70
|
+
FileUtils.cp_r(template, work)
|
|
71
|
+
work
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The whole per-app surface: one xcconfig (identity), two JSON resources
|
|
75
|
+
# (runtime config + path configuration with baked tabs), one PNG (icon),
|
|
76
|
+
# and the Extensions/ buildable folder (app-authored Swift + generated
|
|
77
|
+
# registry). The pbxproj is never touched.
|
|
78
|
+
def stamp!(work)
|
|
79
|
+
UI.step("stamping shell #{UI.dim("(#{bundle_id} → #{UI.short_path(work)})")}")
|
|
80
|
+
write_xcconfig(work)
|
|
81
|
+
File.write(File.join(work, "App", "Resources", "everywhere.json"),
|
|
82
|
+
@config.to_shell_json(target: @target))
|
|
83
|
+
# Same document MobileConfigEndpoint serves live — this baked copy is
|
|
84
|
+
# the offline/first-launch fallback the shell loads synchronously.
|
|
85
|
+
File.write(File.join(work, "App", "Resources", "path-configuration.json"),
|
|
86
|
+
@config.path_configuration_json("ios"))
|
|
87
|
+
write_launch_background(work)
|
|
88
|
+
write_usage_strings(work)
|
|
89
|
+
write_app_icon(work)
|
|
90
|
+
write_extensions(work)
|
|
91
|
+
write_native_packages(work)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Native extensions: the app repo's native/ios/*.swift copied into the
|
|
95
|
+
# template's Extensions/ folder, plus a generated registry naming what
|
|
96
|
+
# everywhere.yml declares (native.ios). Extensions/ is a filesystem-
|
|
97
|
+
# synchronized (buildable) group, so dropping files in IS the
|
|
98
|
+
# integration — the stamp-only contract holds.
|
|
99
|
+
def write_extensions(work)
|
|
100
|
+
errors = @config.native_ios_errors
|
|
101
|
+
UI.die!("native.ios in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
102
|
+
|
|
103
|
+
dest = File.join(work, "App", "Extensions")
|
|
104
|
+
source = File.join(@root, "native", "ios")
|
|
105
|
+
sources = File.directory?(source) ? Dir.glob(File.join(source, "**", "*.swift")).sort : []
|
|
106
|
+
|
|
107
|
+
sources.each do |file|
|
|
108
|
+
relative = file.delete_prefix("#{source}/")
|
|
109
|
+
if File.basename(file) == "EverywhereExtensions.swift"
|
|
110
|
+
UI.die!("native/ios/#{relative}: EverywhereExtensions.swift is generated by " \
|
|
111
|
+
"`every build` — declare your types under native.ios in config/everywhere.yml instead")
|
|
112
|
+
end
|
|
113
|
+
target_file = File.join(dest, relative)
|
|
114
|
+
FileUtils.mkdir_p(File.dirname(target_file))
|
|
115
|
+
FileUtils.cp(file, target_file)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if @config.native_ios?
|
|
119
|
+
UI.step("bundling native extensions #{UI.dim("(#{sources.length} Swift files from native/ios)")}")
|
|
120
|
+
FileUtils.mkdir_p(dest)
|
|
121
|
+
File.write(File.join(dest, "EverywhereExtensions.swift"), extensions_registry)
|
|
122
|
+
elsif sources.any?
|
|
123
|
+
UI.warn "native/ios has Swift files but everywhere.yml declares nothing under native.ios — " \
|
|
124
|
+
"they compile in, but no components/screens/splash are hooked up"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The generated half of the extension seam (the template ships an empty
|
|
129
|
+
# default). Declared names are validated as Swift identifiers above, so
|
|
130
|
+
# interpolation is safe; a declared-but-missing type fails the build
|
|
131
|
+
# with a Swift error naming it — the honest signal.
|
|
132
|
+
def extensions_registry
|
|
133
|
+
components = @config.native_ios_components.map { |type| "#{type}.self" }.join(", ")
|
|
134
|
+
screens = @config.native_ios_screens
|
|
135
|
+
splash = @config.native_ios_splash
|
|
136
|
+
|
|
137
|
+
screen_body =
|
|
138
|
+
if screens.empty?
|
|
139
|
+
" nil"
|
|
140
|
+
else
|
|
141
|
+
[" switch identifier {",
|
|
142
|
+
*screens.map { |id, type| " case #{id.inspect}: return everywhereHost(#{type}(url: url))" },
|
|
143
|
+
" default: return nil",
|
|
144
|
+
" }"].join("\n")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
<<~SWIFT
|
|
148
|
+
// Written by `every build` from config/everywhere.yml (native.ios) — do
|
|
149
|
+
// not edit; changes belong in native/ios/ and everywhere.yml.
|
|
150
|
+
import HotwireNative
|
|
151
|
+
import SwiftUI
|
|
152
|
+
import UIKit
|
|
153
|
+
|
|
154
|
+
enum EverywhereExtensions {
|
|
155
|
+
static let components: [BridgeComponent.Type] = [#{components}]
|
|
156
|
+
|
|
157
|
+
static func screen(for identifier: String, url: URL) -> UIViewController? {
|
|
158
|
+
#{screen_body}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static func splashViewController() -> UIViewController? {
|
|
162
|
+
#{splash ? "everywhereHost(#{splash}())" : "nil"}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
SWIFT
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Third-party Swift packages → the template's local NativeExtensions
|
|
169
|
+
# package. The App target already links (a fixed, frozen pbxproj entry)
|
|
170
|
+
# this local package; we only regenerate its manifest and the re-export
|
|
171
|
+
# shim, so app-authored native/ios code reaches every pinned product with
|
|
172
|
+
# `import NativeExtensions` — and the stamp-only contract holds (the
|
|
173
|
+
# pbxproj is never touched, packages flow through Package.swift instead).
|
|
174
|
+
def write_native_packages(work)
|
|
175
|
+
packages = @config.native_ios_packages
|
|
176
|
+
pkg_dir = File.join(work, "NativeExtensions")
|
|
177
|
+
sources = File.join(pkg_dir, "Sources", "NativeExtensions")
|
|
178
|
+
FileUtils.mkdir_p(sources)
|
|
179
|
+
# A stray Xcode/SPM index cache in the template must not leak into the
|
|
180
|
+
# app's build (it pins a different dependency graph).
|
|
181
|
+
FileUtils.rm_rf(File.join(pkg_dir, ".build"))
|
|
182
|
+
|
|
183
|
+
File.write(File.join(pkg_dir, "Package.swift"), native_package_manifest(packages))
|
|
184
|
+
File.write(File.join(sources, "Exports.swift"), native_package_exports(packages))
|
|
185
|
+
|
|
186
|
+
return if packages.empty?
|
|
187
|
+
|
|
188
|
+
UI.step("pinning native packages #{UI.dim("(#{packages.length} SPM " \
|
|
189
|
+
"dependenc#{packages.length == 1 ? "y" : "ies"} in NativeExtensions)")}")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The generated NativeExtensions/Package.swift. Every string interpolated
|
|
193
|
+
# here is validated by Config#native_ios_package_errors (URLs, semver,
|
|
194
|
+
# module names), and .inspect renders Swift-compatible string literals.
|
|
195
|
+
def native_package_manifest(packages)
|
|
196
|
+
dependencies = packages.map do |pkg|
|
|
197
|
+
".package(url: #{pkg["url"].inspect}, #{swift_requirement(pkg["requirement"])})"
|
|
198
|
+
end
|
|
199
|
+
products = packages.flat_map do |pkg|
|
|
200
|
+
identity = @config.package_identity(pkg["url"])
|
|
201
|
+
pkg["products"].map { |name| ".product(name: #{name.inspect}, package: #{identity.inspect})" }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
<<~SWIFT
|
|
205
|
+
// swift-tools-version:5.9
|
|
206
|
+
// Written by `every build` from config/everywhere.yml (native.ios.packages)
|
|
207
|
+
// — do not edit; changes belong in config/everywhere.yml.
|
|
208
|
+
import PackageDescription
|
|
209
|
+
|
|
210
|
+
let package = Package(
|
|
211
|
+
name: "NativeExtensions",
|
|
212
|
+
platforms: [.iOS(.v15)],
|
|
213
|
+
products: [
|
|
214
|
+
.library(name: "NativeExtensions", targets: ["NativeExtensions"])
|
|
215
|
+
],
|
|
216
|
+
dependencies: #{swift_array(dependencies, indent: 8)},
|
|
217
|
+
targets: [
|
|
218
|
+
.target(name: "NativeExtensions", dependencies: #{swift_array(products, indent: 12)})
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
SWIFT
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The re-export shim: one `@_exported import` per pinned product, so the
|
|
225
|
+
# App target's native/ios code sees every module through NativeExtensions.
|
|
226
|
+
def native_package_exports(packages)
|
|
227
|
+
header = <<~SWIFT
|
|
228
|
+
// Written by `every build` from config/everywhere.yml (native.ios.packages)
|
|
229
|
+
// — re-exports each pinned product so native/ios code reaches it with a
|
|
230
|
+
// single `import NativeExtensions`. Do not edit.
|
|
231
|
+
SWIFT
|
|
232
|
+
modules = packages.flat_map { |pkg| pkg["products"] }.uniq.sort
|
|
233
|
+
return header if modules.empty?
|
|
234
|
+
|
|
235
|
+
"#{header}#{modules.map { |m| "@_exported import #{m}" }.join("\n")}\n"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def swift_requirement(requirement)
|
|
239
|
+
value = requirement["value"].inspect
|
|
240
|
+
"#{requirement["kind"]}: #{value}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A Swift array literal: "[]" when empty, else one element per line at
|
|
244
|
+
# `indent` spaces with the closing bracket de-dented by four.
|
|
245
|
+
def swift_array(elements, indent:)
|
|
246
|
+
return "[]" if elements.empty?
|
|
247
|
+
|
|
248
|
+
pad = " " * indent
|
|
249
|
+
"[\n#{elements.map { |element| "#{pad}#{element}" }.join(",\n")}\n#{" " * (indent - 4)}]"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Permission usage strings → Info.plist. iOS crashes a permission
|
|
253
|
+
# request whose usage-description key is missing, so declarations are
|
|
254
|
+
# validated here (good strings are mandatory) and stamped with plutil.
|
|
255
|
+
def write_usage_strings(work)
|
|
256
|
+
errors = @config.permission_errors("ios")
|
|
257
|
+
UI.die!("permissions in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
258
|
+
|
|
259
|
+
plist = File.join(work, "App", "Info.plist")
|
|
260
|
+
@config.permissions.each do |name, usage|
|
|
261
|
+
key = Everywhere::Config::IOS_USAGE_KEYS[name] or next
|
|
262
|
+
Shellout.run!({}, "plutil", "-replace", key, "-string", usage["ios"], plist)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# The launch-screen (and splash overlay) background — the storyboard's
|
|
267
|
+
# LaunchBackground named color, stamped from appearance.background_color
|
|
268
|
+
# so the launch frame matches the app instead of the template default.
|
|
269
|
+
def write_launch_background(work)
|
|
270
|
+
colors = @config.background_color
|
|
271
|
+
light = Icon.hex_rgb(colors&.fetch("light", nil)) or return
|
|
272
|
+
dark = Icon.hex_rgb(colors["dark"]) || light
|
|
273
|
+
|
|
274
|
+
entry = lambda do |rgb, appearances|
|
|
275
|
+
{ "color" => {
|
|
276
|
+
"color-space" => "srgb",
|
|
277
|
+
"components" => {
|
|
278
|
+
"alpha" => "1.000",
|
|
279
|
+
"red" => format("0x%02X", rgb[0]),
|
|
280
|
+
"green" => format("0x%02X", rgb[1]),
|
|
281
|
+
"blue" => format("0x%02X", rgb[2])
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"idiom" => "universal" }.merge(appearances)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
require "json"
|
|
288
|
+
dest = File.join(work, "App", "Assets.xcassets", "LaunchBackground.colorset")
|
|
289
|
+
FileUtils.mkdir_p(dest)
|
|
290
|
+
File.write(
|
|
291
|
+
File.join(dest, "Contents.json"),
|
|
292
|
+
JSON.pretty_generate(
|
|
293
|
+
"colors" => [
|
|
294
|
+
entry.call(light, {}),
|
|
295
|
+
entry.call(dark, "appearances" => [{ "appearance" => "luminosity", "value" => "dark" }])
|
|
296
|
+
],
|
|
297
|
+
"info" => { "author" => "xcode", "version" => 1 }
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def write_xcconfig(work)
|
|
303
|
+
File.write(File.join(work, "App", "App.xcconfig"), <<~XCCONFIG)
|
|
304
|
+
// Written by `every build --target #{@target}` — do not edit; changes
|
|
305
|
+
// belong in config/everywhere.yml.
|
|
306
|
+
PRODUCT_BUNDLE_IDENTIFIER = #{bundle_id}
|
|
307
|
+
MARKETING_VERSION = #{@config.version(target: @target)}
|
|
308
|
+
CURRENT_PROJECT_VERSION = 1
|
|
309
|
+
INFOPLIST_KEY_CFBundleDisplayName = #{name}
|
|
310
|
+
DEVELOPMENT_TEAM =
|
|
311
|
+
XCCONFIG
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def write_app_icon(work)
|
|
315
|
+
source = @config.icon
|
|
316
|
+
unless source && File.exist?(source)
|
|
317
|
+
UI.warn "no app icon (set app.icon or add icon.png) — shipping the placeholder"
|
|
318
|
+
return
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
background = Icon.hex_rgb(@config.background_color&.fetch("light", nil))
|
|
322
|
+
dest = File.join(work, "App", "Assets.xcassets", "AppIcon.appiconset")
|
|
323
|
+
return if Icon.write_ios_appiconset(source, dest, background: background)
|
|
324
|
+
|
|
325
|
+
UI.warn "couldn't read #{File.basename(source)} — shipping the placeholder icon"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def xcodebuild!(work, configuration, dist_dir)
|
|
329
|
+
UI.step("building iOS shell #{UI.dim("(xcodebuild, #{configuration}, first run compiles HotwireNative)")}")
|
|
330
|
+
FileUtils.mkdir_p(dist_dir)
|
|
331
|
+
log = File.join(dist_dir, "ios-build.log")
|
|
332
|
+
Shellout.run_logged!({},
|
|
333
|
+
["xcodebuild", "build",
|
|
334
|
+
"-project", File.join(work, "App.xcodeproj"),
|
|
335
|
+
"-scheme", SCHEME,
|
|
336
|
+
"-configuration", configuration,
|
|
337
|
+
"-destination", "generic/platform=iOS Simulator",
|
|
338
|
+
"-derivedDataPath", Paths.ios_derived_data_dir,
|
|
339
|
+
"-clonedSourcePackagesDirPath", Paths.ios_packages_dir,
|
|
340
|
+
# Ad-hoc sign (no certs needed on the Simulator).
|
|
341
|
+
# CODE_SIGNING_ALLOWED=NO ships an UNSIGNED app,
|
|
342
|
+
# and securityd rejects its data-protection
|
|
343
|
+
# keychain writes (SecAccessControl items, e.g.
|
|
344
|
+
# the Face ID login credential) with -34018.
|
|
345
|
+
"CODE_SIGN_IDENTITY=-"],
|
|
346
|
+
log: log, filter: LogFilter.for(:xcodebuild))
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Copy the product out of DerivedData. The ".simulator.app" suffix keeps
|
|
350
|
+
# it visibly distinct from the desktop dist/<Name>.app (and out of
|
|
351
|
+
# release.rb's dist/*.app glob).
|
|
352
|
+
def collect(configuration, dist_dir)
|
|
353
|
+
built = File.join(Paths.ios_derived_data_dir, "Build", "Products",
|
|
354
|
+
"#{configuration}-iphonesimulator", "App.app")
|
|
355
|
+
UI.die!("xcodebuild reported success but #{built} is missing") unless File.directory?(built)
|
|
356
|
+
|
|
357
|
+
dest = File.join(dist_dir, "#{name}.simulator.app")
|
|
358
|
+
FileUtils.rm_rf(dest)
|
|
359
|
+
FileUtils.cp_r(built, dest)
|
|
360
|
+
dest
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
data/lib/everywhere/cli.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "framework"
|
|
|
6
6
|
require_relative "commands/build"
|
|
7
7
|
require_relative "commands/clean"
|
|
8
8
|
require_relative "commands/dev"
|
|
9
|
+
require_relative "commands/logs"
|
|
9
10
|
require_relative "commands/icon"
|
|
10
11
|
require_relative "commands/doctor"
|
|
11
12
|
require_relative "commands/install"
|
|
@@ -37,6 +38,7 @@ module Everywhere
|
|
|
37
38
|
register "doctor", Commands::Doctor
|
|
38
39
|
register "install", Commands::Install
|
|
39
40
|
register "dev", Commands::Dev
|
|
41
|
+
register "logs", Commands::Logs
|
|
40
42
|
register "build", Commands::Build
|
|
41
43
|
register "clean", Commands::Clean
|
|
42
44
|
register "icon", Commands::Icon
|
|
@@ -9,6 +9,7 @@ require_relative "../log_filter"
|
|
|
9
9
|
require_relative "../png"
|
|
10
10
|
require_relative "../icon"
|
|
11
11
|
require_relative "../paths"
|
|
12
|
+
require_relative "../builders/ios"
|
|
12
13
|
|
|
13
14
|
module Everywhere
|
|
14
15
|
module Commands
|
|
@@ -23,7 +24,7 @@ module Everywhere
|
|
|
23
24
|
class Build < Dry::CLI::Command
|
|
24
25
|
SHIM = File.expand_path("~/.tebako-shims/clang++")
|
|
25
26
|
|
|
26
|
-
desc "Package the app
|
|
27
|
+
desc "Package the app natively — desktop (tebako press + shell + .app) or iOS (--target ios-arm64)"
|
|
27
28
|
|
|
28
29
|
option :root, default: ".", desc: "App root directory"
|
|
29
30
|
option :output, default: nil, desc: "Output binary path (default: dist/<app>)"
|
|
@@ -38,6 +39,21 @@ module Everywhere
|
|
|
38
39
|
app_name: nil, target: "macos-arm64", shell_dir: nil, skip_press: false, **)
|
|
39
40
|
root_path = File.expand_path(root)
|
|
40
41
|
config = Everywhere::Config.load(root_path)
|
|
42
|
+
|
|
43
|
+
# Builder dispatch on the target's os (build-engine.md §5): macOS is
|
|
44
|
+
# the tebako+tauri path below; iOS is a Hotwire Native shell built by
|
|
45
|
+
# its own builder. Anything else has no builder yet.
|
|
46
|
+
case Everywhere::Config.os_of(target)
|
|
47
|
+
when "ios"
|
|
48
|
+
app = Builders::Ios.new(config: config, root: root_path, target: target)
|
|
49
|
+
.build!(dist_dir: File.expand_path("dist", root_path))
|
|
50
|
+
UI.success("built #{rel(app, root_path)}")
|
|
51
|
+
return
|
|
52
|
+
when "macos" # the rest of this method
|
|
53
|
+
else
|
|
54
|
+
UI.die!("no builder for target #{target} — macos-* and ios-* are supported")
|
|
55
|
+
end
|
|
56
|
+
|
|
41
57
|
# The os of `target` selects any per-platform overrides (bundle id,
|
|
42
58
|
# version, name) — local builds always target the host, macOS today.
|
|
43
59
|
app_name ||= config.name(target: target)
|
|
@@ -5,19 +5,28 @@ require "fileutils"
|
|
|
5
5
|
|
|
6
6
|
module Everywhere
|
|
7
7
|
module Commands
|
|
8
|
-
# Wipes the shared
|
|
9
|
-
# $RUBYEVERYWHERE_HOME)
|
|
10
|
-
#
|
|
8
|
+
# Wipes the shared build caches under ~/.rubyeverywhere (or
|
|
9
|
+
# $RUBYEVERYWHERE_HOME): the cargo shell cache plus the iOS DerivedData /
|
|
10
|
+
# SPM / stamped-project dirs. Safe to run any time: only compiled shell
|
|
11
|
+
# output lives there, so the next `every dev`/`every build` recompiles cold.
|
|
11
12
|
class Clean < Dry::CLI::Command
|
|
12
|
-
desc "Remove the
|
|
13
|
+
desc "Remove the shell build caches (~/.rubyeverywhere: shell-target, ios-*)"
|
|
13
14
|
|
|
14
15
|
def call(**)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
caches = [Everywhere::Paths.cargo_target_dir,
|
|
17
|
+
Everywhere::Paths.ios_derived_data_dir,
|
|
18
|
+
Everywhere::Paths.ios_packages_dir,
|
|
19
|
+
File.join(Everywhere::Paths.cache_dir, "ios")].select { |d| File.exist?(d) }
|
|
20
|
+
return UI.step("nothing to clean #{UI.dim("(no build caches under #{Everywhere::Paths.cache_dir})")}") if caches.empty?
|
|
21
|
+
|
|
22
|
+
freed = 0
|
|
23
|
+
caches.each do |dir|
|
|
24
|
+
bytes = dir_bytes(dir)
|
|
25
|
+
freed += bytes
|
|
26
|
+
FileUtils.rm_rf(dir)
|
|
27
|
+
UI.ok("removed #{dir} #{UI.dim("(#{human_size(bytes)})")}")
|
|
28
|
+
end
|
|
29
|
+
UI.success("#{human_size(freed)} freed — the next build recompiles the shells cold")
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
private
|