Class: Capybara::Lightpanda::Binary
- Inherits:
-
Object
- Object
- Capybara::Lightpanda::Binary
- Defined in:
- lib/capybara/lightpanda/binary.rb
Constant Summary collapse
- GITHUB_RELEASE_URL =
"https://github.com/lightpanda-io/browser/releases/download"- PLATFORMS =
Upstream publishes all four arch/OS combinations on every channel — verified 2026-07-26 against the
nightlytag and release 0.3.6 (the MINIMUM_RELEASE floor, so any acceptable pin carries them too). Intel macOS and arm64 Linux were absent here and raised UnsupportedPlatformError on machines upstream ships a binary for: Intel MacBooks, and Graviton / arm64 CI runners. normalize_arch folds arm64 -> aarch64, so the arm64 rows are unreachable defensive duplicates kept for symmetry with the pre-existing arm64-darwin one. { %w[x86_64 linux] => "lightpanda-x86_64-linux", %w[aarch64 linux] => "lightpanda-aarch64-linux", %w[arm64 linux] => "lightpanda-aarch64-linux", %w[x86_64 darwin] => "lightpanda-x86_64-macos", %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
- .cache_time ⇒ Object
- .install_dir ⇒ Object
- .logger ⇒ Object
-
.proxy_addr ⇒ Object
Returns the value of attribute proxy_addr.
-
.proxy_pass ⇒ Object
Returns the value of attribute proxy_pass.
-
.proxy_port ⇒ Object
Returns the value of attribute proxy_port.
-
.proxy_user ⇒ Object
Returns the value of attribute proxy_user.
-
.required_version ⇒ Object
Set a specific release tag (e.g. "0.3.0") to pin downloads to that release.
Class Method Summary collapse
- .configure {|_self| ... } ⇒ Object
-
.current_version ⇒ Object
Returns the
lightpanda versionoutput of the cached binary, or nil if the binary isn't present / not runnable. - .default_binary_path ⇒ Object
-
.download ⇒ Object
Downloads into a sibling temp file and renames it into place only once the transfer finished.
-
.install_path ⇒ Object
Path the gem writes the downloaded binary to.
- .platform_binary ⇒ Object
-
.remove ⇒ Object
Delete the cached binary.
-
.update ⇒ Object
Canonical entrypoint: ensure the binary at install_path is current, download if needed, return its path.
-
.update_hint(binary_path) ⇒ Object
Build a path-appropriate "how to update" command for Process's too-old-binary error.
Class Attribute Details
.cache_time ⇒ Object
52 53 54 |
# File 'lib/capybara/lightpanda/binary.rb', line 52 def cache_time @cache_time ||= Integer(ENV.fetch("LIGHTPANDA_CACHE_TIME", DEFAULT_CACHE_TIME)) end |
.install_dir ⇒ Object
56 57 58 |
# File 'lib/capybara/lightpanda/binary.rb', line 56 def install_dir @install_dir ||= File.dirname(default_binary_path) end |
.logger ⇒ Object
60 61 62 63 64 65 |
# File 'lib/capybara/lightpanda/binary.rb', line 60 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_addr ⇒ Object
Returns the value of attribute proxy_addr.
50 51 52 |
# File 'lib/capybara/lightpanda/binary.rb', line 50 def proxy_addr @proxy_addr end |
.proxy_pass ⇒ Object
Returns the value of attribute proxy_pass.
50 51 52 |
# File 'lib/capybara/lightpanda/binary.rb', line 50 def proxy_pass @proxy_pass end |
.proxy_port ⇒ Object
Returns the value of attribute proxy_port.
50 51 52 |
# File 'lib/capybara/lightpanda/binary.rb', line 50 def proxy_port @proxy_port end |
.proxy_user ⇒ Object
Returns the value of attribute proxy_user.
50 51 52 |
# File 'lib/capybara/lightpanda/binary.rb', line 50 def proxy_user @proxy_user end |
.required_version ⇒ Object
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.
45 46 47 |
# File 'lib/capybara/lightpanda/binary.rb', line 45 def required_version @required_version end |
Class Method Details
.configure {|_self| ... } ⇒ Object
67 68 69 |
# File 'lib/capybara/lightpanda/binary.rb', line 67 def configure yield self end |
.current_version ⇒ Object
Returns the lightpanda version output of the cached binary, or nil
if the binary isn't present / not runnable.
147 148 149 150 151 152 153 154 155 |
# File 'lib/capybara/lightpanda/binary.rb', line 147 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_path ⇒ Object
245 246 247 248 249 |
# File 'lib/capybara/lightpanda/binary.rb', line 245 def default_binary_path cache_dir = ENV.fetch("XDG_CACHE_HOME") { File.("~/.cache") } File.join(cache_dir, "lightpanda", "lightpanda") end |
.download ⇒ Object
Downloads into a sibling temp file and renames it into place only once the transfer finished.
Writing straight to destination corrupted a working binary on any
interrupted transfer: File.open(_, "wb") truncates the existing file
the moment the request starts, and truncation KEEPS the mode bits —
so a dropped connection left a partial file that still answered
File.executable? => true. #update's "fall back to the cached binary"
rescue then handed that corpse back as if it were the usable stale
binary, warning as though nothing was wrong. That is the common path,
not an edge case: past cache_time, #update calls #download precisely
when a good binary is already sitting at destination.
rename(2) within one directory is atomic, so a concurrent reader sees either the old binary or the new one, never a half-written one. The temp file is a sibling (not Dir.tmpdir) so the rename never crosses a filesystem, and chmod happens before it so the binary is never visible non-executable. Two racing downloads get distinct temp names and both rename a complete file — last writer wins, both are valid.
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/capybara/lightpanda/binary.rb', line 176 def download binary_name = platform_binary tag = required_version || "nightly" url = "#{release_url}/#{tag}/#{binary_name}" destination = install_path log("Downloading #{binary_name} (#{tag}) → #{destination}") FileUtils.mkdir_p(File.dirname(destination)) # ::Process, not Process — Capybara::Lightpanda::Process would win. temp = "#{destination}.download-#{::Process.pid}" begin download_file(url, temp) FileUtils.chmod(0o755, temp) File.rename(temp, destination) ensure # No-op on success (rename consumed it). ensure, not rescue, so an # Interrupt mid-download cleans up too; rm_f ignores a missing file. FileUtils.rm_f(temp) end destination end |
.install_path ⇒ Object
Path the gem writes the downloaded binary to. Honors a user-configured install_dir; otherwise falls back to default_binary_path.
A pin gets its own filename (lightpanda-0.3.5) rather than sharing
the rolling-nightly one. update only checks that a file EXISTS at
this path, not which version it holds, so on a shared path a nightly
left over from an earlier run would be accepted as "the pin" — setting
required_version on any machine with a warm cache (every CI runner
restoring a cache, every existing dev checkout) would silently keep
running the nightly it was meant to replace. Version-scoping makes the
pin self-verifying and lets several pins coexist in one cache dir.
262 263 264 265 266 267 |
# File 'lib/capybara/lightpanda/binary.rb', line 262 def install_path dir = @install_dir || File.dirname(default_binary_path) basename = required_version ? "lightpanda-#{required_version}" : "lightpanda" File.join(dir, basename) end |
.platform_binary ⇒ Object
238 239 240 241 242 243 |
# File 'lib/capybara/lightpanda/binary.rb', line 238 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 |
.remove ⇒ Object
Delete the cached binary. Returns the path that was deleted, or nil if nothing was there.
133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/capybara/lightpanda/binary.rb', line 133 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 |
.update ⇒ Object
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.
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 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/capybara/lightpanda/binary.rb', line 78 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.}); " \ "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; suggestbrew 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 rakein development), and outside Rails they're never loaded at all. The one-liner requires the gem explicitly, so it works from any environment. Theremovestep is required becauseupdatehonorscache_timeand would otherwise no-op on a too-old-but-not-yet-expired file. - Path equals our cache AND a pin is set → the version is a deliberate choice, so tell the user to raise the pin. The re-provision one-liner would be a dead end here: it re-downloads the same pinned release and lands on the identical "too old" error.
- Anything else (user-managed install at a custom path) → keep the curl-overwrite suggestion, since we don't know how the file got there.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/capybara/lightpanda/binary.rb', line 223 def update_hint(binary_path) if brew_managed?(binary_path) "brew update && brew upgrade lightpanda" elsif binary_path == install_path && required_version "Capybara::Lightpanda::Binary.required_version is pinned to " \ "#{required_version} — raise the pin to a newer release, " \ "or unset it to track the rolling nightly." 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 |