Skip to content

Refine ask‐option dependency resolution and strengthen tests #20033

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

Open
wants to merge 43 commits into
base: master
Choose a base branch
from

Conversation

tyuwags
Copy link
Contributor

@tyuwags tyuwags commented May 31, 2025

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes? Here's an example.
  • Have you successfully run brew style with your changes locally?
  • Have you successfully run brew typecheck with your changes locally?
  • Have you successfully run brew tests with your changes locally?

These changes resolve an error a finding the whole hierarchy for the ask option, pruning the build dependencies and be able to find the whole dependants hierarchy of the formula.
These changes also create and update tests to make sure the whole hierarchy is displayed in the ask prompt.

@tyuwags
Copy link
Contributor Author

tyuwags commented May 31, 2025

I saw a problem, with this example, I don't know if it's the expected behaviour.
Here the deps of fonttools
fonttools
└── python@3.13
├── mpdecimal
├── openssl@3
│ └── ca-certificates
├── sqlite
│ └── readline
├── xz
└── expat
Fonttools and sqlite are outdated but not python@3.13
My function found correctly that if I do brew upgrade --ask fonttools that fonttools and sqlite need to be upgraded as I look into the recursive dependencies.
But as python@3.13 is up-to-date, sqlite isn't upgraded by brew, I don't know if it's a normal behaviour and I should modify my function in consequence or if I should open an issue for the upgrade function.

@tyuwags tyuwags reopened this Jun 1, 2025
@tyuwags tyuwags changed the title Fixing dependants and dependencies research in ask option and making tests stronger Refine ask‐option dependency resolution and strengthen tests Jun 1, 2025
@MikeMcQuaid
Copy link
Member

look into the recursive dependencies.
But as python@3.13 is up-to-date, sqlite isn't upgraded by brew, I don't know if it's a normal behaviour and I should modify my function in consequence or if I should open an issue for the upgrade function.

@tyuwags Not sure I understand the question. It's expected that some dependencies may need to be upgraded and others may not. Does that answer your question?

@tyuwags
Copy link
Contributor Author

tyuwags commented Jun 2, 2025

Thanks for the reply! Let me clarify the issue a bit:

In this example:

  • fonttools is outdated
  • sqlite is also outdated, and it's a dependency of python@3.13
  • python@3.13 itself is not outdated
    When I run brew upgrade --ask fonttools, my function detects both fonttools and sqlite as outdated by looking through recursive dependencies — which seems logical.

However, during the actual upgrade, Homebrew skips sqlite because its direct parent (python@3.13) is not being rebuilt.

So my actual question is:

Should my function consider only the outdated recursive dependencies whose parent will also be rebuilt?
Or is it fine (or even expected) that brew upgrade skips outdated indirect deps when their parent is not being upgraded?
I'm trying to align my implementation logic with how brew upgrade behaves internally. Thanks again!

@MikeMcQuaid
Copy link
Member

Or is it fine (or even expected) that brew upgrade skips outdated indirect deps when their parent is not being upgraded?

This is expected.

I'm trying to align my implementation logic with how brew upgrade behaves internally.

Thanks for this. Ideally this would be done by using the same code rather than having two code paths that have to mirror each other.

@tyuwags
Copy link
Contributor Author

tyuwags commented Jun 2, 2025

Thanks for this. Ideally this would be done by using the same code rather than having two code paths that have to mirror each other.

That was my initial thought as well. However, from what I’ve seen, brew doesn’t seem to know upfront which formulae will need to be upgraded. Instead, it determines the necessary dependencies and dependents dynamically using:

Upgrade.upgrade_formulae formulae_to_install
Upgrade.check_installed_dependents formulae_to_install

What I'd like is for these functions to return the full list of formulae that need to be upgraded, without performing the upgrade yet. Then, I could pass that list to a separate function that handles the actual upgrade logic.

This way, I could reuse the existing resolution logic and keep everything consistent, without duplicating or mirroring code paths.

@MikeMcQuaid
Copy link
Member

@tyuwags I'm open to seeing PR(s) to modify those functions to the logic can be shared. Let me know when this one is ready for review and maybe make it draft until then?

@MikeMcQuaid MikeMcQuaid requested a review from Copilot June 11, 2025 15:41
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors how upgrade, install, and reinstall commands resolve and act on dependencies when --ask is used, and strengthens integration tests to cover full dependency hierarchies.

  • Split dependency resolution from execution by introducing get_formulae_dependencies, get_dependants, upgrade_dependents, and get_hierarchy.
  • Refactored Reinstall and Install modules to parallel Upgrade’s two-phase flow.
  • Enhanced test helpers and added end-to-end integration tests covering nested formula dependency trees.

Reviewed Changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated no comments.

Show a summary per file
File Description
Library/Homebrew/upgrade.rb Extracted dependency-gathering into get_formulae_dependencies and get_dependants; separated upgrade execution into upgrade_formulae and upgrade_dependents.
Library/Homebrew/install.rb Renamed first install_formulae to get_formulae_dependencies; added two-phase flow and get_hierarchy for ask-mode.
Library/Homebrew/reinstall.rb Introduced get_formula_to_reinstall and a Formula_keg struct; moved prelude/fetch into new reinstall_formula.
Library/Homebrew/cmd/upgrade.rb Updated CLI to use new dependency resolution methods and only call ask_formulae when appropriate.
Library/Homebrew/cmd/install.rb Mirrored upgrade changes: using get_formulae_dependencies, ask_formulae, upgrade_dependents.
Library/Homebrew/cmd/reinstall.rb Adjusted to call get_formula_to_reinstall and adopt two-phase ask-mode.
Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb Extended setup_test_formula to handle bottled testballs and generalized program names.
Library/Homebrew/test/cmd/upgrade_spec.rb Updated descriptions, test setup, and added a new integration test for full dependant hierarchy.
Library/Homebrew/test/cmd/install_spec.rb Aligned expected output regex and test formulas to new ask-mode behavior.
Comments suppressed due to low confidence (2)

Library/Homebrew/reinstall.rb:10

  • [nitpick] The constant Formula_keg does not follow Ruby’s CamelCase naming convention. Consider renaming it to FormulaKeg for consistency with typical Ruby style.
Formula_keg = Struct.new(:formula_installer, :keg, :formula, :options)

Library/Homebrew/upgrade.rb:98

  • In the rescue block you reference an undefined variable formula. You should reference the installer’s formula, for example fi.formula.full_name, to report the correct formula name.
ofail "#{formula}: #{e}"

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 @tyuwags! Looking good so far.

dry_run: args.dry_run?,
)
if args.ask?
dependants = Upgrade.get_dependants(
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to have a single call to this rather than two? Does Install.install_formulae change the output here always/sometimes/maybe/never?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I have to make two calls or change the behaviour of this method
In the flow, I have this line

installed_formulae = (dry_run ? formulae : FormulaInstaller.installed.to_a).dup

which used formulae or FormulaInstaller.installed.to_a
the problem is that I have to get the dependants to print them out before installing
so I must call this function before installing or upgrading and use formulae rather than FormulaInstaller.installed.to_a as its empty for now
I could refactor to only use formulae but the original flow was to install formulae and get the dependants of the freshly installed formulae so I keep both flow depending of the option

Copy link
Member

Choose a reason for hiding this comment

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

the problem is that I have to get the dependants to print them out before installing

Yeh, makes sense. I guess the question I have is: can you get these dependents and use them regardless of whether ask? is used. If not: can you explain why not and what needs to be different/breaks/etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can but I will get the dépendants based on formulae that I want to install instead of which formulae brew really installed. Normally, it shouldn't have any difference but im not sure about that

Copy link
Member

Choose a reason for hiding this comment

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

@tyuwags Can you try it out and see? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tests are all running as expected after just using formulae.dup and I didn't see any problems when using brew locally without ask

@MikeMcQuaid
Copy link
Member

Library/Homebrew/reinstall.rb:10

  • [nitpick] The constant Formula_keg does not follow Ruby’s CamelCase naming convention. Consider renaming it to FormulaKeg for consistency with typical Ruby style.
Formula_keg = Struct.new(:formula_installer, :keg, :formula, :options)

Library/Homebrew/upgrade.rb:98

  • In the rescue block you reference an undefined variable formula. You should reference the installer’s formula, for example fi.formula.full_name, to report the correct formula name.
ofail "#{formula}: #{e}"

@tyuwags ☝🏻 these both seem correct

@tyuwags
Copy link
Contributor Author

tyuwags commented Jun 18, 2025

@MikeMcQuaid, everything is ready, everything is resolved

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.

A few suggested style tweaks but the bulk of this looks great, thanks @tyuwags! Feel like we're really close now. Gonna ask a few other maintainers to review.

Copy link
Member

@Bo98 Bo98 left a comment

Choose a reason for hiding this comment

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

Looks good overall. Mostly some naming comments here, though with a couple of logic comments at the end which are more important.

@@ -95,34 +95,4 @@ def install

expect(HOMEBREW_CELLAR/"testball1/0.1/bin/test").to be_a_file
end

it "installs with asking for user prompts with installed dependent checks", :integration_test do
Copy link
Member

Choose a reason for hiding this comment

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

Why are these tests deleted? If they're redundant, it's probably better to handle in a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created those tests to make sure all the new methods created ran correctly. As @MikeMcQuaid said before, those tests are too long so I deleted them

Comment on lines 70 to 77
if !dry_run && dependents && fi.bottle_tab_runtime_dependencies.presence&.all? do |dependency, hash|
minimum_version = Version.new(hash["version"]) if hash["version"].present?
Dependency.new(dependency).installed?(minimum_version:, minimum_revision: hash["revision"])
end
# Don't need to install this bottle if all of the runtime
# dependencies have the same or newer version already installed.
next if dependents && fi.bottle_tab_runtime_dependencies.presence&.all? do |dependency, hash|
minimum_version = Version.new(hash["version"]) if hash["version"].present?
Dependency.new(dependency).installed?(minimum_version:, minimum_revision: hash["revision"])
end

fi.fetch
next
end
Copy link
Member

Choose a reason for hiding this comment

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

The double end here is a bit confusing. I'd move things about a bit so the first end doesn't look like it's ending the if block.

Ideally go for next if if possible.

Comment on lines 69 to 70
fi.prelude

if !dry_run && dependents && fi.bottle_tab_runtime_dependencies.presence&.all? do |dependency, hash|
Copy link
Member

@Bo98 Bo98 Jun 19, 2025

Choose a reason for hiding this comment

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

I don't think this logic is correct. fi.bottle_tab_runtime_dependencies requires fi.fetch_bottle_tab to have run first, which was previously called by fi.prelude that we're removing here.

Comment on lines 278 to 290
installed_formulae = (dry_run ? formulae : FormulaInstaller.installed.to_a).dup
installed_formulae = formulae.dup
installed_formulae.reject! { |f| f.core_formula? && f.versioned_formula? }
return if installed_formulae.empty?
Copy link
Member

Choose a reason for hiding this comment

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

This sounds like it'll now upgrade dependents of things that failed to upgrade.

We may need to group dependents by formula and then filter out those that didn't actually install later.

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.

3 participants