Merge pull request #19254 from tyuwags/install-size

Install size
This commit is contained in:
Mike McQuaid 2025-03-11 09:54:03 +00:00 committed by GitHub
commit ae7b0c10f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 277 additions and 13 deletions

View File

@ -120,6 +120,11 @@ module Homebrew
[:switch, "--overwrite", { [:switch, "--overwrite", {
description: "Delete files that already exist in the prefix while linking.", description: "Delete files that already exist in the prefix while linking.",
}], }],
[:switch, "--ask", {
description: "Ask for confirmation before downloading and installing formulae. " \
"Print bottles and dependencies download size and install size.",
env: :ask,
}],
].each do |args| ].each do |args|
options = args.pop options = args.pop
send(*args, **options) send(*args, **options)
@ -302,6 +307,8 @@ module Homebrew
Install.perform_preinstall_checks_once Install.perform_preinstall_checks_once
Install.check_cc_argv(args.cc) Install.check_cc_argv(args.cc)
Install.ask(formulae, args: args) if args.ask?
Install.install_formulae( Install.install_formulae(
installed_formulae, installed_formulae,
build_bottle: args.build_bottle?, build_bottle: args.build_bottle?,

View File

@ -63,6 +63,11 @@ module Homebrew
[:switch, "-g", "--git", { [:switch, "-g", "--git", {
description: "Create a Git repository, useful for creating patches to the software.", description: "Create a Git repository, useful for creating patches to the software.",
}], }],
[:switch, "--ask", {
description: "Ask for confirmation before downloading and upgrading formulae. " \
"Print bottles and dependencies download size, install and net install size.",
env: :ask,
}],
].each do |args| ].each do |args|
options = args.pop options = args.pop
send(*args, **options) send(*args, **options)
@ -126,6 +131,9 @@ module Homebrew
unless formulae.empty? unless formulae.empty?
Install.perform_preinstall_checks_once Install.perform_preinstall_checks_once
# If asking the user is enabled, show dependency and size information.
Install.ask(formulae, args: args) if args.ask?
formulae.each do |formula| formulae.each do |formula|
if formula.pinned? if formula.pinned?
onoe "#{formula.full_name} is pinned. You must unpin it to reinstall." onoe "#{formula.full_name} is pinned. You must unpin it to reinstall."

View File

@ -71,6 +71,11 @@ module Homebrew
[:switch, "--overwrite", { [:switch, "--overwrite", {
description: "Delete files that already exist in the prefix while linking.", description: "Delete files that already exist in the prefix while linking.",
}], }],
[:switch, "--ask", {
description: "Ask for confirmation before downloading and upgrading formulae. " \
"Print bottles and dependencies download size, install and net install size.",
env: :ask,
}],
].each do |args| ].each do |args|
options = args.pop options = args.pop
send(*args, **options) send(*args, **options)
@ -211,11 +216,14 @@ module Homebrew
"#{f.full_specified_name} #{f.pkg_version}" "#{f.full_specified_name} #{f.pkg_version}"
end end
end end
puts formulae_upgrades.join("\n") puts formulae_upgrades.join("\n") unless args.ask?
end end
Install.perform_preinstall_checks_once Install.perform_preinstall_checks_once
# Main block: if asking the user is enabled, show dependency and size information.
Install.ask(formulae_to_install, args: args) if args.ask?
Upgrade.upgrade_formulae( Upgrade.upgrade_formulae(
formulae_to_install, formulae_to_install,
flags: args.flags_only, flags: args.flags_only,

View File

@ -48,6 +48,11 @@ module Homebrew
"trying any other/default URLs.", "trying any other/default URLs.",
boolean: true, boolean: true,
}, },
HOMEBREW_ASK: {
description: "If set, pass `--ask`to all formulae `brew install`, `brew upgrade` and `brew reinstall` " \
"commands.",
boolean: true,
},
HOMEBREW_AUTO_UPDATE_SECS: { HOMEBREW_AUTO_UPDATE_SECS: {
description: "Run `brew update` once every `$HOMEBREW_AUTO_UPDATE_SECS` seconds before some commands, " \ description: "Run `brew update` once every `$HOMEBREW_AUTO_UPDATE_SECS` seconds before some commands, " \
"e.g. `brew install`, `brew upgrade` and `brew tap`. Alternatively, " \ "e.g. `brew install`, `brew upgrade` and `brew tap`. Alternatively, " \

View File

@ -459,13 +459,13 @@ module Kernel
end end
def disk_usage_readable(size_in_bytes) def disk_usage_readable(size_in_bytes)
if size_in_bytes >= 1_073_741_824 if size_in_bytes.abs >= 1_073_741_824
size = size_in_bytes.to_f / 1_073_741_824 size = size_in_bytes.to_f / 1_073_741_824
unit = "GB" unit = "GB"
elsif size_in_bytes >= 1_048_576 elsif size_in_bytes.abs >= 1_048_576
size = size_in_bytes.to_f / 1_048_576 size = size_in_bytes.to_f / 1_048_576
unit = "MB" unit = "MB"
elsif size_in_bytes >= 1_024 elsif size_in_bytes.abs >= 1_024
size = size_in_bytes.to_f / 1_024 size = size_in_bytes.to_f / 1_024
unit = "KB" unit = "KB"
else else

View File

@ -327,6 +327,22 @@ module Homebrew
puts formula_names.join(" ") puts formula_names.join(" ")
end end
# If asking the user is enabled, show dependency and size information.
def ask(formulae, args:)
ohai "Looking for bottles..."
sized_formulae = compute_sized_formulae(formulae, args: args)
sizes = compute_total_sizes(sized_formulae, debug: args.debug?)
puts "#{::Utils.pluralize("Formula", sized_formulae.count, plural: "e")} \
(#{sized_formulae.count}): #{sized_formulae.join(", ")}\n\n"
puts "Download Size: #{disk_usage_readable(sizes[:download])}"
puts "Install Size: #{disk_usage_readable(sizes[:installed])}"
puts "Net Install Size: #{disk_usage_readable(sizes[:net])}" if sizes[:net] != 0
ask_input
end
private private
def perform_preinstall_checks(all_fatal: false) def perform_preinstall_checks(all_fatal: false)
@ -363,6 +379,86 @@ module Homebrew
Upgrade.install_formula(formula_installer, upgrade:) Upgrade.install_formula(formula_installer, upgrade:)
end end
def ask_input
ohai "Do you want to proceed with the installation? [Y/y/yes/N/n]"
accepted_inputs = %w[y yes]
declined_inputs = %w[n no]
loop do
result = $stdin.gets
return unless result
result = result.chomp.strip.downcase
if accepted_inputs.include?(result)
break
elsif declined_inputs.include?(result)
exit 1
else
puts "Invalid input. Please enter 'Y', 'y', or 'yes' to proceed, or 'N' to abort."
end
end
end
# Build a unique list of formulae to size by including:
# 1. The original formulae to install.
# 2. Their outdated dependents (subject to pruning criteria).
# 3. Optionally, any installed formula that depends on one of these and is outdated.
def compute_sized_formulae(formulae, args:)
sized_formulae = formulae.flat_map do |formula|
# Always include the formula itself.
formula_list = [formula]
deps = args.build_from_source? ? formula.deps.build : formula.deps.required
outdated_dependents = deps.map(&:to_formula).reject(&:pinned?).select do |dep|
dep.installed_kegs.empty? || (dep.bottled? && dep.outdated?)
end
deps.map(&:to_formula).each do |f|
outdated_dependents.concat(f.recursive_dependencies.map(&:to_formula).reject(&:pinned?).select do |dep|
dep.installed_kegs.empty? || (dep.bottled? && dep.outdated?)
end)
end
formula_list.concat(outdated_dependents)
formula_list
end
# Add any installed formula that depends on one of the sized formulae and is outdated.
unless Homebrew::EnvConfig.no_installed_dependents_check?
sized_formulae.concat(Formula.installed.select do |installed_formula|
installed_formula.bottled? && installed_formula.outdated? &&
installed_formula.deps.required.map(&:to_formula).intersect?(sized_formulae)
end)
end
sized_formulae.uniq(&:to_s).compact
end
# Compute the total sizes (download, installed, and net) for the given formulae.
def compute_total_sizes(sized_formulae, debug: false)
total_download_size = 0
total_installed_size = 0
total_net_size = 0
sized_formulae.select(&:bottle).each do |formula|
bottle = formula.bottle
# Fetch additional bottle metadata (if necessary).
bottle.fetch_tab(quiet: !debug)
total_download_size += bottle.bottle_size.to_i if bottle.bottle_size
total_installed_size += bottle.installed_size.to_i if bottle.installed_size
# Sum disk usage for all installed kegs of the formula.
next if formula.installed_kegs.none?
kegs_dep_size = formula.installed_kegs.sum { |keg| keg.disk_usage.to_i }
total_net_size += bottle.installed_size.to_i - kegs_dep_size if bottle.installed_size
end
{ download: total_download_size,
installed: total_installed_size,
net: total_net_size }
end
end end
end end
end end

View File

@ -20,6 +20,9 @@ class Homebrew::Cmd::InstallCmd::Args < Homebrew::CLI::Args
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def appdir; end def appdir; end
sig { returns(T::Boolean) }
def ask?; end
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def audio_unit_plugindir; end def audio_unit_plugindir; end

View File

@ -17,6 +17,9 @@ class Homebrew::Cmd::Reinstall::Args < Homebrew::CLI::Args
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def appdir; end def appdir; end
sig { returns(T::Boolean) }
def ask?; end
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def audio_unit_plugindir; end def audio_unit_plugindir; end

View File

@ -14,6 +14,9 @@ class Homebrew::Cmd::UpgradeCmd::Args < Homebrew::CLI::Args
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def appdir; end def appdir; end
sig { returns(T::Boolean) }
def ask?; end
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def audio_unit_plugindir; end def audio_unit_plugindir; end

View File

@ -28,6 +28,9 @@ module Homebrew::EnvConfig
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def artifact_domain_no_fallback?; end def artifact_domain_no_fallback?; end
sig { returns(T::Boolean) }
def ask?; end
sig { returns(T.nilable(::String)) } sig { returns(T.nilable(::String)) }
def auto_update_secs; end def auto_update_secs; end

View File

@ -4,6 +4,7 @@ require "cmd/install"
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
RSpec.describe Homebrew::Cmd::InstallCmd do RSpec.describe Homebrew::Cmd::InstallCmd do
include FileUtils
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "installs formulae", :integration_test do it "installs formulae", :integration_test do
@ -84,4 +85,44 @@ RSpec.describe Homebrew::Cmd::InstallCmd do
expect(HOMEBREW_CELLAR/"testball1/0.1/bin/test.dSYM/Contents/Resources/DWARF/test").to be_a_file if OS.mac? expect(HOMEBREW_CELLAR/"testball1/0.1/bin/test.dSYM/Contents/Resources/DWARF/test").to be_a_file if OS.mac?
expect(HOMEBREW_CACHE/"Sources/testball1").to be_a_directory expect(HOMEBREW_CACHE/"Sources/testball1").to be_a_directory
end end
it "installs with asking for user prompts without installed dependent checks", :integration_test do
setup_test_formula "testball1"
expect do
brew "install", "--ask", "testball1"
end.to output(/.*Formula\s*\(1\):\s*testball1.*/).to_stdout.and not_to_output.to_stderr
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
setup_test_formula "testball1", <<~RUBY
depends_on "testball5"
# should work as its not building but test doesnt pass if dependant
# depends_on "build" => :build
depends_on "installed"
RUBY
setup_test_formula "installed"
setup_test_formula "testball5", <<~RUBY
depends_on "testball4"
RUBY
setup_test_formula "testball4", ""
setup_test_formula "hiop"
setup_test_formula "build"
# Mock `Formula#any_version_installed?` by creating the tab in a plausible keg directory
keg_dir = HOMEBREW_CELLAR/"installed"/"1.0"
keg_dir.mkpath
touch keg_dir/AbstractTab::FILENAME
expect do
brew "install", "--ask", "testball1"
end.to output(/.*Formulae\s*\(3\):\s*testball1\s*,?\s*testball5\s*,?\s*testball4.*/).to_stdout
.and not_to_output.to_stderr
expect(HOMEBREW_CELLAR/"testball1/0.1/bin/test").to be_a_file
expect(HOMEBREW_CELLAR/"testball4/0.1/bin/testball4").to be_a_file
expect(HOMEBREW_CELLAR/"testball5/0.1/bin/testball5").to be_a_file
end
end end

View File

@ -20,4 +20,18 @@ RSpec.describe Homebrew::Cmd::Reinstall do
expect(foo_dir).to exist expect(foo_dir).to exist
end end
it "reinstalls a Formula with ask input", :integration_test do
install_test_formula "testball"
foo_dir = HOMEBREW_CELLAR/"testball/0.1/bin"
expect(foo_dir).to exist
FileUtils.rm_r(foo_dir)
expect { brew "reinstall", "--ask", "testball" }
.to output(/.*Formula\s*\(1\):\s*testball.*/).to_stdout
.and not_to_output.to_stderr
.and be_a_success
expect(foo_dir).to exist
end
end end

View File

@ -4,6 +4,7 @@ require "cmd/shared_examples/args_parse"
require "cmd/upgrade" require "cmd/upgrade"
RSpec.describe Homebrew::Cmd::UpgradeCmd do RSpec.describe Homebrew::Cmd::UpgradeCmd do
include FileUtils
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "upgrades a Formula and cleans up old versions", :integration_test do it "upgrades a Formula and cleans up old versions", :integration_test do
@ -15,4 +16,68 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
expect(HOMEBREW_CELLAR/"testball/0.0.1").not_to exist expect(HOMEBREW_CELLAR/"testball/0.0.1").not_to exist
end end
it "upgrades with asking for user prompts", :integration_test do
setup_test_formula "testball"
(HOMEBREW_CELLAR/"testball/0.0.1/foo").mkpath
expect do
brew "upgrade", "--ask"
end.to output(/.*Formula\s*\(1\):\s*testball.*/).to_stdout.and not_to_output.to_stderr
expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
expect(HOMEBREW_CELLAR/"testball/0.0.1").not_to exist
end
it "upgrades with asking for user prompts with dependants checks", :integration_test do
setup_test_formula "testball", <<~RUBY
depends_on "testball5"
# should work as its not building but test doesnt pass if dependant
# depends_on "build" => :build
depends_on "installed"
RUBY
setup_test_formula "installed"
setup_test_formula "testball5", <<~RUBY
depends_on "testball4"
RUBY
setup_test_formula "testball4"
setup_test_formula "hiop"
setup_test_formula "build"
(HOMEBREW_CELLAR/"testball/0.0.1/foo").mkpath
(HOMEBREW_CELLAR/"testball5/0.0.1/foo").mkpath
(HOMEBREW_CELLAR/"testball4/0.0.1/foo").mkpath
keg_dir = HOMEBREW_CELLAR/"installed"/"1.0"
keg_dir.mkpath
touch keg_dir/AbstractTab::FILENAME
regex = /
Formulae\s*\(3\):\s*
(
testball|testball5|testball4
)
\s*,\s*
(?!\1)
(
testball|testball5|testball4
)
\s*,\s*
(?!\1|\2)
(
testball|testball5|testball4
)
/x
expect do
brew "upgrade", "--ask"
end.to output(regex)
.to_stdout.and not_to_output.to_stderr
expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
expect(HOMEBREW_CELLAR/"testball/0.0.1").not_to exist
expect(HOMEBREW_CELLAR/"testball5/0.1").to be_a_directory
expect(HOMEBREW_CELLAR/"testball5/0.0.1").not_to exist
expect(HOMEBREW_CELLAR/"testball4/0.1").to be_a_directory
expect(HOMEBREW_CELLAR/"testball4/0.0.1").not_to exist
end
end end

View File

@ -134,13 +134,21 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
bottle_block: nil, tab_attributes: nil) bottle_block: nil, tab_attributes: nil)
case name case name
when /^testball/ when /^testball/
# Use a different tarball for testball2 to avoid lock errors when writing concurrency tests case name
prefix = (name == "testball2") ? "testball2" : "testball" when "testball4", "testball5"
tarball = if OS.linux? prefix = name
TEST_FIXTURE_DIR/"tarballs/#{prefix}-0.1-linux.tbz" program_name = name
when "testball2"
prefix = name
program_name = "test"
else else
TEST_FIXTURE_DIR/"tarballs/#{prefix}-0.1.tbz" prefix = "testball"
program_name = "test"
end end
tarball_name = "#{prefix}-0.1#{"-linux" if OS.linux?}.tbz"
tarball = TEST_FIXTURE_DIR / "tarballs/#{tarball_name}"
content = <<~RUBY content = <<~RUBY
desc "Some test" desc "Some test"
homepage "https://brew.sh/#{name}" homepage "https://brew.sh/#{name}"
@ -150,12 +158,12 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
option "with-foo", "Build with foo" option "with-foo", "Build with foo"
#{bottle_block} #{bottle_block}
def install def install
(prefix/"foo"/"test").write("test") if build.with? "foo" (prefix/"foo"/"#{program_name}").write("#{program_name}") if build.with? "foo"
prefix.install Dir["*"] prefix.install Dir["*"]
(buildpath/"test.c").write \ (buildpath/"#{program_name}.c").write \
"#include <stdio.h>\\nint main(){printf(\\"test\\");return 0;}" "#include <stdio.h>\\nint main(){printf(\\"#{program_name}\\");return 0;}"
bin.mkpath bin.mkpath
system ENV.cc, "test.c", "-o", bin/"test" system ENV.cc, "#{program_name}.c", "-o", bin/"#{program_name}"
end end
#{content} #{content}