ruflet 0.0.18 → 0.0.20
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 +54 -1
- data/lib/ruflet/cli/build_command.rb +697 -118
- data/lib/ruflet/cli/extra_command.rb +11 -8
- data/lib/ruflet/cli/flutter_sdk.rb +42 -7
- data/lib/ruflet/cli/new_command.rb +112 -36
- data/lib/ruflet/cli/run_command.rb +326 -70
- data/lib/ruflet/cli/templates.rb +8 -5
- data/lib/ruflet/cli.rb +3 -3
- data/lib/ruflet/hot_reload/harness.rb +32 -0
- data/lib/ruflet/hot_reload.rb +226 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +3 -1
|
@@ -16,20 +16,23 @@ module Ruflet
|
|
|
16
16
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
17
17
|
fix = args.delete("--fix")
|
|
18
18
|
client_dir = detect_client_dir
|
|
19
|
-
template_root =
|
|
19
|
+
template_root =
|
|
20
|
+
if fix
|
|
21
|
+
ensure_cached_ruflet_client_template!(force: true, verbose: !!verbose)
|
|
22
|
+
else
|
|
23
|
+
resolve_ruflet_client_template_root
|
|
24
|
+
end
|
|
20
25
|
puts "Ruflet doctor"
|
|
21
26
|
puts " Ruby: #{RUBY_VERSION}"
|
|
22
27
|
puts " Flutter host target: #{flutter_host || 'unsupported'}"
|
|
23
28
|
if template_root
|
|
24
29
|
puts " Template: #{template_root}"
|
|
30
|
+
revision = cached_template_revision if respond_to?(:cached_template_revision, true)
|
|
31
|
+
puts " Template revision: #{revision}" if revision
|
|
25
32
|
elsif fix
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
warn "Failed to fetch the Ruflet Flutter template from GitHub."
|
|
30
|
-
return 1
|
|
31
|
-
end
|
|
32
|
-
puts " Template: #{template_root}"
|
|
33
|
+
warn " Template: missing"
|
|
34
|
+
warn "Failed to fetch the Ruflet Flutter template from GitHub."
|
|
35
|
+
return 1
|
|
33
36
|
else
|
|
34
37
|
warn " Template: missing"
|
|
35
38
|
warn "Run `ruflet doctor --fix` to fetch the Flutter template from GitHub."
|
|
@@ -47,9 +47,18 @@ module Ruflet
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
49
|
def flutter_tools(client_dir: nil, auto_install: true)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
desired = desired_flutter_spec(client_dir: client_dir)
|
|
51
|
+
|
|
52
|
+
# Environment and .fvmrc versions are intentional pins. Flutter's
|
|
53
|
+
# generated .metadata revision only records which SDK created the
|
|
54
|
+
# project; it must not force every build to download that old SDK.
|
|
55
|
+
if %i[env fvmrc].include?(desired[:source])
|
|
56
|
+
fvm_tools = flutter_tools_via_fvm(client_dir: client_dir, auto_install: auto_install)
|
|
57
|
+
return fvm_tools if fvm_tools
|
|
58
|
+
else
|
|
59
|
+
system_tools = flutter_tools_via_system
|
|
60
|
+
return system_tools if system_tools
|
|
61
|
+
end
|
|
53
62
|
|
|
54
63
|
managed_tools = flutter_tools_via_managed_sdk(client_dir: client_dir, auto_install: auto_install)
|
|
55
64
|
return managed_tools if managed_tools
|
|
@@ -57,6 +66,13 @@ module Ruflet
|
|
|
57
66
|
nil
|
|
58
67
|
end
|
|
59
68
|
|
|
69
|
+
def flutter_tools_via_system
|
|
70
|
+
flutter = which_command("flutter")
|
|
71
|
+
return nil unless flutter
|
|
72
|
+
|
|
73
|
+
tools_from_flutter_bin(flutter)
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
def flutter_tools_via_managed_sdk(client_dir: nil, auto_install: true)
|
|
61
77
|
return nil unless auto_install
|
|
62
78
|
|
|
@@ -115,7 +131,21 @@ module Ruflet
|
|
|
115
131
|
end
|
|
116
132
|
|
|
117
133
|
def fvm_env
|
|
118
|
-
|
|
134
|
+
utf8_locale_env.merge("PATH" => "#{pub_cache_bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# CocoaPods aborts with "Unicode Normalization not appropriate for
|
|
138
|
+
# ASCII-8BIT" when it inherits a non-UTF-8 locale, which takes the whole
|
|
139
|
+
# iOS and macOS build with it. Hand the toolchain a UTF-8 locale unless
|
|
140
|
+
# the environment already has one.
|
|
141
|
+
def utf8_locale_env
|
|
142
|
+
return {} if utf8_locale?(ENV["LC_ALL"]) || utf8_locale?(ENV["LANG"])
|
|
143
|
+
|
|
144
|
+
{ "LANG" => "en_US.UTF-8", "LC_ALL" => "en_US.UTF-8" }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def utf8_locale?(value)
|
|
148
|
+
value.to_s.match?(/utf-?8/i)
|
|
119
149
|
end
|
|
120
150
|
|
|
121
151
|
def tools_from_flutter_bin(flutter_bin)
|
|
@@ -127,11 +157,11 @@ module Ruflet
|
|
|
127
157
|
{
|
|
128
158
|
flutter: flutter_bin,
|
|
129
159
|
dart: (File.executable?(dart) ? dart : "dart"),
|
|
130
|
-
env:
|
|
160
|
+
env: utf8_locale_env.merge(
|
|
131
161
|
"PATH" => "#{bin_dir}#{File::PATH_SEPARATOR}#{pub_cache_bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}",
|
|
132
162
|
"FLUTTER_ROOT" => sdk_root,
|
|
133
163
|
"FVM_FLUTTER_SDK" => sdk_root
|
|
134
|
-
|
|
164
|
+
)
|
|
135
165
|
}
|
|
136
166
|
end
|
|
137
167
|
|
|
@@ -199,7 +229,12 @@ module Ruflet
|
|
|
199
229
|
return { version: fvm, source: :fvmrc } if fvm
|
|
200
230
|
|
|
201
231
|
metadata = parse_flutter_metadata(find_flutter_metadata(client_dir))
|
|
202
|
-
|
|
232
|
+
if metadata
|
|
233
|
+
return {
|
|
234
|
+
channel: metadata[:channel] || DEFAULT_FLUTTER_CHANNEL,
|
|
235
|
+
source: :metadata
|
|
236
|
+
}
|
|
237
|
+
end
|
|
203
238
|
|
|
204
239
|
{ channel: DEFAULT_FLUTTER_CHANNEL, version: DEFAULT_FLUTTER_CHANNEL, source: :default }
|
|
205
240
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "open3"
|
|
4
5
|
require "tmpdir"
|
|
5
6
|
require "yaml"
|
|
6
7
|
|
|
@@ -8,10 +9,10 @@ module Ruflet
|
|
|
8
9
|
module CLI
|
|
9
10
|
module NewCommand
|
|
10
11
|
TEMPLATE_REPO_URL = ENV.fetch("RUFLET_TEMPLATE_REPO_URL", "https://github.com/AdamMusa/ruflet-template.git")
|
|
12
|
+
TEMPLATE_REPO_REF = ENV.fetch("RUFLET_TEMPLATE_REPO_REF", "main")
|
|
11
13
|
RUNTIME_REPO_URL = ENV.fetch("RUFLET_RUNTIME_REPO_URL", "https://github.com/AdamMusa/ruflet.git")
|
|
12
14
|
|
|
13
15
|
CLIENT_EXTENSION_MAP = {
|
|
14
|
-
"ads" => { package: "flet_ads", alias: "ruflet_ads" },
|
|
15
16
|
"audio" => { package: "flet_audio", alias: "ruflet_audio" },
|
|
16
17
|
"audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
17
18
|
"camera" => { package: "flet_camera", alias: "ruflet_camera" },
|
|
@@ -24,6 +25,7 @@ module Ruflet
|
|
|
24
25
|
"lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
|
|
25
26
|
"map" => { package: "flet_map", alias: "ruflet_map" },
|
|
26
27
|
"permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
|
|
28
|
+
"qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" },
|
|
27
29
|
"rive" => { package: "flet_rive", alias: "ruflet_rive" },
|
|
28
30
|
"secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
|
|
29
31
|
"video" => { package: "flet_video", alias: "ruflet_video" },
|
|
@@ -53,11 +55,11 @@ module Ruflet
|
|
|
53
55
|
puts "Run:"
|
|
54
56
|
puts " cd #{project_name}"
|
|
55
57
|
puts " bundle install"
|
|
56
|
-
puts "
|
|
58
|
+
puts " ruflet run main.rb"
|
|
57
59
|
puts
|
|
58
60
|
puts "Build:"
|
|
59
|
-
puts "
|
|
60
|
-
puts "
|
|
61
|
+
puts " ruflet build android --self"
|
|
62
|
+
puts " ruflet build ios --self"
|
|
61
63
|
0
|
|
62
64
|
end
|
|
63
65
|
|
|
@@ -69,8 +71,10 @@ module Ruflet
|
|
|
69
71
|
|
|
70
72
|
target = hidden_flutter_client_dir(root)
|
|
71
73
|
FileUtils.mkdir_p(File.dirname(target))
|
|
74
|
+
FileUtils.rm_rf(target)
|
|
72
75
|
FileUtils.cp_r(template_root, target)
|
|
73
76
|
prune_client_template(target)
|
|
77
|
+
write_client_template_revision(target, installed_template_revision(template_root))
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def hidden_flutter_client_dir(root = Dir.pwd)
|
|
@@ -78,29 +82,29 @@ module Ruflet
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def resolve_ruflet_client_template_root
|
|
81
|
-
[
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
explicit = ENV["RUFLET_TEMPLATE_ROOT"].to_s.strip
|
|
86
|
+
unless explicit.empty?
|
|
87
|
+
explicit_template = [
|
|
88
|
+
explicit,
|
|
89
|
+
File.join(explicit, "templates", "ruflet_flutter_template")
|
|
90
|
+
].find { |path| valid_ruflet_template?(path) }
|
|
91
|
+
return explicit_template if explicit_template
|
|
85
92
|
end
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
sibling_template = File.expand_path(
|
|
95
|
+
"../../../../../../ruflet-template/templates/ruflet_flutter_template",
|
|
96
|
+
__dir__
|
|
97
|
+
)
|
|
98
|
+
return sibling_template if valid_ruflet_template?(sibling_template)
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
File.expand_path("../../../../../ruflet_client", __dir__)
|
|
93
|
-
].each do |fallback|
|
|
94
|
-
return fallback if Dir.exist?(fallback)
|
|
95
|
-
end
|
|
100
|
+
cached_template = cached_ruflet_client_template_root
|
|
101
|
+
return cached_template if valid_ruflet_template?(cached_template)
|
|
96
102
|
|
|
97
103
|
nil
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
def ensure_cached_ruflet_client_template!(force: false, verbose: false)
|
|
101
107
|
cached_template = cached_ruflet_client_template_root
|
|
102
|
-
return cached_template if !force && Dir.exist?(cached_template)
|
|
103
|
-
|
|
104
108
|
download_ruflet_template(force: force, verbose: verbose)
|
|
105
109
|
Dir.exist?(cached_template) ? cached_template : nil
|
|
106
110
|
end
|
|
@@ -113,6 +117,10 @@ module Ruflet
|
|
|
113
117
|
File.join(template_cache_root, "ruflet_flutter_template")
|
|
114
118
|
end
|
|
115
119
|
|
|
120
|
+
def cached_ruflet_client_template_revision_path
|
|
121
|
+
File.join(template_cache_root, "ruflet_flutter_template.revision")
|
|
122
|
+
end
|
|
123
|
+
|
|
116
124
|
def cached_ruby_runtime_root
|
|
117
125
|
File.join(cache_root, "ruby_runtime")
|
|
118
126
|
end
|
|
@@ -126,40 +134,104 @@ module Ruflet
|
|
|
126
134
|
end
|
|
127
135
|
|
|
128
136
|
def download_ruflet_assets(force: false, verbose: false)
|
|
129
|
-
|
|
130
|
-
return true if !force && Dir.exist?(template_target)
|
|
131
|
-
|
|
132
|
-
Dir.exist?(template_target) || download_ruflet_template(force: force, verbose: verbose)
|
|
137
|
+
!ensure_cached_ruflet_client_template!(force: force, verbose: verbose).nil?
|
|
133
138
|
end
|
|
134
139
|
|
|
135
140
|
def download_ruflet_template(force: false, verbose: false)
|
|
136
141
|
target = cached_ruflet_client_template_root
|
|
137
|
-
|
|
142
|
+
remote_revision = remote_template_revision(verbose: verbose)
|
|
143
|
+
current_revision = cached_template_revision
|
|
144
|
+
cache_available = valid_ruflet_template?(target)
|
|
145
|
+
if !force && cache_available && remote_revision && current_revision == remote_revision
|
|
146
|
+
return target
|
|
147
|
+
end
|
|
148
|
+
if !force && cache_available && remote_revision.nil?
|
|
149
|
+
build_log(verbose, "Unable to check the Ruflet template revision; using cached #{current_revision || 'template'}") if respond_to?(:build_log, true)
|
|
150
|
+
return target
|
|
151
|
+
end
|
|
138
152
|
|
|
139
153
|
FileUtils.mkdir_p(template_cache_root)
|
|
140
154
|
|
|
141
155
|
Dir.mktmpdir("ruflet-assets") do |tmp|
|
|
142
156
|
repo_dir = File.join(tmp, "Ruflet")
|
|
143
|
-
clone_cmd = ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", TEMPLATE_REPO_URL, repo_dir]
|
|
144
|
-
return
|
|
145
|
-
return
|
|
157
|
+
clone_cmd = ["git", "clone", "--depth", "1", "--branch", TEMPLATE_REPO_REF, "--filter=blob:none", "--sparse", TEMPLATE_REPO_URL, repo_dir]
|
|
158
|
+
return target_if_present(target) unless run_template_command(clone_cmd, verbose: verbose)
|
|
159
|
+
return target_if_present(target) unless run_template_command(["git", "-C", repo_dir, "sparse-checkout", "set", "templates/ruflet_flutter_template"], verbose: verbose)
|
|
146
160
|
|
|
147
161
|
source = File.join(repo_dir, "templates", "ruflet_flutter_template")
|
|
148
|
-
return
|
|
162
|
+
return target_if_present(target) unless valid_ruflet_template?(source)
|
|
149
163
|
|
|
164
|
+
fetched_revision = git_revision(repo_dir) || remote_revision
|
|
165
|
+
staged_target = File.join(tmp, "ruflet_flutter_template")
|
|
166
|
+
FileUtils.cp_r(source, staged_target)
|
|
150
167
|
FileUtils.rm_rf(target)
|
|
151
|
-
FileUtils.
|
|
168
|
+
FileUtils.mv(staged_target, target)
|
|
169
|
+
File.write(cached_ruflet_client_template_revision_path, "#{fetched_revision}\n") if fetched_revision
|
|
152
170
|
end
|
|
153
171
|
|
|
154
172
|
target
|
|
155
173
|
rescue StandardError => e
|
|
156
174
|
warn "Failed to fetch Ruflet template: #{e.class}: #{e.message}"
|
|
157
|
-
|
|
175
|
+
target_if_present(target)
|
|
158
176
|
end
|
|
159
177
|
|
|
160
178
|
def run_template_command(cmd, verbose: false)
|
|
161
179
|
output = verbose ? $stdout : File::NULL
|
|
162
|
-
|
|
180
|
+
env = { "GIT_TERMINAL_PROMPT" => "0" }
|
|
181
|
+
system(env, *cmd, out: output, err: verbose ? $stderr : File::NULL)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def remote_template_revision(verbose: false)
|
|
185
|
+
return @ruflet_template_remote_revision if defined?(@ruflet_template_remote_revision)
|
|
186
|
+
|
|
187
|
+
env = { "GIT_TERMINAL_PROMPT" => "0" }
|
|
188
|
+
stdout, stderr, status = Open3.capture3(env, "git", "ls-remote", TEMPLATE_REPO_URL, "refs/heads/#{TEMPLATE_REPO_REF}")
|
|
189
|
+
warn stderr unless status.success? || !verbose || stderr.empty?
|
|
190
|
+
@ruflet_template_remote_revision = status.success? ? stdout.split.first : nil
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
warn "Failed to check Ruflet template revision: #{e.message}" if verbose
|
|
193
|
+
@ruflet_template_remote_revision = nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def cached_template_revision
|
|
197
|
+
return nil unless File.file?(cached_ruflet_client_template_revision_path)
|
|
198
|
+
|
|
199
|
+
File.read(cached_ruflet_client_template_revision_path).strip.then { |value| value.empty? ? nil : value }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def installed_template_revision(template_root)
|
|
203
|
+
return cached_template_revision if File.expand_path(template_root) == File.expand_path(cached_ruflet_client_template_root)
|
|
204
|
+
|
|
205
|
+
git_revision(File.expand_path("../..", template_root))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def write_client_template_revision(target, revision)
|
|
209
|
+
return unless revision
|
|
210
|
+
|
|
211
|
+
File.write(File.join(target, ".ruflet-template-revision"), "#{revision}\n")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def client_template_current?(target, template_root)
|
|
215
|
+
expected = installed_template_revision(template_root)
|
|
216
|
+
return false unless expected
|
|
217
|
+
|
|
218
|
+
marker = File.join(target, ".ruflet-template-revision")
|
|
219
|
+
File.file?(marker) && File.read(marker).strip == expected
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def git_revision(repo_dir)
|
|
223
|
+
stdout, _stderr, status = Open3.capture3("git", "-C", repo_dir, "rev-parse", "HEAD")
|
|
224
|
+
status.success? ? stdout.strip : nil
|
|
225
|
+
rescue StandardError
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def valid_ruflet_template?(path)
|
|
230
|
+
Dir.exist?(path) && File.file?(File.join(path, "pubspec.yaml")) && File.file?(File.join(path, "lib", "main.dart"))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def target_if_present(target)
|
|
234
|
+
valid_ruflet_template?(target) ? target : nil
|
|
163
235
|
end
|
|
164
236
|
|
|
165
237
|
def prune_client_template(target)
|
|
@@ -186,12 +258,6 @@ module Ruflet
|
|
|
186
258
|
def write_default_ruflet_config(root, app_name)
|
|
187
259
|
File.write(File.join(root, "ruflet.yaml"), <<~YAML)
|
|
188
260
|
app:
|
|
189
|
-
name: #{app_name}
|
|
190
|
-
display_name: #{humanize_name(app_name)}
|
|
191
|
-
package_name: #{app_name.gsub(/[^a-zA-Z0-9_]+/, "_").downcase}
|
|
192
|
-
organization: com.example
|
|
193
|
-
version: 1.0.0+1
|
|
194
|
-
description: A new Ruflet app.
|
|
195
261
|
# Required for server-driven builds: `ruflet build ios`, `apk`, `web`, etc. without `--self`.
|
|
196
262
|
# Example: https://api.example.com
|
|
197
263
|
backend_url: ""
|
|
@@ -215,6 +281,16 @@ module Ruflet
|
|
|
215
281
|
YAML
|
|
216
282
|
|
|
217
283
|
File.write(File.join(root, "services.yaml"), <<~YAML)
|
|
284
|
+
# Application identity used by the Flutter build pipeline. Ruflet derives
|
|
285
|
+
# the mobile identifier as organization.package_name and passes it to
|
|
286
|
+
# change_app_package_name for Android and iOS builds.
|
|
287
|
+
app:
|
|
288
|
+
app_name: #{humanize_name(app_name)}
|
|
289
|
+
package_name: #{app_name.gsub(/[^a-zA-Z0-9_]+/, "_").downcase}
|
|
290
|
+
organization: com.example
|
|
291
|
+
version: 1.0.0+1
|
|
292
|
+
description: A new Ruflet app.
|
|
293
|
+
|
|
218
294
|
# Native capabilities that require platform permissions. Ruflet activates
|
|
219
295
|
# the matching client extensions and writes Android/iOS permissions.
|
|
220
296
|
# Supported services: camera, microphone, location, motion
|