ruflet 0.0.17 → 0.0.18

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.
@@ -12,23 +12,16 @@ require "uri"
12
12
  require "thread"
13
13
  require "io/console"
14
14
  require "time"
15
- require "yaml"
16
15
 
17
16
  module Ruflet
18
17
  module CLI
19
18
  module RunCommand
20
- DEFAULT_BACKEND_PORTS = {
21
- "web" => 8550,
22
- "desktop" => 8560,
23
- "mobile" => 8570
24
- }.freeze
25
-
26
19
  def command_run(args)
27
- options = { target: "mobile", requested_port: nil }
20
+ options = { target: "mobile", requested_port: 8550 }
28
21
  parser = OptionParser.new do |o|
29
22
  o.on("--web") { options[:target] = "web" }
30
23
  o.on("--desktop") { options[:target] = "desktop" }
31
- o.on("--port PORT", Integer, "Backend port (defaults: web 8550, desktop 8560, mobile 8570)") { |v| options[:requested_port] = v }
24
+ o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
32
25
  end
33
26
  parser.parse!(args)
34
27
 
@@ -40,33 +33,18 @@ module Ruflet
40
33
  return 1
41
34
  end
42
35
 
43
- requested_port = options[:requested_port] || default_backend_port(options[:target])
44
- selected_port = resolve_backend_port(options[:target], requested_port: requested_port)
36
+ selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
45
37
  return 1 unless selected_port
46
38
  env = {
47
39
  "RUFLET_TARGET" => options[:target],
48
40
  "RUFLET_SUPPRESS_SERVER_BANNER" => "1",
49
- "RUFLET_PORT" => selected_port.to_s,
50
- "RUFLET_STRICT_PORT" => "1"
41
+ "RUFLET_PORT" => selected_port.to_s
51
42
  }
52
43
  apply_local_ruflet_dev_overrides(env)
53
44
  assets_dir = File.join(File.dirname(script_path), "assets")
54
45
  env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
55
46
 
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)
47
+ print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port)
70
48
  print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
71
49
 
72
50
  gemfile_path = find_nearest_gemfile(Dir.pwd)
@@ -171,14 +149,16 @@ module Ruflet
171
149
 
172
150
  def print_run_banner(target:, requested_port:, port:)
173
151
  if port != requested_port.to_i
174
- puts "Port #{requested_port} is busy; using #{port} for #{target}."
152
+ puts "Requested port #{requested_port} is busy; bound to #{port}"
175
153
  end
176
154
  if target == "desktop"
177
155
  puts "Ruflet desktop URL: http://localhost:#{port}"
178
156
  elsif target == "mobile"
179
157
  puts "Ruflet target: #{target}"
158
+ else
159
+ puts "Ruflet target: #{target}"
160
+ puts "Ruflet URL: http://localhost:#{port}"
180
161
  end
181
- # web: launch_web_client prints the single app URL after the server boots
182
162
  end
183
163
 
184
164
  def launch_target_client(target, port)
@@ -194,19 +174,30 @@ module Ruflet
194
174
  end
195
175
  end
196
176
 
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.
201
177
  def launch_web_client(port)
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
178
+ web_dir = detect_web_client_dir
179
+ unless web_dir
180
+ warn "Web client build not found and prebuilt download failed."
181
+ return []
182
+ end
183
+
184
+ web_port = find_available_port(port + 1)
185
+ 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)
186
+ Process.detach(web_pid)
187
+ wait_for_server_boot(web_port)
188
+ backend_url = "http://localhost:#{port}"
189
+ web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}"
190
+ browser_pid = open_in_browser_app_mode(web_url)
191
+ open_in_browser(web_url) if browser_pid.nil?
192
+ puts "Ruflet web client: #{web_url}"
193
+ puts "Ruflet backend ws: ws://localhost:#{port}/ws"
194
+ [web_pid, browser_pid].compact
195
+ rescue Errno::ENOENT
196
+ warn "python3 is required to host web client locally."
197
+ warn "Install Python 3 and rerun."
198
+ []
207
199
  rescue StandardError => e
208
- warn "Failed to open web client: #{e.class}: #{e.message}"
209
- warn "Open it manually: http://localhost:#{port}/"
200
+ warn "Failed to launch web client: #{e.class}: #{e.message}"
210
201
  []
211
202
  end
212
203
 
@@ -316,36 +307,45 @@ module Ruflet
316
307
  end
317
308
 
318
309
  def detect_desktop_client_command(url)
319
- root = configured_client_root
320
-
321
- command = desktop_client_command_from_root(root, url)
322
- return command if command
323
-
324
- cached_root = ensure_prebuilt_client(desktop: true)
325
- desktop_client_command_from_root(cached_root, url)
326
- end
327
-
328
- def desktop_client_command_from_root(root, url)
310
+ root = ENV["RUFLET_CLIENT_DIR"]
311
+ root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
312
+ root = nil unless Dir.exist?(root)
313
+ root ||= ensure_prebuilt_client(desktop: true)
329
314
  return nil unless root && Dir.exist?(root)
330
315
 
331
316
  host_os = RbConfig::CONFIG["host_os"]
332
317
  if host_os.match?(/darwin/i)
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)
318
+ release_bin = File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
319
+ debug_bin = File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
320
+ prebuilt_bin = File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
321
+ executable = [release_bin, debug_bin].find { |p| File.file?(p) && File.executable?(p) }
322
+ executable ||= prebuilt_bin if File.file?(prebuilt_bin) && File.executable?(prebuilt_bin)
323
+ return [executable, url] if executable
336
324
  elsif host_os.match?(/mswin|mingw|cygwin/i)
337
- exe = File.join(root, "desktop", "ruflet_client.exe")
325
+ exe = File.join(root, "build", "windows", "x64", "runner", "Release", "ruflet_client.exe")
326
+ prebuilt = File.join(root, "desktop", "ruflet_client.exe")
327
+ exe = prebuilt if !File.file?(exe) && File.file?(prebuilt)
338
328
  return [exe, url] if File.file?(exe)
339
329
  else
340
- direct = File.join(root, "desktop", "ruflet_client")
330
+ direct = File.join(root, "build", "linux", "x64", "release", "bundle", "ruflet_client")
331
+ prebuilt_direct = File.join(root, "desktop", "ruflet_client")
332
+ direct = prebuilt_direct if !File.file?(direct) && File.file?(prebuilt_direct)
341
333
  return [direct, url] if File.file?(direct)
334
+ bundle_dir = File.join(root, "build", "linux", "x64", "release", "bundle")
335
+ if Dir.exist?(bundle_dir)
336
+ candidate = Dir.children(bundle_dir).map { |f| File.join(bundle_dir, f) }
337
+ .find { |path| File.file?(path) && File.executable?(path) }
338
+ return [candidate, url] if candidate
339
+ end
342
340
  end
343
341
 
344
342
  nil
345
343
  end
346
344
 
347
345
  def detect_web_client_dir
348
- root = configured_client_root
346
+ root = ENV["RUFLET_CLIENT_DIR"]
347
+ root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
348
+ root = nil unless Dir.exist?(root)
349
349
  root ||= ensure_prebuilt_client(web: true)
350
350
  return nil unless root && Dir.exist?(root)
351
351
 
@@ -357,14 +357,6 @@ module Ruflet
357
357
  nil
358
358
  end
359
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
-
368
360
  def ensure_prebuilt_client(web: false, desktop: false, platform: nil, force: false)
369
361
  platform ||= host_platform_name
370
362
  return nil if platform.nil?
@@ -379,12 +371,10 @@ module Ruflet
379
371
  return nil if desktop_asset.nil?
380
372
  wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
381
373
  end
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)
374
+ if !force && (wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop, platform: platform))
375
+ ensure_client_manifest(cache_root, platform: platform)
385
376
  return cache_root
386
377
  end
387
- force = true if cached && !compatible_cache
388
378
 
389
379
  release = fetch_release_for_version
390
380
  return nil unless release
@@ -446,9 +436,7 @@ module Ruflet
446
436
 
447
437
  case platform
448
438
  when "macos"
449
- app_path = File.join(root, "desktop", "ruflet_client.app")
450
- bin = File.join(app_path, "Contents", "MacOS", "ruflet_client")
451
- File.file?(bin) && ensure_macos_file_picker_entitlement(app_path)
439
+ File.file?(File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client"))
452
440
  when "linux"
453
441
  File.file?(File.join(root, "desktop", "ruflet_client"))
454
442
  when "windows"
@@ -480,33 +468,11 @@ module Ruflet
480
468
  end
481
469
 
482
470
  def fetch_release_for_version
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"])
471
+ release_by_tag("v#{ruflet_version}") ||
472
+ release_by_tag(ruflet_version) ||
473
+ release_by_tag("prebuild") ||
474
+ release_by_tag("prebuild-main") ||
475
+ release_latest
510
476
  end
511
477
 
512
478
  def ruflet_version
@@ -516,59 +482,16 @@ module Ruflet
516
482
  Ruflet::VERSION
517
483
  end
518
484
 
485
+ def release_latest
486
+ github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/latest")
487
+ end
488
+
519
489
  def release_by_tag(tag)
520
490
  github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/tags/#{tag}")
521
491
  rescue StandardError
522
492
  nil
523
493
  end
524
494
 
525
- def ensure_macos_file_picker_entitlement(app_path)
526
- return true if macos_app_has_file_picker_entitlement?(app_path)
527
-
528
- Dir.mktmpdir("ruflet-entitlements-") do |dir|
529
- entitlements_path = File.join(dir, "ruflet_file_picker.entitlements")
530
- File.write(entitlements_path, macos_file_picker_entitlements_plist)
531
- system(
532
- "/usr/bin/codesign",
533
- "--force",
534
- "--deep",
535
- "--sign",
536
- "-",
537
- "--entitlements",
538
- entitlements_path,
539
- app_path,
540
- out: File::NULL,
541
- err: File::NULL
542
- )
543
- end
544
-
545
- macos_app_has_file_picker_entitlement?(app_path)
546
- end
547
-
548
- def macos_app_has_file_picker_entitlement?(app_path)
549
- output = IO.popen(["/usr/bin/codesign", "-d", "--entitlements", ":-", app_path], err: [:child, :out], &:read)
550
- $?.success? && output.include?("com.apple.security.files.user-selected.read-write")
551
- rescue StandardError
552
- false
553
- end
554
-
555
- def macos_file_picker_entitlements_plist
556
- <<~PLIST
557
- <?xml version="1.0" encoding="UTF-8"?>
558
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
559
- <plist version="1.0">
560
- <dict>
561
- \t<key>com.apple.security.app-sandbox</key>
562
- \t<true/>
563
- \t<key>com.apple.security.files.user-selected.read-write</key>
564
- \t<true/>
565
- \t<key>com.apple.security.network.client</key>
566
- \t<true/>
567
- </dict>
568
- </plist>
569
- PLIST
570
- end
571
-
572
495
  def fallback_release_asset(assets, wanted)
573
496
  kind = wanted[:kind]
574
497
  platform = wanted[:platform]
@@ -607,8 +530,6 @@ module Ruflet
607
530
  return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
608
531
 
609
532
  raise "GitHub API failed (#{response.code})"
610
- rescue OpenSSL::SSL::SSLError
611
- JSON.parse(curl_get(url, headers: ["Accept: application/vnd.github+json", "User-Agent: ruflet-cli"]))
612
533
  end
613
534
 
614
535
  def download_file(url, destination, limit: 5)
@@ -630,31 +551,6 @@ module Ruflet
630
551
  end
631
552
  end
632
553
  end
633
- rescue OpenSSL::SSL::SSLError
634
- curl_download(url, destination)
635
- end
636
-
637
- def curl_get(url, headers: [])
638
- args = ["curl", "-fsSL"]
639
- headers.each do |header|
640
- args += ["-H", header]
641
- end
642
- args << url
643
- output = IO.popen(args, err: File::NULL, &:read)
644
- return output if $?.success?
645
-
646
- raise "curl failed while requesting #{url}"
647
- rescue Errno::ENOENT
648
- raise "Ruby SSL verification failed and curl is not available"
649
- end
650
-
651
- def curl_download(url, destination)
652
- ok = system("curl", "-fL", "--retry", "2", "--connect-timeout", "20", "-o", destination, url, out: File::NULL, err: File::NULL)
653
- return destination if ok
654
-
655
- raise "curl failed while downloading #{url}"
656
- rescue Errno::ENOENT
657
- raise "Ruby SSL verification failed and curl is not available"
658
554
  end
659
555
 
660
556
  def extract_archive(archive, destination)
@@ -685,6 +581,19 @@ module Ruflet
685
581
  nil
686
582
  end
687
583
 
584
+ def ensure_client_manifest(root, platform:)
585
+ return if read_client_manifest(root)
586
+
587
+ assets = []
588
+ assets << { "kind" => "web", "platform" => platform, "asset_name" => nil } if File.file?(File.join(root, "web", "index.html"))
589
+ if prebuilt_desktop_present?(root, platform: platform)
590
+ assets << { "kind" => "desktop", "platform" => platform, "asset_name" => nil }
591
+ end
592
+ return if assets.empty?
593
+
594
+ write_client_manifest(root, platform: platform, release: nil, assets: assets)
595
+ end
596
+
688
597
  def write_client_manifest(root, platform:, release:, assets:)
689
598
  FileUtils.mkdir_p(root)
690
599
  payload = {
@@ -717,24 +626,26 @@ module Ruflet
717
626
  port = start_port.to_i
718
627
 
719
628
  max_attempts.times do
720
- return port if port_available?(port)
721
-
722
- port += 1
629
+ begin
630
+ begin
631
+ probe = TCPServer.new("0.0.0.0", port)
632
+ rescue Errno::EACCES, Errno::EPERM
633
+ probe = TCPServer.new("127.0.0.1", port)
634
+ end
635
+ probe.close
636
+ return port
637
+ rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
638
+ port += 1
639
+ end
723
640
  end
724
641
 
725
- nil
726
- end
727
-
728
- def default_backend_port(target)
729
- DEFAULT_BACKEND_PORTS.fetch(target.to_s, DEFAULT_BACKEND_PORTS.fetch("mobile"))
642
+ start_port
730
643
  end
731
644
 
732
- def resolve_backend_port(target, requested_port: nil)
645
+ def resolve_backend_port(_target, requested_port: 8550)
733
646
  base = requested_port.to_i
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
647
+ base = 8550 if base <= 0
648
+ find_available_port(base)
738
649
  end
739
650
 
740
651
  def port_available?(port)
@@ -746,7 +657,7 @@ module Ruflet
746
657
  probe = TCPServer.new("127.0.0.1", port)
747
658
  end
748
659
  true
749
- rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
660
+ rescue Errno::EADDRINUSE
750
661
  false
751
662
  ensure
752
663
  probe&.close
@@ -5,9 +5,16 @@ module Ruflet
5
5
  MAIN_TEMPLATE = <<~RUBY
6
6
  require "ruflet"
7
7
  Ruflet.run do |page|
8
- page.title = "%<app_title>s"
8
+ page.title = "Counter Demo"
9
9
  count = 0
10
10
  count_text = text(count.to_s, style: {size: 40})
11
+ page.floating_action_button = fab(
12
+ icon: "add",
13
+ on_click: ->(_e) do
14
+ count += 1
15
+ page.update(count_text, value: count.to_s)
16
+ end
17
+ )
11
18
  page.add(
12
19
  container(
13
20
  expand: true,
@@ -20,13 +27,6 @@ module Ruflet
20
27
  count_text
21
28
  ]
22
29
  )
23
- ),
24
- floating_action_button: fab(
25
- icon: "add",
26
- on_click: ->(_e) do
27
- count += 1
28
- page.update(count_text, value: count.to_s)
29
- end
30
30
  )
31
31
  )
32
32
  end
@@ -36,7 +36,6 @@ module Ruflet
36
36
  GEMFILE_TEMPLATE = <<~GEMFILE
37
37
  source "https://rubygems.org"
38
38
 
39
- gem "ruflet", ">= #{Ruflet::VERSION}"
40
39
  gem "ruflet_core", ">= #{Ruflet::VERSION}"
41
40
  gem "ruflet_server", ">= #{Ruflet::VERSION}"
42
41
  GEMFILE
data/lib/ruflet/cli.rb CHANGED
@@ -2,23 +2,10 @@
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
-
16
5
  require_relative "version"
17
6
  require_relative "cli/templates"
18
7
  require_relative "cli/new_command"
19
8
  require_relative "cli/flutter_sdk"
20
- require_relative "cli/environment_setup"
21
- require_relative "cli/android_sdk"
22
9
  require_relative "cli/run_command"
23
10
  require_relative "cli/update_command"
24
11
  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.17" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.18" 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.17
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -37,9 +37,7 @@ files:
37
37
  - assets/splash.png
38
38
  - bin/ruflet
39
39
  - lib/ruflet/cli.rb
40
- - lib/ruflet/cli/android_sdk.rb
41
40
  - lib/ruflet/cli/build_command.rb
42
- - lib/ruflet/cli/environment_setup.rb
43
41
  - lib/ruflet/cli/extra_command.rb
44
42
  - lib/ruflet/cli/flutter_sdk.rb
45
43
  - lib/ruflet/cli/new_command.rb