Homebrew / brew

🍺 The missing package manager for macOS (or Linux)
https://brew.sh
BSD 2-Clause "Simplified" License
40.81k stars 9.58k forks source link

`brew deps` output shows inconsistent dependency information between tree and non-tree output #16032

Open lorentzforces opened 11 months ago

lorentzforces commented 11 months ago

brew doctor output

Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!

Warning: Putting non-prefixed coreutils in your path can cause GMP builds to fail.

Warning: Homebrew's "sbin" was not found in your PATH but you have installed
formulae that put executables in /opt/homebrew/sbin.
Consider setting your PATH for example like so:
  echo 'export PATH="/opt/homebrew/sbin:$PATH"' >> /Users/51195/.bash_profile

Verification

brew config output

HOMEBREW_VERSION: 4.1.13
ORIGIN: https://github.com/Homebrew/brew
HEAD: a8519f78fb63f2f2266950bdd8141037da69f8bd
Last commit: 6 hours ago
Core tap JSON: 25 Sep 18:03 UTC
HOMEBREW_PREFIX: /opt/homebrew
HOMEBREW_CASK_OPTS: []
HOMEBREW_EDITOR: nvim
HOMEBREW_MAKE_JOBS: 10
Homebrew Ruby: 2.6.10 => /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby
CPU: 10-core 64-bit arm_firestorm_icestorm
Clang: 14.0.3 build 1403
Git: 2.42.0 => /opt/homebrew/bin/git
Curl: 8.1.2 => /usr/bin/curl
macOS: 13.5.2-arm64
CLT: 14.3.1.0.1.1683849156
Xcode: N/A
Rosetta 2: false

What were you trying to do (and why)?

When checking to see which of my installed homebrew packages had updates, I noticed that I had both OpenSSL@1.1 and OpenSSL@3 installed. I didn't realize I had two versions, and I wanted to know which packages depended on each respective version, if any. I have found a few unused packages recently in my Homebrew installation, and wanted to clean up packages that weren't being used.

I ran brew deps --tree --installed to look at all dependencies, and searched for "openssl@1.1" in that output.

What happened (include all command output)?

On initial check, I did not see "openssl@1.1" listed anywhere in the command's output.

I narrowed this down and found an inconsistency in brew deps output as follows.

Output of brew deps --installed tmux:

ca-certificates
libevent
ncurses
openssl@1.1
utf8proc

Output of brew deps --tree --installed tmux:

tmux
β”œβ”€β”€ libevent
β”‚   └── openssl@3
β”‚       └── ca-certificates
β”œβ”€β”€ ncurses
└── utf8proc

Note the different versions of openssl reported between these two commands.

What did you expect to happen?

I expect the same specific version of a package to be reported as a dependency regardless of which command or command arguments are used to view dependencies.

Step-by-step reproduction instructions (by running brew commands)

brew install tmux tmuxp
brew deps --installed tmux
brew deps --tree --installed tmux
lorentzforces commented 11 months ago

Created as a homebrew issue and not a formula issue as this behavior seems more likely to be on the homebrew side.

apainintheneck commented 11 months ago

I haven't looked into this but I can say that we have different dependency resolution code for the normal and tree versions of the brew deps command. For that reason I'm not completely surprised that they might return different results. Definitely seems like a bug though on our side.

ZhongRuoyu commented 11 months ago

I think this is similar to #13717 (see also https://github.com/orgs/Homebrew/discussions/4784) -- there is a discrepancy between dependency information from the tab (i.e., install receipt) and that from the formulae which can be updated after the installation. In this case, tmux was likely installed before libevent was switched from openssl@1.1 to openssl@3. When libevent was upgraded, its updated dependency info was not reflected in tmux's install receipt.

apainintheneck commented 11 months ago

@ZhongRuoyu Yep, you're right. That's exactly what's happening here. We end up calling Formula#runtime_dependencies internally and that defaults to the tab if there's a keg already installed. We could easily change that to always skip the tab in these situations but I'm not sure if that'd be less confusing.

https://github.com/Homebrew/brew/blob/f1d345a60e2c02f7e5c1b7930601b1921432cfb6/Library/Homebrew/formula.rb#L2120-L2122

MikeMcQuaid commented 11 months ago

We could easily change that to always skip the tab in these situations but I'm not sure if that'd be less confusing.

I think it should always use rather than always skip the tab but: yes, I agree it would be much better (and would fix this bug) to be consistent here.

apainintheneck commented 11 months ago

I'm hesitant to change this since it's one of the few places where we get a glimpse of what's happening in the tab and for that reason it's occasionally useful for debugging. Ideally we'd store more up-to-date information in the tab but that's a very difficult problem as described in the original issue and I assume that's why nobody picked it up at the time.

I'm not sure if users expect this command to reflect installed versions or latest versions of packages in the event that an installed package is on an older version (through pinning or not updating it yet). Right now it seems to default to the installed version which seems reasonable.

apainintheneck commented 11 months ago

It does seem like brew deps --tree is more accurate in this case because it manually checks the direct dependencies at each level of the tree instead of using the recursive dependencies found in the tab.

lorentzforces commented 11 months ago

My own expectations as a user:

It makes sense to me that for certain purposes, especially debugging, that you might want to know exactly what package and version were installed via tracking a receipt for a given install operation. That makes total sense. What doesn't make sense to me is that you would show this type of information as the default for any command, especially for the baseline command I would reach for if I was looking for dependency information.

Would it be unreasonable to add a separate, different option to show information from the "install receipt / tab," and show the current dependency information (same as the --tree option) if the command is invoked without that option present? Something like --from-receipt or just straight up --read-from-tab (to match that function parameter)?

MikeMcQuaid commented 11 months ago

What doesn't make sense to me is that you would show this type of information as the default for any command, especially for the baseline command I would reach for if I was looking for dependency information.

It didn't use to be the default and people complained that it didn't match what's happening on their machine. There's not a single default that everyone will agree on and not find confusing.

Would it be unreasonable to add a separate, different option to show information from the "install receipt / tab,"

We could/should add an option for the opposite: to discard the information from your tap and use only what the formula says.

apainintheneck commented 11 months ago

@lorentzforces Just to be sure would you be willing to share the install receipt for this package? Something like this should work.

cat $(brew --cellar tmux)/*/INSTALL_RECEIPT.json

I also wonder what the output of this is?

brew deps tmux

The command you showed brew deps --installed tmux doesn't even try to evaluate the dependency tree but just directly returns what's in the tab with the #runtime_dependencies.

https://github.com/Homebrew/brew/blob/5ec560a4ba0111d9fe223ce40b3db01c183fe1d0/Library/Homebrew/cmd/deps.rb#L200-L203

I'm also inclined to open a new issue for the outdated install receipt since it seems like it's still causing unexpected behavior with multiple commands.

lorentzforces commented 11 months ago

@apainintheneck sure!

Output of cat $(brew --cellar tmux)/*/INSTALL_RECEIPT.json

{
  "homebrew_version": "4.0.11-95-gd15f571",
  "used_options": [

  ],
  "unused_options": [

  ],
  "built_as_bottle": true,
  "poured_from_bottle": true,
  "loaded_from_api": true,
  "installed_as_dependency": false,
  "installed_on_request": true,
  "changed_files": [
    "share/man/man1/tmux.1"
  ],
  "time": 1680893890,
  "source_modified_time": 1654774350,
  "compiler": "clang",
  "aliases": [

  ],
  "runtime_dependencies": [
    {
      "full_name": "ca-certificates",
      "version": "2023-01-10",
      "declared_directly": false
    },
    {
      "full_name": "openssl@1.1",
      "version": "1.1.1t",
      "declared_directly": false
    },
    {
      "full_name": "libevent",
      "version": "2.1.12",
      "declared_directly": true
    },
    {
      "full_name": "ncurses",
      "version": "6.4",
      "declared_directly": true
    },
    {
      "full_name": "utf8proc",
      "version": "2.8.0",
      "declared_directly": true
    }
  ],
  "source": {
    "spec": "stable",
    "versions": {
      "stable": "3.3a",
      "head": null,
      "version_scheme": 0
    },
    "path": "/opt/homebrew/Library/Taps/homebrew/homebrew-core/Formula/tmux.rb",
    "tap_git_head": null,
    "tap": "homebrew/core"
  },
  "arch": "arm64",
  "built_on": {
    "os": "Macintosh",
    "os_version": "macOS 13",
    "cpu_family": "dunno",
    "xcode": "14.2",
    "clt": "14.2.0.0.1.1668646533",
    "preferred_perl": "5.30"
  }
}

Output of brew deps tmux

ca-certificates
libevent
ncurses
openssl@1.1
utf8proc

It didn't use to be the default and people complained that it didn't match what's happening on their machine.

@MikeMcQuaid this seems like an odd statement to me; I have a pretty big conceptual gulf between "what is happening on the machine" and "what operations were historically performed on the machine," and to me that first one is much more important in my day-to-day. (I'm also defining "what is happening on the machine" as "what gets run when I run this program," not "what did homebrew do," which may also be different from "typical" usage)

That might be naivete on my part - I'm not someone who administrates machines or deployments, just someone who uses Homebrew to install programs for my dev environment on MacOS or Linuxbrew to keep packages outside my standard package manager up to date easily.

We could/should add an option for the opposite: to discard the information from your tap and use only what the formula says.

This is absolutely reasonable, and I'm 100% happy with such an option. As long as I can keep a specific option in my head that will do what I expect/want every time, that solves all my problems.

apainintheneck commented 11 months ago

Thanks for the debugging info!

After looking at things again, brew deps tmux is equivalent to brew deps --installed tmux internally if tmux is already installed so I shouldn't be surprised that the results are the same. If any of the other options are passed to the command, the results should be correct. These are the only two versions of the command that read all recursive dependencies from the tab. The other ones only read direct dependencies and recurse down the tree to get all of them.

For example, brew deps --skip-recommended tmux should return the results you're looking for. Of course, that's a workaround (we don't use recommended dependencies in core) but at least it points to the recursive dependency algorithms as being correct. Adding another option here is fine for me too though we'd ideally fix this at some point.

Bo98 commented 11 months ago

For example, brew deps --skip-recommended tmux should return the results you're looking for.

yeah that's exactly the hack I have used numerous times myself.

Bo98 commented 11 months ago

I'm also inclined to open a new issue for the outdated install receipt since it seems like it's still causing unexpected behavior with multiple commands.

We have this issue every time there's a OpenSSL migration.

Ignoring the brew deps for a moment (where there's probably room for an extra flag somewhere) and focussing on the tab specifically: by the letter of how it is designed to work, it is correct. That's not to say it's necessarily 100% ideal. The tab is fairly eager with dependencies, mostly because it's tricky to do anything else safely. We collect recursive dependencies but don't really properly know what's actually a runtime dependency (linkage is fairly safe bet for some things, but not everything). But when we do OpenSSL migrations, we only revision bump things that actually have linkage to OpenSSL (usually direct dependencies plus anything recursive that's flagged in CI). In this case tmux did not need revision bumping, but the tab will think OpenSSL is a runtime dependency because it doesn't know otherwise.

But yes it does cause issues when brew uninstall openssl@1.1 is blocked when in reality it's safe (except for the very few formulae left that directly depend on it), meaning people are told to keep something we've phased out as EOL.

(Though yeah, this is treading towards a separate issue rather than brew deps specifically)

MikeMcQuaid commented 11 months ago

This is absolutely reasonable, and I'm 100% happy with such an option. As long as I can keep a specific option in my head that will do what I expect/want every time, that solves all my problems.

Better still may be to make this both an option an a documented environment variable so you can set it once and always do what you want.


Another thing worth noted about preferring/deciding between using the tab and recalculating the dependencies: the prior is way quicker.

EricFromCanada commented 11 months ago

There was recently added a paragraph to the command's docs addressing this:

If any version of each formula argument is installed and no other options are passed, this command displays their actual runtime dependencies (similar to brew linkage), which may differ from the current versons’ stated dependencies if the installed versions are outdated.

zenspider commented 11 months ago

I'm debugging something and I'm not sure it is related or not (happy to file a separate issue). What I'm seeing is severe differences between brew missing, brew deps --tree X, and what I'm calculating the missing dependencies to be... Here's a simple example where the tab(?) seems stale despite the brew being up to date:

# confirmed HOMEBREW_NO_INSTALL_FROM_API=1 makes no difference to this output:

# git-imerge is up to date
brew outdated | grep git-imerge 
# => nothing

# the version I currently have:
brew list --versions git-imerge
# => git-imerge 1.2.0_1

# git-imerge is missing python@3.10 ?
brew missing | grep git-imerge
# => git-imerge: python@3.10

# why? because that's what it says here:
cat $(brew --cellar git-imerge)/*/.brew/git-imerge.rb | grep depend
# => depends_on "python@3.10"

# and here:
cat $(brew --cellar git-imerge)/*/INSTALL_RECEIPT.json | jq ".runtime_dependencies.[] | select(.declared_directly) | .full_name"
# => "python@3.10"

# but not here?!?! isn't it up to date?
brew cat git-imerge | grep depend
# => depends_on "python@3.12"

It seems to me that this is hinting at stale install info possibly causing some of the pain for OP.

Please feel free to tell me this is unrelated and to file a separate issue.

HOMEBREW_VERSION: 4.1.14-60-g5349b76-dirty

(dirty is unrelated... I think I found a bug in Homebrew/env_config.rb on default value for HOMEBREW_LIVECHECK_WATCHLIST)

trinitronx commented 1 month ago

I'm debugging something and I'm not sure it is related or not (happy to file a separate issue). What I'm seeing is severe differences between brew missing, brew deps --tree X, and what I'm calculating the missing dependencies to be...

For what it's worth, other related Homebrew commands such as brew uses --recursive --installed <formula> and brew deps --tree <formula> still show discrepancies as well in some rather common cases.

πŸ“œπŸ§ To refer back to the historic records, it seems like something similar to this was discussed in Homebrew/legacy-homebrew#50068. In that case, it had a lot to do with what they call a "requirement" versus a generic package name-based "dependency". The example given was with an old version of Python, which is considered a "requirement" as in depends_on :python (as opposed to: _depends_on "python"_). The string "python" as package name is a normal dependency, meanwhile the Ruby Symbol :python is a "requirement" and was a shortcut for depends_on PythonRequirement.

This seems related to your example, because it mentions python@3.10 as a "dependency" if we assume output of brew deps's "deps" resolves to (or conflates to in the user's mind?) "dependency", but maybe it could have been pulled in as a "requirement" (in Homebrew terms) when first installed? 🀷 I'm not 100% sure, but I suspect that if that were the case, the requirement resolved to python@3.10 when installed. If it never was specified as a requirement in the Formula Ruby code, as it seems was the case, then it was probably simply a "dependency" on that specific Python version. We do see evidence in the INSTALL_RECEIPT.json that it contains the python@3.10 version. We also see that the backed-up Formula's Ruby code (.brew/git-imerge.rb), if we can trust this, has depends_on "python@3.10", which would make it a "dependency" on the package name: "python@3.10". In this case, if we can trust the INSTALL_RECEIPT.json and the .brew/*.rb formulae, then we must deduce that the old version of the formula specified a dependency on python@3.10 and it was later updated to python@3.12 in the Formula's Ruby code. πŸ”πŸ•΅οΈβ€β™‚οΈ

Meanwhile, the updated Ruby code Formula git-imerge.rb has depends_on "python@3.12", which appears to be a "dependency" on that specific version of python (3.12). I'm not sure which would have been better (e.g. "requirement" or "dependency") given that Homebrew's dependency reporting commands (brew uses --recursive X, brew deps --tree X) seem to be commonly misleading people in these types of situations. In the case of Homebrew/legacy-homebrew#50068, it was a "requirement" and brew uses --recursive X misleading the user. In your case, it was likely a "dependency" and brew deps --tree X misleading you (as a user).

This particular discrepancy when reporting dependencies is still manifesting in current Homebrew. Given these various dependency reporting discrepancies, perhaps it's worth discussing whether a general solution for Homebrew's dependency & reverse dependency commands should be reworked to return what most people using them might expect? 🀷

As a user, they should be able to invoke brew deps ... and brew uses ... commands to answer the following questions:

  1. "What's happening on my system now?"
  2. "What Formula(e) depends on what (now)?"
  3. "What Formula(e) historically pulled in X dependency?" (e.g. "What depended on what back then?")
  4. "What am I safe to upgrade now?" vs. "What must I hold back?" (e.g. something still needs python@2 as a common example)

The output of such commands should be unsurprising to the majority of users... and for those users who think it is still surprising... at least make it consistent and document it so they can become informed as to what to expect.

For users, it seems there are two major areas of concern in answering those questions:

  1. What's happening now? (e.g. Current Formulae / Casks dependency resolution both forwards & reverse recursively)
  2. What happened in the past / historically? (e.g. INSTALL_RECIEPT.json, "tab", .brew/*.rb, Git repos: homebrew-core, homebrew-cask etc...)

I hope this helps enliven the discussion surrounding the more general dependency reporting & resolving commands inside brew! Happy sleuthing! πŸ”πŸ•΅οΈβ€β™‚οΈ