Pouring Bottle from Private Tap Fails

At my workplace I have created a private tap which is working fine except for bottles. I’m using brew install --build-bottle and brew bottle to create the bottle spec for the formula. I’ve checked the formula I’m currently working with using both audit and style and neither report any issues. Since I’m using the private repository to also host the bottles I’m using a custom root_url. FYI using the brew-test-bot is not an option for us.

The problem is that the program is not moved to the expected location under /usr/local/Cellar/<package>/<version>/bin/<tool>, instead a file named <root_url host> is put in the Cellar root and pouring the bottle fails with Error: /usr/local/Cellar/<tool>/1.0.0 is not a directory.

Below is an example of the situation. Homebrew tries to install the bottle and falls back to building from source, which works just fine. If I uninstall and make the expected tool directory under the Cellar, pouring the bottle completes without error. However, there is no bin directory created and the tool is left as /usr/local/Cellar/gitlab.example.com.

$ brew install <tool>
Updating Homebrew...
==> Installing <tool> from <org>/tools
==> Downloading https://gitlab.example.com/<org>/<tool>/uploads/abf92f20447a33f126859c0ee54a7ef0/<tool>-1.0.0.mojave.bottle.tar.gz
######################################################################## 100.0%
==> Pouring gitlab.example.com
Error: /usr/local/Cellar/<tool>/1.0.0 is not a directory
Warning: Bottle installation failed: building from source.
==> Downloading https://gitlab.example.com/<org>/<tool>/-/archive/v1.0.0/<tool>-v1.0.0.tar.gz
######################################################################## 100.0%
==> ./build.sh
🍺  /usr/local/Cellar/<tool>/1.0.0: 4 files, 16.4MB, built in 13 seconds
$ brew rm <tool>
Uninstalling /usr/local/Cellar/<tool>/1.0.0... (4 files, 16.4MB)
$ mkdir -p /usr/local/Cellar/<tool>/1.0.0
$ brew install <tool>
==> Installing <tool> from <org>/tools
==> Downloading https://gitlab.example.com/<org>/<tool>/uploads/abf92f20447a33f126859c0ee54a7ef0/<tool>-1.0.0.mojave.bottle.tar.gz
Already downloaded: /Users/<user>/Library/Caches/Homebrew/downloads/32c45eadf01aa53bb3ee3d8eb4fa83b816b36e9fdb49f35e12fabe8e22721622--gitlab.example.com
==> Pouring gitlab.example.com
🍺  /usr/local/Cellar/<tool>/1.0.0: 630B
$ ls -l /usr/local/Cellar/<tool>/1.0.0/
total 8
-rw-r--r--  1 user  admin  534 Oct 24 17:17 INSTALL_RECEIPT.json
$ ls -l /usr/local/Cellar/gitlab.example.com
-rw-r--r--  1 user  staff  8126070 Oct 24 15:25 /usr/local/Cellar/gitlab.example.com

Running brew install --verbose --debug does not offer any additional insight. Only this additional output is emitted:

==> Pouring gitlab.example.com
cp -p /Users/<user>/Library/Caches/Homebrew/downloads/32c45eadf01aa53bb3ee3d8eb4fa83b816b36e9fdb49f35e12fabe8e22721622--gitlab.example.com /usr/local/Cellar/gitlab.example.com
Error: /usr/local/Cellar/<tool>/1.0.0 is not a directory
Warning: Bottle installation failed: building from source.

This smells like a corner case where some internal variable values are unset but I’m not familiar enough to know where to look. What am I missing in order to make this work?

Output from brew doctor:

$ brew doctor
Your system is ready to brew.

Are you uploading unmodified the brew bottle output files? Does brew install $BREW_BOTTLE_OUTPUT_FILE_ON_LOCAL_DISK work? What does your bottle do block look like?

Are you uploading unmodified the brew bottle output files?

Yes, I upload them exactly as they are written, except for renaming from <tool>--1.0.0.mojave.bottle.tar.gz to <tool>-1.0.0.mojave.bottle.tar.gz. I’ve noticed that others have mentioned the same naming issue on their blogs.

Does brew install $BREW_BOTTLE_OUTPUT_FILE_ON_LOCAL_DISK work?

Yes:

$ brew install <tool>--1.0.0.mojave.bottle.tar.gz
Updating Homebrew...
==> Auto-updated Homebrew!
Updated Homebrew from e6cfb4a3c to 06ed951f5.
Updated 2 taps (homebrew/core and homebrew/cask).
==> Updated Formulae
check ✔               arp-scan              certbot               dvc                   neko                  python@2
abcmidi               aws-sdk-cpp           checkbashisms         dxpy                  pango                 swiftlint
ansible@1.9           calcurse              confluent-platform    exploitdb             php@7.1               teleport
ansible@2.0           ccache                cutter                jetty                 php@7.2

Warning: Unreadable formula in /Users/<user>/<tool>--1.0.0.mojave.bottle.tar.gz:
<tool>: cannot load such file -- /Users/lib/strategy.rb
Warning: Unreadable formula in /Users/<user>/<tool>--1.0.0.mojave.bottle.tar.gz:
<tool>: cannot load such file -- /Users/lib/strategy.rb
==> Installing <tool> from <org>/tools
🍺  /usr/local/Cellar/<tool>/1.0.0: 5 files, 16.4MB
$ ls -lR /usr/local/Cellar/<tool>/1.0.0/
total 16
-rw-r--r--  1 user  staff   565 Oct 25 10:53 INSTALL_RECEIPT.json
-rw-r--r--  1 user  staff  2638 Oct 23 16:46 README.md
drwxr-xr-x  3 user  staff    96 Oct 23 16:46 bin

/usr/local/Cellar/<tool>/1.0.0//bin:
total 33656
-r-xr-xr-x  1 user  staff  17230428 Oct 23 16:46 <tool>

It does issue warnings about the source file for my custom upload strategy, but otherwise works correctly. I can install directly using the bottle file either from the original produced by brew bottle or from the one I download by hand from the repository.

What does your bottle do block look like?

As follows:

  bottle do
    root_url "https://gitlab.example.com/<org>/<tool>/uploads/717aa5528a826c0bf280890260aa30e8", :using => PrivateRepositoryDownloadStrategy
    cellar :any_skip_relocation
    sha256 "cf271a935ac773a3b0d211a2dddad712ae64dbb8ce40a32b434ebe313ffc6657" => :mojave
  end

The root URL refers to the attachment to the tag I made for this version. Homebrew is able to download and verify the bottle files just fine AFAICT. Something is going awry in handling the bottle after it is fetched.

Here is the custom strategy if that is helpful:

require "download_strategy"

class PrivateRepositoryDownloadStrategy < CurlDownloadStrategy
  def initialize(url, name, version, **meta)
    super
    set_gitlab_token
  end

  private

  def _fetch(url:, resolved_url:)
    args = ["--header", "Private-Token: #{@gitlab_token}"]
    curl_download(@url, *args, to: temporary_path)
  end

  def set_gitlab_token
    @gitlab_token = ENV["HOMEBREW_GITLAB_ACCESS_TKN"]
    unless @github_token
      raise CurlDownloadStrategyError, "Environment variable HOMEBREW_GITLAB_ACCESS_TKN is required."
    end
  end
end

This seems likely as to why this isn’t working. That warning means the internal formula cannot be read which will prevent bottle installation.

Additionally, the download strategy ensures that the files are in the right place/format to be unpacked. It’s likely your download strategy is why you’re getting a failure on bottle installation.

You can find more debugging information with brew --debug install ...

1 Like

Could you elaborate on this? It’s my understanding that the bottle will not contain anything of the formula, only the final compiled files organized by their target location. My formula comes from a tap, which was read to kick off the bottle download to begin with so it stands to reason that the formula is readable. Similarly, the custom download strategy is used for both the source archive and the bottle URLs so that file is also readable.

I would think that there would be an error instead of a warning if the problem was fatal.

I could use some assistance in understanding what might be wrong with the download strategy. I’ve posted it upthread, if there’s anything wrong you can point to I would be very appreciative.

In my original message I mention that I ran brew install --verbose --debug but the warnings don’t appear, I am not dumped into the debugger and there is no output specific to the error line. The full curl commands emitted look fine and contain the additional header I wanted to inject. As you can see from the download strategy code above the bottle is written to temporary_path. Is there somewhere else that bottles should be staged after download?

I’m still at a loss as to where to investigate next.

Can you make a public tap that has a fully reproducible example of the failure? We can speculate about what might be wrong, but it’s hard without having something more concrete.

Check the .bottle and .brew subdirectories of the kegs of extracted bottles. .brew will contain the formula file.

A general programming/software principle that’s wise to adopt: if things aren’t working as expected then check out any output warnings.

Sorry, I’m not willing to debug code that exists to consume non-open source software in my free time.

Odd. You may also want to try export HOMEBREW_DEVELOPER=1. If that fails: you’ll have to edit Homebrew’s code to try and figure it out.

That error means “the bottle extraction hasn’t installed anything” which normally means “it was extracted to the wrong place”. Hope that helps.

Thank you @kevinbirch for you snippet. It helped me a lot, getting the idea.
You have a small copy paster: it should be unless @gitlab_token and not unless @github_token.

When I run brew install mytap/mytool --debug I can see that the custom --header is not used but only the default options from the CurlDownloadStrategy.

Any idea how I can hook into those curl args to pass in the new custom header? I’d love to get this working to be able to host a brew tab in my private gitlab, but I’m too new to ruby

Hi, I fixed it as described here: https://stackoverflow.com/questions/61841275/homebrew-extend-curldownloadstrategy-with-curl-option