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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/bridge/README.md +57 -2
  3. data/bridge/everywhere/bridge.js +803 -9
  4. data/bridge/package.json +1 -1
  5. data/lib/everywhere/builders/ios.rb +364 -0
  6. data/lib/everywhere/cli.rb +2 -0
  7. data/lib/everywhere/commands/build.rb +17 -1
  8. data/lib/everywhere/commands/clean.rb +19 -10
  9. data/lib/everywhere/commands/dev.rb +182 -19
  10. data/lib/everywhere/commands/doctor.rb +45 -3
  11. data/lib/everywhere/commands/icon.rb +14 -0
  12. data/lib/everywhere/commands/install.rb +37 -6
  13. data/lib/everywhere/commands/logs.rb +56 -0
  14. data/lib/everywhere/commands/platform/build.rb +3 -3
  15. data/lib/everywhere/commands/platform/runner.rb +1 -1
  16. data/lib/everywhere/commands/release.rb +1 -1
  17. data/lib/everywhere/commands/shell_dir.rb +11 -4
  18. data/lib/everywhere/config.rb +366 -1
  19. data/lib/everywhere/engine.rb +33 -1
  20. data/lib/everywhere/icon.rb +50 -0
  21. data/lib/everywhere/log_filter.rb +28 -2
  22. data/lib/everywhere/mobile_config_endpoint.rb +75 -0
  23. data/lib/everywhere/mobile_configs_controller.rb +46 -0
  24. data/lib/everywhere/native_helper.rb +156 -0
  25. data/lib/everywhere/paths.rb +41 -0
  26. data/lib/everywhere/raster.rb +17 -0
  27. data/lib/everywhere/shellout.rb +3 -1
  28. data/lib/everywhere/simulator.rb +74 -0
  29. data/lib/everywhere/ui.rb +18 -0
  30. data/lib/everywhere/version.rb +1 -1
  31. data/support/mobile/ios/App/App.xcconfig +6 -0
  32. data/support/mobile/ios/App/AppDelegate.swift +163 -0
  33. data/support/mobile/ios/App/Assets.xcassets/AccentColor.colorset/Contents.json +20 -0
  34. data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  35. data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  36. data/support/mobile/ios/App/Assets.xcassets/Contents.json +6 -0
  37. data/support/mobile/ios/App/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
  38. data/support/mobile/ios/App/Base.lproj/LaunchScreen.storyboard +32 -0
  39. data/support/mobile/ios/App/Bridge/BiometricsComponent.swift +276 -0
  40. data/support/mobile/ios/App/Bridge/HapticsComponent.swift +47 -0
  41. data/support/mobile/ios/App/Bridge/NotificationComponent.swift +56 -0
  42. data/support/mobile/ios/App/Bridge/PermissionsComponent.swift +142 -0
  43. data/support/mobile/ios/App/Bridge/StorageComponent.swift +63 -0
  44. data/support/mobile/ios/App/ErrorViewController.swift +64 -0
  45. data/support/mobile/ios/App/EverywhereConfig.swift +268 -0
  46. data/support/mobile/ios/App/EverywhereHost.swift +34 -0
  47. data/support/mobile/ios/App/Extensions/EverywhereExtensions.swift +32 -0
  48. data/support/mobile/ios/App/Info.plist +28 -0
  49. data/support/mobile/ios/App/Resources/everywhere.json +8 -0
  50. data/support/mobile/ios/App/Resources/path-configuration.json +19 -0
  51. data/support/mobile/ios/App/SceneDelegate.swift +443 -0
  52. data/support/mobile/ios/App.xcodeproj/project.pbxproj +454 -0
  53. data/support/mobile/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  54. data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +14 -0
  55. data/support/mobile/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +77 -0
  56. data/support/mobile/ios/NativeExtensions/Package.swift +26 -0
  57. data/support/mobile/ios/NativeExtensions/Sources/NativeExtensions/Exports.swift +5 -0
  58. data/support/mobile/ios/README.md +66 -0
  59. metadata +35 -1
data/bridge/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubyeverywhere/bridge",
3
- "version": "0.3.0",
3
+ "version": "0.2.0",
4
4
  "description": "One API for browser, desktop, and mobile — the JS half of RubyEverywhere",
5
5
  "type": "module",
6
6
  "main": "everywhere/bridge.js",
@@ -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
@@ -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 into a double-clickable desktop app (tebako press + shell + .app bundle)"
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 cargo build cache under ~/.rubyeverywhere (or
9
- # $RUBYEVERYWHERE_HOME). Safe to run any time: only compiled shell output
10
- # lives there, so the next `every dev`/`every build` just recompiles cold.
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 cargo shell-build cache (~/.rubyeverywhere/shell-target)"
13
+ desc "Remove the shell build caches (~/.rubyeverywhere: shell-target, ios-*)"
13
14
 
14
15
  def call(**)
15
- target = Everywhere::Paths.cargo_target_dir
16
- return UI.step("nothing to clean #{UI.dim("(#{target} doesn't exist)")}") unless File.exist?(target)
17
-
18
- size = human_size(dir_bytes(target))
19
- FileUtils.rm_rf(target)
20
- UI.success("removed #{target} (#{size} freed) — the next build recompiles the shell cold")
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