ruby_everywhere 0.3.0 → 0.4.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/README.md +255 -0
- data/bridge/everywhere/bridge.js +60 -3
- data/lib/everywhere/android_resources.rb +217 -0
- data/lib/everywhere/android_sdk.rb +330 -0
- data/lib/everywhere/asset_catalog.rb +185 -0
- data/lib/everywhere/auth_handoff.rb +398 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/ios.rb +40 -3
- data/lib/everywhere/commands/build.rb +51 -9
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +46 -8
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +1 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/config.rb +409 -13
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +12 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/log_filter.rb +74 -0
- data/lib/everywhere/native_helper.rb +2 -5
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +47 -0
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/android/README.md +217 -0
- data/support/mobile/android/app/build.gradle.kts +156 -0
- data/support/mobile/android/app/everywhere.properties +8 -0
- data/support/mobile/android/app/native-packages.gradle.kts +16 -0
- data/support/mobile/android/app/proguard-rules.pro +21 -0
- data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
- data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
- data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
- data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
- data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
- data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
- data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
- data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
- data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
- data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
- data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
- data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
- data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
- data/support/mobile/android/build.gradle.kts +8 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- data/support/mobile/android/gradle.properties +14 -0
- data/support/mobile/android/gradlew +251 -0
- data/support/mobile/android/gradlew.bat +94 -0
- data/support/mobile/android/settings.gradle.kts +28 -0
- data/support/mobile/ios/App/AppDelegate.swift +5 -0
- data/support/mobile/ios/App/AuthFlow.swift +101 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
- data/support/mobile/ios/App/SceneDelegate.swift +103 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
- data/support/mobile/ios/README.md +4 -1
- metadata +67 -1
|
@@ -5,6 +5,7 @@ require_relative "../paths"
|
|
|
5
5
|
require_relative "../shellout"
|
|
6
6
|
require_relative "../log_filter"
|
|
7
7
|
require_relative "../icon"
|
|
8
|
+
require_relative "../asset_catalog"
|
|
8
9
|
require_relative "../ui"
|
|
9
10
|
|
|
10
11
|
module Everywhere
|
|
@@ -72,9 +73,9 @@ module Everywhere
|
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# The whole per-app surface: one xcconfig (identity), two JSON resources
|
|
75
|
-
# (runtime config + path configuration with baked tabs),
|
|
76
|
-
# and the Extensions/ buildable folder (app-authored
|
|
77
|
-
# registry). The pbxproj is never touched.
|
|
76
|
+
# (runtime config + path configuration with baked tabs), the asset catalog
|
|
77
|
+
# (icon + app images), and the Extensions/ buildable folder (app-authored
|
|
78
|
+
# Swift + generated registry). The pbxproj is never touched.
|
|
78
79
|
def stamp!(work)
|
|
79
80
|
UI.step("stamping shell #{UI.dim("(#{bundle_id} → #{UI.short_path(work)})")}")
|
|
80
81
|
write_xcconfig(work)
|
|
@@ -86,8 +87,10 @@ module Everywhere
|
|
|
86
87
|
@config.path_configuration_json("ios"))
|
|
87
88
|
write_launch_background(work)
|
|
88
89
|
write_usage_strings(work)
|
|
90
|
+
write_url_scheme(work)
|
|
89
91
|
write_entitlements(work)
|
|
90
92
|
write_app_icon(work)
|
|
93
|
+
write_asset_catalog(work)
|
|
91
94
|
write_extensions(work)
|
|
92
95
|
write_native_packages(work)
|
|
93
96
|
end
|
|
@@ -291,6 +294,25 @@ module Everywhere
|
|
|
291
294
|
end
|
|
292
295
|
end
|
|
293
296
|
|
|
297
|
+
# The custom URL scheme third-party sign-in returns through
|
|
298
|
+
# (everywhere.yml auth:) → Info.plist. ASWebAuthenticationSession claims
|
|
299
|
+
# the callback for itself while it's running, so this registration isn't
|
|
300
|
+
# what makes the flow work — it's what stops another app from quietly
|
|
301
|
+
# owning the scheme, and what makes `xcrun simctl openurl` usable for
|
|
302
|
+
# testing the return leg by hand.
|
|
303
|
+
def write_url_scheme(work)
|
|
304
|
+
errors = @config.auth_errors
|
|
305
|
+
UI.die!("auth in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
306
|
+
return unless @config.oauth?
|
|
307
|
+
|
|
308
|
+
require "json"
|
|
309
|
+
scheme = @config.auth_scheme
|
|
310
|
+
UI.step("stamping sign-in callback scheme #{UI.dim("(#{scheme}://)")}")
|
|
311
|
+
Shellout.run!({}, "plutil", "-replace", "CFBundleURLTypes", "-json",
|
|
312
|
+
JSON.generate([{ "CFBundleURLName" => bundle_id, "CFBundleURLSchemes" => [scheme] }]),
|
|
313
|
+
File.join(work, "App", "Info.plist"))
|
|
314
|
+
end
|
|
315
|
+
|
|
294
316
|
# The launch-screen (and splash overlay) background — the storyboard's
|
|
295
317
|
# LaunchBackground named color, stamped from appearance.background_color
|
|
296
318
|
# so the launch frame matches the app instead of the template default.
|
|
@@ -357,6 +379,21 @@ module Everywhere
|
|
|
357
379
|
UI.warn "couldn't read #{File.basename(source)} — shipping the placeholder icon"
|
|
358
380
|
end
|
|
359
381
|
|
|
382
|
+
# App-supplied images: native/ios/assets/** → imagesets in the template's
|
|
383
|
+
# Assets.xcassets, so native/ios Swift code reaches them by filename with
|
|
384
|
+
# Image("logo") / UIImage(named: "logo").
|
|
385
|
+
def write_asset_catalog(work)
|
|
386
|
+
source = File.join(@root, "native", "ios", "assets")
|
|
387
|
+
return unless File.directory?(source)
|
|
388
|
+
|
|
389
|
+
names, errors = AssetCatalog.write!(source, File.join(work, "App", "Assets.xcassets"))
|
|
390
|
+
UI.die!("native/ios/assets:\n#{errors.join("\n")}") unless errors.empty?
|
|
391
|
+
return if names.empty?
|
|
392
|
+
|
|
393
|
+
listed = names.length > 6 ? "#{names.take(6).join(", ")}, …" : names.join(", ")
|
|
394
|
+
UI.step("bundling image assets #{UI.dim("(#{names.length} from native/ios/assets: #{listed})")}")
|
|
395
|
+
end
|
|
396
|
+
|
|
360
397
|
def xcodebuild!(work, configuration, dist_dir)
|
|
361
398
|
UI.step("building iOS shell #{UI.dim("(xcodebuild, #{configuration}, first run compiles HotwireNative)")}")
|
|
362
399
|
FileUtils.mkdir_p(dist_dir)
|
|
@@ -10,6 +10,7 @@ require_relative "../png"
|
|
|
10
10
|
require_relative "../icon"
|
|
11
11
|
require_relative "../paths"
|
|
12
12
|
require_relative "../builders/ios"
|
|
13
|
+
require_relative "../builders/android"
|
|
13
14
|
|
|
14
15
|
module Everywhere
|
|
15
16
|
module Commands
|
|
@@ -24,34 +25,47 @@ module Everywhere
|
|
|
24
25
|
class Build < Dry::CLI::Command
|
|
25
26
|
SHIM = File.expand_path("~/.tebako-shims/clang++")
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
DEFAULT_TARGET = "macos-arm64"
|
|
29
|
+
IOS_TARGET = "ios-arm64"
|
|
30
|
+
# The APK/AAB is universal, so the arch half is nominal — it exists only
|
|
31
|
+
# so every target reads as os-arch.
|
|
32
|
+
ANDROID_TARGET = "android-arm64"
|
|
33
|
+
|
|
34
|
+
# The --ios/--android shorthands and the os they stand for, so adding a
|
|
35
|
+
# mobile platform is one entry rather than a third copy of resolve_target.
|
|
36
|
+
SHORTHAND_TARGETS = { ios: IOS_TARGET, android: ANDROID_TARGET }.freeze
|
|
37
|
+
|
|
38
|
+
desc "Package the app natively — desktop (tebako press + shell + .app), iOS (--ios) or Android (--android)"
|
|
28
39
|
|
|
29
40
|
option :root, default: ".", desc: "App root directory"
|
|
30
41
|
option :output, default: nil, desc: "Output binary path (default: dist/<app>)"
|
|
31
42
|
option :ruby, default: "4.0.6", desc: "Ruby version to package"
|
|
32
43
|
option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
|
|
33
44
|
option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
|
|
34
|
-
option :target, default:
|
|
45
|
+
option :target, default: nil, desc: "Build target (os-arch) — selects any platforms: overrides, default: \"#{DEFAULT_TARGET}\""
|
|
35
46
|
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
47
|
+
option :ios, type: :boolean, default: false, desc: "Build the iOS shell (shorthand for --target #{IOS_TARGET})"
|
|
48
|
+
option :android, type: :boolean, default: false, desc: "Build the Android shell (shorthand for --target #{ANDROID_TARGET})"
|
|
36
49
|
option :skip_press, type: :boolean, default: false, desc: "Reuse the existing pressed binary, only rebuild the .app"
|
|
37
50
|
|
|
38
51
|
def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb",
|
|
39
|
-
app_name: nil, target:
|
|
52
|
+
app_name: nil, target: nil, shell_dir: nil, ios: false, android: false, skip_press: false, **)
|
|
53
|
+
target = resolve_target(target, ios: ios, android: android)
|
|
40
54
|
root_path = File.expand_path(root)
|
|
41
55
|
config = Everywhere::Config.load(root_path)
|
|
42
56
|
|
|
43
57
|
# Builder dispatch on the target's os (build-engine.md §5): macOS is
|
|
44
|
-
# the tebako+tauri path below;
|
|
45
|
-
# its own builder. Anything else has no builder yet.
|
|
58
|
+
# the tebako+tauri path below; the mobile targets are Hotwire Native
|
|
59
|
+
# shells, each built by its own builder. Anything else has no builder yet.
|
|
46
60
|
case Everywhere::Config.os_of(target)
|
|
47
|
-
when "ios"
|
|
48
|
-
app =
|
|
49
|
-
|
|
61
|
+
when "ios", "android"
|
|
62
|
+
app = mobile_builder(target).new(config: config, root: root_path, target: target)
|
|
63
|
+
.build!(dist_dir: File.expand_path("dist", root_path))
|
|
50
64
|
UI.success("built #{rel(app, root_path)}")
|
|
51
65
|
return
|
|
52
66
|
when "macos" # the rest of this method
|
|
53
67
|
else
|
|
54
|
-
UI.die!("no builder for target #{target} — macos-* and
|
|
68
|
+
UI.die!("no builder for target #{target} — macos-*, ios-* and android-* are supported")
|
|
55
69
|
end
|
|
56
70
|
|
|
57
71
|
# The os of `target` selects any per-platform overrides (bundle id,
|
|
@@ -95,6 +109,34 @@ module Everywhere
|
|
|
95
109
|
|
|
96
110
|
private
|
|
97
111
|
|
|
112
|
+
# `--ios` is the shorthand `every dev` already has; --target stays the
|
|
113
|
+
# general escape hatch. Both given is only an error when they disagree —
|
|
114
|
+
# `--ios --target ios-arm64` is redundant, not wrong.
|
|
115
|
+
def mobile_builder(target)
|
|
116
|
+
Everywhere::Config.os_of(target) == "android" ? Builders::Android : Builders::Ios
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# --ios/--android are shorthand for their target, and disagreeing with an
|
|
120
|
+
# explicit --target is a mistake worth naming rather than silently
|
|
121
|
+
# resolving one way: both spellings were deliberate, so we can't know
|
|
122
|
+
# which one the user meant.
|
|
123
|
+
def resolve_target(target, ios:, android:)
|
|
124
|
+
asked = { ios: ios, android: android }.select { |_flag, given| given }.keys
|
|
125
|
+
if asked.length > 1
|
|
126
|
+
UI.die!("#{asked.map { |f| "--#{f}" }.join(" and ")} are separate builds — run one at a time")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
flag = asked.first or return target || DEFAULT_TARGET
|
|
130
|
+
shorthand = SHORTHAND_TARGETS.fetch(flag)
|
|
131
|
+
return shorthand if target.nil?
|
|
132
|
+
|
|
133
|
+
unless Everywhere::Config.os_of(target) == flag.to_s
|
|
134
|
+
UI.die!("--#{flag} and --target #{target} disagree — drop one " \
|
|
135
|
+
"(--#{flag} means --target #{shorthand})")
|
|
136
|
+
end
|
|
137
|
+
target
|
|
138
|
+
end
|
|
139
|
+
|
|
98
140
|
def rel(path, root_path) = UI.short_path(path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", ""))
|
|
99
141
|
|
|
100
142
|
# The precompile step runs against the HOST Ruby — unlike tebako press,
|
|
@@ -6,17 +6,22 @@ require "fileutils"
|
|
|
6
6
|
module Everywhere
|
|
7
7
|
module Commands
|
|
8
8
|
# Wipes the shared build caches under ~/.rubyeverywhere (or
|
|
9
|
-
# $RUBYEVERYWHERE_HOME): the cargo shell cache
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# $RUBYEVERYWHERE_HOME): the cargo shell cache, the iOS DerivedData / SPM /
|
|
10
|
+
# stamped-project dirs, and the Android Gradle home / icon font / stamped
|
|
11
|
+
# projects. Safe to run any time: everything here is derived output, so the
|
|
12
|
+
# next `every dev`/`every build` rebuilds it — cold, and for Android that
|
|
13
|
+
# includes re-downloading Gradle and the Material icon font.
|
|
12
14
|
class Clean < Dry::CLI::Command
|
|
13
|
-
desc "Remove the shell build caches (~/.rubyeverywhere: shell-target, ios-*)"
|
|
15
|
+
desc "Remove the shell build caches (~/.rubyeverywhere: shell-target, ios-*, android-*)"
|
|
14
16
|
|
|
15
17
|
def call(**)
|
|
16
18
|
caches = [Everywhere::Paths.cargo_target_dir,
|
|
17
19
|
Everywhere::Paths.ios_derived_data_dir,
|
|
18
20
|
Everywhere::Paths.ios_packages_dir,
|
|
19
|
-
File.join(Everywhere::Paths.cache_dir, "ios")
|
|
21
|
+
File.join(Everywhere::Paths.cache_dir, "ios"),
|
|
22
|
+
Everywhere::Paths.android_gradle_home,
|
|
23
|
+
Everywhere::Paths.android_fonts_dir,
|
|
24
|
+
File.join(Everywhere::Paths.cache_dir, "android")].select { |d| File.exist?(d) }
|
|
20
25
|
return UI.step("nothing to clean #{UI.dim("(no build caches under #{Everywhere::Paths.cache_dir})")}") if caches.empty?
|
|
21
26
|
|
|
22
27
|
freed = 0
|
|
@@ -6,7 +6,9 @@ require "shellwords"
|
|
|
6
6
|
require_relative "../shellout"
|
|
7
7
|
require_relative "../paths"
|
|
8
8
|
require_relative "../builders/ios"
|
|
9
|
+
require_relative "../builders/android"
|
|
9
10
|
require_relative "../simulator"
|
|
11
|
+
require_relative "../emulator"
|
|
10
12
|
require_relative "logs"
|
|
11
13
|
|
|
12
14
|
module Everywhere
|
|
@@ -21,16 +23,24 @@ module Everywhere
|
|
|
21
23
|
class Dev < Dry::CLI::Command
|
|
22
24
|
desc "Run the dev server with native shells on demand (live reload, no packaging)"
|
|
23
25
|
|
|
26
|
+
IOS_TARGET = "ios-arm64"
|
|
27
|
+
ANDROID_TARGET = "android-arm64"
|
|
28
|
+
|
|
24
29
|
option :port, default: "3000", desc: "Dev server port"
|
|
25
30
|
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
26
|
-
option :target, default: nil, desc: "Single shell to open: macos (desktop)
|
|
31
|
+
option :target, default: nil, desc: "Single shell to open: macos (desktop), ios (Simulator) or android (emulator)"
|
|
27
32
|
option :desktop, type: :boolean, default: false, desc: "Open the desktop shell"
|
|
28
33
|
option :ios, type: :boolean, default: false, desc: "Open the iOS Simulator shell"
|
|
34
|
+
option :android, type: :boolean, default: false, desc: "Open the Android emulator shell"
|
|
29
35
|
option :browser, type: :boolean, default: false, desc: "Open the app in the default browser"
|
|
36
|
+
option :dev_url, default: nil, desc: "Explicit URL for the device to load (e.g. a LAN address for a physical Android device)"
|
|
30
37
|
|
|
31
|
-
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false,
|
|
38
|
+
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false, android: false,
|
|
39
|
+
browser: false, dev_url: nil, **)
|
|
32
40
|
@shell_dir = shell_dir
|
|
33
|
-
|
|
41
|
+
@port = port
|
|
42
|
+
@dev_url_override = dev_url
|
|
43
|
+
targets = requested_targets(target, desktop: desktop, ios: ios, android: android, browser: browser)
|
|
34
44
|
framework, @config = detect_app
|
|
35
45
|
|
|
36
46
|
start_server(framework, port)
|
|
@@ -54,10 +64,15 @@ module Everywhere
|
|
|
54
64
|
|
|
55
65
|
private
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
# --target names ONE shell; the boolean flags are additive. Desktop is the
|
|
68
|
+
# fallback for any --target that isn't a mobile os, which keeps
|
|
69
|
+
# `--target macos-arm64` (and a bare `--target`) meaning what it always did.
|
|
70
|
+
def requested_targets(target, desktop:, ios:, android:, browser:)
|
|
71
|
+
target_os = target && Everywhere::Config.os_of(target)
|
|
58
72
|
targets = []
|
|
59
|
-
targets << "desktop" if desktop || (target &&
|
|
60
|
-
targets << "ios" if ios ||
|
|
73
|
+
targets << "desktop" if desktop || (target && !%w[ios android].include?(target_os))
|
|
74
|
+
targets << "ios" if ios || target_os == "ios"
|
|
75
|
+
targets << "android" if android || target_os == "android"
|
|
61
76
|
targets << "browser" if browser
|
|
62
77
|
targets
|
|
63
78
|
end
|
|
@@ -100,6 +115,7 @@ module Everywhere
|
|
|
100
115
|
def launch(target)
|
|
101
116
|
case target
|
|
102
117
|
when "ios" then launch_ios
|
|
118
|
+
when "android" then launch_android
|
|
103
119
|
when "browser" then launch_browser
|
|
104
120
|
else launch_desktop
|
|
105
121
|
end
|
|
@@ -134,6 +150,26 @@ module Everywhere
|
|
|
134
150
|
Simulator.launch(udid, @config.bundle_id(target: "ios-arm64"), env: { "EVERYWHERE_DEV_URL" => @dev_url })
|
|
135
151
|
end
|
|
136
152
|
|
|
153
|
+
# The Android counterpart of launch_ios: build Debug (warm after the first
|
|
154
|
+
# run), put it on a booted emulator and launch it. Unlike the Simulator,
|
|
155
|
+
# which shares the Mac's loopback, an Android device reaches the host
|
|
156
|
+
# through `adb reverse` — so the URL is resolved per-device rather than
|
|
157
|
+
# reused from @dev_url (see Emulator.dev_url for the fallback ladder).
|
|
158
|
+
# Pressing a again rebuilds and relaunches, so it doubles as a reload.
|
|
159
|
+
def launch_android
|
|
160
|
+
serial = Emulator.boot_default!
|
|
161
|
+
url = Emulator.dev_url(@dev_url, serial: serial, port: @port, override: @dev_url_override)
|
|
162
|
+
|
|
163
|
+
app = Builders::Android.new(config: @config, root: @config.root, target: ANDROID_TARGET)
|
|
164
|
+
.build!(dist_dir: File.expand_path("dist", @config.root), dev: true, dev_url: url)
|
|
165
|
+
|
|
166
|
+
application_id = @config.bundle_id(target: ANDROID_TARGET)
|
|
167
|
+
UI.step("installing #{UI.bold(File.basename(app))}")
|
|
168
|
+
Emulator.install(serial, app, application_id: application_id)
|
|
169
|
+
UI.step("launching → #{UI.cyan(url)}")
|
|
170
|
+
Emulator.launch(serial, application_id)
|
|
171
|
+
end
|
|
172
|
+
|
|
137
173
|
def launch_browser
|
|
138
174
|
UI.step("opening browser → #{UI.cyan(@dev_url)}")
|
|
139
175
|
opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
|
|
@@ -164,14 +200,16 @@ module Everywhere
|
|
|
164
200
|
# -icanon -echo disables only line buffering and echo, leaving output
|
|
165
201
|
# processing (and Ctrl-C as SIGINT) intact.
|
|
166
202
|
def interact
|
|
167
|
-
UI.note("press #{UI.bold("d")} desktop · #{UI.bold("i")} iOS Simulator · #{UI.bold("
|
|
168
|
-
"#{UI.bold("l")} iOS logs · #{UI.bold("q")} quit",
|
|
203
|
+
UI.note("press #{UI.bold("d")} desktop · #{UI.bold("i")} iOS Simulator · #{UI.bold("a")} Android · " \
|
|
204
|
+
"#{UI.bold("b")} browser · #{UI.bold("l")} iOS logs · #{UI.bold("q")} quit",
|
|
205
|
+
marker: "⌨", color: :cyan)
|
|
169
206
|
saved_tty = `stty -g`.chomp
|
|
170
207
|
system("stty", "-icanon", "-echo")
|
|
171
208
|
loop do
|
|
172
209
|
case $stdin.getc
|
|
173
210
|
when "d" then launch("desktop")
|
|
174
211
|
when "i" then launch("ios")
|
|
212
|
+
when "a" then launch("android")
|
|
175
213
|
when "b" then launch("browser")
|
|
176
214
|
when "l" then toggle_logs
|
|
177
215
|
when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
|
|
@@ -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
|
|
11
|
-
#
|
|
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
|
|
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)
|
|
@@ -187,7 +187,7 @@ module Everywhere
|
|
|
187
187
|
# # # (`every build` / `every dev --ios`) work today;
|
|
188
188
|
# # # hosted platform builds for iOS aren't live yet.
|
|
189
189
|
# # - android-arm64 # future — mobile is remote mode only
|
|
190
|
-
#
|
|
190
|
+
# capabilities: [notifications, filesystem:read, clipboard] # desktop OS integration
|
|
191
191
|
|
|
192
192
|
# Per-platform overrides. Keys are platform names (macos / ios / android /
|
|
193
193
|
# 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.
|
|
10
|
-
# log
|
|
11
|
-
# --last replays recent history
|
|
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 (
|
|
16
|
-
option :
|
|
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
|
-
|
|
32
|
-
|
|
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
|