ruflet 0.0.7 → 0.0.9
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/bin/ruflet +6 -0
- data/lib/ruflet/cli/build_command.rb +372 -0
- data/lib/ruflet/cli/extra_command.rb +146 -0
- data/lib/ruflet/cli/flutter_sdk.rb +359 -0
- data/lib/ruflet/cli/new_command.rb +221 -0
- data/lib/ruflet/cli/run_command.rb +699 -0
- data/lib/ruflet/cli/templates.rb +68 -0
- data/lib/ruflet/cli/update_command.rb +111 -0
- data/lib/ruflet/cli.rb +85 -0
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_cli.rb +3 -0
- metadata +59 -75
- data/lib/ruflet/manifest_compiler.rb +0 -62
- data/lib/ruflet.rb +0 -40
- data/lib/ruflet_protocol/ruflet/protocol.rb +0 -62
- data/lib/ruflet_protocol.rb +0 -4
- data/lib/ruflet_ui/ruflet/app.rb +0 -31
- data/lib/ruflet_ui/ruflet/colors.rb +0 -234
- data/lib/ruflet_ui/ruflet/control.rb +0 -168
- data/lib/ruflet_ui/ruflet/dsl.rb +0 -227
- data/lib/ruflet_ui/ruflet/event.rb +0 -28
- data/lib/ruflet_ui/ruflet/icon_data.rb +0 -62
- data/lib/ruflet_ui/ruflet/icons/cupertino/cupertino_icons.rb +0 -54
- data/lib/ruflet_ui/ruflet/icons/cupertino_icon_lookup.rb +0 -112
- data/lib/ruflet_ui/ruflet/icons/material_icon_lookup.rb +0 -112
- data/lib/ruflet_ui/ruflet/icons/material_icons.rb +0 -55
- data/lib/ruflet_ui/ruflet/page.rb +0 -741
- data/lib/ruflet_ui/ruflet/ui/control_factory.rb +0 -23
- data/lib/ruflet_ui/ruflet/ui/control_methods.rb +0 -16
- data/lib/ruflet_ui/ruflet/ui/control_registry.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_action_sheet_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_alert_dialog_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_button_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_dialog_action_control.rb +0 -24
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_filled_button_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_navigation_bar_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_slider_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_switch_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/cupertino/cupertino_text_field_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/alert_dialog_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/app_bar_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/bottom_sheet_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/button_control.rb +0 -24
- data/lib/ruflet_ui/ruflet/ui/controls/material/checkbox_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/column_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/container_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/drag_target_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/draggable_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/elevated_button_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/filled_button_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/floating_action_button_control.rb +0 -28
- data/lib/ruflet_ui/ruflet/ui/controls/material/gesture_detector_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/grid_view_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/icon_button_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/icon_control.rb +0 -24
- data/lib/ruflet_ui/ruflet/ui/controls/material/image_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/markdown_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/navigation_bar_destination_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/radio_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/radio_group_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/row_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/snack_bar_control.rb +0 -68
- data/lib/ruflet_ui/ruflet/ui/controls/material/stack_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/tab_bar_view_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/tab_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/tabs_control.rb +0 -63
- data/lib/ruflet_ui/ruflet/ui/controls/material/text_button_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/text_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/text_field_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/controls/material/view_control.rb +0 -13
- data/lib/ruflet_ui/ruflet/ui/cupertino_control_factory.rb +0 -40
- data/lib/ruflet_ui/ruflet/ui/cupertino_control_methods.rb +0 -26
- data/lib/ruflet_ui/ruflet/ui/cupertino_control_registry.rb +0 -49
- data/lib/ruflet_ui/ruflet/ui/material_control_factory.rb +0 -97
- data/lib/ruflet_ui/ruflet/ui/material_control_methods.rb +0 -128
- data/lib/ruflet_ui/ruflet/ui/material_control_registry.rb +0 -154
- data/lib/ruflet_ui/ruflet/ui/shared_control_forwarders.rb +0 -89
- data/lib/ruflet_ui/ruflet/ui/widget_builder.rb +0 -55
- data/lib/ruflet_ui.rb +0 -111
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
require "uri"
|
|
10
|
+
require "yaml"
|
|
11
|
+
|
|
12
|
+
module Ruflet
|
|
13
|
+
module CLI
|
|
14
|
+
module FlutterSdk
|
|
15
|
+
RELEASES_BASE = "https://storage.googleapis.com/flutter_infra_release/releases".freeze
|
|
16
|
+
DEFAULT_FLUTTER_CHANNEL = "stable".freeze
|
|
17
|
+
|
|
18
|
+
def ensure_flutter!(command_name, client_dir: nil, auto_install: true)
|
|
19
|
+
tools = flutter_tools(client_dir: client_dir, auto_install: auto_install)
|
|
20
|
+
return tools if tools
|
|
21
|
+
|
|
22
|
+
warn "Flutter is required for `ruflet #{command_name}` and FVM bootstrap failed."
|
|
23
|
+
warn "Set RUFLET_FLUTTER_VERSION or add .fvmrc to the project."
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def flutter_version_summary(tools)
|
|
28
|
+
flutter_bin = tools[:flutter]
|
|
29
|
+
env = tools[:env] || {}
|
|
30
|
+
|
|
31
|
+
machine_output, status = Open3.capture2e(env, flutter_bin, "--version", "--machine")
|
|
32
|
+
if status.success?
|
|
33
|
+
parsed = JSON.parse(machine_output) rescue nil
|
|
34
|
+
version = parsed && parsed["frameworkVersion"].to_s.strip
|
|
35
|
+
channel = parsed && parsed["channel"].to_s.strip
|
|
36
|
+
return [version, channel].reject(&:empty?).join(" ") unless version.to_s.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
text_output, text_status = Open3.capture2e(env, flutter_bin, "--version")
|
|
40
|
+
return text_output.lines.first.to_s.strip if text_status.success?
|
|
41
|
+
|
|
42
|
+
File.basename(flutter_bin.to_s)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
File.basename(flutter_bin.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def flutter_tools(client_dir: nil, auto_install: true)
|
|
50
|
+
# Always use FVM so Flutter/Dart match pinned SDK.
|
|
51
|
+
fvm_tools = flutter_tools_via_fvm(client_dir: client_dir, auto_install: auto_install)
|
|
52
|
+
return fvm_tools if fvm_tools
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def flutter_tools_via_fvm(client_dir: nil, auto_install: true)
|
|
58
|
+
version = desired_flutter_version(client_dir: client_dir)
|
|
59
|
+
return nil if version.to_s.strip.empty?
|
|
60
|
+
|
|
61
|
+
project_dir = fvm_project_dir(client_dir: client_dir)
|
|
62
|
+
flutter = existing_fvm_flutter_bin(project_dir)
|
|
63
|
+
return tools_from_flutter_bin(flutter) if flutter
|
|
64
|
+
|
|
65
|
+
return nil unless auto_install
|
|
66
|
+
|
|
67
|
+
fvm = ensure_fvm_available(client_dir: client_dir)
|
|
68
|
+
return nil unless fvm
|
|
69
|
+
FileUtils.mkdir_p(project_dir)
|
|
70
|
+
fvmrc_path = File.join(project_dir, ".fvmrc")
|
|
71
|
+
unless File.file?(fvmrc_path)
|
|
72
|
+
File.write(fvmrc_path, "{\"flutter\":\"#{version}\"}\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
system(fvm_env, fvm, "install", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
|
|
76
|
+
system(fvm_env, fvm, "use", "--force", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
|
|
77
|
+
|
|
78
|
+
flutter = File.join(project_dir, ".fvm", "flutter_sdk", "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
79
|
+
return nil unless File.executable?(flutter)
|
|
80
|
+
|
|
81
|
+
tools_from_flutter_bin(flutter)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
warn "FVM bootstrap failed: #{e.class}: #{e.message}"
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ensure_fvm_available(client_dir: nil)
|
|
88
|
+
fvm = which_command("fvm")
|
|
89
|
+
return fvm if fvm
|
|
90
|
+
|
|
91
|
+
dart = which_command("dart")
|
|
92
|
+
unless dart
|
|
93
|
+
sdk_root = ensure_flutter_sdk_downloaded(client_dir: client_dir)
|
|
94
|
+
dart = sdk_root ? File.join(sdk_root, "bin", windows_host? ? "dart.bat" : "dart") : nil
|
|
95
|
+
end
|
|
96
|
+
return nil unless dart && File.executable?(dart)
|
|
97
|
+
|
|
98
|
+
system(dart, "pub", "global", "activate", "fvm", out: File::NULL, err: File::NULL)
|
|
99
|
+
which_command("fvm")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def fvm_env
|
|
103
|
+
pub_bin = File.join(Dir.home, ".pub-cache", "bin")
|
|
104
|
+
{ "PATH" => "#{pub_bin}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def tools_from_flutter_bin(flutter_bin)
|
|
108
|
+
return nil unless File.executable?(flutter_bin)
|
|
109
|
+
|
|
110
|
+
bin_dir = File.dirname(flutter_bin)
|
|
111
|
+
dart = File.join(bin_dir, windows_host? ? "dart.bat" : "dart")
|
|
112
|
+
{
|
|
113
|
+
flutter: flutter_bin,
|
|
114
|
+
dart: (File.executable?(dart) ? dart : "dart"),
|
|
115
|
+
env: { "PATH" => "#{bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ensure_flutter_sdk_downloaded(client_dir: nil)
|
|
120
|
+
release_info = resolve_flutter_release(client_dir: client_dir)
|
|
121
|
+
return nil unless release_info
|
|
122
|
+
|
|
123
|
+
release = release_info[:release]
|
|
124
|
+
host = release_info[:host]
|
|
125
|
+
archive = release.fetch("archive")
|
|
126
|
+
install_root = File.join(Dir.home, ".ruflet", "flutter", release.fetch("version"), host)
|
|
127
|
+
sdk_root = File.join(install_root, "flutter")
|
|
128
|
+
flutter_bin = File.join(sdk_root, "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
129
|
+
return sdk_root if File.executable?(flutter_bin)
|
|
130
|
+
|
|
131
|
+
FileUtils.mkdir_p(install_root)
|
|
132
|
+
Dir.mktmpdir("ruflet-flutter-sdk-") do |tmpdir|
|
|
133
|
+
archive_path = File.join(tmpdir, File.basename(archive))
|
|
134
|
+
download_file("#{RELEASES_BASE}/#{archive}", archive_path)
|
|
135
|
+
extract_archive(archive_path, install_root)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return sdk_root if File.executable?(flutter_bin)
|
|
139
|
+
|
|
140
|
+
# Some archives may unpack into a different folder name.
|
|
141
|
+
guessed = Dir.glob(File.join(install_root, "**", windows_host? ? "flutter.bat" : "flutter"))
|
|
142
|
+
.map { |p| File.expand_path("../..", p) }
|
|
143
|
+
.find { |root| File.executable?(File.join(root, "bin", windows_host? ? "flutter.bat" : "flutter")) }
|
|
144
|
+
return guessed if guessed
|
|
145
|
+
|
|
146
|
+
nil
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
warn "Flutter auto-install failed: #{e.class}: #{e.message}"
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def resolve_flutter_release(client_dir: nil)
|
|
153
|
+
host = flutter_host
|
|
154
|
+
return nil unless host
|
|
155
|
+
|
|
156
|
+
manifest = fetch_releases_manifest(host)
|
|
157
|
+
return nil unless manifest
|
|
158
|
+
|
|
159
|
+
desired = desired_flutter_spec(client_dir: client_dir)
|
|
160
|
+
release = pick_release(
|
|
161
|
+
manifest,
|
|
162
|
+
version: desired[:version],
|
|
163
|
+
revision: desired[:revision],
|
|
164
|
+
channel: desired[:channel]
|
|
165
|
+
)
|
|
166
|
+
return nil unless release
|
|
167
|
+
|
|
168
|
+
{ release: release, host: host }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def desired_flutter_version(client_dir: nil)
|
|
172
|
+
desired_flutter_spec(client_dir: client_dir)[:version] || DEFAULT_FLUTTER_CHANNEL
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def desired_flutter_spec(client_dir: nil)
|
|
176
|
+
env = ENV["RUFLET_FLUTTER_VERSION"].to_s.strip
|
|
177
|
+
return { version: env, source: :env } unless env.empty?
|
|
178
|
+
|
|
179
|
+
fvm = parse_fvmrc(find_fvmrc(client_dir))
|
|
180
|
+
return { version: fvm, source: :fvmrc } if fvm
|
|
181
|
+
|
|
182
|
+
metadata = parse_flutter_metadata(find_flutter_metadata(client_dir))
|
|
183
|
+
return metadata.merge(source: :metadata) if metadata
|
|
184
|
+
|
|
185
|
+
{ channel: DEFAULT_FLUTTER_CHANNEL, version: DEFAULT_FLUTTER_CHANNEL, source: :default }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def fvm_project_dir(client_dir: nil)
|
|
189
|
+
return client_dir if client_dir
|
|
190
|
+
|
|
191
|
+
cwd_fvmrc = find_fvmrc(nil)
|
|
192
|
+
return File.dirname(cwd_fvmrc) if cwd_fvmrc
|
|
193
|
+
|
|
194
|
+
File.join(Dir.home, ".ruflet", "fvm_project")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def existing_fvm_flutter_bin(project_dir)
|
|
198
|
+
flutter = File.join(project_dir, ".fvm", "flutter_sdk", "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
199
|
+
return flutter if File.executable?(flutter)
|
|
200
|
+
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def find_fvmrc(client_dir)
|
|
205
|
+
candidates = []
|
|
206
|
+
candidates << File.join(client_dir, ".fvmrc") if client_dir
|
|
207
|
+
candidates << File.join(Dir.pwd, ".fvmrc")
|
|
208
|
+
candidates.find { |p| File.file?(p) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def find_flutter_metadata(client_dir)
|
|
212
|
+
candidates = []
|
|
213
|
+
candidates << File.join(client_dir, ".metadata") if client_dir
|
|
214
|
+
repo_client = File.expand_path("../../../../../ruflet_client/.metadata", __dir__)
|
|
215
|
+
template_client = File.expand_path("../../../../../templates/ruflet_flutter_template/.metadata", __dir__)
|
|
216
|
+
candidates << repo_client
|
|
217
|
+
candidates << template_client
|
|
218
|
+
candidates.find { |path| File.file?(path) }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def parse_fvmrc(path)
|
|
222
|
+
return nil unless path && File.file?(path)
|
|
223
|
+
|
|
224
|
+
raw = File.read(path).strip
|
|
225
|
+
return nil if raw.empty?
|
|
226
|
+
|
|
227
|
+
if raw.start_with?("{")
|
|
228
|
+
json = JSON.parse(raw) rescue {}
|
|
229
|
+
val = json["flutter"] || json["flutterSdkVersion"] || json["flutter_version"]
|
|
230
|
+
return val.to_s.strip unless val.to_s.strip.empty?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
raw
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def parse_flutter_metadata(path)
|
|
237
|
+
return nil unless path && File.file?(path)
|
|
238
|
+
|
|
239
|
+
parsed = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
240
|
+
version_info = parsed["version"]
|
|
241
|
+
return nil unless version_info.is_a?(Hash)
|
|
242
|
+
|
|
243
|
+
revision = version_info["revision"].to_s.strip
|
|
244
|
+
channel = version_info["channel"].to_s.strip
|
|
245
|
+
return nil if revision.empty?
|
|
246
|
+
|
|
247
|
+
{
|
|
248
|
+
revision: revision,
|
|
249
|
+
channel: channel.empty? ? DEFAULT_FLUTTER_CHANNEL : channel
|
|
250
|
+
}
|
|
251
|
+
rescue StandardError
|
|
252
|
+
nil
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def fetch_releases_manifest(host)
|
|
256
|
+
url = "#{RELEASES_BASE}/releases_#{host}.json"
|
|
257
|
+
uri = URI(url)
|
|
258
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
259
|
+
req = Net::HTTP::Get.new(uri)
|
|
260
|
+
req["User-Agent"] = "ruflet-cli"
|
|
261
|
+
http.request(req)
|
|
262
|
+
end
|
|
263
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
264
|
+
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def pick_release(manifest, version: nil, revision: nil, channel: nil)
|
|
269
|
+
releases = manifest.fetch("releases", [])
|
|
270
|
+
if revision
|
|
271
|
+
pinned = releases.find { |r| r["hash"] == revision }
|
|
272
|
+
return pinned if pinned
|
|
273
|
+
warn "Requested Flutter revision #{revision} not found for host #{flutter_host}; falling back."
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
if version
|
|
277
|
+
pinned = releases.find { |r| r["channel"] == "stable" && r["version"] == version }
|
|
278
|
+
return pinned if pinned
|
|
279
|
+
warn "Requested Flutter #{version} not found in stable releases; falling back to latest stable."
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
current = manifest.fetch("current_release", {})[(channel || "stable")]
|
|
283
|
+
if current
|
|
284
|
+
by_hash = releases.find { |r| r["hash"] == current }
|
|
285
|
+
return by_hash if by_hash
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
releases.reverse.find { |r| r["channel"] == (channel || "stable") } ||
|
|
289
|
+
releases.reverse.find { |r| r["channel"] == "stable" }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def flutter_host
|
|
293
|
+
os = RbConfig::CONFIG["host_os"]
|
|
294
|
+
if os.match?(/darwin/i)
|
|
295
|
+
return machine_arch.include?("arm") ? "macos_arm64" : "macos"
|
|
296
|
+
end
|
|
297
|
+
return "linux" if os.match?(/linux/i)
|
|
298
|
+
return "windows" if os.match?(/mswin|mingw|cygwin/i)
|
|
299
|
+
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def machine_arch
|
|
304
|
+
RbConfig::CONFIG["host_cpu"].to_s.downcase
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def windows_host?
|
|
308
|
+
RbConfig::CONFIG["host_os"].match?(/mswin|mingw|cygwin/i)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def which_command(name)
|
|
312
|
+
exts = windows_host? ? ENV.fetch("PATHEXT", ".EXE;.BAT;.CMD").split(";") : [""]
|
|
313
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
314
|
+
exts.each do |ext|
|
|
315
|
+
candidate = File.join(dir, "#{name}#{ext}")
|
|
316
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def download_file(url, destination, limit: 5)
|
|
323
|
+
raise "Too many redirects while downloading #{url}" if limit <= 0
|
|
324
|
+
|
|
325
|
+
uri = URI(url)
|
|
326
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
327
|
+
req = Net::HTTP::Get.new(uri)
|
|
328
|
+
req["User-Agent"] = "ruflet-cli"
|
|
329
|
+
http.request(req) do |res|
|
|
330
|
+
case res
|
|
331
|
+
when Net::HTTPSuccess
|
|
332
|
+
File.open(destination, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
|
|
333
|
+
return destination
|
|
334
|
+
when Net::HTTPRedirection
|
|
335
|
+
return download_file(res["location"], destination, limit: limit - 1)
|
|
336
|
+
else
|
|
337
|
+
raise "Download failed (#{res.code})"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def extract_archive(archive, destination)
|
|
344
|
+
if archive.end_with?(".zip")
|
|
345
|
+
if windows_host?
|
|
346
|
+
return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
|
|
347
|
+
end
|
|
348
|
+
return system("unzip", "-oq", archive, "-d", destination)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
if archive.end_with?(".tar.xz") || archive.end_with?(".tar.gz") || archive.end_with?(".tgz")
|
|
352
|
+
return system("tar", "-xf", archive, "-C", destination)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
false
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Ruflet
|
|
7
|
+
module CLI
|
|
8
|
+
module NewCommand
|
|
9
|
+
CLIENT_EXTENSION_MAP = {
|
|
10
|
+
"ads" => { package: "flet_ads", alias: "ruflet_ads" },
|
|
11
|
+
"audio" => { package: "flet_audio", alias: "ruflet_audio" },
|
|
12
|
+
"audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
13
|
+
"camera" => { package: "flet_camera", alias: "ruflet_camera" },
|
|
14
|
+
"charts" => { package: "flet_charts", alias: "ruflet_charts" },
|
|
15
|
+
"code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" },
|
|
16
|
+
"color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" },
|
|
17
|
+
"datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" },
|
|
18
|
+
"flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" },
|
|
19
|
+
"geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" },
|
|
20
|
+
"lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
|
|
21
|
+
"map" => { package: "flet_map", alias: "ruflet_map" },
|
|
22
|
+
"permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
|
|
23
|
+
"secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
|
|
24
|
+
"video" => { package: "flet_video", alias: "ruflet_video" },
|
|
25
|
+
"webview" => { package: "flet_webview", alias: "ruflet_webview" }
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def command_new(args)
|
|
29
|
+
app_name = args.shift
|
|
30
|
+
if app_name.nil? || app_name.strip.empty?
|
|
31
|
+
warn "Usage: ruflet new <appname>"
|
|
32
|
+
return 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
root = File.expand_path(app_name, Dir.pwd)
|
|
36
|
+
if Dir.exist?(root)
|
|
37
|
+
warn "Directory already exists: #{root}"
|
|
38
|
+
return 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
FileUtils.mkdir_p(root)
|
|
42
|
+
File.write(File.join(root, "main.rb"), format(Ruflet::CLI::MAIN_TEMPLATE, app_title: humanize_name(File.basename(root))))
|
|
43
|
+
File.write(File.join(root, "Gemfile"), Ruflet::CLI::GEMFILE_TEMPLATE)
|
|
44
|
+
File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root)))
|
|
45
|
+
write_default_ruflet_config(root, File.basename(root))
|
|
46
|
+
copy_ruflet_client_template(root)
|
|
47
|
+
configure_ruflet_client(root)
|
|
48
|
+
|
|
49
|
+
project_name = File.basename(root)
|
|
50
|
+
puts "Run:"
|
|
51
|
+
puts " cd #{project_name}"
|
|
52
|
+
puts " bundle install"
|
|
53
|
+
puts " bundle exec ruflet run main.rb"
|
|
54
|
+
puts
|
|
55
|
+
puts "Client template:"
|
|
56
|
+
puts " cd ruflet_client"
|
|
57
|
+
puts " flutter pub get"
|
|
58
|
+
puts " flutter run"
|
|
59
|
+
0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def copy_ruflet_client_template(root)
|
|
65
|
+
template_root = File.expand_path("../../../../../ruflet_client", __dir__)
|
|
66
|
+
return unless Dir.exist?(template_root)
|
|
67
|
+
|
|
68
|
+
target = File.join(root, "ruflet_client")
|
|
69
|
+
FileUtils.cp_r(template_root, target)
|
|
70
|
+
prune_client_template(target)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def prune_client_template(target)
|
|
74
|
+
paths = %w[
|
|
75
|
+
.dart_tool
|
|
76
|
+
.idea
|
|
77
|
+
build
|
|
78
|
+
ios/Pods
|
|
79
|
+
ios/.symlinks
|
|
80
|
+
ios/Podfile.lock
|
|
81
|
+
macos/Pods
|
|
82
|
+
macos/Podfile.lock
|
|
83
|
+
android/.gradle
|
|
84
|
+
android/.kotlin
|
|
85
|
+
android/local.properties
|
|
86
|
+
]
|
|
87
|
+
paths.each do |path|
|
|
88
|
+
full = File.join(target, path)
|
|
89
|
+
FileUtils.rm_rf(full) if File.exist?(full)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_default_ruflet_config(root, app_name)
|
|
94
|
+
File.write(File.join(root, "ruflet.yaml"), <<~YAML)
|
|
95
|
+
app:
|
|
96
|
+
name: #{app_name}
|
|
97
|
+
# Optional production client endpoint used by `ruflet build`.
|
|
98
|
+
# Example: https://api.example.com
|
|
99
|
+
ruflet_client_url: ""
|
|
100
|
+
|
|
101
|
+
# Source of truth for Flutter client extensions/plugins.
|
|
102
|
+
# Examples: camera, video, audio, flashlight, webview, map
|
|
103
|
+
services: []
|
|
104
|
+
|
|
105
|
+
# Build assets configuration consumed by `ruflet build`.
|
|
106
|
+
# Paths are relative to this file unless absolute.
|
|
107
|
+
assets:
|
|
108
|
+
dir: assets
|
|
109
|
+
splash_screen: assets/splash.png
|
|
110
|
+
icon_launcher: assets/icon.png
|
|
111
|
+
|
|
112
|
+
build:
|
|
113
|
+
splash_color: "#FFFFFF"
|
|
114
|
+
splash_dark_color: "#0B0B0B"
|
|
115
|
+
icon_background: "#FFFFFF"
|
|
116
|
+
theme_color: "#FFFFFF"
|
|
117
|
+
YAML
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def configure_ruflet_client(root)
|
|
121
|
+
config_path = File.join(root, "ruflet.yaml")
|
|
122
|
+
return unless File.file?(config_path)
|
|
123
|
+
|
|
124
|
+
config = YAML.safe_load(File.read(config_path), aliases: true) || {}
|
|
125
|
+
extension_keys = extract_extension_keys(config)
|
|
126
|
+
extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
|
|
127
|
+
extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
|
|
128
|
+
|
|
129
|
+
client_dir = File.join(root, "ruflet_client")
|
|
130
|
+
apply_client_manifest!(client_dir, extension_packages, extension_aliases)
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
warn "Failed to configure ruflet_client from ruflet.yaml: #{e.class}: #{e.message}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def extract_extension_keys(config)
|
|
136
|
+
from_services = Array(config["services"])
|
|
137
|
+
|
|
138
|
+
from_services
|
|
139
|
+
.map { |v| normalize_extension_key(v) }
|
|
140
|
+
.compact
|
|
141
|
+
.uniq
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def normalize_extension_key(value)
|
|
145
|
+
key = value.to_s.strip.downcase
|
|
146
|
+
return nil if key.empty?
|
|
147
|
+
|
|
148
|
+
key.tr!("-", "_")
|
|
149
|
+
key.gsub!(/\A(flet_)+/, "")
|
|
150
|
+
key.gsub!(/\Aservice_/, "")
|
|
151
|
+
key.gsub!(/\Acontrol_/, "")
|
|
152
|
+
key = "file_picker" if key == "filepicker"
|
|
153
|
+
key
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def apply_client_manifest!(client_dir, extension_packages, extension_aliases)
|
|
157
|
+
return unless Dir.exist?(client_dir)
|
|
158
|
+
|
|
159
|
+
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
160
|
+
main_path = File.join(client_dir, "lib", "main.dart")
|
|
161
|
+
prune_client_pubspec(pubspec_path, extension_packages) if File.file?(pubspec_path)
|
|
162
|
+
prune_client_main(main_path, extension_aliases) if File.file?(main_path)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def prune_client_pubspec(path, selected_packages)
|
|
166
|
+
data = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
167
|
+
deps = (data["dependencies"] || {}).dup
|
|
168
|
+
|
|
169
|
+
deps.keys.each do |name|
|
|
170
|
+
next unless name.start_with?("flet_")
|
|
171
|
+
next if name == "flet"
|
|
172
|
+
next if selected_packages.include?(name)
|
|
173
|
+
|
|
174
|
+
deps.delete(name)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
data["dependencies"] = deps
|
|
178
|
+
File.write(path, YAML.dump(data))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def prune_client_main(path, selected_aliases)
|
|
182
|
+
lines = File.readlines(path)
|
|
183
|
+
alias_to_package = {}
|
|
184
|
+
|
|
185
|
+
lines.each do |line|
|
|
186
|
+
match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
|
|
187
|
+
next unless match
|
|
188
|
+
|
|
189
|
+
alias_to_package[match[2]] = match[1]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
kept = lines.select do |line|
|
|
193
|
+
import_match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
|
|
194
|
+
if import_match
|
|
195
|
+
package_name = import_match[1]
|
|
196
|
+
next true if package_name == "flet"
|
|
197
|
+
next true if selected_aliases.include?(import_match[2])
|
|
198
|
+
next false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
extension_match = line.match(/\A\s*([a-zA-Z0-9_]+)\.Extension\(\),\s*\z/)
|
|
202
|
+
if extension_match
|
|
203
|
+
extension_alias = extension_match[1]
|
|
204
|
+
package_name = alias_to_package[extension_alias]
|
|
205
|
+
next true if package_name.nil? # non-Flet extension lines
|
|
206
|
+
next true if selected_aliases.include?(extension_alias)
|
|
207
|
+
next false
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
File.write(path, kept.join)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def humanize_name(name)
|
|
217
|
+
name.to_s.gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|