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
|
@@ -30,6 +30,7 @@ module Everywhere
|
|
|
30
30
|
when :notarize then notarize(line)
|
|
31
31
|
when :tebako then tebako(line)
|
|
32
32
|
when :xcodebuild then xcodebuild(line)
|
|
33
|
+
when :gradle then gradle(line)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -153,6 +154,79 @@ module Everywhere
|
|
|
153
154
|
end
|
|
154
155
|
end
|
|
155
156
|
|
|
157
|
+
# --- gradle (Android shell) -----------------------------------------------
|
|
158
|
+
#
|
|
159
|
+
# Gradle is the quiet one: a cold build is ~120 lines, ~83 of them
|
|
160
|
+
# "> Task :app:…". So this filter isn't fighting volume like the two above —
|
|
161
|
+
# it exists because of two specific noises that make a healthy build look
|
|
162
|
+
# broken. Gradle 9 ends every build with a four-line "incompatible with
|
|
163
|
+
# Gradle 10" deprecation banner earned by the Android Gradle Plugin, not by
|
|
164
|
+
# anything the user wrote; and hotwire-native-android 1.3.0 compiled with
|
|
165
|
+
# Kotlin 2.3.0 emits exactly 7 `w:` warnings from navigation-fragments
|
|
166
|
+
# (deprecated overrides, getParcelableArrayList, KT-73255) that no app can
|
|
167
|
+
# act on. Both are suppressed here and both stay in the on-disk log.
|
|
168
|
+
#
|
|
169
|
+
# The line that decides a warning's fate is its path: app-authored Kotlin is
|
|
170
|
+
# stamped into extensions/, and that's the only code the user can fix.
|
|
171
|
+
MILESTONES = {
|
|
172
|
+
/:process\w*Manifest\b/ => "merging manifests",
|
|
173
|
+
/:merge\w*Resources\b/ => "merging resources",
|
|
174
|
+
/:merge\w*Assets\b/ => "bundling assets",
|
|
175
|
+
/:compile\w*Kotlin\b/ => "compiling Kotlin",
|
|
176
|
+
/:compile\w*JavaWithJavac\b/ => "compiling Java",
|
|
177
|
+
/:package\w+\b/ => "packaging APK"
|
|
178
|
+
}.freeze
|
|
179
|
+
|
|
180
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
181
|
+
def gradle(line)
|
|
182
|
+
short = UI.short_path(line)
|
|
183
|
+
case line
|
|
184
|
+
# Kotlin/javac/AAPT failures, and Gradle's own failure block.
|
|
185
|
+
when /\Ae: /, /\berror:/i, /\AFAILURE: /, /\A\* What went wrong:/, /\ABUILD FAILED/
|
|
186
|
+
" #{UI.red(short)}"
|
|
187
|
+
when /\Aw: /
|
|
188
|
+
return " #{UI.yellow(short)}" if line.include?("/extensions/")
|
|
189
|
+
|
|
190
|
+
once(:shell_warning) { UI.note_line("Kotlin warnings from the shell template — see the full log") }
|
|
191
|
+
when /\ABUILD SUCCESSFUL(?: in (.+))?/
|
|
192
|
+
UI.note_line("build succeeded#{Regexp.last_match(1) && " (#{Regexp.last_match(1)})"}",
|
|
193
|
+
marker: "✓", color: :green)
|
|
194
|
+
when /\A\d+ actionable tasks?:/
|
|
195
|
+
UI.note_line(line)
|
|
196
|
+
|
|
197
|
+
# First-run downloads are the whole reason a cold build takes minutes.
|
|
198
|
+
when /\ADownloading https:\/\/services\.gradle\.org/
|
|
199
|
+
once(:gradle_dist) { UI.note_line("downloading Gradle (first run only)") }
|
|
200
|
+
when /\AStarting a Gradle Daemon/
|
|
201
|
+
once(:daemon) { UI.note_line("starting the Gradle daemon") }
|
|
202
|
+
|
|
203
|
+
# ~83 task lines. Milestones once each; the rest, including everything
|
|
204
|
+
# already up to date, is background.
|
|
205
|
+
when /\A> Task (\S+)/
|
|
206
|
+
task = Regexp.last_match(1)
|
|
207
|
+
return :drop if line.match?(/(UP-TO-DATE|FROM-CACHE|NO-SOURCE|SKIPPED)\s*\z/)
|
|
208
|
+
|
|
209
|
+
label = MILESTONES.find { |pattern, _| task.match?(pattern) }&.last or return :drop
|
|
210
|
+
|
|
211
|
+
once("task-#{label}") { UI.note_line(label) }
|
|
212
|
+
|
|
213
|
+
# Gradle 9's deprecation banner and its follow-on lines, the incubating
|
|
214
|
+
# problems report, the daemon's welcome, and any progress redraw that
|
|
215
|
+
# survives --console=plain.
|
|
216
|
+
when /Deprecated Gradle features were used in this build/,
|
|
217
|
+
/--warning-mode all/,
|
|
218
|
+
/\AFor more information[, ].*docs\.gradle\.org/,
|
|
219
|
+
/\A\[Incubating\] Problems report/,
|
|
220
|
+
/\AWelcome to Gradle /,
|
|
221
|
+
/\A<[=\-]+>/,
|
|
222
|
+
/\A> (Configure|Starting|Loading|Calculating|IDLE)/
|
|
223
|
+
:drop
|
|
224
|
+
else
|
|
225
|
+
nil # unrecognized -> caller shows it dimmed
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
229
|
+
|
|
156
230
|
# Show a line only the first time its key is seen; :drop the repeats. Keeps a
|
|
157
231
|
# long notarization from scrolling a wall of identical "waiting" lines (and
|
|
158
232
|
# notarytool's thrice-echoed submission id from showing three times).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
4
|
require "json"
|
|
5
|
+
require_relative "config"
|
|
5
6
|
|
|
6
7
|
module Everywhere
|
|
7
8
|
# Server-side view helpers mirroring the JS bridge's platform detection, so
|
|
@@ -25,11 +26,7 @@ module Everywhere
|
|
|
25
26
|
# :ios, :android, or nil in a plain browser. Reads Hotwire Native's UA
|
|
26
27
|
# marker, so it's correct even before the JS bridge has booted.
|
|
27
28
|
def native_platform
|
|
28
|
-
|
|
29
|
-
return :ios if ua.include?("Hotwire Native iOS")
|
|
30
|
-
return :android if ua.include?("Hotwire Native Android")
|
|
31
|
-
|
|
32
|
-
nil
|
|
29
|
+
Everywhere.native_platform_of(_everywhere_user_agent)
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
# The shell's version as a Gem::Version (from the "RubyEverywhere/<ver>"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "auth_handoff"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# OmniAuth 2 and the native sign-in flow, introduced to each other.
|
|
7
|
+
#
|
|
8
|
+
# Since CVE-2015-9284, OmniAuth only starts a provider flow from a POST
|
|
9
|
+
# carrying a CSRF token (omniauth-rails_csrf_protection). Right for browsers —
|
|
10
|
+
# and impossible for the native flow, which enters the provider path through
|
|
11
|
+
# /everywhere/auth/start's redirect: a GET, in a browser session that has no
|
|
12
|
+
# CSRF token to give. Without help, tapping "Sign in with GitHub" in the app
|
|
13
|
+
# ends on OmniAuth's failure page.
|
|
14
|
+
#
|
|
15
|
+
# # config/initializers/omniauth.rb, after your provider setup
|
|
16
|
+
# require "everywhere/omniauth"
|
|
17
|
+
# Everywhere::OmniAuth.protect!
|
|
18
|
+
#
|
|
19
|
+
# Browsers keep exactly the validation they had — whatever
|
|
20
|
+
# request_validation_phase was set to keeps running. The one extra shape
|
|
21
|
+
# allowed through is the native flow's entry: a top-level, not-cross-site GET
|
|
22
|
+
# carrying a marker cookie that /everywhere/auth/start sealed seconds ago.
|
|
23
|
+
# Nobody else can mint that marker (AES-GCM under the app's secret), start
|
|
24
|
+
# refuses cross-site initiators outright, and the freshness window means a
|
|
25
|
+
# marker lingering in Safari's jar after a real sign-in stops vouching for
|
|
26
|
+
# anything almost immediately.
|
|
27
|
+
module OmniAuth
|
|
28
|
+
# How long after start seals the marker its GET still reads as the native
|
|
29
|
+
# flow arriving. The real gap is one redirect — milliseconds — but the
|
|
30
|
+
# sheet can sit on our page briefly on a slow network; anything past this
|
|
31
|
+
# is a browser wandering in.
|
|
32
|
+
ENTRY_WINDOW = 90
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Wire OmniAuth's global config: allow GET request phases (they're
|
|
36
|
+
# refused per-request by the validator unless they prove themselves),
|
|
37
|
+
# and wrap the configured validation phase so everything that isn't the
|
|
38
|
+
# native entry is judged exactly as before. Safe to call more than once —
|
|
39
|
+
# re-running replaces the wrapper, never stacks it.
|
|
40
|
+
def protect!(entry_window: ENTRY_WINDOW)
|
|
41
|
+
unless defined?(::OmniAuth) && ::OmniAuth.respond_to?(:config)
|
|
42
|
+
raise "Everywhere::OmniAuth.protect! needs OmniAuth loaded first — " \
|
|
43
|
+
"call it below your OmniAuth/Devise setup (config/initializers/omniauth.rb)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
config = ::OmniAuth.config
|
|
47
|
+
config.allowed_request_methods = Array(config.allowed_request_methods) | [:get]
|
|
48
|
+
config.silence_get_warning = true if config.respond_to?(:silence_get_warning=)
|
|
49
|
+
|
|
50
|
+
fallback = config.request_validation_phase
|
|
51
|
+
fallback = fallback.fallback if fallback.is_a?(RequestValidator)
|
|
52
|
+
config.request_validation_phase = RequestValidator.new(fallback: fallback, entry_window: entry_window)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# OmniAuth request_validation_phase: passes the native flow's entry,
|
|
57
|
+
# delegates everything else. A nil fallback means the app had no request
|
|
58
|
+
# validation configured, and it still has none — this never tightens.
|
|
59
|
+
class RequestValidator
|
|
60
|
+
attr_reader :fallback
|
|
61
|
+
|
|
62
|
+
def initialize(fallback: nil, entry_window: ENTRY_WINDOW)
|
|
63
|
+
@fallback = fallback
|
|
64
|
+
@entry_window = entry_window
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call(env)
|
|
68
|
+
return if native_entry?(env)
|
|
69
|
+
|
|
70
|
+
@fallback&.call(env)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# The one shape the native flow produces: a GET, not from a cross-site
|
|
76
|
+
# initiator, carrying a marker minted moments ago. POSTs never qualify —
|
|
77
|
+
# they come from rendered pages that have a CSRF token to present.
|
|
78
|
+
def native_entry?(env)
|
|
79
|
+
return false unless env["REQUEST_METHOD"] == "GET"
|
|
80
|
+
return false if env["HTTP_SEC_FETCH_SITE"] == "cross-site"
|
|
81
|
+
|
|
82
|
+
marker = AuthHandoff.marker(env)
|
|
83
|
+
issued = marker && marker["iat"]
|
|
84
|
+
!!issued && Time.now.to_i - issued.to_i <= @entry_window
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/everywhere/paths.rb
CHANGED
|
@@ -54,6 +54,30 @@ module Everywhere
|
|
|
54
54
|
"reinstall ruby_everywhere or pass --template-dir")
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# The Hotwire Native Android shell template bundled inside the gem:
|
|
58
|
+
# support/mobile/android/ holds a frozen Gradle project (wrapper committed)
|
|
59
|
+
# the CLI stamps per-app — everywhere.properties + assets + res, never a
|
|
60
|
+
# build script. Same freeze contract the iOS template has.
|
|
61
|
+
def bundled_android_dir
|
|
62
|
+
File.join(gem_root, "support", "mobile", "android")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Absolute path to the Android template, or nil when it isn't there. Unlike
|
|
66
|
+
# #ios_dir, nil is the NORMAL state until the template lands — callers must
|
|
67
|
+
# read it as "Android isn't buildable in this release yet", not as a corrupt
|
|
68
|
+
# install. settings.gradle.kts is the marker because it's the one file every
|
|
69
|
+
# Gradle project must have at its root.
|
|
70
|
+
def android_dir
|
|
71
|
+
bundled_android_dir if File.exist?(File.join(bundled_android_dir, "settings.gradle.kts"))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Like #android_dir but aborts with a helpful message when the template is
|
|
75
|
+
# missing. The --template-dir hint covers dev on a modified template.
|
|
76
|
+
def android_dir!
|
|
77
|
+
android_dir or UI.die!("couldn't find the bundled Android template at #{bundled_android_dir}; " \
|
|
78
|
+
"reinstall ruby_everywhere or pass --template-dir")
|
|
79
|
+
end
|
|
80
|
+
|
|
57
81
|
# Per-user cache dir. Keeps heavy build state out of the gem and the app.
|
|
58
82
|
def cache_dir
|
|
59
83
|
File.expand_path(ENV["RUBYEVERYWHERE_HOME"] || "~/.rubyeverywhere")
|
|
@@ -79,6 +103,29 @@ module Everywhere
|
|
|
79
103
|
File.join(cache_dir, "ios-packages")
|
|
80
104
|
end
|
|
81
105
|
|
|
106
|
+
# The stamped copy of the Android template for one app, keyed by
|
|
107
|
+
# applicationId. Persistent for the same reasons ios_work_dir is: Gradle's
|
|
108
|
+
# incremental state is path-sensitive, and the stamped project opens in
|
|
109
|
+
# Android Studio unmodified.
|
|
110
|
+
def android_work_dir(application_id)
|
|
111
|
+
File.join(cache_dir, "android", application_id)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# GRADLE_USER_HOME for shell builds. Redirected here so the multi-GB Gradle
|
|
115
|
+
# distribution, the Android Gradle Plugin and the Hotwire artifacts are
|
|
116
|
+
# downloaded once and stay warm across every app — and never land inside the
|
|
117
|
+
# installed gem. Same trick as cargo_target_dir.
|
|
118
|
+
def android_gradle_home
|
|
119
|
+
File.join(cache_dir, "android-gradle")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Where the Material Symbols icon font is cached after its one-time fetch.
|
|
123
|
+
# The font is ~10 MB, so it's fetched on first Android build instead of
|
|
124
|
+
# riding along in a 229 KB gem that most users never build Android with.
|
|
125
|
+
def android_fonts_dir
|
|
126
|
+
File.join(cache_dir, "android-fonts")
|
|
127
|
+
end
|
|
128
|
+
|
|
82
129
|
# Where cargo writes the shell's build output (CARGO_TARGET_DIR). Redirected
|
|
83
130
|
# here so cargo never dumps a multi-GB target/ into the installed gem, and so
|
|
84
131
|
# the shell compiles warm across projects — the shell source is identical for
|
data/lib/everywhere/version.rb
CHANGED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# RubyEverywhere Android Shell Template
|
|
2
|
+
|
|
3
|
+
A canonical [Hotwire Native Android][1] shell that wraps a deployed Ruby web app
|
|
4
|
+
(remote URL). The RubyEverywhere CLI stamps a copy of this directory per app —
|
|
5
|
+
the template itself is **never edited per-app**.
|
|
6
|
+
|
|
7
|
+
The iOS template is the reference implementation; this one is a feature-for-feature
|
|
8
|
+
port of `support/mobile/ios/`, and where the two differ the difference is forced
|
|
9
|
+
by the platform and is explained in a comment at the site.
|
|
10
|
+
|
|
11
|
+
## Freeze rules
|
|
12
|
+
|
|
13
|
+
Every build script, layout, theme and Kotlin source here is written once and
|
|
14
|
+
frozen. When `every build --target android-*` stamps a copy, it overwrites
|
|
15
|
+
**exactly** these files and nothing else:
|
|
16
|
+
|
|
17
|
+
1. `app/everywhere.properties` — applicationId, versionName, versionCode, app
|
|
18
|
+
label. The direct analogue of iOS's `App.xcconfig`: identity is stamped
|
|
19
|
+
without ever touching a build script. `app/build.gradle.kts` reads it at
|
|
20
|
+
configuration time.
|
|
21
|
+
2. `app/src/main/assets/everywhere.json` — runtime config (remote URL, entry
|
|
22
|
+
path, colours, permissions, auth, universal-link hosts);
|
|
23
|
+
`Config#to_shell_json`.
|
|
24
|
+
3. `app/src/main/assets/json/path-configuration.json` — baked rules +
|
|
25
|
+
`settings.tabs` (`Config#path_configuration_json("android")`). The same
|
|
26
|
+
document is served live at `/everywhere/android_v1.json` by the gem's
|
|
27
|
+
`MobileConfigEndpoint`, which the shell loads as its second, auth-aware
|
|
28
|
+
source.
|
|
29
|
+
4. `app/src/main/assets/fonts/` — the selected Material icon font and its
|
|
30
|
+
`.codepoints` map. The bundled classic set
|
|
31
|
+
(`MaterialIcons-Regular.ttf`, 357 KB, 2,235 icons) is the offline fallback;
|
|
32
|
+
`icon_font: symbols` fetches the 10.6 MB variable font into
|
|
33
|
+
`~/.rubyeverywhere/android-fonts/` on first use. The two files must always
|
|
34
|
+
travel together — 282 names exist in both fonts with *different* codepoints.
|
|
35
|
+
5. `app/src/main/res/values/colors.xml` and `values-night/colors.xml` — the
|
|
36
|
+
light and dark halves of `appearance.tint_color` / `background_color`.
|
|
37
|
+
Everything in the theme derives from those two colours.
|
|
38
|
+
6. `app/src/main/res/values/ic_launcher_background.xml` and
|
|
39
|
+
`res/drawable-<dpi>/ic_launcher_foreground.png` — the adaptive launcher
|
|
40
|
+
icon's two layers. `minSdk 28` means there are no legacy
|
|
41
|
+
`mipmap-<dpi>/ic_launcher.png` fallbacks to generate.
|
|
42
|
+
7. *(optionally)* `app/src/stamped/AndroidManifest.xml` — deep-link intent
|
|
43
|
+
filters, the auth callback scheme, and runtime `<uses-permission>` entries.
|
|
44
|
+
Written **only** when `everywhere.yml` declares `deep_linking:`, `auth:` or
|
|
45
|
+
`permissions:`. It is a separate manifest, folded in by the merger, so the
|
|
46
|
+
main manifest stays frozen — the analogue of iOS's optional
|
|
47
|
+
`App.entitlements`. See the note below on how it is wired.
|
|
48
|
+
8. *(optionally)* `app/native-packages.gradle.kts` — Maven coordinates from
|
|
49
|
+
`native.android.packages`, and
|
|
50
|
+
`app/src/main/java/com/rubyeverywhere/shell/extensions/` — the app repo's
|
|
51
|
+
`native/android/*.kt` plus the generated `EverywhereExtensions.kt` registry.
|
|
52
|
+
Both ship as empty defaults so the raw template builds unchanged.
|
|
53
|
+
9. `app/src/stamped/res/drawable*/` — the app's own images, generated from
|
|
54
|
+
`native/android/assets/**` by `Everywhere::AndroidResources`, reachable from
|
|
55
|
+
extension code as `R.drawable.<name>`. Registered as a second `res` srcDir on
|
|
56
|
+
`main` (see `sourceSets` in `app/build.gradle.kts`) and **cleared on every
|
|
57
|
+
build**, which is why it is its own directory rather than more files in
|
|
58
|
+
`main/res`: stamping merges over the work dir instead of wiping it, so an
|
|
59
|
+
asset the app deleted has to be removed by the generator that wrote it.
|
|
60
|
+
Filenames are the whole API and match iOS's exactly — `logo@2x~dark.png`
|
|
61
|
+
becomes `drawable-night-xhdpi/logo.png`.
|
|
62
|
+
10. *(dev only)* `app/src/debug/assets/dev-url.txt` — the dev-server URL.
|
|
63
|
+
Android has no `SIMCTL_CHILD_*`, so `every dev --android` stamps this and
|
|
64
|
+
rebuilds, which it has to do anyway. Read only in debug builds; the debug
|
|
65
|
+
source set never ships in a release.
|
|
66
|
+
|
|
67
|
+
**Structural changes** — new source files, new dependencies, a plugin bump, a
|
|
68
|
+
new layout — mean deliberately editing this template and recommitting, never
|
|
69
|
+
editing a stamped copy.
|
|
70
|
+
|
|
71
|
+
### Why `src/stamped/` is wired the way it is
|
|
72
|
+
|
|
73
|
+
AGP merges one manifest per *source set*, and without product flavours a variant
|
|
74
|
+
has exactly two: `main` and its build type. The generated manifest is therefore
|
|
75
|
+
registered as **both** build types' manifest in `app/build.gradle.kts`:
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
sourceSets {
|
|
79
|
+
getByName("debug") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
80
|
+
getByName("release") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A product flavour would have been the textbook answer, but it renames every task
|
|
85
|
+
(`assembleStampedDebug`), and the builder, the dev loop and this README all
|
|
86
|
+
depend on plain `:app:assembleDebug`.
|
|
87
|
+
|
|
88
|
+
That leaves the build-type manifest slot occupied, so the debug-only cleartext
|
|
89
|
+
policy is expressed as a **resource overlay** instead:
|
|
90
|
+
`src/debug/res/xml/network_security_config.xml` overrides the release-safe
|
|
91
|
+
`src/main/res/xml/network_security_config.xml` of the same name. The main
|
|
92
|
+
manifest's `android:networkSecurityConfig` attribute never changes.
|
|
93
|
+
|
|
94
|
+
## Dependency pin policy
|
|
95
|
+
|
|
96
|
+
The SPM equivalent. Plugin versions are pinned in the root `build.gradle.kts`,
|
|
97
|
+
library versions in `app/build.gradle.kts`, and both are **exact** — no ranges,
|
|
98
|
+
no `+`, no version catalog. Moving a pin is a deliberate edit here.
|
|
99
|
+
|
|
100
|
+
| | pinned at |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| Gradle | 9.2.0 (`-bin`, see below) |
|
|
103
|
+
| Android Gradle Plugin | 8.13.2 |
|
|
104
|
+
| Kotlin / kotlinx.serialization plugin | 2.3.0 |
|
|
105
|
+
| `dev.hotwire:core`, `dev.hotwire:navigation-fragments` | 1.3.0 |
|
|
106
|
+
| `compileSdk` / `targetSdk` | 35 |
|
|
107
|
+
| `minSdk` | 28 — Hotwire Native Android's floor, not negotiable |
|
|
108
|
+
| Java toolchain / JVM target | 17 |
|
|
109
|
+
|
|
110
|
+
The wrapper deliberately uses `gradle-9.2.0-**bin**.zip`, not `-all`: the `-all`
|
|
111
|
+
distribution unpacks to 522 MB in `GRADLE_USER_HOME` and the difference is
|
|
112
|
+
Gradle's own sources and javadoc, which only an IDE indexing the Gradle API ever
|
|
113
|
+
reads.
|
|
114
|
+
|
|
115
|
+
`settings.gradle.kts` keeps the `foojay-resolver-convention` plugin so the Java
|
|
116
|
+
toolchain can be provisioned independently of the launcher JVM — without it the
|
|
117
|
+
build fails on a machine whose only JDK is 21.
|
|
118
|
+
|
|
119
|
+
## Building standalone
|
|
120
|
+
|
|
121
|
+
The raw template builds green with placeholder values, unsigned:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
|
|
125
|
+
export ANDROID_HOME=$HOME/Library/Android/sdk
|
|
126
|
+
./gradlew :app:assembleDebug
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Output: `app/build/outputs/apk/debug/app-debug.apk`.
|
|
130
|
+
|
|
131
|
+
The first build downloads roughly 1.4 GB (Gradle, the Android Gradle Plugin, the
|
|
132
|
+
Hotwire artifacts) and takes a couple of minutes, almost all of it network. Warm
|
|
133
|
+
no-op rebuilds are around 7 seconds. The CLI redirects `GRADLE_USER_HOME` to
|
|
134
|
+
`~/.rubyeverywhere/android-gradle` so that cache is shared across apps and never
|
|
135
|
+
lands inside the installed gem.
|
|
136
|
+
|
|
137
|
+
## Dev loop
|
|
138
|
+
|
|
139
|
+
The shell is remote-mode only (`remote_url` in `everywhere.json`). For local
|
|
140
|
+
development, `every dev --android` stamps `src/debug/assets/dev-url.txt`,
|
|
141
|
+
rebuilds, reinstalls and relaunches — the Android equivalent of pressing `i` in
|
|
142
|
+
the iOS loop. The URL resolves down a three-step ladder:
|
|
143
|
+
|
|
144
|
+
1. `adb reverse tcp:<port> tcp:<port>`, then `http://127.0.0.1:<port>` — correct
|
|
145
|
+
on emulators *and* physical devices. Preferred.
|
|
146
|
+
2. `http://10.0.2.2:<port>` — the emulator's alias for the host loopback, used
|
|
147
|
+
automatically when `adb reverse` fails.
|
|
148
|
+
3. An explicit `--dev-url` LAN address, for physical devices.
|
|
149
|
+
|
|
150
|
+
Cleartext to those hosts is allowed only in debug builds, and only for the hosts
|
|
151
|
+
themselves — never `cleartextTrafficPermitted` globally. See
|
|
152
|
+
`app/src/debug/res/xml/network_security_config.xml`.
|
|
153
|
+
|
|
154
|
+
## Shape of the shell
|
|
155
|
+
|
|
156
|
+
| file | role |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `EverywhereApplication.kt` | Hotwire config, bridge + destination + route-handler registration, first path-configuration load |
|
|
159
|
+
| `EverywhereConfig.kt` | `everywhere.json` decode; roots, dev override, instance override, auth, deep links |
|
|
160
|
+
| `EverywhereEvents.kt` | page → shell control messages, replacing iOS's NotificationCenter |
|
|
161
|
+
| `MainActivity.kt` | the SceneDelegate counterpart: tabs, splash, reset, badges, deep links, sign-in |
|
|
162
|
+
| `MainTabs.kt` | `settings.tabs` → the fixed pool of five NavigatorHosts |
|
|
163
|
+
| `IconFont.kt` | Material icons resolved by name from the bundled font |
|
|
164
|
+
| `WebFragment.kt`, `WebBottomSheetFragment.kt` | the web destinations |
|
|
165
|
+
| `bridge/` | the six `everywhere--*` bridge components and the control channel |
|
|
166
|
+
| `AuthFlow.kt` | third-party sign-in in a Custom Tab |
|
|
167
|
+
| `EverywhereHost.kt` | what app-authored `native/android/` code calls: `everywhereVisit`, `everywhereFetch` |
|
|
168
|
+
| `extensions/EverywhereExtensions.kt` | generated app-extension registry (empty by default) |
|
|
169
|
+
|
|
170
|
+
### What app-authored screens get
|
|
171
|
+
|
|
172
|
+
`native/android/*.kt` compiles into `extensions/` and must declare
|
|
173
|
+
`package com.rubyeverywhere.shell.extensions` — the generated registry names
|
|
174
|
+
those types unqualified, and Kotlin ties nothing to the directory, so the package
|
|
175
|
+
line is what makes them referenceable. `every build` refuses a file without it.
|
|
176
|
+
|
|
177
|
+
The two helpers in `EverywhereHost.kt` are named to match iOS's
|
|
178
|
+
`EverywhereHost.swift` so extension code reads the same on both platforms:
|
|
179
|
+
|
|
180
|
+
* `everywhereVisit("/path")` — route through the active navigator, so a native
|
|
181
|
+
screen's links get the full path-configuration treatment (modals, other native
|
|
182
|
+
screens) like any web-driven visit.
|
|
183
|
+
* `everywhereFetch("/builds.json")` — GET from the app's own server carrying the
|
|
184
|
+
WebView's session cookie and the native User-Agent, so `native_app?` holds
|
|
185
|
+
server-side and an authenticated endpoint answers. Suspending; throws
|
|
186
|
+
`EverywhereFetchError` on a non-2xx.
|
|
187
|
+
|
|
188
|
+
Images live in `native/android/assets/**` and arrive as `R.drawable.<name>`
|
|
189
|
+
(freeze rule 9). There is no `everywhereHost` counterpart to iOS's — a Hotwire
|
|
190
|
+
Android destination is already a `Fragment`.
|
|
191
|
+
|
|
192
|
+
### Three invariants that will break the app if they are violated
|
|
193
|
+
|
|
194
|
+
Measured on a device during the Phase 0 spike, not inferred:
|
|
195
|
+
|
|
196
|
+
1. **`navigatorConfigurations()` must return a configuration for all five hosts,
|
|
197
|
+
on every launch.** `NavigatorHost.onCreate` throws
|
|
198
|
+
`IllegalStateException("No configuration found for NavigatorHost")` for any
|
|
199
|
+
host in `activity_main.xml` it can't find one for. "Not a tab right now" is
|
|
200
|
+
`isVisible = false`, **never** a shorter list — `switchTab` only hides hosts
|
|
201
|
+
present in the new list, so a dropped host stays on screen.
|
|
202
|
+
2. **`lazyLoadTabs = true` is mandatory.** With eager loading all five pooled
|
|
203
|
+
hosts cold-boot a WebView and fetch their start location at launch,
|
|
204
|
+
including the hidden auth-gated ones.
|
|
205
|
+
3. **A second `load()` is cheap but lossy.** Fragments and WebViews survive
|
|
206
|
+
(~96 ms), but `loadMenu()` opens with `menu.clear()`, which destroys badges
|
|
207
|
+
and icons, and resets Material's `selectedItemId` to 0 — which then takes the
|
|
208
|
+
*reselect* path and clears the selected tab's backstack whenever the selected
|
|
209
|
+
tab is the first one. `MainActivity.applyTabs` is the only caller and does
|
|
210
|
+
all three fix-ups; nothing else may call `load()`.
|
|
211
|
+
|
|
212
|
+
Tab **path** changes are not covered by any of that: `NavigatorHost` caches its
|
|
213
|
+
configuration in `onCreate`, so a changed `startLocation` needs
|
|
214
|
+
`Activity.recreate()`. `MainTabs` keeps two fingerprints so the cheap path is
|
|
215
|
+
taken whenever it can be.
|
|
216
|
+
|
|
217
|
+
[1]: https://github.com/hotwired/hotwire-native-android
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
2
|
+
import java.util.Properties
|
|
3
|
+
|
|
4
|
+
plugins {
|
|
5
|
+
id("com.android.application")
|
|
6
|
+
id("org.jetbrains.kotlin.android")
|
|
7
|
+
id("org.jetbrains.kotlin.plugin.serialization")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Per-app identity, read from a properties file rather than written into this
|
|
11
|
+
// script. `every build` overwrites everywhere.properties and nothing else here
|
|
12
|
+
// — the same contract as iOS's App.xcconfig, for the same reason: a build
|
|
13
|
+
// script the CLI edits is a build script that drifts.
|
|
14
|
+
val everywhere = Properties().apply {
|
|
15
|
+
file("everywhere.properties").inputStream().use { load(it) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fun stamped(key: String, default: String): String =
|
|
19
|
+
everywhere.getProperty("everywhere.$key")?.takeIf { it.isNotBlank() } ?: default
|
|
20
|
+
|
|
21
|
+
android {
|
|
22
|
+
// The namespace is the R class / BuildConfig package and is frozen. It is
|
|
23
|
+
// deliberately NOT the applicationId: apps rename their bundle id freely,
|
|
24
|
+
// and a namespace change would move every generated class.
|
|
25
|
+
namespace = "com.rubyeverywhere.shell"
|
|
26
|
+
compileSdk = 35
|
|
27
|
+
|
|
28
|
+
defaultConfig {
|
|
29
|
+
applicationId = stamped("applicationId", "com.rubyeverywhere.app")
|
|
30
|
+
versionName = stamped("versionName", "0.1.0")
|
|
31
|
+
versionCode = stamped("versionCode", "1").toInt()
|
|
32
|
+
|
|
33
|
+
// minSdk 28 is Hotwire Native Android's floor and is not negotiable.
|
|
34
|
+
// It also clears API 26 for Typeface.Builder.setFontVariationSettings,
|
|
35
|
+
// which IconFont needs for the Material Symbols FILL axis.
|
|
36
|
+
minSdk = 28
|
|
37
|
+
targetSdk = 35
|
|
38
|
+
|
|
39
|
+
// Consumed by AndroidManifest.xml. The label is a placeholder so the
|
|
40
|
+
// manifest itself stays frozen when the app is renamed.
|
|
41
|
+
manifestPlaceholders["everywhereLabel"] = stamped("label", "RubyEverywhere")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sourceSets {
|
|
45
|
+
// Deep-link intent filters, the auth callback scheme and runtime
|
|
46
|
+
// <uses-permission> entries are generated into their own manifest so
|
|
47
|
+
// the main one never changes. AGP merges one manifest per build type,
|
|
48
|
+
// and the generated file is identical for both — hence the same path
|
|
49
|
+
// twice. See src/stamped/AndroidManifest.xml.
|
|
50
|
+
getByName("debug") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
51
|
+
getByName("release") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
52
|
+
|
|
53
|
+
// The app's own images (native/android/assets/**) are generated into
|
|
54
|
+
// src/stamped/res as drawables. A second res dir on `main` rather than
|
|
55
|
+
// more files in main/res, because the generator owns the directory
|
|
56
|
+
// outright and clears it every build — a deleted asset has to stop
|
|
57
|
+
// shipping, and stamping merges over the work dir instead of wiping it.
|
|
58
|
+
//
|
|
59
|
+
// On `main`, deliberately, not on a build type: same-variant duplicates
|
|
60
|
+
// are a merge ERROR, so an asset that collides with a template drawable
|
|
61
|
+
// fails the build instead of silently overriding it. AGP ignores the
|
|
62
|
+
// directory when it doesn't exist, which is how the raw template builds.
|
|
63
|
+
getByName("main") { res.srcDir("src/stamped/res") }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
buildTypes {
|
|
67
|
+
getByName("debug") {
|
|
68
|
+
isDebuggable = true
|
|
69
|
+
}
|
|
70
|
+
release {
|
|
71
|
+
isMinifyEnabled = false
|
|
72
|
+
proguardFiles(
|
|
73
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
74
|
+
"proguard-rules.pro"
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
buildFeatures {
|
|
80
|
+
// BuildConfig.DEBUG gates web-view debugging, verbose logging and the
|
|
81
|
+
// dev-server asset. Nothing else in the shell branches on build type.
|
|
82
|
+
buildConfig = true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
compileOptions {
|
|
86
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
87
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lint {
|
|
91
|
+
// `lintVitalRelease` crashes on this exact toolchain (AGP 8.13.2 +
|
|
92
|
+
// Kotlin 2.3.0). Its build-script visitor builds light classes for
|
|
93
|
+
// every `.gradle.kts` in the project and dies inside the Kotlin
|
|
94
|
+
// compiler with "findFirCompiledSymbol only works on compiled
|
|
95
|
+
// declarations" the moment one script `apply(from:)`s another — which
|
|
96
|
+
// native-packages.gradle.kts is, by design.
|
|
97
|
+
//
|
|
98
|
+
// Verified by bisection: commenting out the `apply(from:)` below makes
|
|
99
|
+
// lintVitalAnalyzeRelease pass, and nothing else does. So this disables
|
|
100
|
+
// a check that cannot run, not one that was failing us. Revisit when
|
|
101
|
+
// either version moves.
|
|
102
|
+
checkReleaseBuilds = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
packaging {
|
|
106
|
+
// The icon font is already a compressed container; deflating it again
|
|
107
|
+
// costs build time and saves nothing.
|
|
108
|
+
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
kotlin {
|
|
113
|
+
compilerOptions {
|
|
114
|
+
jvmTarget.set(JvmTarget.JVM_17)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
java {
|
|
119
|
+
toolchain {
|
|
120
|
+
languageVersion = JavaLanguageVersion.of(17)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
dependencies {
|
|
125
|
+
// Hotwire Native. Pinned exactly, not to a range: this is the template's
|
|
126
|
+
// dependency pin policy, the analogue of iOS's Package.resolved.
|
|
127
|
+
implementation("dev.hotwire:core:1.3.0")
|
|
128
|
+
implementation("dev.hotwire:navigation-fragments:1.3.0")
|
|
129
|
+
|
|
130
|
+
// Material — BottomNavigationView, badges, bottom sheets, theming.
|
|
131
|
+
implementation("com.google.android.material:material:1.12.0")
|
|
132
|
+
|
|
133
|
+
// activity_main.xml's root.
|
|
134
|
+
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
|
135
|
+
|
|
136
|
+
// WebViewCompat: addWebMessageListener (the control channel) and
|
|
137
|
+
// addDocumentStartJavaScript (window.__EVERYWHERE_CONFIG__). Android
|
|
138
|
+
// WebView has no user-script API of its own; these are the substitutes.
|
|
139
|
+
implementation("androidx.webkit:webkit:1.12.1")
|
|
140
|
+
|
|
141
|
+
// Custom Tabs — the ASWebAuthenticationSession stand-in for third-party
|
|
142
|
+
// sign-in, and what Hotwire's own BrowserTabRouteDecisionHandler uses.
|
|
143
|
+
implementation("androidx.browser:browser:1.8.0")
|
|
144
|
+
|
|
145
|
+
// Biometrics + the encrypted store they gate.
|
|
146
|
+
implementation("androidx.biometric:biometric:1.1.0")
|
|
147
|
+
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
|
148
|
+
|
|
149
|
+
// everywhere.json, the bridge components' message payloads, and the
|
|
150
|
+
// control channel's postMessage bodies.
|
|
151
|
+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// GENERATED: Maven coordinates from `native.android.packages`. Applied last so
|
|
155
|
+
// an app's own dependency can override a version above if it must.
|
|
156
|
+
apply(from = "native-packages.gradle.kts")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# STAMPED by `every build --android`. The direct analogue of iOS's
|
|
2
|
+
# App.xcconfig: identity lives here so no build script is ever edited per app.
|
|
3
|
+
# The values below are the template's placeholders — the raw template builds
|
|
4
|
+
# green with them.
|
|
5
|
+
everywhere.applicationId=com.rubyeverywhere.app
|
|
6
|
+
everywhere.versionName=0.1.0
|
|
7
|
+
everywhere.versionCode=1
|
|
8
|
+
everywhere.label=RubyEverywhere
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// GENERATED by `every build --android` from `native.android.packages` in
|
|
2
|
+
// config/everywhere.yml. The checked-in copy is the empty default, so the raw
|
|
3
|
+
// template builds unchanged.
|
|
4
|
+
//
|
|
5
|
+
// native:
|
|
6
|
+
// android:
|
|
7
|
+
// packages:
|
|
8
|
+
// - "com.airbnb.android:lottie:6.4.0"
|
|
9
|
+
//
|
|
10
|
+
// This is a script plugin applied from app/build.gradle.kts, so it has no
|
|
11
|
+
// typed `implementation(...)` accessor — dependencies are added by
|
|
12
|
+
// configuration name. Keeping the generated surface to a single file means
|
|
13
|
+
// the frozen build script never changes shape when an app adds a package.
|
|
14
|
+
dependencies {
|
|
15
|
+
// add("implementation", "group:artifact:version")
|
|
16
|
+
}
|