ruby_everywhere 0.3.0 → 0.5.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +339 -0
  3. data/bridge/README.md +70 -1
  4. data/bridge/everywhere/bridge.js +211 -3
  5. data/bridge/everywhere/native.css +61 -0
  6. data/lib/everywhere/android_resources.rb +217 -0
  7. data/lib/everywhere/android_sdk.rb +330 -0
  8. data/lib/everywhere/asset_catalog.rb +185 -0
  9. data/lib/everywhere/auth_handoff.rb +404 -0
  10. data/lib/everywhere/auth_token.rb +118 -0
  11. data/lib/everywhere/builders/android.rb +844 -0
  12. data/lib/everywhere/builders/desktop.rb +326 -0
  13. data/lib/everywhere/builders/ios.rb +40 -3
  14. data/lib/everywhere/child_processes.rb +74 -0
  15. data/lib/everywhere/commands/build.rb +95 -14
  16. data/lib/everywhere/commands/clean.rb +10 -5
  17. data/lib/everywhere/commands/dev.rb +462 -57
  18. data/lib/everywhere/commands/doctor.rb +89 -3
  19. data/lib/everywhere/commands/install.rb +21 -1
  20. data/lib/everywhere/commands/logs.rb +117 -8
  21. data/lib/everywhere/commands/release.rb +2 -2
  22. data/lib/everywhere/config.rb +703 -13
  23. data/lib/everywhere/console.rb +117 -0
  24. data/lib/everywhere/desktop_assets.rb +150 -0
  25. data/lib/everywhere/dock/footer.rb +150 -0
  26. data/lib/everywhere/dock/screen.rb +114 -0
  27. data/lib/everywhere/dock/state.rb +59 -0
  28. data/lib/everywhere/dock.rb +238 -0
  29. data/lib/everywhere/emulator.rb +405 -0
  30. data/lib/everywhere/engine.rb +12 -0
  31. data/lib/everywhere/fatal.rb +20 -0
  32. data/lib/everywhere/icon.rb +196 -0
  33. data/lib/everywhere/line_pump.rb +89 -0
  34. data/lib/everywhere/log_filter.rb +111 -0
  35. data/lib/everywhere/native_helper.rb +39 -9
  36. data/lib/everywhere/omniauth.rb +88 -0
  37. data/lib/everywhere/paths.rb +109 -11
  38. data/lib/everywhere/relay.rb +77 -0
  39. data/lib/everywhere/shellout.rb +90 -15
  40. data/lib/everywhere/task_pool.rb +123 -0
  41. data/lib/everywhere/ui.rb +54 -11
  42. data/lib/everywhere/version.rb +1 -1
  43. data/support/desktop/README.md +121 -0
  44. data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
  45. data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
  46. data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
  47. data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
  48. data/support/desktop/src-tauri/src/extension_host.rs +53 -0
  49. data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
  50. data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
  51. data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
  52. data/support/mobile/android/README.md +217 -0
  53. data/support/mobile/android/app/build.gradle.kts +156 -0
  54. data/support/mobile/android/app/everywhere.properties +8 -0
  55. data/support/mobile/android/app/native-packages.gradle.kts +16 -0
  56. data/support/mobile/android/app/proguard-rules.pro +21 -0
  57. data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
  58. data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
  59. data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
  60. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
  61. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
  62. data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
  63. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
  64. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
  65. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
  66. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
  67. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
  68. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
  69. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
  70. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
  71. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
  72. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
  73. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
  74. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
  75. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
  76. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
  77. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
  78. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
  79. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
  80. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
  81. data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
  82. data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
  83. data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
  84. data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
  85. data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
  86. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
  87. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
  88. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
  89. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
  90. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
  91. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  92. data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
  93. data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
  94. data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
  95. data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
  96. data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
  97. data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
  98. data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
  99. data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
  100. data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
  101. data/support/mobile/android/build.gradle.kts +8 -0
  102. data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  103. data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  104. data/support/mobile/android/gradle.properties +14 -0
  105. data/support/mobile/android/gradlew +251 -0
  106. data/support/mobile/android/gradlew.bat +94 -0
  107. data/support/mobile/android/settings.gradle.kts +28 -0
  108. data/support/mobile/ios/App/AppDelegate.swift +5 -0
  109. data/support/mobile/ios/App/AuthFlow.swift +101 -0
  110. data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
  111. data/support/mobile/ios/App/SceneDelegate.swift +103 -0
  112. data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
  113. data/support/mobile/ios/README.md +4 -1
  114. data/support/{macos → release/macos}/notarize.sh +2 -2
  115. metadata +97 -17
  116. data/support/github/build.yml +0 -85
  117. data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
  118. /data/support/{shell → desktop}/splash/index.html +0 -0
  119. /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
  120. /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
  121. /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
  122. /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
  123. /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
  124. /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
  125. /data/support/{macos → release/macos}/entitlements.plist +0 -0
@@ -3,14 +3,15 @@
3
3
  require "dry/cli"
4
4
  require_relative "../shellout"
5
5
  require_relative "../paths"
6
+ require_relative "../android_sdk"
6
7
 
7
8
  module Everywhere
8
9
  module Commands
9
10
  # Check that this machine can build native apps. Desktop checks always
10
- # run; the iOS section joins in when the app's build.targets include an
11
- # ios target, or explicitly via --target ios-*.
11
+ # run; the iOS and Android sections join in when the app's build.targets
12
+ # include such a target, or explicitly via --target ios-* / android-*.
12
13
  class Doctor < Dry::CLI::Command
13
- desc "Check the toolchain (rbe-tebako, rust, brew deps; Xcode for iOS targets)"
14
+ desc "Check the toolchain (rbe-tebako, rust, brew deps; Xcode for iOS, SDK/JDK for Android)"
14
15
 
15
16
  option :root, default: ".", desc: "App root (its build.targets pick the sections)"
16
17
  option :target, default: nil, desc: "Also check a specific target's toolchain (e.g. ios-arm64)"
@@ -22,6 +23,7 @@ module Everywhere
22
23
  ok &= check("homebrew bison") { !`brew --prefix bison 2>/dev/null`.strip.empty? }
23
24
 
24
25
  ok &= ios_checks if ios_wanted?(root, target)
26
+ ok &= android_checks if android_wanted?(root, target)
25
27
 
26
28
  puts
27
29
  ok ? UI.success("ready to build") : UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
@@ -64,6 +66,90 @@ module Everywhere
64
66
  ok
65
67
  end
66
68
 
69
+ def android_wanted?(root, target)
70
+ return true if target && Everywhere::Config.os_of(target) == "android"
71
+
72
+ Everywhere::Config.load(File.expand_path(root)).targets
73
+ .any? { |t| Everywhere::Config.os_of(t) == "android" }
74
+ rescue StandardError
75
+ false
76
+ end
77
+
78
+ # Every check here resolves by location through AndroidSdk, never off
79
+ # PATH: Android Studio installs the SDK, adb, the emulator and a JDK
80
+ # without exporting any of them, so a `which adb` would report a broken
81
+ # machine that in fact builds fine. Nothing auto-installs — SDK components
82
+ # are gigabytes — so each failure carries the exact command to run.
83
+ def android_checks
84
+ puts
85
+ UI.step("Android target")
86
+ sdk = Everywhere::AndroidSdk
87
+ root = sdk.sdk_root
88
+
89
+ ok = fix_check(labelled("Android SDK", root && UI.short_path(root)),
90
+ "install Android Studio (it provisions the SDK, adb, the emulator and a " \
91
+ "JDK), or point ANDROID_HOME at an existing SDK") { !root.nil? }
92
+
93
+ jdk = sdk.java_version
94
+ ok &= fix_check(labelled("JDK #{sdk::MINIMUM_JDK}+", jdk && "found #{jdk}"),
95
+ "install Android Studio (it bundles a JDK) or set JAVA_HOME to a " \
96
+ "JDK #{sdk::MINIMUM_JDK} or newer") { !jdk.nil? && jdk >= sdk::MINIMUM_JDK }
97
+
98
+ # This one has to come before adb, the emulator, the AVD and the
99
+ # platform, because every one of those remediations is an sdkmanager
100
+ # line — and sdkmanager is exactly what can be missing. Phase 0 found a
101
+ # machine with a full SDK, adb, the emulator, build-tools and an AVD but
102
+ # no cmdline-tools anywhere, so "run this sdkmanager command" was the
103
+ # one piece of advice that couldn't be followed. Reported, but NOT
104
+ # folded into `ok`: that machine builds Android apps fine, and failing
105
+ # doctor over a component it doesn't need would be a lie.
106
+ if sdk.cmdline_tools?
107
+ UI.ok(labelled("Android SDK command-line tools", UI.short_path(sdk.sdkmanager)))
108
+ else
109
+ UI.warn("Android SDK command-line tools missing — nothing below can be installed " \
110
+ "until they are: #{sdk.cmdline_tools_hint}")
111
+ end
112
+
113
+ ok &= fix_check(labelled("adb", sdk.adb && UI.short_path(sdk.adb)),
114
+ sdk.install_hint("platform-tools")) { !sdk.adb.nil? }
115
+
116
+ ok &= fix_check(labelled("emulator", sdk.emulator && UI.short_path(sdk.emulator)),
117
+ sdk.install_hint("emulator")) { !sdk.emulator.nil? }
118
+
119
+ avds = sdk.avds
120
+ ok &= fix_check(labelled("an Android virtual device", avds.first),
121
+ sdk.create_avd_hint) { avds.any? }
122
+
123
+ platform = "platforms;android-#{sdk::COMPILE_SDK}"
124
+ ok &= fix_check(labelled(platform, listed(sdk.installed_platforms)),
125
+ sdk.install_hint(platform)) { sdk.platform? }
126
+
127
+ ok &= fix_check("bundled Android shell template",
128
+ "not shipped in this release of ruby_everywhere yet, so Android " \
129
+ "builds aren't available") { !Everywhere::Paths.android_dir.nil? }
130
+ ok
131
+ end
132
+
133
+ # "adb (~/Library/Android/sdk/platform-tools/adb)" — the resolved location
134
+ # matters on Android in a way it doesn't on iOS, because there are several
135
+ # plausible SDK roots and the user can't tell from `which` which one we
136
+ # picked.
137
+ def labelled(label, detail) = detail ? "#{label} #{UI.dim("(#{detail})")}" : label
138
+
139
+ def listed(items) = items.empty? ? "none installed" : "have #{items.join(", ")}"
140
+
141
+ # check() prints one fixed label, but an Android remediation is only
142
+ # knowable after resolution (the sdkmanager path depends on where the SDK
143
+ # turned out to be), so a failure appends the exact command to run.
144
+ def fix_check(label, fix)
145
+ passed = yield
146
+ passed ? UI.ok(label) : UI.bad("#{label} — #{fix}")
147
+ passed
148
+ rescue StandardError
149
+ UI.bad("#{label} — #{fix}")
150
+ false
151
+ end
152
+
67
153
  def check(label)
68
154
  passed = yield
69
155
  passed ? UI.ok(label) : UI.bad(label)
@@ -158,6 +158,17 @@ module Everywhere
158
158
  # - label: Quit
159
159
  # action: quit
160
160
 
161
+ # Desktop window chrome. title_bar: overlay floats the macOS traffic
162
+ # lights over your page with no title text (reserve the space with the
163
+ # .everywhere-titlebar-inset class from everywhere/native.css);
164
+ # frameless removes the system chrome entirely, and the page draws its
165
+ # own title bar and calls Everywhere.window.* for the controls.
166
+ # window:
167
+ # title_bar: overlay # decorated (default) | overlay | frameless
168
+ # title: false # draw the title text
169
+ # size: [1100, 750]
170
+ # min_size: [600, 400]
171
+
161
172
  # Mobile tab bar (iOS today, Android soon). Served live from
162
173
  # /everywhere/ios_v1.json too, so tab changes deploy with the app —
163
174
  # no app-store release. Icons: ios = SF Symbol name.
@@ -177,6 +188,15 @@ module Everywhere
177
188
  # ios:
178
189
  # lazy_load_tabs: true # defer each tab's first load until it's tapped
179
190
  # # (false = load every tab up front; omit = default)
191
+ #
192
+ # Rust you keep in native/desktop/ and compile into the desktop
193
+ # shell. Declaring anything here switches the desktop build to a
194
+ # per-app cargo compile — the first one takes minutes.
195
+ # desktop:
196
+ # commands: [scan_ports] # reachable as Everywhere.desktop.invoke("scan_ports")
197
+ # setup: true # run native/desktop/setup.rs at boot
198
+ # crates:
199
+ # serialport: "4.3"
180
200
 
181
201
  # Durable build knobs (CLI flags still override per-run).
182
202
  # build:
@@ -187,7 +207,7 @@ module Everywhere
187
207
  # # # (`every build` / `every dev --ios`) work today;
188
208
  # # # hosted platform builds for iOS aren't live yet.
189
209
  # # - android-arm64 # future — mobile is remote mode only
190
- # permissions: [notifications, filesystem:read, clipboard]
210
+ # capabilities: [notifications, filesystem:read, clipboard] # desktop OS integration
191
211
 
192
212
  # Per-platform overrides. Keys are platform names (macos / ios / android /
193
213
  # windows / linux); anything here overrides the app: defaults above for that
@@ -1,20 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/cli"
4
+ require_relative "../android_sdk"
5
+ require_relative "../config"
6
+ require_relative "../emulator"
4
7
  require_relative "../simulator"
5
8
  require_relative "../ui"
6
9
 
7
10
  module Everywhere
8
11
  module Commands
9
- # Tail the native shell's logs. iOS only so far: the Simulator's unified
10
- # log, scoped to the shell process. Watches (log stream) by default;
11
- # --last replays recent history (log show) and exits.
12
+ # Tail the native shell's logs. Two platforms, one shape: the iOS Simulator's
13
+ # unified log scoped to the shell process, or Android's logcat scoped to the
14
+ # shell's process id. Watches by default; --last replays recent history and
15
+ # exits.
12
16
  class Logs < Dry::CLI::Command
13
- desc "Stream native shell logs (iOS Simulator; watches by default)"
17
+ desc "Stream native shell logs (iOS Simulator or Android; watches by default)"
14
18
 
15
- option :ios, type: :boolean, default: false, desc: "iOS Simulator logs (the only target so far)"
16
- option :last, default: nil, desc: 'Show recent history instead of watching (e.g. "5m", "1h")'
19
+ option :ios, type: :boolean, default: false, desc: "iOS Simulator logs (unified log)"
20
+ option :android, type: :boolean, default: false, desc: "Android logs (logcat, scoped to the app's process)"
21
+ option :last, default: nil, desc: 'Show recent history instead of watching (e.g. "5m", "1h"; Android also takes a line count)'
17
22
  option :udid, default: nil, desc: "Simulator UDID (default: the booted one)"
23
+ option :serial, default: nil, desc: "Android device serial (default: the running emulator)"
24
+ option :app_id, default: nil, desc: "Android applicationId (default: the one in config/everywhere.yml)"
18
25
  option :verbose, type: :boolean, default: false, desc: "Everything the shell process logs, debug level included"
19
26
 
20
27
  # The template contract fixes the product name (App.app), so the shell's
@@ -28,14 +35,56 @@ module Everywhere
28
35
  PREDICATE = 'process == "App" AND eventType == logEvent AND NOT subsystem BEGINSWITH "com.apple"'
29
36
  VERBOSE_PREDICATE = 'process == "App"'
30
37
 
31
- def call(ios: false, last: nil, udid: nil, verbose: false, **)
32
- UI.die!("only the iOS Simulator is supported so far run: every logs --ios") unless ios
38
+ # Android has no predicate language, so the same job is split in two:
39
+ # `--pid=` fixes the process (see #android_logs) and a filterspec fixes
40
+ # the priority floor.
41
+ #
42
+ # Inside our own process the analogue of iOS's Apple-framework chatter is
43
+ # the platform's own: ART's GC bookkeeping, binder transactions,
44
+ # chromium's internal tracing. Those sit at VERBOSE, so the default floor
45
+ # is DEBUG — which keeps Hotwire Native's logging (Hotwire-Core and
46
+ # Hotwire-Navigation both log at debug), keeps every WebView console
47
+ # message (chromium reports them at INFO), and drops the runtime's
48
+ # housekeeping. --verbose lowers the floor to VERBOSE.
49
+ #
50
+ # Deliberately NOT a tag list: filtering on Hotwire-Core/Hotwire-Navigation
51
+ # would be the obvious way to cut noise and would hide the two things a
52
+ # shell developer most wants — their own Log calls and their page's
53
+ # console output. Scoping by process keeps everything that is ours and
54
+ # nothing that isn't, which is the same trade the iOS predicate makes.
55
+ DEFAULT_LEVEL = "*:D"
56
+ VERBOSE_LEVEL = "*:V"
33
57
 
58
+ # `-v time` is the compact-style analogue: with the process already fixed,
59
+ # repeating its pid on every line is noise. --verbose widens to threadtime,
60
+ # which adds the thread id you need once you're reading runtime internals.
61
+ DEFAULT_FORMAT = "time"
62
+ VERBOSE_FORMAT = "threadtime"
63
+
64
+ def call(ios: false, android: false, last: nil, udid: nil, serial: nil, app_id: nil, verbose: false, **)
65
+ UI.die!("pick a platform — run: every logs --ios, or every logs --android") unless ios || android
66
+ UI.die!("--ios and --android are two separate log streams — pick one") if ios && android
67
+
68
+ if android
69
+ android_logs(serial: serial, app_id: app_id, last: last, verbose: verbose)
70
+ else
71
+ ios_logs(udid: udid, last: last, verbose: verbose)
72
+ end
73
+ end
74
+
75
+ # --- iOS ------------------------------------------------------------------
76
+
77
+ def ios_logs(udid: nil, last: nil, verbose: false)
34
78
  udid ||= Everywhere::Simulator.booted_udid
35
79
  UI.die!("no booted iOS Simulator — start one with `every dev --ios`") unless udid
36
80
 
37
81
  UI.step(last ? "iOS Simulator logs from the last #{UI.bold(last)}"
38
82
  : "watching iOS Simulator logs #{UI.dim("(Ctrl-C to stop)")}")
83
+ # exec replaces this process, so anything still sitting in Ruby's stdout
84
+ # buffer dies with it — and stdout IS buffered whenever it's a pipe,
85
+ # which is exactly how `every dev`'s log toggle runs us. Without this
86
+ # the header above silently vanishes in the one context that spawns us.
87
+ $stdout.flush
39
88
  exec(*argv(udid, last: last, verbose: verbose))
40
89
  end
41
90
 
@@ -51,6 +100,66 @@ module Everywhere
51
100
  end
52
101
  argv + ["--style", "compact", "--predicate", verbose ? VERBOSE_PREDICATE : PREDICATE]
53
102
  end
103
+
104
+ # --- Android --------------------------------------------------------------
105
+
106
+ # logcat carries every process on the device, so the pid lookup is the
107
+ # whole filter — and it's also why "the app isn't running" is a distinct,
108
+ # extremely common state that has to be said plainly rather than shown as
109
+ # an empty stream. Unlike iOS, the process name isn't fixed by the
110
+ # template (it's the applicationId), so the config supplies it.
111
+ def android_logs(serial: nil, app_id: nil, last: nil, verbose: false)
112
+ serial ||= Everywhere::Emulator.booted_serial
113
+ UI.die!("no Android emulator or device — start one with `every dev --android`") unless serial
114
+
115
+ application_id = app_id || Everywhere::Config.load(Dir.pwd).bundle_id(target: "android-arm64")
116
+ pid = Everywhere::Emulator.pid_of(serial, application_id)
117
+ unless pid
118
+ UI.die!("#{application_id} isn't running on #{serial} — start it with `every dev --android` " \
119
+ "(or pass --app-id if that isn't this app's applicationId)")
120
+ end
121
+
122
+ UI.step(last ? "Android logs from the last #{UI.bold(window_label(last))}"
123
+ : "watching Android logs for #{UI.bold(application_id)} #{UI.dim("(Ctrl-C to stop)")}")
124
+ $stdout.flush # see ios_logs — exec discards a buffered header
125
+ exec(*android_argv(serial, pid, last: last, verbose: verbose))
126
+ end
127
+
128
+ # `-d` dumps what's buffered and exits — the `log show` half of the iOS
129
+ # split — and `-t` bounds that dump. Without `-d`, logcat follows.
130
+ def android_argv(serial, pid, last: nil, verbose: false)
131
+ argv = [Everywhere::AndroidSdk.adb!, "-s", serial, "logcat", "--pid=#{pid}"]
132
+ argv += ["-d", "-t", logcat_window(last)] if last
133
+ argv + ["-v", verbose ? VERBOSE_FORMAT : DEFAULT_FORMAT,
134
+ verbose ? VERBOSE_LEVEL : DEFAULT_LEVEL]
135
+ end
136
+
137
+ # logcat's -t takes a line count or a timestamp; it has no notion of "5m".
138
+ # So a bare number passes straight through as a count, and a duration in
139
+ # the iOS --last spelling is converted to the timestamp logcat wants.
140
+ #
141
+ # That timestamp is compared against the *device's* clock. An emulator
142
+ # inherits the host's clock and timezone so it lines up exactly; a
143
+ # physical device whose clock has drifted will return a little more or
144
+ # less than asked. A line count has no such ambiguity, which is why both
145
+ # spellings stay.
146
+ LOGCAT_TIME = "%m-%d %H:%M:%S.000"
147
+ DURATION_UNITS = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }.freeze
148
+
149
+ def logcat_window(last)
150
+ text = last.to_s.strip
151
+ return text if text.match?(/\A\d+\z/)
152
+
153
+ if (match = text.match(/\A(\d+)([smhd])\z/i))
154
+ seconds = match[1].to_i * DURATION_UNITS.fetch(match[2].downcase)
155
+ return (Time.now - seconds).strftime(LOGCAT_TIME)
156
+ end
157
+
158
+ UI.die!(%(--last takes a line count ("200") or a duration ("30s", "5m", "2h") — got #{last.inspect}))
159
+ end
160
+
161
+ # "200" reads as a count, everything else as a duration.
162
+ def window_label(last) = last.to_s.match?(/\A\d+\z/) ? "#{last} lines" : last.to_s
54
163
  end
55
164
  end
56
165
  end
@@ -12,7 +12,7 @@ module Everywhere
12
12
  # Produce a signed, notarized, stapled, distributable artifact + release.json.
13
13
  #
14
14
  # `every release` wraps the existing signing engine
15
- # (cli/support/macos/notarize.sh) rather than reimplementing
15
+ # (cli/support/release/macos/notarize.sh) rather than reimplementing
16
16
  # codesign/notarytool, and emits the machine-readable receipt every build
17
17
  # surface (local, CI, Platform) must produce. See
18
18
  # platform/docs/build-engine.md §4, §7, §9.
@@ -20,7 +20,7 @@ module Everywhere
20
20
  # Today only the macos builder is live; the target is parameterized so
21
21
  # ios/android/windows/linux slot in as builders later.
22
22
  class Release < Dry::CLI::Command
23
- NOTARIZE = File.expand_path("../../../support/macos/notarize.sh", __dir__)
23
+ NOTARIZE = File.expand_path("../../../support/release/macos/notarize.sh", __dir__)
24
24
 
25
25
  desc "Sign, notarize, staple, and emit release.json for a distributable build"
26
26