Skip to content

download_strategy: fix caching of :latest downloads #20137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 18, 2025

Conversation

EricFromCanada
Copy link
Member

@EricFromCanada EricFromCanada commented Jun 17, 2025

First, the cached_location_valid = false if v.is_a?(Cask::DSL::Version) && v.latest? line was never being run, because version is one of (NilClass, String, Version), not Cask::DSL::Version. It's intended to ensure that :latest casks are always re-downloaded on each upgrade, but with what we have now we can do better than that.

Further down is the cached location invalidation logic, which compares the cached file's modification date and file size against the results from a HEAD request on the download URL and invalidates if either are different. An earlier PR made this logic never run if the final download URL was a redirect. For anything with the version number in the URL this isn't usually a problem, but for resources behind a public URL that stays static and redirects to a new secondary URL on each update, they end up never being updated, e.g. :latest casks hosted on GitHub or GCloud or AWS.

Currently the above change doesn't appear to be necessary (although this may not have always been the case). For example, this is the redirect that occurs for downloading chromium:

$ curl -IL https://download-chromium.appspot.com/dl/Mac_Arm?type=snapshots
HTTP/2 302 
content-type: text/html; charset=utf-8
location: https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1475052/chrome-mac.zip
x-cloud-trace-context: 18003b12623c5b9f91c3034bb31cedc6
content-length: 383
date: Tue, 17 Jun 2025 17:49:24 GMT
server: Google Frontend
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

HTTP/2 200 
content-type: application/zip
x-guploader-uploadid: ABgVH88K3wiPIBKfU8U_vEO8Wqc-gPDIbdIK7aQtq4HTcXqpXjUSMZI-QTTbSPJQEP3FrCSC
expires: Tue, 17 Jun 2025 18:49:24 GMT
date: Tue, 17 Jun 2025 17:49:24 GMT
cache-control: public, max-age=3600
last-modified: Tue, 17 Jun 2025 17:35:45 GMT
etag: "956331c642e10e3be369a3a1c97a3eeb"
x-goog-generation: 1750181745929583
x-goog-metageneration: 1
x-goog-stored-content-encoding: identity
x-goog-stored-content-length: 147858426
x-goog-hash: crc32c=GqJrPQ==
x-goog-hash: md5=lWMxxkLhDjvjaaOhyXo+6w==
x-goog-storage-class: STANDARD
accept-ranges: bytes
content-length: 147858426
server: UploadServer
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Note how the first request has a small content-length value. With some odebug added to download_strategy.rb, we see that this isn't considered when evaluating the upstream file size.

$ brew fetch --cask -d chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/c/chromium.rb
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head --request GET --http1.1 https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
==> Downloading https://download-chromium.appspot.com/dl/Mac_Arm?type=snapshots
==>   upstream file_size: 147858426
==> Downloading from https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1475052/chrome-mac.zip
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --fail --progress-bar --retry 3 --remote-time --output /Users/eric/Library/Caches/Homebrew/downloads/9689e554ae2d938027c2d26f29176cdf362d24ff5f9f0bd338550e4a524e40f5--chrome-mac.zip.incomplete --location https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1475052/chrome-mac.zip
################################################################################################# 100.0%
...
Downloaded to: /Users/eric/Library/Caches/Homebrew/downloads/9689e554ae2d938027c2d26f29176cdf362d24ff5f9f0bd338550e4a524e40f5--chrome-mac.zip
SHA256: 2ebf334f3a8f8999e7d93c72f8afc7ba928f3e70d279bdcdeff07d852a29b134

$ brew fetch --cask -d chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading chromium
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/c/chromium.rb
==> Downloading https://download-chromium.appspot.com/dl/Mac_Arm?type=snapshots
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head --request GET --http1.1 https://download-chromium.appspot.com/dl/Mac_Arm\?type=snapshots
==>   upstream file_size: 147858426
==> cached_location.size: 147858426
Already downloaded: /Users/eric/Library/Caches/Homebrew/downloads/9689e554ae2d938027c2d26f29176cdf362d24ff5f9f0bd338550e4a524e40f5--chrome-mac.zip
...
SHA256: 2ebf334f3a8f8999e7d93c72f8afc7ba928f3e70d279bdcdeff07d852a29b134

With an older nightly build of wezterm@nightly cached, we currently get:

$ brew fetch --cask -d wezterm@nightly 
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading wezterm@nightly
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/w/[email protected]
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==> Downloading https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==>   upstream file_size: 112521600
==> cached_location.size: 112521859
Already downloaded: /Users/eric/Library/Caches/Homebrew/downloads/c57ec1344ecbf5ea80285241b68da2b5ba5475b74af75d8da93f1090e9badafa--WezTerm-macos-nightly.zip
...
SHA256: bbe95c7b8a95c525f4eb5c0d2e40df1b9f4e6802b1b388357c6e823896b2a0e3

After making these changes:

$ brew fetch --cask -d wezterm@nightly 
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading wezterm@nightly
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/w/[email protected]
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==> Downloading https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==>   upstream file_size: 112521600
==> cached_location.size: 112521859
==> Downloading from https://objects.githubusercontent.com/github-production-release-asset-2e65be/120568143/4ce0fc9b-b2a4-4207-a007-96a0d50bb67c?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=releaseassetproduc
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --fail --progress-bar --retry 3 --remote-time --output /Users/eric/Library/Caches/Homebrew/downloads/c57ec1344ecbf5ea80285241b68da2b5ba5475b74af75d8da93f1090e9badafa--WezTerm-macos-nightly.zip.incomplete --location https://objects.githubusercontent.com/github-production-release-asset-2e65be/120568143/4ce0fc9b-b2a4-4207-a007-96a0d50bb67c\?X-Amz-Algorithm=AWS4-HMAC-SHA256\&X-Amz-Credential=releaseassetproduction\%2F20250617\%2Fus-east-1\%2Fs3\%2Faws4_request\&X-Amz-Date=20250617T180837Z\&X-Amz-Expires=300\&X-Amz-Signature=803c8697ae843fef0310c92614e852fa79b5493eaace6b57b89689ba396c69f1\&X-Amz-SignedHeaders=host\&response-content-disposition=attachment\%3B\%20filename\%3DWezTerm-macos-nightly.zip\&response-content-type=application\%2Foctet-stream
################################################################################################# 100.0%
...
SHA256: b9f48ca00383f887579f26ccb64121ec8452f391a408f83e47d3824e69abef0f

$ brew fetch --cask -d wezterm@nightly 
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromNameLoader): loading wezterm@nightly
/opt/homebrew/Library/Homebrew/brew.rb (Cask::CaskLoader::FromPathLoader): loading /opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/w/[email protected]
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 -V
/usr/bin/env /opt/homebrew/Library/Homebrew/shims/shared/curl --disable --cookie /dev/null --globoff --show-error --user-agent Homebrew/4.5.7-27-g3c30845-dirty\ \(Macintosh\;\ arm64\ Mac\ OS\ X\ 14.7.4\)\ curl/8.7.1 --header Accept-Language:\ en --retry 3 --fail --location --silent --head https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==> Downloading https://github.com/wezterm/wezterm/releases/download/nightly/WezTerm-macos-nightly.zip
==>   upstream file_size: 112521600
==> cached_location.size: 112521600
Already downloaded: /Users/eric/Library/Caches/Homebrew/downloads/c57ec1344ecbf5ea80285241b68da2b5ba5475b74af75d8da93f1090e9badafa--WezTerm-macos-nightly.zip
...
SHA256: b9f48ca00383f887579f26ccb64121ec8452f391a408f83e47d3824e69abef0f

Fixes #20084. If there's still cases of cached locations being invalidated when they shouldn't be, please comment below. @boblail

@EricFromCanada EricFromCanada force-pushed the cached-location-validation branch from 305167e to 5ff58d2 Compare June 17, 2025 18:51
Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @EricFromCanada!

@MikeMcQuaid MikeMcQuaid added this pull request to the merge queue Jun 18, 2025
Merged via the queue into master with commit 76470c0 Jun 18, 2025
31 checks passed
@MikeMcQuaid MikeMcQuaid deleted the cached-location-validation branch June 18, 2025 07:47
@vvvvv
Copy link

vvvvv commented Jun 19, 2025

Appreciate the quick fix @EricFromCanada .

@boblail
Copy link
Contributor

boblail commented Jun 24, 2025

@EricFromCanada, this is actually a breaking change for my scenario.

We have a proxy (Cloudflare) in front of a private host for internal bottles. If the user's authentication has lapsed, the proxy will redirect to a login page that serves HTML whose Content-Length has nothing to do with the cached downloads.

To increase the determinism of our devenv, we seed Homebrew's downloads cache bottles we know engineers will need. We wouldn't want a redirect to Cloudflare's login page to invalidate this cache.

Notably, the login page does not serve a Content-Disposition header 🤔 We seem to expect that of the HEAD request but not the GET request. Would it make sense to disregard redirects that don't end in a response that includes content-disposition: attachment...?

I've made a change to our private tap that bypasses this logic, so I'm not blocked; but I wanted to describe a use-case where the cache invalidation logic might not quite be dialed in yet.

@EricFromCanada
Copy link
Member Author

Thanks for the background info. Seems logical; I'll attempt to cook up a test case.

@EricFromCanada
Copy link
Member Author

@boblail What's the (anonymized) output of curl -IL for the Cloudflare login page? It seems the presence of Content-Disposition isn't as reliable an indicator that an HTTP response is downloadable as the presence of Content-Type starting with "text" is that it's not downloadable.

@boblail
Copy link
Contributor

boblail commented Jun 30, 2025

@boblail What's the (anonymized) output of curl -IL for the Cloudflare login page? It seems the presence of Content-Disposition isn't as reliable an indicator that an HTTP response is downloadable as the presence of Content-Type starting with "text" is that it's not downloadable.

@EricFromCanada,

Here's what I ran

$ curl -IL https://{HOST}/{BOTTLE_PATH}

HTTP/2 302
date: Mon, 30 Jun 2025 18:24:13 GMT
content-type: text/html
content-length: 143
location: https://{TENANT}.cloudflareaccess.com/cdn-cgi/access/login/{HOST}?{ARGS}
set-cookie: {COOKIE}
access-control-allow-credentials: true
cache-control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
expires: Thu, 01 Jan 1970 00:00:01 GMT
set-cookie: {COOKIE}
server: cloudflare
cf-ray: 957fb3065beab9f8-SEA

HTTP/2 200
date: Mon, 30 Jun 2025 18:24:14 GMT
content-type: text/html
content-length: 30070
set-cookie: {COOKIE}
strict-transport-security: max-age=31536000; includeSubDomains
cf-access-domain: {HOST}
cf-version: 20-f198bbf
content-security-policy: frame-ancestors 'none'; connect-src 'self' http://127.0.0.1:*; default-src https: 'unsafe-inline'
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
server: cloudflare
cf-ray: 957fb306e969fa3b-SEA

@EricFromCanada
Copy link
Member Author

@boblail See if #20200 is a fix for your situation.

@boblail
Copy link
Contributor

boblail commented Jul 13, 2025

@EricFromCanada, yep! That fix works for my scenario 👍 Thanks!

I can remove the monkey patch I referenced earlier:

I've made a change to our private tap that bypasses this logic, so I'm not blocked...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Casks with :latest and :no-check do not update
4 participants