ruflet_cli 0.0.4 → 0.0.6
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/lib/ruflet/cli/build_command.rb +16 -13
- data/lib/ruflet/cli/extra_command.rb +25 -73
- data/lib/ruflet/cli/flutter_sdk.rb +275 -0
- data/lib/ruflet/cli/new_command.rb +0 -2
- data/lib/ruflet/cli/run_command.rb +97 -13
- data/lib/ruflet/cli/templates.rb +30 -60
- data/lib/ruflet/cli.rb +1 -9
- data/lib/ruflet/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 716c849e84715c603f30e6ad789cdf3d8d0dc3ef4be40a436af77546dde1d629
|
|
4
|
+
data.tar.gz: 765cf04a191c9e915b43ef525d1a21f6d42a7226a36db9d47b490117392a2a9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 239d1dc8d36a8db268eaf17e0c6aed617ae786a59c09dc8c12f2b7d1c15ec3778d8bceff8f57fd51973f0da7be015013b8cb936d39ed566aed099c88739d18eb
|
|
7
|
+
data.tar.gz: 24be0703279d8cda99245929f7fdd4975e278a1d9768fd46785701c1aa5a478f8c7fd3b9f73b3ebf6be9f5b08cbadac582880213102709fb8c5d9b66ee91926d
|
|
@@ -6,6 +6,8 @@ require "yaml"
|
|
|
6
6
|
module Ruflet
|
|
7
7
|
module CLI
|
|
8
8
|
module BuildCommand
|
|
9
|
+
include FlutterSdk
|
|
10
|
+
|
|
9
11
|
def command_build(args)
|
|
10
12
|
platform = (args.shift || "").downcase
|
|
11
13
|
if platform.empty?
|
|
@@ -26,10 +28,11 @@ module Ruflet
|
|
|
26
28
|
return 1
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
tools = ensure_flutter!("build", client_dir: client_dir)
|
|
32
|
+
ok = prepare_flutter_client(client_dir, tools: tools)
|
|
30
33
|
return 1 unless ok
|
|
31
34
|
|
|
32
|
-
ok = system(*flutter_cmd, *args, chdir: client_dir)
|
|
35
|
+
ok = system(tools[:env], tools[:flutter], *flutter_cmd, *args, chdir: client_dir)
|
|
33
36
|
ok ? 0 : 1
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -48,19 +51,19 @@ module Ruflet
|
|
|
48
51
|
nil
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
def prepare_flutter_client(client_dir)
|
|
54
|
+
def prepare_flutter_client(client_dir, tools:)
|
|
52
55
|
apply_build_config(client_dir)
|
|
53
|
-
unless system(
|
|
56
|
+
unless system(tools[:env], tools[:flutter], "pub", "get", chdir: client_dir)
|
|
54
57
|
warn "flutter pub get failed"
|
|
55
58
|
return false
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
unless system(
|
|
61
|
+
unless system(tools[:env], tools[:dart], "run", "flutter_native_splash:create", chdir: client_dir)
|
|
59
62
|
warn "flutter_native_splash failed"
|
|
60
63
|
return false
|
|
61
64
|
end
|
|
62
65
|
|
|
63
|
-
unless system(
|
|
66
|
+
unless system(tools[:env], tools[:dart], "run", "flutter_launcher_icons", chdir: client_dir)
|
|
64
67
|
warn "flutter_launcher_icons failed"
|
|
65
68
|
return false
|
|
66
69
|
end
|
|
@@ -213,19 +216,19 @@ module Ruflet
|
|
|
213
216
|
def flutter_build_command(platform)
|
|
214
217
|
case platform
|
|
215
218
|
when "apk", "android"
|
|
216
|
-
["
|
|
219
|
+
["build", "apk"]
|
|
217
220
|
when "aab", "appbundle"
|
|
218
|
-
["
|
|
221
|
+
["build", "appbundle"]
|
|
219
222
|
when "ios"
|
|
220
|
-
["
|
|
223
|
+
["build", "ios", "--no-codesign"]
|
|
221
224
|
when "web"
|
|
222
|
-
["
|
|
225
|
+
["build", "web"]
|
|
223
226
|
when "macos"
|
|
224
|
-
["
|
|
227
|
+
["build", "macos"]
|
|
225
228
|
when "windows"
|
|
226
|
-
["
|
|
229
|
+
["build", "windows"]
|
|
227
230
|
when "linux"
|
|
228
|
-
["
|
|
231
|
+
["build", "linux"]
|
|
229
232
|
else
|
|
230
233
|
nil
|
|
231
234
|
end
|
|
@@ -5,6 +5,8 @@ require "optparse"
|
|
|
5
5
|
module Ruflet
|
|
6
6
|
module CLI
|
|
7
7
|
module ExtraCommand
|
|
8
|
+
include FlutterSdk
|
|
9
|
+
|
|
8
10
|
def command_create(args)
|
|
9
11
|
command_new(args)
|
|
10
12
|
end
|
|
@@ -13,23 +15,20 @@ module Ruflet
|
|
|
13
15
|
verbose = args.delete("--verbose") || args.delete("-v")
|
|
14
16
|
puts "Ruflet doctor"
|
|
15
17
|
puts " Ruby: #{RUBY_VERSION}"
|
|
16
|
-
|
|
17
|
-
puts " Flutter: #{flutter
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return $?.exitstatus || 0
|
|
21
|
-
end
|
|
22
|
-
1
|
|
18
|
+
tools = ensure_flutter!("doctor")
|
|
19
|
+
puts " Flutter: #{tools[:flutter]}"
|
|
20
|
+
system(tools[:env], tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
|
|
21
|
+
$?.exitstatus || 1
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
def command_devices(args)
|
|
26
|
-
ensure_flutter!("devices")
|
|
27
|
-
system(
|
|
25
|
+
tools = ensure_flutter!("devices")
|
|
26
|
+
system(tools[:env], tools[:flutter], "devices", *args)
|
|
28
27
|
$?.exitstatus || 1
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def command_emulators(args)
|
|
32
|
-
ensure_flutter!("emulators")
|
|
31
|
+
tools = ensure_flutter!("emulators")
|
|
33
32
|
action = nil
|
|
34
33
|
emulator_id = nil
|
|
35
34
|
verbose = false
|
|
@@ -48,58 +47,22 @@ module Ruflet
|
|
|
48
47
|
warn "Missing --emulator for start"
|
|
49
48
|
return 1
|
|
50
49
|
end
|
|
51
|
-
cmd = [
|
|
50
|
+
cmd = [tools[:flutter], "emulators", "--launch", emulator_id]
|
|
52
51
|
cmd << "-v" if verbose
|
|
53
|
-
system(*cmd)
|
|
52
|
+
system(tools[:env], *cmd)
|
|
54
53
|
$?.exitstatus || 1
|
|
55
54
|
when "create", "delete"
|
|
56
55
|
warn "ruflet emulators --#{action} is not implemented yet. Use your platform tools."
|
|
57
56
|
1
|
|
58
57
|
else
|
|
59
|
-
cmd = [
|
|
58
|
+
cmd = [tools[:flutter], "emulators"]
|
|
60
59
|
cmd << "-v" if verbose
|
|
61
|
-
system(*cmd)
|
|
60
|
+
system(tools[:env], *cmd)
|
|
62
61
|
$?.exitstatus || 1
|
|
63
62
|
end
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
def command_serve(args)
|
|
67
|
-
options = { port: 8550, root: Dir.pwd }
|
|
68
|
-
parser = OptionParser.new do |o|
|
|
69
|
-
o.on("-p", "--port PORT", Integer, "Port (default: 8550)") { |v| options[:port] = v }
|
|
70
|
-
o.on("-r", "--root PATH", "Root directory (default: current dir)") { |v| options[:root] = v }
|
|
71
|
-
end
|
|
72
|
-
parser.parse!(args)
|
|
73
|
-
|
|
74
|
-
require "webrick"
|
|
75
|
-
root = File.expand_path(options[:root])
|
|
76
|
-
server = WEBrick::HTTPServer.new(
|
|
77
|
-
Port: options[:port],
|
|
78
|
-
DocumentRoot: root,
|
|
79
|
-
AccessLog: [],
|
|
80
|
-
Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN)
|
|
81
|
-
)
|
|
82
|
-
trap("INT") { server.shutdown }
|
|
83
|
-
puts "Serving #{root} on http://127.0.0.1:#{options[:port]}"
|
|
84
|
-
server.start
|
|
85
|
-
0
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def command_pack(args)
|
|
89
|
-
platform = default_desktop_platform
|
|
90
|
-
unless platform
|
|
91
|
-
warn "pack is only supported on desktop hosts (macOS, Windows, Linux)"
|
|
92
|
-
return 1
|
|
93
|
-
end
|
|
94
|
-
command_build([platform] + args)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def command_publish(args)
|
|
98
|
-
command_build(["web"] + args)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
65
|
def command_debug(args)
|
|
102
|
-
ensure_flutter!("debug")
|
|
103
66
|
options = {
|
|
104
67
|
platform: nil,
|
|
105
68
|
device_id: nil,
|
|
@@ -117,7 +80,17 @@ module Ruflet
|
|
|
117
80
|
parser.parse!(args)
|
|
118
81
|
|
|
119
82
|
options[:platform] ||= args.shift
|
|
120
|
-
|
|
83
|
+
client_dir = detect_client_dir
|
|
84
|
+
unless client_dir
|
|
85
|
+
warn "Could not find Flutter client directory."
|
|
86
|
+
warn "Set RUFLET_CLIENT_DIR or place client at ./ruflet_client"
|
|
87
|
+
warn "`ruflet debug` requires Flutter client source code."
|
|
88
|
+
warn "For prebuilt clients, use: `ruflet run --web` or `ruflet run --desktop`."
|
|
89
|
+
return 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
tools = ensure_flutter!("debug", client_dir: client_dir)
|
|
93
|
+
cmd = [tools[:flutter], "run"]
|
|
121
94
|
cmd << "--release" if options[:release]
|
|
122
95
|
cmd << "-v" if options[:verbose]
|
|
123
96
|
cmd += ["--web-renderer", options[:web_renderer]] if options[:web_renderer]
|
|
@@ -135,14 +108,7 @@ module Ruflet
|
|
|
135
108
|
end
|
|
136
109
|
end
|
|
137
110
|
|
|
138
|
-
|
|
139
|
-
unless client_dir
|
|
140
|
-
warn "Could not find Flutter client directory."
|
|
141
|
-
warn "Set RUFLET_CLIENT_DIR or place client at ./ruflet_client"
|
|
142
|
-
return 1
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
system(*cmd, chdir: client_dir)
|
|
111
|
+
system(tools[:env], *cmd, chdir: client_dir)
|
|
146
112
|
$?.exitstatus || 1
|
|
147
113
|
end
|
|
148
114
|
|
|
@@ -161,20 +127,6 @@ module Ruflet
|
|
|
161
127
|
nil
|
|
162
128
|
end
|
|
163
129
|
|
|
164
|
-
def default_desktop_platform
|
|
165
|
-
host = RbConfig::CONFIG["host_os"]
|
|
166
|
-
return "macos" if host =~ /darwin/i
|
|
167
|
-
return "windows" if host =~ /mswin|mingw|cygwin/i
|
|
168
|
-
return "linux" if host =~ /linux/i
|
|
169
|
-
nil
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def ensure_flutter!(command_name)
|
|
173
|
-
return if system("which flutter > /dev/null 2>&1")
|
|
174
|
-
|
|
175
|
-
warn "Flutter is required for `ruflet #{command_name}`. Install Flutter and ensure it is on PATH."
|
|
176
|
-
exit 1
|
|
177
|
-
end
|
|
178
130
|
end
|
|
179
131
|
end
|
|
180
132
|
end
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module Ruflet
|
|
11
|
+
module CLI
|
|
12
|
+
module FlutterSdk
|
|
13
|
+
RELEASES_BASE = "https://storage.googleapis.com/flutter_infra_release/releases".freeze
|
|
14
|
+
DEFAULT_FLUTTER_CHANNEL = "stable".freeze
|
|
15
|
+
|
|
16
|
+
def ensure_flutter!(command_name, client_dir: nil)
|
|
17
|
+
tools = flutter_tools(client_dir: client_dir)
|
|
18
|
+
return tools if tools
|
|
19
|
+
|
|
20
|
+
warn "Flutter is required for `ruflet #{command_name}` and FVM bootstrap failed."
|
|
21
|
+
warn "Set RUFLET_FLUTTER_VERSION or add .fvmrc to the project."
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def flutter_tools(client_dir: nil)
|
|
28
|
+
# Always use FVM so Flutter/Dart match pinned SDK.
|
|
29
|
+
fvm_tools = flutter_tools_via_fvm(client_dir: client_dir)
|
|
30
|
+
return fvm_tools if fvm_tools
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flutter_tools_via_fvm(client_dir: nil)
|
|
36
|
+
version = desired_flutter_version(client_dir: client_dir)
|
|
37
|
+
return nil if version.to_s.strip.empty?
|
|
38
|
+
|
|
39
|
+
project_dir = fvm_project_dir(client_dir: client_dir)
|
|
40
|
+
fvm = ensure_fvm_available(client_dir: client_dir)
|
|
41
|
+
return nil unless fvm
|
|
42
|
+
|
|
43
|
+
FileUtils.mkdir_p(project_dir)
|
|
44
|
+
fvmrc_path = File.join(project_dir, ".fvmrc")
|
|
45
|
+
unless File.file?(fvmrc_path)
|
|
46
|
+
File.write(fvmrc_path, "{\"flutter\":\"#{version}\"}\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
system(fvm_env, fvm, "install", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
|
|
50
|
+
system(fvm_env, fvm, "use", "--force", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
|
|
51
|
+
|
|
52
|
+
flutter = File.join(project_dir, ".fvm", "flutter_sdk", "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
53
|
+
return nil unless File.executable?(flutter)
|
|
54
|
+
|
|
55
|
+
tools_from_flutter_bin(flutter)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
warn "FVM bootstrap failed: #{e.class}: #{e.message}"
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ensure_fvm_available(client_dir: nil)
|
|
62
|
+
fvm = which_command("fvm")
|
|
63
|
+
return fvm if fvm
|
|
64
|
+
|
|
65
|
+
dart = which_command("dart")
|
|
66
|
+
unless dart
|
|
67
|
+
sdk_root = ensure_flutter_sdk_downloaded(client_dir: client_dir)
|
|
68
|
+
dart = sdk_root ? File.join(sdk_root, "bin", windows_host? ? "dart.bat" : "dart") : nil
|
|
69
|
+
end
|
|
70
|
+
return nil unless dart && File.executable?(dart)
|
|
71
|
+
|
|
72
|
+
system(dart, "pub", "global", "activate", "fvm", out: File::NULL, err: File::NULL)
|
|
73
|
+
which_command("fvm")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fvm_env
|
|
77
|
+
pub_bin = File.join(Dir.home, ".pub-cache", "bin")
|
|
78
|
+
{ "PATH" => "#{pub_bin}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def tools_from_flutter_bin(flutter_bin)
|
|
82
|
+
return nil unless File.executable?(flutter_bin)
|
|
83
|
+
|
|
84
|
+
bin_dir = File.dirname(flutter_bin)
|
|
85
|
+
dart = File.join(bin_dir, windows_host? ? "dart.bat" : "dart")
|
|
86
|
+
{
|
|
87
|
+
flutter: flutter_bin,
|
|
88
|
+
dart: (File.executable?(dart) ? dart : "dart"),
|
|
89
|
+
env: { "PATH" => "#{bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ensure_flutter_sdk_downloaded(client_dir: nil)
|
|
94
|
+
release_info = resolve_flutter_release(client_dir: client_dir)
|
|
95
|
+
return nil unless release_info
|
|
96
|
+
|
|
97
|
+
release = release_info[:release]
|
|
98
|
+
host = release_info[:host]
|
|
99
|
+
archive = release.fetch("archive")
|
|
100
|
+
install_root = File.join(Dir.home, ".ruflet", "flutter", release.fetch("version"), host)
|
|
101
|
+
sdk_root = File.join(install_root, "flutter")
|
|
102
|
+
flutter_bin = File.join(sdk_root, "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
103
|
+
return sdk_root if File.executable?(flutter_bin)
|
|
104
|
+
|
|
105
|
+
FileUtils.mkdir_p(install_root)
|
|
106
|
+
Dir.mktmpdir("ruflet-flutter-sdk-") do |tmpdir|
|
|
107
|
+
archive_path = File.join(tmpdir, File.basename(archive))
|
|
108
|
+
download_file("#{RELEASES_BASE}/#{archive}", archive_path)
|
|
109
|
+
extract_archive(archive_path, install_root)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return sdk_root if File.executable?(flutter_bin)
|
|
113
|
+
|
|
114
|
+
# Some archives may unpack into a different folder name.
|
|
115
|
+
guessed = Dir.glob(File.join(install_root, "**", windows_host? ? "flutter.bat" : "flutter"))
|
|
116
|
+
.map { |p| File.expand_path("../..", p) }
|
|
117
|
+
.find { |root| File.executable?(File.join(root, "bin", windows_host? ? "flutter.bat" : "flutter")) }
|
|
118
|
+
return guessed if guessed
|
|
119
|
+
|
|
120
|
+
nil
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
warn "Flutter auto-install failed: #{e.class}: #{e.message}"
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_flutter_release(client_dir: nil)
|
|
127
|
+
host = flutter_host
|
|
128
|
+
return nil unless host
|
|
129
|
+
|
|
130
|
+
manifest = fetch_releases_manifest(host)
|
|
131
|
+
return nil unless manifest
|
|
132
|
+
|
|
133
|
+
version = desired_flutter_version(client_dir: client_dir)
|
|
134
|
+
release = pick_release(manifest, version: version)
|
|
135
|
+
return nil unless release
|
|
136
|
+
|
|
137
|
+
{ release: release, host: host }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def desired_flutter_version(client_dir: nil)
|
|
141
|
+
env = ENV["RUFLET_FLUTTER_VERSION"].to_s.strip
|
|
142
|
+
return env unless env.empty?
|
|
143
|
+
|
|
144
|
+
fvm = parse_fvmrc(find_fvmrc(client_dir))
|
|
145
|
+
return fvm if fvm
|
|
146
|
+
|
|
147
|
+
DEFAULT_FLUTTER_CHANNEL
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def fvm_project_dir(client_dir: nil)
|
|
151
|
+
return client_dir if client_dir
|
|
152
|
+
|
|
153
|
+
File.join(Dir.home, ".ruflet", "fvm_project")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def find_fvmrc(client_dir)
|
|
157
|
+
candidates = []
|
|
158
|
+
candidates << File.join(client_dir, ".fvmrc") if client_dir
|
|
159
|
+
candidates << File.join(Dir.pwd, ".fvmrc")
|
|
160
|
+
candidates.find { |p| File.file?(p) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def parse_fvmrc(path)
|
|
164
|
+
return nil unless path && File.file?(path)
|
|
165
|
+
|
|
166
|
+
raw = File.read(path).strip
|
|
167
|
+
return nil if raw.empty?
|
|
168
|
+
|
|
169
|
+
if raw.start_with?("{")
|
|
170
|
+
json = JSON.parse(raw) rescue {}
|
|
171
|
+
val = json["flutter"] || json["flutterSdkVersion"] || json["flutter_version"]
|
|
172
|
+
return val.to_s.strip unless val.to_s.strip.empty?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
raw
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def fetch_releases_manifest(host)
|
|
179
|
+
url = "#{RELEASES_BASE}/releases_#{host}.json"
|
|
180
|
+
uri = URI(url)
|
|
181
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
182
|
+
req = Net::HTTP::Get.new(uri)
|
|
183
|
+
req["User-Agent"] = "ruflet-cli"
|
|
184
|
+
http.request(req)
|
|
185
|
+
end
|
|
186
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
187
|
+
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pick_release(manifest, version: nil)
|
|
192
|
+
releases = manifest.fetch("releases", [])
|
|
193
|
+
if version
|
|
194
|
+
pinned = releases.find { |r| r["channel"] == "stable" && r["version"] == version }
|
|
195
|
+
return pinned if pinned
|
|
196
|
+
warn "Requested Flutter #{version} not found in stable releases; falling back to latest stable."
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
current = manifest.fetch("current_release", {})["stable"]
|
|
200
|
+
if current
|
|
201
|
+
by_hash = releases.find { |r| r["hash"] == current }
|
|
202
|
+
return by_hash if by_hash
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
releases.reverse.find { |r| r["channel"] == "stable" }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def flutter_host
|
|
209
|
+
os = RbConfig::CONFIG["host_os"]
|
|
210
|
+
if os.match?(/darwin/i)
|
|
211
|
+
return machine_arch.include?("arm") ? "macos_arm64" : "macos"
|
|
212
|
+
end
|
|
213
|
+
return "linux" if os.match?(/linux/i)
|
|
214
|
+
return "windows" if os.match?(/mswin|mingw|cygwin/i)
|
|
215
|
+
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def machine_arch
|
|
220
|
+
RbConfig::CONFIG["host_cpu"].to_s.downcase
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def windows_host?
|
|
224
|
+
RbConfig::CONFIG["host_os"].match?(/mswin|mingw|cygwin/i)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def which_command(name)
|
|
228
|
+
exts = windows_host? ? ENV.fetch("PATHEXT", ".EXE;.BAT;.CMD").split(";") : [""]
|
|
229
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
230
|
+
exts.each do |ext|
|
|
231
|
+
candidate = File.join(dir, "#{name}#{ext}")
|
|
232
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def download_file(url, destination, limit: 5)
|
|
239
|
+
raise "Too many redirects while downloading #{url}" if limit <= 0
|
|
240
|
+
|
|
241
|
+
uri = URI(url)
|
|
242
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
243
|
+
req = Net::HTTP::Get.new(uri)
|
|
244
|
+
req["User-Agent"] = "ruflet-cli"
|
|
245
|
+
http.request(req) do |res|
|
|
246
|
+
case res
|
|
247
|
+
when Net::HTTPSuccess
|
|
248
|
+
File.open(destination, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
|
|
249
|
+
return destination
|
|
250
|
+
when Net::HTTPRedirection
|
|
251
|
+
return download_file(res["location"], destination, limit: limit - 1)
|
|
252
|
+
else
|
|
253
|
+
raise "Download failed (#{res.code})"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def extract_archive(archive, destination)
|
|
260
|
+
if archive.end_with?(".zip")
|
|
261
|
+
if windows_host?
|
|
262
|
+
return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
|
|
263
|
+
end
|
|
264
|
+
return system("unzip", "-oq", archive, "-d", destination)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if archive.end_with?(".tar.xz") || archive.end_with?(".tar.gz") || archive.end_with?(".tgz")
|
|
268
|
+
return system("tar", "-xf", archive, "-C", destination)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
false
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -19,10 +19,8 @@ module Ruflet
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
FileUtils.mkdir_p(root)
|
|
22
|
-
FileUtils.mkdir_p(File.join(root, ".bundle"))
|
|
23
22
|
File.write(File.join(root, "main.rb"), format(Ruflet::CLI::MAIN_TEMPLATE, app_title: humanize_name(File.basename(root))))
|
|
24
23
|
File.write(File.join(root, "Gemfile"), Ruflet::CLI::GEMFILE_TEMPLATE)
|
|
25
|
-
File.write(File.join(root, ".bundle", "config"), Ruflet::CLI::BUNDLE_CONFIG_TEMPLATE)
|
|
26
24
|
File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root)))
|
|
27
25
|
copy_ruflet_client_template(root)
|
|
28
26
|
|
|
@@ -30,7 +30,8 @@ module Ruflet
|
|
|
30
30
|
return 1
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
selected_port =
|
|
33
|
+
selected_port = resolve_backend_port(options[:target])
|
|
34
|
+
return 1 unless selected_port
|
|
34
35
|
env = {
|
|
35
36
|
"RUFLET_TARGET" => options[:target],
|
|
36
37
|
"RUFLET_SUPPRESS_SERVER_BANNER" => "1",
|
|
@@ -42,10 +43,10 @@ module Ruflet
|
|
|
42
43
|
print_run_banner(target: options[:target], port: selected_port)
|
|
43
44
|
print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
|
|
44
45
|
|
|
46
|
+
gemfile_path = find_nearest_gemfile(Dir.pwd)
|
|
45
47
|
cmd =
|
|
46
|
-
if
|
|
47
|
-
env["
|
|
48
|
-
env["BUNDLE_DISABLE_SHARED_GEMS"] = "true"
|
|
48
|
+
if gemfile_path
|
|
49
|
+
env["BUNDLE_GEMFILE"] = gemfile_path
|
|
49
50
|
bundle_ready = system(env, "bundle", "check", out: File::NULL, err: File::NULL)
|
|
50
51
|
return 1 unless bundle_ready || system(env, "bundle", "install")
|
|
51
52
|
|
|
@@ -106,8 +107,21 @@ module Ruflet
|
|
|
106
107
|
nil
|
|
107
108
|
end
|
|
108
109
|
|
|
110
|
+
def find_nearest_gemfile(start_dir)
|
|
111
|
+
current = File.expand_path(start_dir)
|
|
112
|
+
loop do
|
|
113
|
+
candidate = File.join(current, "Gemfile")
|
|
114
|
+
return candidate if File.file?(candidate)
|
|
115
|
+
|
|
116
|
+
parent = File.expand_path("..", current)
|
|
117
|
+
return nil if parent == current
|
|
118
|
+
|
|
119
|
+
current = parent
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
109
123
|
def print_run_banner(target:, port:)
|
|
110
|
-
if port != 8550
|
|
124
|
+
if target == "mobile" && port != 8550
|
|
111
125
|
puts "Requested port 8550 is busy; bound to #{port}"
|
|
112
126
|
end
|
|
113
127
|
if target == "desktop"
|
|
@@ -316,15 +330,15 @@ module Ruflet
|
|
|
316
330
|
platform = host_platform_name
|
|
317
331
|
return nil if platform.nil?
|
|
318
332
|
|
|
319
|
-
cache_root = File.join(Dir.home, ".ruflet", "client",
|
|
333
|
+
cache_root = File.join(Dir.home, ".ruflet", "client", ruflet_version, platform)
|
|
320
334
|
FileUtils.mkdir_p(cache_root)
|
|
321
335
|
|
|
322
336
|
wanted_assets = []
|
|
323
|
-
wanted_assets << "ruflet_client-web.tar.gz" if web
|
|
337
|
+
wanted_assets << { kind: :web, name: "ruflet_client-web.tar.gz" } if web
|
|
324
338
|
if desktop
|
|
325
339
|
desktop_asset = desktop_asset_name_for(platform)
|
|
326
340
|
return nil if desktop_asset.nil?
|
|
327
|
-
wanted_assets << desktop_asset
|
|
341
|
+
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
328
342
|
end
|
|
329
343
|
return cache_root if wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop)
|
|
330
344
|
|
|
@@ -332,20 +346,26 @@ module Ruflet
|
|
|
332
346
|
return nil unless release
|
|
333
347
|
|
|
334
348
|
assets = release.fetch("assets", [])
|
|
349
|
+
asset_names = assets.map { |a| a["name"].to_s }
|
|
335
350
|
Dir.mktmpdir("ruflet-prebuilt-") do |tmpdir|
|
|
336
|
-
wanted_assets.each do |
|
|
351
|
+
wanted_assets.each do |wanted|
|
|
352
|
+
asset_name = wanted.fetch(:name)
|
|
337
353
|
asset = assets.find { |a| a["name"] == asset_name }
|
|
354
|
+
asset ||= fallback_release_asset(assets, wanted)
|
|
338
355
|
unless asset
|
|
339
356
|
warn "Missing release asset: #{asset_name}"
|
|
357
|
+
warn "Available assets: #{asset_names.join(', ')}" unless asset_names.empty?
|
|
340
358
|
return nil
|
|
341
359
|
end
|
|
342
|
-
|
|
360
|
+
resolved_name = asset.fetch("name")
|
|
361
|
+
puts "Downloading prebuilt client asset: #{resolved_name}"
|
|
362
|
+
archive_path = File.join(tmpdir, resolved_name)
|
|
343
363
|
download_file(asset.fetch("browser_download_url"), archive_path)
|
|
344
|
-
subdir =
|
|
364
|
+
subdir = wanted[:kind] == :web ? "web" : "desktop"
|
|
345
365
|
target = File.join(cache_root, subdir)
|
|
346
366
|
FileUtils.mkdir_p(target)
|
|
347
367
|
unless extract_archive(archive_path, target)
|
|
348
|
-
warn "Failed to extract asset: #{
|
|
368
|
+
warn "Failed to extract asset: #{resolved_name}"
|
|
349
369
|
return nil
|
|
350
370
|
end
|
|
351
371
|
end
|
|
@@ -399,7 +419,18 @@ module Ruflet
|
|
|
399
419
|
end
|
|
400
420
|
|
|
401
421
|
def fetch_release_for_version
|
|
402
|
-
release_by_tag("v#{
|
|
422
|
+
release_by_tag("v#{ruflet_version}") ||
|
|
423
|
+
release_by_tag(ruflet_version) ||
|
|
424
|
+
release_by_tag("prebuild") ||
|
|
425
|
+
release_by_tag("prebuild-main") ||
|
|
426
|
+
release_latest
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def ruflet_version
|
|
430
|
+
return Ruflet::VERSION if Ruflet.const_defined?(:VERSION)
|
|
431
|
+
|
|
432
|
+
require_relative "../version"
|
|
433
|
+
Ruflet::VERSION
|
|
403
434
|
end
|
|
404
435
|
|
|
405
436
|
def release_latest
|
|
@@ -412,6 +443,33 @@ module Ruflet
|
|
|
412
443
|
nil
|
|
413
444
|
end
|
|
414
445
|
|
|
446
|
+
def fallback_release_asset(assets, wanted)
|
|
447
|
+
kind = wanted[:kind]
|
|
448
|
+
platform = wanted[:platform]
|
|
449
|
+
candidates = assets.select { |asset| release_asset_matches?(asset.fetch("name", ""), kind, platform) }
|
|
450
|
+
candidates.first
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def release_asset_matches?(name, kind, platform)
|
|
454
|
+
n = name.to_s.downcase
|
|
455
|
+
return false unless n.include?("ruflet_client")
|
|
456
|
+
|
|
457
|
+
if kind == :web
|
|
458
|
+
return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
case platform
|
|
462
|
+
when "macos"
|
|
463
|
+
n.include?("macos") && n.end_with?(".zip")
|
|
464
|
+
when "linux"
|
|
465
|
+
n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
|
|
466
|
+
when "windows"
|
|
467
|
+
n.include?("windows") && n.end_with?(".zip")
|
|
468
|
+
else
|
|
469
|
+
false
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
415
473
|
def github_get_json(url)
|
|
416
474
|
uri = URI(url)
|
|
417
475
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
@@ -497,6 +555,32 @@ module Ruflet
|
|
|
497
555
|
start_port
|
|
498
556
|
end
|
|
499
557
|
|
|
558
|
+
def resolve_backend_port(target)
|
|
559
|
+
return find_available_port(8550) if target == "mobile"
|
|
560
|
+
|
|
561
|
+
return 8550 if port_available?(8550)
|
|
562
|
+
|
|
563
|
+
warn "Port 8550 is required for `ruflet run --#{target}`."
|
|
564
|
+
warn "Stop the process using 8550 and run again."
|
|
565
|
+
nil
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def port_available?(port)
|
|
569
|
+
probe = nil
|
|
570
|
+
begin
|
|
571
|
+
begin
|
|
572
|
+
probe = TCPServer.new("0.0.0.0", port)
|
|
573
|
+
rescue Errno::EACCES, Errno::EPERM
|
|
574
|
+
probe = TCPServer.new("127.0.0.1", port)
|
|
575
|
+
end
|
|
576
|
+
true
|
|
577
|
+
rescue Errno::EADDRINUSE
|
|
578
|
+
false
|
|
579
|
+
ensure
|
|
580
|
+
probe&.close
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
500
584
|
def best_lan_host
|
|
501
585
|
ips = Socket.ip_address_list
|
|
502
586
|
addr = ips.find { |ip| ip.ipv4_private? && !ip.ipv4_loopback? }
|
data/lib/ruflet/cli/templates.rb
CHANGED
|
@@ -3,64 +3,44 @@
|
|
|
3
3
|
module Ruflet
|
|
4
4
|
module CLI
|
|
5
5
|
MAIN_TEMPLATE = <<~RUBY
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
expand: true,
|
|
24
|
-
alignment: Ruflet::MainAxisAlignment::CENTER,
|
|
25
|
-
horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
|
|
26
|
-
spacing: 12,
|
|
27
|
-
controls: [
|
|
28
|
-
page.text(value: "You have pushed the button this many times:"),
|
|
29
|
-
count_text
|
|
30
|
-
]
|
|
31
|
-
)
|
|
32
|
-
),
|
|
33
|
-
appbar: page.app_bar(
|
|
34
|
-
title: page.text(value: "Counter Demo")
|
|
35
|
-
),
|
|
36
|
-
floating_action_button: page.fab(
|
|
37
|
-
page.icon(icon: Ruflet::MaterialIcons::ADD),
|
|
38
|
-
on_click: ->(_e) {
|
|
39
|
-
@count += 1
|
|
40
|
-
page.update(count_text, value: @count.to_s)
|
|
41
|
-
}
|
|
42
|
-
)
|
|
6
|
+
require "ruflet"
|
|
7
|
+
Ruflet.run do |page|
|
|
8
|
+
page.title = "Counter Demo"
|
|
9
|
+
count = 0
|
|
10
|
+
count_text = nil
|
|
11
|
+
count_text ||= text(value: count.to_s, size: 40)
|
|
12
|
+
page.add(
|
|
13
|
+
container(
|
|
14
|
+
expand: true,
|
|
15
|
+
alignment: Ruflet::MainAxisAlignment::CENTER,
|
|
16
|
+
content: column(
|
|
17
|
+
alignment: Ruflet::MainAxisAlignment::CENTER,
|
|
18
|
+
horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
|
|
19
|
+
children: [
|
|
20
|
+
text(value: "You have pushed the button this many times:"),
|
|
21
|
+
count_text
|
|
22
|
+
]
|
|
43
23
|
)
|
|
44
|
-
|
|
45
|
-
|
|
24
|
+
),
|
|
25
|
+
floating_action_button: fab(
|
|
26
|
+
icon(icon: Ruflet::MaterialIcons::ADD),
|
|
27
|
+
on_click: ->(_e) do
|
|
28
|
+
count += 1
|
|
29
|
+
page.update(count_text, value: count.to_s)
|
|
30
|
+
end
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
46
34
|
|
|
47
|
-
MainApp.new.run
|
|
48
35
|
RUBY
|
|
49
36
|
|
|
50
37
|
GEMFILE_TEMPLATE = <<~GEMFILE
|
|
51
38
|
source "https://rubygems.org"
|
|
52
39
|
|
|
53
|
-
gem "ruflet", ">= 0.0.
|
|
54
|
-
gem "
|
|
55
|
-
gem "ruflet_server", ">= 0.0.3"
|
|
40
|
+
gem "ruflet", ">= 0.0.6"
|
|
41
|
+
gem "ruflet_server", ">= 0.0.6"
|
|
56
42
|
GEMFILE
|
|
57
43
|
|
|
58
|
-
BUNDLE_CONFIG_TEMPLATE = <<~YAML
|
|
59
|
-
---
|
|
60
|
-
BUNDLE_PATH: "vendor/bundle"
|
|
61
|
-
BUNDLE_DISABLE_SHARED_GEMS: "true"
|
|
62
|
-
YAML
|
|
63
|
-
|
|
64
44
|
README_TEMPLATE = <<~MD
|
|
65
45
|
# %<app_name>s
|
|
66
46
|
|
|
@@ -75,17 +55,7 @@ module Ruflet
|
|
|
75
55
|
## Run
|
|
76
56
|
|
|
77
57
|
```bash
|
|
78
|
-
bundle exec ruflet run main
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Client Template
|
|
82
|
-
|
|
83
|
-
`ruflet_client` template is generated inside this app.
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
cd ruflet_client
|
|
87
|
-
flutter pub get
|
|
88
|
-
flutter run
|
|
58
|
+
bundle exec ruflet run main
|
|
89
59
|
```
|
|
90
60
|
|
|
91
61
|
## Build
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "optparse"
|
|
|
5
5
|
require_relative "cli/templates"
|
|
6
6
|
require_relative "cli/new_command"
|
|
7
7
|
require_relative "cli/run_command"
|
|
8
|
+
require_relative "cli/flutter_sdk"
|
|
8
9
|
require_relative "cli/build_command"
|
|
9
10
|
require_relative "cli/extra_command"
|
|
10
11
|
|
|
@@ -30,12 +31,6 @@ module Ruflet
|
|
|
30
31
|
command_debug(argv)
|
|
31
32
|
when "build"
|
|
32
33
|
command_build(argv)
|
|
33
|
-
when "pack"
|
|
34
|
-
command_pack(argv)
|
|
35
|
-
when "publish"
|
|
36
|
-
command_publish(argv)
|
|
37
|
-
when "serve"
|
|
38
|
-
command_serve(argv)
|
|
39
34
|
when "devices"
|
|
40
35
|
command_devices(argv)
|
|
41
36
|
when "emulators"
|
|
@@ -62,9 +57,6 @@ module Ruflet
|
|
|
62
57
|
ruflet run [scriptname|path] [--web|--mobile|--desktop]
|
|
63
58
|
ruflet debug [scriptname|path]
|
|
64
59
|
ruflet build <apk|ios|aab|web|macos|windows|linux>
|
|
65
|
-
ruflet pack
|
|
66
|
-
ruflet publish
|
|
67
|
-
ruflet serve [--port N] [--root PATH]
|
|
68
60
|
ruflet devices
|
|
69
61
|
ruflet emulators
|
|
70
62
|
ruflet doctor
|
data/lib/ruflet/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- lib/ruflet/cli.rb
|
|
37
37
|
- lib/ruflet/cli/build_command.rb
|
|
38
38
|
- lib/ruflet/cli/extra_command.rb
|
|
39
|
+
- lib/ruflet/cli/flutter_sdk.rb
|
|
39
40
|
- lib/ruflet/cli/new_command.rb
|
|
40
41
|
- lib/ruflet/cli/run_command.rb
|
|
41
42
|
- lib/ruflet/cli/templates.rb
|