ruflet 0.0.14 → 0.0.16

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.
@@ -17,12 +17,18 @@ require "yaml"
17
17
  module Ruflet
18
18
  module CLI
19
19
  module RunCommand
20
+ DEFAULT_BACKEND_PORTS = {
21
+ "web" => 8550,
22
+ "desktop" => 8560,
23
+ "mobile" => 8570
24
+ }.freeze
25
+
20
26
  def command_run(args)
21
- options = { target: "mobile", requested_port: 8550 }
27
+ options = { target: "mobile", requested_port: nil }
22
28
  parser = OptionParser.new do |o|
23
29
  o.on("--web") { options[:target] = "web" }
24
30
  o.on("--desktop") { options[:target] = "desktop" }
25
- o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
31
+ o.on("--port PORT", Integer, "Backend port (defaults: web 8550, desktop 8560, mobile 8570)") { |v| options[:requested_port] = v }
26
32
  end
27
33
  parser.parse!(args)
28
34
 
@@ -34,18 +40,33 @@ module Ruflet
34
40
  return 1
35
41
  end
36
42
 
37
- selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
43
+ requested_port = options[:requested_port] || default_backend_port(options[:target])
44
+ selected_port = resolve_backend_port(options[:target], requested_port: requested_port)
38
45
  return 1 unless selected_port
39
46
  env = {
40
47
  "RUFLET_TARGET" => options[:target],
41
48
  "RUFLET_SUPPRESS_SERVER_BANNER" => "1",
42
- "RUFLET_PORT" => selected_port.to_s
49
+ "RUFLET_PORT" => selected_port.to_s,
50
+ "RUFLET_STRICT_PORT" => "1"
43
51
  }
44
52
  apply_local_ruflet_dev_overrides(env)
45
53
  assets_dir = File.join(File.dirname(script_path), "assets")
46
54
  env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
47
55
 
48
- print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port)
56
+ # Web: the Ruby backend serves the Flutter web client itself (same
57
+ # origin/port as /ws), so no separate static server or proxy is needed.
58
+ if options[:target] == "web"
59
+ web_dir = detect_web_client_dir
60
+ if web_dir
61
+ env["RUFLET_WEB_CLIENT_DIR"] = web_dir
62
+ else
63
+ warn "Ruflet web client unavailable for version #{ruflet_version}."
64
+ warn "Install the matching GitHub release client before running --web."
65
+ return 1
66
+ end
67
+ end
68
+
69
+ print_run_banner(target: options[:target], requested_port: requested_port, port: selected_port)
49
70
  print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
50
71
 
51
72
  gemfile_path = find_nearest_gemfile(Dir.pwd)
@@ -150,16 +171,14 @@ module Ruflet
150
171
 
151
172
  def print_run_banner(target:, requested_port:, port:)
152
173
  if port != requested_port.to_i
153
- puts "Requested port #{requested_port} is busy; bound to #{port}"
174
+ puts "Port #{requested_port} is busy; using #{port} for #{target}."
154
175
  end
155
176
  if target == "desktop"
156
177
  puts "Ruflet desktop URL: http://localhost:#{port}"
157
178
  elsif target == "mobile"
158
179
  puts "Ruflet target: #{target}"
159
- else
160
- puts "Ruflet target: #{target}"
161
- puts "Ruflet URL: http://localhost:#{port}"
162
180
  end
181
+ # web: launch_web_client prints the single app URL after the server boots
163
182
  end
164
183
 
165
184
  def launch_target_client(target, port)
@@ -175,30 +194,19 @@ module Ruflet
175
194
  end
176
195
  end
177
196
 
197
+ # The Ruby backend (Ruflet::Server) serves the Flutter web client on its
198
+ # own port, so the client loads and opens its websocket on that same
199
+ # origin. The prebuilt web client derives its backend from Uri.base, so
200
+ # the public URL stays clean and dynamic port selection works naturally.
178
201
  def launch_web_client(port)
179
- web_dir = detect_web_client_dir
180
- unless web_dir
181
- warn "Web client build not found and prebuilt download failed."
182
- return []
183
- end
184
-
185
- web_port = find_available_port(port + 1)
186
- web_pid = Process.spawn("python3", "-m", "http.server", web_port.to_s, "--bind", "127.0.0.1", chdir: web_dir, out: File::NULL, err: File::NULL)
187
- Process.detach(web_pid)
188
- wait_for_server_boot(web_port)
189
- backend_url = "http://localhost:#{port}"
190
- web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}"
191
- browser_pid = open_in_browser_app_mode(web_url)
192
- open_in_browser(web_url) if browser_pid.nil?
193
- puts "Ruflet web client: #{web_url}"
194
- puts "Ruflet backend ws: ws://localhost:#{port}/ws"
195
- [web_pid, browser_pid].compact
196
- rescue Errno::ENOENT
197
- warn "python3 is required to host web client locally."
198
- warn "Install Python 3 and rerun."
199
- []
202
+ url = "http://localhost:#{port}/"
203
+ browser_pid = open_in_browser_app_mode(url)
204
+ open_in_browser(url) if browser_pid.nil?
205
+ puts "Ruflet web app: #{url}"
206
+ [browser_pid].compact
200
207
  rescue StandardError => e
201
- warn "Failed to launch web client: #{e.class}: #{e.message}"
208
+ warn "Failed to open web client: #{e.class}: #{e.message}"
209
+ warn "Open it manually: http://localhost:#{port}/"
202
210
  []
203
211
  end
204
212
 
@@ -286,7 +294,7 @@ module Ruflet
286
294
  end
287
295
 
288
296
  def launch_desktop_client(url)
289
- cmd = detect_project_desktop_client_command(url) || detect_desktop_client_command(url)
297
+ cmd = detect_desktop_client_command(url)
290
298
  unless cmd
291
299
  warn "Desktop client executable not found."
292
300
  warn "Set RUFLET_CLIENT_DIR to your client path."
@@ -307,119 +315,37 @@ module Ruflet
307
315
  []
308
316
  end
309
317
 
310
- def detect_project_desktop_client_command(url)
311
- return nil unless project_run_requires_managed_client?
312
- return nil unless respond_to?(:ensure_flutter_client_dir, true)
313
- return nil unless respond_to?(:prepare_flutter_client, true)
314
- return nil unless respond_to?(:ensure_flutter!, true)
315
-
316
- platform = host_platform_name
317
- return nil unless %w[macos windows linux].include?(platform)
318
-
319
- ensure_ruflet_build_assets(verbose: false) if respond_to?(:ensure_ruflet_build_assets, true)
320
- client_dir = ensure_flutter_client_dir(verbose: false)
321
- return nil unless client_dir
322
-
323
- config = project_run_config
324
- tools = ensure_flutter!("run", client_dir: client_dir)
325
- env = build_tool_env(tools[:env], platform, client_dir)
326
- return nil unless prepare_flutter_client(
327
- client_dir,
328
- platform: platform,
329
- tools: tools,
330
- config: config,
331
- self_contained: false,
332
- verbose: false
333
- )
334
-
335
- [
336
- env,
337
- tools[:flutter],
338
- "run",
339
- "-d",
340
- platform,
341
- "--target",
342
- "lib/main.server.dart",
343
- "--dart-define",
344
- "RUFLET_BACKEND_URL=#{url}"
345
- ]
346
- rescue StandardError => e
347
- warn "Project desktop client setup failed: #{e.class}: #{e.message}"
348
- nil
349
- end
350
-
351
- def project_run_requires_managed_client?
352
- return false unless defined?(Ruflet::CLI::BuildCommand::CLIENT_EXTENSION_MAP)
353
-
354
- services = Array(project_run_config["services"]).map { |value| normalize_run_extension_key(value) }.compact
355
- return false if services.empty?
356
-
357
- extension_keys = Ruflet::CLI::BuildCommand::CLIENT_EXTENSION_MAP.keys
358
- (services & extension_keys).any?
359
- end
360
-
361
- def project_run_config
362
- config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml")
363
- return {} unless File.file?(config_path)
364
-
365
- YAML.safe_load(File.read(config_path), aliases: true) || {}
366
- rescue StandardError
367
- {}
368
- end
318
+ def detect_desktop_client_command(url)
319
+ root = configured_client_root
369
320
 
370
- def normalize_run_extension_key(value)
371
- key = value.to_s.strip.downcase
372
- return nil if key.empty?
321
+ command = desktop_client_command_from_root(root, url)
322
+ return command if command
373
323
 
374
- key.tr!("-", "_")
375
- key.gsub!(/\A(flet_)+/, "")
376
- key.gsub!(/\Aservice_/, "")
377
- key
324
+ cached_root = ensure_prebuilt_client(desktop: true)
325
+ desktop_client_command_from_root(cached_root, url)
378
326
  end
379
327
 
380
- def detect_desktop_client_command(url)
381
- root = ENV["RUFLET_CLIENT_DIR"]
382
- root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
383
- root = nil unless Dir.exist?(root)
384
- root ||= ensure_prebuilt_client(desktop: true)
328
+ def desktop_client_command_from_root(root, url)
385
329
  return nil unless root && Dir.exist?(root)
386
330
 
387
331
  host_os = RbConfig::CONFIG["host_os"]
388
332
  if host_os.match?(/darwin/i)
389
- app_path = [
390
- File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app"),
391
- File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app"),
392
- File.join(root, "desktop", "ruflet_client.app")
393
- ].find do |candidate|
394
- bin = File.join(candidate, "Contents", "MacOS", "ruflet_client")
395
- File.file?(bin) && File.executable?(bin) && ensure_macos_file_picker_entitlement(candidate)
396
- end
397
- return [File.join(app_path, "Contents", "MacOS", "ruflet_client"), url] if app_path
333
+ app_path = File.join(root, "desktop", "ruflet_client.app")
334
+ bin = File.join(app_path, "Contents", "MacOS", "ruflet_client")
335
+ return [bin, url] if File.file?(bin) && File.executable?(bin) && ensure_macos_file_picker_entitlement(app_path)
398
336
  elsif host_os.match?(/mswin|mingw|cygwin/i)
399
- exe = File.join(root, "build", "windows", "x64", "runner", "Release", "ruflet_client.exe")
400
- prebuilt = File.join(root, "desktop", "ruflet_client.exe")
401
- exe = prebuilt if !File.file?(exe) && File.file?(prebuilt)
337
+ exe = File.join(root, "desktop", "ruflet_client.exe")
402
338
  return [exe, url] if File.file?(exe)
403
339
  else
404
- direct = File.join(root, "build", "linux", "x64", "release", "bundle", "ruflet_client")
405
- prebuilt_direct = File.join(root, "desktop", "ruflet_client")
406
- direct = prebuilt_direct if !File.file?(direct) && File.file?(prebuilt_direct)
340
+ direct = File.join(root, "desktop", "ruflet_client")
407
341
  return [direct, url] if File.file?(direct)
408
- bundle_dir = File.join(root, "build", "linux", "x64", "release", "bundle")
409
- if Dir.exist?(bundle_dir)
410
- candidate = Dir.children(bundle_dir).map { |f| File.join(bundle_dir, f) }
411
- .find { |path| File.file?(path) && File.executable?(path) }
412
- return [candidate, url] if candidate
413
- end
414
342
  end
415
343
 
416
344
  nil
417
345
  end
418
346
 
419
347
  def detect_web_client_dir
420
- root = ENV["RUFLET_CLIENT_DIR"]
421
- root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
422
- root = nil unless Dir.exist?(root)
348
+ root = configured_client_root
423
349
  root ||= ensure_prebuilt_client(web: true)
424
350
  return nil unless root && Dir.exist?(root)
425
351
 
@@ -431,6 +357,14 @@ module Ruflet
431
357
  nil
432
358
  end
433
359
 
360
+ def configured_client_root
361
+ root = ENV["RUFLET_CLIENT_DIR"].to_s.strip
362
+ return nil if root.empty?
363
+
364
+ expanded = File.expand_path(root)
365
+ Dir.exist?(expanded) ? expanded : nil
366
+ end
367
+
434
368
  def ensure_prebuilt_client(web: false, desktop: false, platform: nil, force: false)
435
369
  platform ||= host_platform_name
436
370
  return nil if platform.nil?
@@ -445,10 +379,12 @@ module Ruflet
445
379
  return nil if desktop_asset.nil?
446
380
  wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
447
381
  end
448
- if !force && (wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform))
449
- ensure_client_manifest(cache_root, platform: platform)
382
+ cached = prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform)
383
+ compatible_cache = cached && compatible_client_cache?(cache_root, platform: platform)
384
+ if !force && (wanted_assets.empty? || compatible_cache)
450
385
  return cache_root
451
386
  end
387
+ force = true if cached && !compatible_cache
452
388
 
453
389
  release = fetch_release_for_version
454
390
  return nil unless release
@@ -544,11 +480,33 @@ module Ruflet
544
480
  end
545
481
 
546
482
  def fetch_release_for_version
547
- release_by_tag("v#{ruflet_version}") ||
548
- release_by_tag(ruflet_version) ||
549
- release_by_tag("prebuild") ||
550
- release_by_tag("prebuild-main") ||
551
- release_latest
483
+ client_release_tags.each do |tag|
484
+ release = release_by_tag(tag)
485
+ return release if release
486
+ end
487
+
488
+ nil
489
+ end
490
+
491
+ def client_release_tags
492
+ channel = ENV["RUFLET_CLIENT_CHANNEL"].to_s.strip
493
+ unless channel.empty?
494
+ raise ArgumentError, "Invalid RUFLET_CLIENT_CHANNEL" unless channel.match?(/\A[A-Za-z0-9._-]+\z/)
495
+
496
+ return [channel]
497
+ end
498
+
499
+ ["v#{ruflet_version}", ruflet_version]
500
+ end
501
+
502
+ def compatible_client_cache?(root, platform:)
503
+ manifest = read_client_manifest(root)
504
+ return false unless manifest
505
+
506
+ manifest["schema"] == 1 &&
507
+ manifest["ruflet_version"] == ruflet_version &&
508
+ manifest["platform"] == platform &&
509
+ client_release_tags.include?(manifest["release_tag"])
552
510
  end
553
511
 
554
512
  def ruflet_version
@@ -558,10 +516,6 @@ module Ruflet
558
516
  Ruflet::VERSION
559
517
  end
560
518
 
561
- def release_latest
562
- github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/latest")
563
- end
564
-
565
519
  def release_by_tag(tag)
566
520
  github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/tags/#{tag}")
567
521
  rescue StandardError
@@ -731,19 +685,6 @@ module Ruflet
731
685
  nil
732
686
  end
733
687
 
734
- def ensure_client_manifest(root, platform:)
735
- return if read_client_manifest(root)
736
-
737
- assets = []
738
- assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if File.file?(File.join(root, "web", "index.html"))
739
- if prebuilt_desktop_present?(root, platform: platform)
740
- assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
741
- end
742
- return if assets.empty?
743
-
744
- write_client_manifest(root, platform: platform, release: nil, assets: assets)
745
- end
746
-
747
688
  def write_client_manifest(root, platform:, release:, assets:)
748
689
  FileUtils.mkdir_p(root)
749
690
  payload = {
@@ -776,26 +717,24 @@ module Ruflet
776
717
  port = start_port.to_i
777
718
 
778
719
  max_attempts.times do
779
- begin
780
- begin
781
- probe = TCPServer.new("0.0.0.0", port)
782
- rescue Errno::EACCES, Errno::EPERM
783
- probe = TCPServer.new("127.0.0.1", port)
784
- end
785
- probe.close
786
- return port
787
- rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
788
- port += 1
789
- end
720
+ return port if port_available?(port)
721
+
722
+ port += 1
790
723
  end
791
724
 
792
- start_port
725
+ nil
726
+ end
727
+
728
+ def default_backend_port(target)
729
+ DEFAULT_BACKEND_PORTS.fetch(target.to_s, DEFAULT_BACKEND_PORTS.fetch("mobile"))
793
730
  end
794
731
 
795
- def resolve_backend_port(_target, requested_port: 8550)
732
+ def resolve_backend_port(target, requested_port: nil)
796
733
  base = requested_port.to_i
797
- base = 8550 if base <= 0
798
- find_available_port(base)
734
+ base = default_backend_port(target) if base <= 0
735
+ selected = find_available_port(base)
736
+ warn "No available Ruflet port found starting at #{base}." unless selected
737
+ selected
799
738
  end
800
739
 
801
740
  def port_available?(port)
@@ -807,7 +746,7 @@ module Ruflet
807
746
  probe = TCPServer.new("127.0.0.1", port)
808
747
  end
809
748
  true
810
- rescue Errno::EADDRINUSE
749
+ rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
811
750
  false
812
751
  ensure
813
752
  probe&.close
data/lib/ruflet/cli.rb CHANGED
@@ -2,10 +2,23 @@
2
2
 
3
3
  require "optparse"
4
4
 
5
+ # Project files (pbxproj, plists, embedded runtime sources) are UTF-8; never
6
+ # depend on the caller's locale for reading them.
7
+ Encoding.default_external = Encoding::UTF_8 if Encoding.default_external == Encoding::US_ASCII
8
+
9
+ # Provisioning (downloads, package installs, SDK extraction) can run for
10
+ # minutes. When stdout is not a TTY (CI logs, pipes, editor consoles) Ruby
11
+ # buffers it, so progress notes would only appear once the whole run finishes.
12
+ # Unbuffer both streams so the pipeline reports progress as it happens.
13
+ $stdout.sync = true
14
+ $stderr.sync = true
15
+
5
16
  require_relative "version"
6
17
  require_relative "cli/templates"
7
18
  require_relative "cli/new_command"
8
19
  require_relative "cli/flutter_sdk"
20
+ require_relative "cli/environment_setup"
21
+ require_relative "cli/android_sdk"
9
22
  require_relative "cli/run_command"
10
23
  require_relative "cli/update_command"
11
24
  require_relative "cli/build_command"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.14" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.16" unless const_defined?(:VERSION)
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -37,7 +37,9 @@ files:
37
37
  - assets/splash.png
38
38
  - bin/ruflet
39
39
  - lib/ruflet/cli.rb
40
+ - lib/ruflet/cli/android_sdk.rb
40
41
  - lib/ruflet/cli/build_command.rb
42
+ - lib/ruflet/cli/environment_setup.rb
41
43
  - lib/ruflet/cli/extra_command.rb
42
44
  - lib/ruflet/cli/flutter_sdk.rb
43
45
  - lib/ruflet/cli/new_command.rb
@@ -47,7 +49,8 @@ files:
47
49
  - lib/ruflet/version.rb
48
50
  - lib/ruflet_cli.rb
49
51
  homepage: https://github.com/AdamMusa/Ruflet
50
- licenses: []
52
+ licenses:
53
+ - MIT
51
54
  metadata: {}
52
55
  rdoc_options: []
53
56
  require_paths: