Class: Capybara::Lightpanda::Binary

Inherits:
Object
  • Object
show all
Defined in:
lib/capybara/lightpanda/binary.rb

Constant Summary collapse

GITHUB_RELEASE_URL =
"https://github.com/lightpanda-io/browser/releases/download"
PLATFORMS =
{
  %w[x86_64 linux] => "lightpanda-x86_64-linux",
  %w[aarch64 darwin] => "lightpanda-aarch64-macos",
  %w[arm64 darwin] => "lightpanda-aarch64-macos",
}.freeze
DEFAULT_CACHE_TIME =
86_400
PROVISION_HINT =

One-liner that re-provisions the binary from a process with no HTTP-stubbing loaded (VCR/WebMock guard the test process itself). Referenced from the stale-fallback warning and BETA_TESTING.md.

"bundle exec ruby -r capybara-lightpanda " \
"-e 'Capybara::Lightpanda::Binary.remove; puts Capybara::Lightpanda::Binary.update'"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.cache_timeObject



41
42
43
# File 'lib/capybara/lightpanda/binary.rb', line 41

def cache_time
  @cache_time ||= Integer(ENV.fetch("LIGHTPANDA_CACHE_TIME", DEFAULT_CACHE_TIME))
end

.install_dirObject



45
46
47
# File 'lib/capybara/lightpanda/binary.rb', line 45

def install_dir
  @install_dir ||= File.dirname(default_binary_path)
end

.loggerObject



49
50
51
52
53
54
# File 'lib/capybara/lightpanda/binary.rb', line 49

def logger
  return @logger if defined?(@logger) && @logger
  return nil unless ENV["LIGHTPANDA_DEBUG"]

  @logger = Capybara::Lightpanda::Logger.new($stderr.tap { |s| s.sync = true })
end

.proxy_addrObject

Returns the value of attribute proxy_addr.



39
40
41
# File 'lib/capybara/lightpanda/binary.rb', line 39

def proxy_addr
  @proxy_addr
end

.proxy_passObject

Returns the value of attribute proxy_pass.



39
40
41
# File 'lib/capybara/lightpanda/binary.rb', line 39

def proxy_pass
  @proxy_pass
end

.proxy_portObject

Returns the value of attribute proxy_port.



39
40
41
# File 'lib/capybara/lightpanda/binary.rb', line 39

def proxy_port
  @proxy_port
end

.proxy_userObject

Returns the value of attribute proxy_user.



39
40
41
# File 'lib/capybara/lightpanda/binary.rb', line 39

def proxy_user
  @proxy_user
end

.required_versionObject

Set a specific release tag (e.g. “0.3.0”) to pin downloads to that release. When nil, the rolling “nightly” tag is used. The pin only affects download URL construction — the gem’s MINIMUM_NIGHTLY_BUILD floor is still enforced at process start.



34
35
36
# File 'lib/capybara/lightpanda/binary.rb', line 34

def required_version
  @required_version
end

Class Method Details

.configure {|_self| ... } ⇒ Object

Yields:

  • (_self)

Yield Parameters:



56
57
58
# File 'lib/capybara/lightpanda/binary.rb', line 56

def configure
  yield self
end

.current_versionObject

Returns the ‘lightpanda version` output of the cached binary, or nil if the binary isn’t present / not runnable.



136
137
138
139
140
141
142
143
144
# File 'lib/capybara/lightpanda/binary.rb', line 136

def current_version
  path = install_path
  return nil unless File.executable?(path)

  stdout, _, status = Open3.capture3(path, "version")
  status.success? ? stdout.strip : nil
rescue Errno::ENOENT
  nil
end

.default_binary_pathObject



198
199
200
201
202
# File 'lib/capybara/lightpanda/binary.rb', line 198

def default_binary_path
  cache_dir = ENV.fetch("XDG_CACHE_HOME") { File.expand_path("~/.cache") }

  File.join(cache_dir, "lightpanda", "lightpanda")
end

.downloadObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/capybara/lightpanda/binary.rb', line 146

def download
  binary_name = platform_binary
  tag = required_version || "nightly"
  url = "#{GITHUB_RELEASE_URL}/#{tag}/#{binary_name}"
  destination = install_path

  log("Downloading #{binary_name} (#{tag}) → #{destination}")
  FileUtils.mkdir_p(File.dirname(destination))

  download_file(url, destination)
  FileUtils.chmod(0o755, destination)

  destination
end

.install_pathObject

Path the gem writes the downloaded binary to. Honors a user-configured install_dir; otherwise falls back to default_binary_path.



206
207
208
209
210
211
212
# File 'lib/capybara/lightpanda/binary.rb', line 206

def install_path
  if @install_dir
    File.join(@install_dir, "lightpanda")
  else
    default_binary_path
  end
end

.platform_binaryObject



191
192
193
194
195
196
# File 'lib/capybara/lightpanda/binary.rb', line 191

def platform_binary
  arch = normalize_arch(RbConfig::CONFIG["host_cpu"])
  os = normalize_os(RbConfig::CONFIG["host_os"])

  PLATFORMS[[arch, os]] || raise(UnsupportedPlatformError, "Unsupported platform: #{arch}-#{os}")
end

.removeObject

Delete the cached binary. Returns the path that was deleted, or nil if nothing was there.



122
123
124
125
126
127
128
129
130
131
132
# File 'lib/capybara/lightpanda/binary.rb', line 122

def remove
  path = install_path
  unless File.exist?(path)
    log("Nothing to remove at #{path}")
    return nil
  end

  File.delete(path)
  log("Removed #{path}")
  path
end

.updateObject

Canonical entrypoint: ensure the binary at install_path is current, download if needed, return its path. Pinned (required_version set) never re-downloads when present. Unpinned re-downloads when older than cache_time. When unpinned and the gem cache is empty/stale, an already-installed ‘lightpanda` on PATH (e.g. via Homebrew) wins over re-downloading — keeps test suites running under VCR/WebMock from triggering surprise HTTP to github.com.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/capybara/lightpanda/binary.rb', line 67

def update
  destination = install_path

  if required_version
    if File.executable?(destination)
      log("Pinned #{required_version} present at #{destination}")
      return destination
    end
    return download
  end

  if cached_fresh?(destination)
    log("Cached binary at #{destination} is fresh (< #{cache_time}s)")
    return destination
  end

  if (system_path = system_binary_path)
    log("Using lightpanda from PATH at #{system_path}")
    return system_path
  end

  # Stale-or-absent cache, nothing on PATH: refresh from the network.
  # If that fails (GitHub 5xx, DNS/connect timeouts, SocketError) but a
  # usable — if stale — binary is already cached, keep using it rather
  # than hard-failing. A cold cache (nothing on disk) still surfaces the
  # error. The MINIMUM_NIGHTLY_BUILD floor is enforced downstream in
  # Process#start, so a sub-floor binary can't slip in.
  #
  # Deliberately StandardError, not Exception: WebMock's
  # NetConnectNotAllowedError descends from Exception so it propagates
  # through app rescue blocks by design — a test suite that blocks net
  # connections SHOULD fail loudly here, not silently fall back. CI
  # pre-provisions the binary outside that guard instead (real-apps.yml).
  begin
    download
  rescue StandardError => e
    raise unless File.executable?(destination)

    # Kernel.warn, not log: log() is silent unless LIGHTPANDA_DEBUG or
    # an explicit logger is set, and this fallback is exactly the
    # moment the user needs to hear about — a VCR-guarded suite (whose
    # UnhandledHTTPRequestError is a StandardError, unlike raw
    # WebMock's Exception) lands here silently, keeps a stale binary,
    # and later hits a confusing MINIMUM_NIGHTLY_BUILD floor error
    # with no trace of the blocked download.
    warn("[capybara-lightpanda] Binary download failed (#{e.class}: #{e.message}); " \
         "falling back to the cached binary at #{destination}. " \
         "If your suite stubs HTTP (VCR/WebMock), pre-provision from an " \
         "unstubbed process: #{PROVISION_HINT}")
    destination
  end
end

.update_hint(binary_path) ⇒ Object

Build a path-appropriate “how to update” command for Process’s too-old-binary error. Three branches:

  • Symlink into a ‘/Cellar/` directory → installed via Homebrew; suggest `brew update && brew upgrade lightpanda` (brew pins each user’s binary at install time and doesn’t refresh on its own when the tap publishes a newer nightly).

  • Path equals our own cache → suggest the require-the-gem one-liner. NOT the lightpanda:binary:* rake tasks: in a Rails app the gem usually sits in the :test Gemfile group, so the tasks only exist under RAILS_ENV=test (the Railtie can’t help a plain ‘bundle exec rake` in development), and outside Rails they’re never loaded at all. The one-liner requires the gem explicitly, so it works from any environment. The ‘remove` step is required because `update` honors `cache_time` and would otherwise no-op on a too-old-but-not-yet-expired file.

  • Anything else (user-managed install at a custom path) → keep the curl-overwrite suggestion, since we don’t know how the file got there.



180
181
182
183
184
185
186
187
188
189
# File 'lib/capybara/lightpanda/binary.rb', line 180

def update_hint(binary_path)
  if brew_managed?(binary_path)
    "brew update && brew upgrade lightpanda"
  elsif binary_path == install_path
    PROVISION_HINT
  else
    "curl -sL #{GITHUB_RELEASE_URL}/nightly/#{platform_binary} " \
      "-o #{binary_path} && chmod +x #{binary_path}"
  end
end