Class: Capybara::Lightpanda::Binary

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

Defined Under Namespace

Classes: Result

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



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

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

.install_dirObject



59
60
61
# File 'lib/capybara/lightpanda/binary.rb', line 59

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

.loggerObject



63
64
65
66
67
68
# File 'lib/capybara/lightpanda/binary.rb', line 63

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.



53
54
55
# File 'lib/capybara/lightpanda/binary.rb', line 53

def proxy_addr
  @proxy_addr
end

.proxy_passObject

Returns the value of attribute proxy_pass.



53
54
55
# File 'lib/capybara/lightpanda/binary.rb', line 53

def proxy_pass
  @proxy_pass
end

.proxy_portObject

Returns the value of attribute proxy_port.



53
54
55
# File 'lib/capybara/lightpanda/binary.rb', line 53

def proxy_port
  @proxy_port
end

.proxy_userObject

Returns the value of attribute proxy_user.



53
54
55
# File 'lib/capybara/lightpanda/binary.rb', line 53

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.



48
49
50
# File 'lib/capybara/lightpanda/binary.rb', line 48

def required_version
  @required_version
end

Class Method Details

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

Yields:

  • (_self)

Yield Parameters:



70
71
72
# File 'lib/capybara/lightpanda/binary.rb', line 70

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.



155
156
157
158
159
160
161
162
163
# File 'lib/capybara/lightpanda/binary.rb', line 155

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



242
243
244
245
246
# File 'lib/capybara/lightpanda/binary.rb', line 242

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

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

.downloadObject



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/capybara/lightpanda/binary.rb', line 189

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)
  @path = destination

  destination
end

.execObject



173
174
175
# File 'lib/capybara/lightpanda/binary.rb', line 173

def exec(*)
  Kernel.exec(path, *)
end

.fetch(url) ⇒ Object

Raises:



177
178
179
180
181
182
# File 'lib/capybara/lightpanda/binary.rb', line 177

def fetch(url)
  result = run("fetch", "--dump", url)
  raise BinaryError, result.stderr unless result.success?

  result.stdout
end

.install_pathObject

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



250
251
252
253
254
255
256
# File 'lib/capybara/lightpanda/binary.rb', line 250

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

.pathObject



74
75
76
# File 'lib/capybara/lightpanda/binary.rb', line 74

def path
  @path ||= update
end

.platform_binaryObject



235
236
237
238
239
240
# File 'lib/capybara/lightpanda/binary.rb', line 235

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.



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/capybara/lightpanda/binary.rb', line 140

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

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

.runObject



165
166
167
168
169
170
171
# File 'lib/capybara/lightpanda/binary.rb', line 165

def run(*)
  stdout, stderr, status = Open3.capture3(path, *)

  Result.new(stdout: stdout, stderr: stderr, status: status)
rescue Errno::ENOENT
  raise BinaryNotFoundError, "Lightpanda binary not found"
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.



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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/capybara/lightpanda/binary.rb', line 85

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.



224
225
226
227
228
229
230
231
232
233
# File 'lib/capybara/lightpanda/binary.rb', line 224

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

.versionObject



184
185
186
187
# File 'lib/capybara/lightpanda/binary.rb', line 184

def version
  result = run("version")
  result.output.strip
end