mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Formula install-step paths now serialise only a base that was explicitly specified. RuboCop prevents relative official-tap paths from relying on the current working directory. Run structured-only API post-installs from the current JSON data so old formula snapshots embedded in bottles cannot restore the removed default. Keep using bottle snapshots for formulae that still have Ruby hooks.
1530 lines
54 KiB
Ruby
1530 lines
54 KiB
Ruby
# typed: false
|
|
# frozen_string_literal: true
|
|
|
|
require "formula"
|
|
require "formula_installer"
|
|
require "keg"
|
|
require "sandbox"
|
|
require "tab"
|
|
require "trust"
|
|
require "cmd/install"
|
|
require "test/support/fixtures/testball"
|
|
require "test/support/fixtures/testball_bottle"
|
|
require "test/support/fixtures/failball"
|
|
require "test/support/fixtures/failball_offline_install"
|
|
|
|
RSpec.describe FormulaInstaller do
|
|
matcher :be_poured_from_bottle do
|
|
match(&:poured_from_bottle)
|
|
end
|
|
|
|
def temporary_install(formula, **options)
|
|
expect(formula).not_to be_latest_version_installed
|
|
|
|
installer = FormulaInstaller.new(formula, **options)
|
|
|
|
with_env(HOMEBREW_NO_INSTALL_FROM_API: "1") do
|
|
installer.fetch
|
|
installer.install
|
|
end
|
|
|
|
keg = Keg.new(formula.prefix)
|
|
|
|
expect(formula).to be_latest_version_installed
|
|
|
|
begin
|
|
Tab.clear_cache
|
|
expect(keg.tab).not_to be_poured_from_bottle
|
|
|
|
yield formula if block_given?
|
|
ensure
|
|
Tab.clear_cache
|
|
keg.unlink
|
|
keg.uninstall
|
|
formula.clear_cache
|
|
# there will be log files when sandbox is enable.
|
|
FileUtils.rm_r(formula.logs) if formula.logs.directory?
|
|
end
|
|
|
|
expect(keg).not_to exist
|
|
expect(formula).not_to be_latest_version_installed
|
|
end
|
|
|
|
specify "basic installation" do
|
|
temporary_install(Testball.new) do |f|
|
|
# Test that things made it into the Keg
|
|
# "readme" is empty, so it should not be installed
|
|
expect(f.prefix/"readme").not_to exist
|
|
|
|
expect(f.bin).to be_a_directory
|
|
expect(f.bin.children.count).to eq(3)
|
|
|
|
expect(f.libexec).to be_a_directory
|
|
expect(f.libexec.children.count).to eq(1)
|
|
|
|
expect(f.prefix/"main.c").not_to exist
|
|
expect(f.prefix/"license").not_to exist
|
|
|
|
# Test that things make it into the Cellar
|
|
keg = Keg.new f.prefix
|
|
keg.link
|
|
|
|
bin = HOMEBREW_PREFIX/"bin"
|
|
expect(bin).to be_a_directory
|
|
expect(bin.children.count).to eq(3)
|
|
expect(f.prefix/".brew/testball.rb").to be_readable
|
|
end
|
|
end
|
|
|
|
specify "offline installation" do
|
|
expect { temporary_install(FailballOfflineInstall.new) }.to raise_error(BuildError) if Sandbox.available?
|
|
end
|
|
|
|
specify "Formula is not poured from bottle when compiler specified" do
|
|
temporary_install(TestballBottle.new, cc: "clang") do |f|
|
|
tab = Tab.for_formula(f)
|
|
expect(tab.compiler).to eq("clang")
|
|
end
|
|
end
|
|
|
|
specify "relocated bottle install does not require developer tools on Apple Silicon", :needs_macos do
|
|
formula = TestballBottle.new
|
|
installer = described_class.new(formula)
|
|
|
|
allow(installer).to receive_messages(lock: nil, pour_bottle?: true, quiet?: true)
|
|
allow(Hardware::CPU).to receive(:arm?).and_return(true)
|
|
allow(formula.bottle_specification).to receive(:skip_relocation?).and_return(false)
|
|
expect(Homebrew::Diagnostic::Checks).not_to receive(:new)
|
|
allow(installer).to receive(:check_conflicts).and_raise("stopped after preinstall checks")
|
|
|
|
expect do
|
|
installer.install
|
|
end.to raise_error("stopped after preinstall checks")
|
|
end
|
|
|
|
describe "#finish" do
|
|
it "runs structured post-install work through the post-install subprocess" do
|
|
formula = formula "finish-install-steps" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
installer = described_class.new(formula)
|
|
tab = instance_double(Tab)
|
|
keg = instance_double(Keg, tab:)
|
|
|
|
allow(Keg).to receive(:new).with(formula.prefix).and_return(keg)
|
|
allow(installer).to receive_messages(
|
|
build_bottle?: false,
|
|
caveats: nil,
|
|
debug_symbols?: false,
|
|
fix_dynamic_linkage: nil,
|
|
install_service: nil,
|
|
link: nil,
|
|
link_manual_command_warning: nil,
|
|
only_deps?: false,
|
|
quiet?: true,
|
|
show_summary_heading?: false,
|
|
skip_post_install?: false,
|
|
summary: "summary",
|
|
verbose?: false,
|
|
)
|
|
allow(formula).to receive_messages(post_install_steps_defined?: true, post_install_defined?: false,
|
|
runtime_dependencies: [])
|
|
allow(CacheStoreDatabase).to receive(:use).with(:linkage)
|
|
allow(Homebrew::EnvConfig).to receive(:sbom?).and_return(false)
|
|
allow(Homebrew::Install).to receive(:global_post_install)
|
|
allow(Tab).to receive_messages(clear_cache: nil, runtime_deps_hash: [])
|
|
allow(tab).to receive(:runtime_dependencies=)
|
|
allow(tab).to receive(:write)
|
|
|
|
expect(formula).to receive(:install_etc_var).ordered
|
|
expect(formula).not_to receive(:run_post_install_steps)
|
|
expect(installer).to receive(:post_install).ordered
|
|
|
|
installer.finish
|
|
end
|
|
end
|
|
|
|
describe "#post_install" do
|
|
it "restores bin/brew after a Landlock-sandboxed post-install replaces it" do
|
|
prefix = mktmpdir
|
|
stub_const("HOMEBREW_PREFIX", prefix)
|
|
brew_file = prefix/"bin/brew"
|
|
original_brew_file = prefix/"Homebrew/bin/brew"
|
|
original_brew_file.dirname.mkpath
|
|
original_brew_file.write "#!/bin/sh\n"
|
|
brew_file.dirname.mkpath
|
|
brew_file.make_relative_symlink original_brew_file
|
|
original_target = brew_file.readlink
|
|
original_directory_mode = brew_file.dirname.stat.mode & 07777
|
|
formula = formula("replace-brew-postinstall") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
installer = described_class.new(formula)
|
|
sandbox = instance_double(Sandbox).as_null_object
|
|
|
|
allow(installer).to receive_messages(post_install_formula_path: formula.path, use_sandbox?: true)
|
|
allow(formula).to receive_messages(logs: mktmpdir, network_access_allowed?: true)
|
|
allow(Sandbox).to receive_messages(full_write_isolation?: false, new: sandbox)
|
|
allow(sandbox).to receive(:run) do
|
|
FileUtils.rm_f brew_file
|
|
brew_file.write "malicious\n"
|
|
brew_file.dirname.chmod 0500
|
|
end
|
|
|
|
installer.post_install
|
|
|
|
expect(brew_file).to be_a_symlink
|
|
expect(brew_file.readlink).to eq(original_target)
|
|
expect(brew_file.dirname.stat.mode & 07777).to eq(original_directory_mode)
|
|
end
|
|
end
|
|
|
|
describe "#post_install_formula_path" do
|
|
it "uses the API formula for structured-only post-installs" do
|
|
formula = formula("api-install-steps") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
installer = described_class.new(formula)
|
|
|
|
allow(formula).to receive_messages(any_installed_prefix: mktmpdir, loaded_from_api?: true,
|
|
post_install_defined?: false)
|
|
|
|
expect(installer.post_install_formula_path).to eq(formula.full_name)
|
|
end
|
|
|
|
it "uses the keg formula for API post-installs with Ruby hooks" do
|
|
formula = formula("api-post-install-hook") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
installer = described_class.new(formula)
|
|
installed_prefix = mktmpdir
|
|
|
|
allow(formula).to receive_messages(any_installed_prefix: installed_prefix, loaded_from_api?: true,
|
|
post_install_defined?: true)
|
|
|
|
expect(installer.post_install_formula_path).to eq(installed_prefix/".brew/api-post-install-hook.rb")
|
|
end
|
|
end
|
|
|
|
describe "#pour" do
|
|
let(:f) do
|
|
formula("missing-bottle-tab") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/missing-bottle-tab-1.0.tar.gz"
|
|
|
|
bottle do
|
|
sha256 cellar: :any_skip_relocation,
|
|
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
|
|
end
|
|
end
|
|
end
|
|
let(:installer) { Class.new(described_class).new(f) }
|
|
let(:downloader) { instance_double(AbstractDownloadStrategy, basename: "missing-bottle-tab", stage: nil) }
|
|
let(:downloadable) { instance_double(Resource, downloader:) }
|
|
let(:tab) do
|
|
instance_double(Tab, changed_files: nil, source: { "versions" => {} }, write: nil).as_null_object
|
|
end
|
|
let(:keg) { instance_double(Keg) }
|
|
|
|
before do
|
|
allow(installer).to receive(:downloadable).and_return(downloadable)
|
|
allow(Utils::Bottles).to receive(:load_tab).with(f).and_return(tab)
|
|
allow(Tab).to receive(:clear_cache)
|
|
allow(Keg).to receive(:new).with(f.prefix).and_return(keg)
|
|
allow(keg).to receive(:replace_placeholders_with_locations)
|
|
allow(f.bottle_specification).to receive(:skip_relocation?).with(tab:).and_return(true)
|
|
end
|
|
|
|
it "preserves the skip-linkage decision for the default bottle domain" do
|
|
expect(keg).to receive(:replace_placeholders_with_locations).with(nil, skip_linkage: true)
|
|
|
|
installer.pour
|
|
end
|
|
|
|
it "relocates dynamic linkage without metadata from a bottle mirror" do
|
|
ENV["HOMEBREW_BOTTLE_DOMAIN"] = "https://mirror.example.com"
|
|
|
|
expect(keg).to receive(:replace_placeholders_with_locations).with(nil, skip_linkage: false)
|
|
|
|
installer.pour
|
|
end
|
|
|
|
it "explains how a bottle mirror can provide metadata once per invocation" do
|
|
ENV["HOMEBREW_BOTTLE_DOMAIN"] = "https://mirror.example.com"
|
|
|
|
expect { 2.times { installer.pour } }
|
|
.to output(satisfy do |stderr|
|
|
stderr.scan("OCI registry proxy").one? &&
|
|
stderr.include?("sh.brew.tab") && stderr.include?("HOMEBREW_ARTIFACT_DOMAIN")
|
|
end).to_stderr
|
|
end
|
|
end
|
|
|
|
describe "#build_bottle_postinstall" do
|
|
let(:f) do
|
|
formula "bottle-config" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
end
|
|
let(:config_file) { HOMEBREW_PREFIX/"etc/bottle-config.conf" }
|
|
|
|
before do
|
|
FileUtils.rm_rf f.rack
|
|
FileUtils.rm_f config_file
|
|
FileUtils.rm_f Pathname("#{config_file}.default")
|
|
end
|
|
|
|
after do
|
|
FileUtils.rm_rf f.rack
|
|
FileUtils.rm_f config_file
|
|
FileUtils.rm_f Pathname("#{config_file}.default")
|
|
end
|
|
|
|
it "stores new prefix config where install_etc_var restores it from" do
|
|
installer = described_class.new(f)
|
|
installer.build_bottle_preinstall
|
|
config_file.dirname.mkpath
|
|
config_file.write "new\n"
|
|
|
|
installer.build_bottle_postinstall
|
|
|
|
expect((f.bottle_prefix/"etc/bottle-config.conf").read).to eq("new\n")
|
|
end
|
|
end
|
|
|
|
describe "#verify_deps_exist" do
|
|
it "does not install an untapped dependency tap" do
|
|
formula = Testball.new
|
|
installer = described_class.new(formula)
|
|
tap = instance_double(Tap, user: "user", repository: "repo", to_s: "user/repo", installed?: false)
|
|
|
|
allow(installer).to receive(:compute_dependencies).and_raise(TapFormulaUnavailableError.new(tap, "foo"))
|
|
|
|
expect(tap).not_to receive(:ensure_installed!)
|
|
|
|
expect { installer.verify_deps_exist }
|
|
.to raise_error(TapFormulaUnavailableError, /If you trust this tap/) { |error|
|
|
expect(error.dependent).to eq(formula.full_name)
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "#fetch_bottle_tab" do
|
|
it "does not enqueue cached bottle manifests" do
|
|
formula = formula("deno") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/deno-2.7.11.tar.gz"
|
|
|
|
bottle do
|
|
root_url HOMEBREW_BOTTLE_DEFAULT_DOMAIN
|
|
sha256 cellar: :any_skip_relocation,
|
|
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
|
|
end
|
|
end
|
|
installer = described_class.new(formula)
|
|
installer.download_queue = instance_double(Homebrew::DownloadQueue)
|
|
manifest_resource = formula.bottle&.github_packages_manifest_resource
|
|
cached_download = manifest_resource&.cached_download
|
|
|
|
allow(manifest_resource).to receive(:downloaded?).and_return(true)
|
|
expect(manifest_resource).to receive(:verify_download_integrity).with(cached_download) do
|
|
expect(Context.current.quiet?).to be(true)
|
|
end
|
|
expect(manifest_resource).not_to receive(:clear_cache)
|
|
expect(installer.download_queue).not_to receive(:enqueue)
|
|
|
|
installer.fetch_bottle_tab(enqueue: true)
|
|
end
|
|
|
|
it "enqueues invalid cached bottle manifests" do
|
|
formula = formula("deno") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/deno-2.7.11.tar.gz"
|
|
|
|
bottle do
|
|
root_url HOMEBREW_BOTTLE_DEFAULT_DOMAIN
|
|
sha256 cellar: :any_skip_relocation,
|
|
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
|
|
end
|
|
end
|
|
installer = described_class.new(formula)
|
|
installer.download_queue = instance_double(Homebrew::DownloadQueue)
|
|
manifest_resource = formula.bottle&.github_packages_manifest_resource
|
|
|
|
allow(manifest_resource).to receive(:downloaded?).and_return(true)
|
|
manifest_resource&.manifest_annotations = {}
|
|
expect(manifest_resource).to receive(:verify_download_integrity) do
|
|
expect(Context.current.quiet?).to be(true)
|
|
raise Resource::BottleManifest::Error
|
|
end
|
|
expect(installer.download_queue).to receive(:enqueue).with(manifest_resource)
|
|
|
|
installer.fetch_bottle_tab(enqueue: true)
|
|
|
|
# Read the raw ivar: the private memoising reader would re-parse the manifest.
|
|
# rubocop:disable Homebrew/NoInstanceVariableAccessInTests
|
|
expect(manifest_resource&.instance_variable_get(:@manifest_annotations)).to be_nil
|
|
# rubocop:enable Homebrew/NoInstanceVariableAccessInTests
|
|
end
|
|
end
|
|
|
|
describe "#enqueue_fetch" do
|
|
let(:formula) { TestballBottle.new }
|
|
let(:installer) { described_class.new(formula) }
|
|
let(:download_queue) { instance_double(Homebrew::DownloadQueue) }
|
|
let(:bottle) { instance_double(Bottle, cached_download: HOMEBREW_CACHE/"downloads/testball-bottle") }
|
|
|
|
before do
|
|
installer.download_queue = download_queue
|
|
|
|
allow(Homebrew::EnvConfig).to receive(:verify_attestations?).and_return(false)
|
|
allow(installer).to receive(:previously_fetched_formula)
|
|
allow(installer).to receive(:pour_bottle?).with(output_warning: true).and_return(true)
|
|
allow(installer).to receive(:downloadable).and_return(bottle)
|
|
allow(installer).to receive(:fetch_bottle_tab)
|
|
end
|
|
|
|
it "starts a bottle download before enqueueing dependencies after the prelude" do
|
|
installer.ran_prelude = true
|
|
|
|
expect(download_queue).to receive(:enqueue)
|
|
.with(bottle, check_attestation: false, stage: false).ordered
|
|
expect(installer).to receive(:fetch_dependencies).ordered
|
|
expect(download_queue).to receive(:enqueue)
|
|
.with(bottle, check_attestation: false).ordered
|
|
|
|
installer.enqueue_fetch
|
|
end
|
|
|
|
it "resolves dependencies before enqueueing a bottle without the prelude" do
|
|
expect(installer).to receive(:fetch_dependencies).ordered
|
|
expect(installer).to receive(:fetch_bottle_tab).with(enqueue: true).ordered
|
|
expect(download_queue).to receive(:enqueue)
|
|
.with(bottle, check_attestation: false).ordered
|
|
|
|
installer.enqueue_fetch
|
|
end
|
|
|
|
it "does not requeue a bottle already enqueued by the prelude fetch" do
|
|
allow(installer).to receive(:pour_bottle?).and_return(true)
|
|
expect(download_queue).to receive(:enqueue)
|
|
.with(bottle, check_attestation: false, stage: true).once
|
|
installer.prelude_fetch
|
|
|
|
expect(installer).to receive(:fetch_dependencies)
|
|
|
|
installer.enqueue_fetch
|
|
end
|
|
end
|
|
|
|
describe "linking defaults" do
|
|
it "links non-keg-only formulae when link_keg is false" do
|
|
ordinary_formula = formula "homebrew-link-default" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
|
|
expect(described_class.new(ordinary_formula, link_keg: false).link_keg).to be true
|
|
end
|
|
|
|
it "links non-keg-only dependencies even when they were not previously linked" do
|
|
dependency_formula = formula "homebrew-link-default-dependency" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
dependency = instance_double(Dependency, to_formula: dependency_formula, name: dependency_formula.name,
|
|
options: Options.new)
|
|
installer = described_class.new(Testball.new)
|
|
# Sorbet doesn't like `T.let(nil, T.nilable(described_class))`, but the
|
|
# RuboCop will always autocorrect to that.
|
|
# rubocop:disable RSpec/DescribedClass
|
|
child_installer = T.let(nil, T.nilable(FormulaInstaller))
|
|
# rubocop:enable RSpec/DescribedClass
|
|
|
|
allow(dependency_formula).to receive_messages(
|
|
linked_keg: Pathname("/tmp/nonexistent-linked-keg"),
|
|
latest_version_installed?: false,
|
|
tap: nil,
|
|
any_version_installed?: false,
|
|
)
|
|
allow(installer).to receive(:oh1)
|
|
allow(described_class).to receive(:new).and_wrap_original do |original, formula, **kwargs|
|
|
instance = original.call(formula, **kwargs)
|
|
next instance if formula != dependency_formula
|
|
|
|
child_installer = instance
|
|
allow(instance).to receive_messages(prelude: true, install: true, finish: true)
|
|
instance
|
|
end
|
|
|
|
installer.install_dependency(dependency)
|
|
|
|
expect(child_installer).not_to be_installed_on_request
|
|
expect(child_installer&.link_keg).to be true
|
|
end
|
|
end
|
|
|
|
describe "#check_conflicts" do
|
|
let(:test_formula) do
|
|
formula "testball" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/testball-0.1.tar.gz"
|
|
conflicts_with "other"
|
|
end
|
|
end
|
|
|
|
let(:conflicting_formula) do
|
|
formula "other" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/other-0.1.tar.gz"
|
|
conflicts_with "testball"
|
|
end
|
|
end
|
|
|
|
before { allow(Formulary).to receive(:factory).with("other").and_return(conflicting_formula) }
|
|
|
|
context "when conflicting formula is installed but not linked" do
|
|
before do
|
|
linked_keg = instance_double(Pathname, exist?: false)
|
|
opt_prefix = instance_double(Pathname, exist?: true)
|
|
allow(conflicting_formula).to receive_messages(linked_keg:, opt_prefix:)
|
|
end
|
|
|
|
it "does not raise an error" do
|
|
installer = described_class.new(test_formula, link_keg: true)
|
|
expect { installer.check_conflicts }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
context "when conflicting formula is installed" do
|
|
before do
|
|
linked_keg = opt_prefix = instance_double(Pathname, exist?: true)
|
|
allow(conflicting_formula).to receive_messages(linked_keg:, opt_prefix:)
|
|
end
|
|
|
|
it "raises an error if linking keg" do
|
|
installer = described_class.new(test_formula, link_keg: true)
|
|
expect { installer.check_conflicts }.to raise_error(FormulaConflictError)
|
|
end
|
|
|
|
it "does not raise an error with force set" do
|
|
installer = described_class.new(test_formula, link_keg: true, force: true)
|
|
expect { installer.check_conflicts }.not_to raise_error
|
|
end
|
|
|
|
it "does not raise an error with skip_link set" do
|
|
installer = described_class.new(test_formula, link_keg: true, skip_link: true)
|
|
expect { installer.check_conflicts }.not_to raise_error
|
|
end
|
|
|
|
it "does not raise an error if not linking keg" do
|
|
allow(test_formula).to receive(:keg_only?).and_return(true)
|
|
installer = described_class.new(test_formula, link_keg: false, installed_on_request: false)
|
|
expect { installer.check_conflicts }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
it "ignores conflicts that name the formula being installed" do
|
|
f = formula("terraform", tap: Tap.fetch("thirdparty", "selfconflict")) do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
conflicts_with "terraform"
|
|
end
|
|
|
|
expect(Formulary).not_to receive(:factory)
|
|
|
|
described_class.new(f).check_conflicts
|
|
ensure
|
|
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
|
|
end
|
|
end
|
|
|
|
describe "#install_dependencies" do
|
|
it "marks only outdated dependencies as upgradable in the header" do
|
|
outdated = formula "outdated-dependency" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
uninstalled = formula "uninstalled-dependency" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
allow(outdated).to receive_messages(any_version_installed?: true, outdated?: true)
|
|
allow(uninstalled).to receive_messages(any_version_installed?: false, outdated?: false)
|
|
deps = [
|
|
instance_double(Dependency, to_formula: outdated, name: outdated.name, to_s: outdated.name),
|
|
instance_double(Dependency, to_formula: uninstalled, name: uninstalled.name, to_s: uninstalled.name),
|
|
]
|
|
installer = described_class.new(Testball.new)
|
|
allow(installer).to receive(:install_dependency)
|
|
allow_any_instance_of(StringIO).to receive(:tty?).and_return(true)
|
|
allow(Homebrew::EnvConfig).to receive(:no_emoji?).and_return(true)
|
|
|
|
expect { installer.install_dependencies(deps) }
|
|
.to output(/outdated-dependency.*\(upgradable\).*and.*uninstalled-dependency[^(]*$/m).to_stdout
|
|
end
|
|
|
|
it "does not render the first dependency name bolder than the rest" do
|
|
ENV["HOMEBREW_COLOR"] = "1"
|
|
dep_a = formula("dep-a") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
dep_b = formula("dep-b") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
[dep_a, dep_b].each { |f| allow(f).to receive_messages(any_version_installed?: true, outdated?: true) }
|
|
deps = [
|
|
instance_double(Dependency, to_formula: dep_a, name: dep_a.name, to_s: dep_a.name),
|
|
instance_double(Dependency, to_formula: dep_b, name: dep_b.name, to_s: dep_b.name),
|
|
]
|
|
installer = described_class.new(Testball.new)
|
|
allow(installer).to receive(:install_dependency)
|
|
allow_any_instance_of(StringIO).to receive(:tty?).and_return(true)
|
|
|
|
expect { installer.install_dependencies(deps) }
|
|
.to output(/:\e\[0m /).to_stdout
|
|
end
|
|
|
|
it "shows the formula header after installing a single dependency" do
|
|
dep = formula("single-dep") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
deps = [instance_double(Dependency, to_formula: dep, name: dep.name, to_s: dep.name)]
|
|
installer = described_class.new(Testball.new)
|
|
allow(installer).to receive(:install_dependency)
|
|
|
|
installer.install_dependencies(deps)
|
|
|
|
expect(installer.show_header?).to be(true)
|
|
end
|
|
end
|
|
|
|
describe "#expand_dependencies_for_formula" do
|
|
it "checks equal dependency satisfaction once per expansion" do
|
|
shared_formula = instance_double(Formula, deps: [], name: "shared", full_name: "shared")
|
|
shared_dep = Dependency.new("shared")
|
|
repeated_shared_dep = Dependency.new("shared")
|
|
expect(shared_dep).to receive(:satisfied?).once.and_return(false)
|
|
expect(repeated_shared_dep).not_to receive(:satisfied?)
|
|
allow(shared_dep).to receive(:to_formula).and_return(shared_formula)
|
|
allow(repeated_shared_dep).to receive(:to_formula).and_return(shared_formula)
|
|
|
|
first_parent = Dependency.new("first-parent")
|
|
first_parent_formula = instance_double(Formula, deps: [shared_dep], name: "first-parent",
|
|
full_name: "first-parent")
|
|
allow(first_parent).to receive_messages(satisfied?: false, to_formula: first_parent_formula)
|
|
|
|
second_parent = Dependency.new("second-parent")
|
|
second_parent_formula = instance_double(Formula, deps: [repeated_shared_dep], name: "second-parent",
|
|
full_name: "second-parent")
|
|
allow(second_parent).to receive_messages(satisfied?: false, to_formula: second_parent_formula)
|
|
|
|
f = formula "homebrew-expand-dependencies-cache" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
allow(f).to receive(:deps).and_return([first_parent, second_parent])
|
|
|
|
installer = described_class.new(f)
|
|
build_options = BuildOptions.new(Options.new, Options.new)
|
|
allow(installer).to receive_messages(effective_build_options_for: build_options, install_bottle_for?: false)
|
|
|
|
deps = installer.expand_dependencies_for_formula(f)
|
|
|
|
expect(deps.map(&:name)).to eq(%w[shared first-parent second-parent])
|
|
end
|
|
|
|
it "checks uses_from_macos dependencies with different bounds separately" do
|
|
shared_formula = instance_double(Formula, deps: [], name: "shared", full_name: "shared")
|
|
first_dep = UsesFromMacOSDependency.new("shared", [], bounds: { since: :ventura })
|
|
second_dep = UsesFromMacOSDependency.new("shared", [], bounds: { since: :sonoma })
|
|
expect(first_dep).to receive(:satisfied?).once.and_return(false)
|
|
expect(second_dep).to receive(:satisfied?).once.and_return(false)
|
|
allow(first_dep).to receive(:to_formula).and_return(shared_formula)
|
|
allow(second_dep).to receive(:to_formula).and_return(shared_formula)
|
|
|
|
first_parent = Dependency.new("first-parent")
|
|
first_parent_formula = instance_double(Formula, deps: [first_dep], name: "first-parent",
|
|
full_name: "first-parent")
|
|
allow(first_parent).to receive_messages(satisfied?: false, to_formula: first_parent_formula)
|
|
|
|
second_parent = Dependency.new("second-parent")
|
|
second_parent_formula = instance_double(Formula, deps: [second_dep], name: "second-parent",
|
|
full_name: "second-parent")
|
|
allow(second_parent).to receive_messages(satisfied?: false, to_formula: second_parent_formula)
|
|
|
|
f = formula "homebrew-expand-uses-from-macos-dependencies-cache" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
allow(f).to receive(:deps).and_return([first_parent, second_parent])
|
|
|
|
installer = described_class.new(f)
|
|
build_options = BuildOptions.new(Options.new, Options.new)
|
|
allow(installer).to receive_messages(effective_build_options_for: build_options, install_bottle_for?: false)
|
|
|
|
deps = installer.expand_dependencies_for_formula(f)
|
|
|
|
expect(deps.map(&:name)).to eq(%w[shared first-parent second-parent])
|
|
end
|
|
end
|
|
|
|
describe "versioned keg-only linking defaults" do
|
|
let(:base_name) { "homebrew-versioned-formula" }
|
|
let(:formula_name) { "#{base_name}@1.0" }
|
|
let(:keg_only_formula) do
|
|
formula formula_name do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
end
|
|
|
|
before do
|
|
allow(keg_only_formula).to receive_messages(any_version_installed?: false,
|
|
link_overwrite_formulae: [])
|
|
end
|
|
|
|
it "does not link by default when it is not installed on request" do
|
|
fi = described_class.new(keg_only_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
|
|
it "links by default when no sibling variants are installed" do
|
|
fi = described_class.new(keg_only_formula, installed_on_request: true)
|
|
|
|
expect(fi.link_keg).to be true
|
|
end
|
|
|
|
it "does not link by default when any version is already installed" do
|
|
allow(keg_only_formula).to receive(:any_version_installed?).and_return(true)
|
|
|
|
fi = described_class.new(keg_only_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
|
|
it "links when explicitly requested" do
|
|
allow(keg_only_formula).to receive(:any_version_installed?).and_return(true)
|
|
|
|
fi = described_class.new(keg_only_formula, link_keg: true)
|
|
|
|
expect(fi.link_keg).to be true
|
|
end
|
|
|
|
it "does not link by default when another @-versioned formula is installed" do
|
|
other_version = formula "#{base_name}@2.0" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-2.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
allow(other_version).to receive(:any_version_installed?).and_return(true)
|
|
allow(keg_only_formula).to receive(:link_overwrite_formulae).and_return([other_version])
|
|
|
|
fi = described_class.new(keg_only_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
|
|
it "does not link by default when the unversioned sibling is installed" do
|
|
unversioned_formula = formula base_name do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
allow(unversioned_formula).to receive(:any_version_installed?).and_return(true)
|
|
allow(keg_only_formula).to receive(:link_overwrite_formulae).and_return([unversioned_formula])
|
|
|
|
fi = described_class.new(keg_only_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
|
|
it "does not link by default when the unversioned sibling is keg-only" do
|
|
unversioned_formula = formula base_name do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
keg_only "some reason"
|
|
end
|
|
allow(keg_only_formula).to receive(:link_overwrite_formulae).and_return([unversioned_formula])
|
|
|
|
fi = described_class.new(keg_only_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
|
|
it "does not link by default when the -full variant is installed" do
|
|
full_variant = formula "#{base_name}-full" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-full-1.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
allow(full_variant).to receive(:any_version_installed?).and_return(true)
|
|
allow(keg_only_formula).to receive(:link_overwrite_formulae).and_return([full_variant])
|
|
|
|
fi = described_class.new(keg_only_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
|
|
it "does not link by default when the non-full variant is installed" do
|
|
full_formula = formula "#{base_name}-full" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-full-1.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
non_full_variant = formula base_name do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
allow(non_full_variant).to receive(:any_version_installed?).and_return(true)
|
|
allow(full_formula).to receive_messages(any_version_installed?: false,
|
|
link_overwrite_formulae: [non_full_variant])
|
|
|
|
fi = described_class.new(full_formula)
|
|
|
|
expect(fi.link_keg).to be false
|
|
end
|
|
end
|
|
|
|
describe "#link" do
|
|
let(:versioned_formula) do
|
|
formula "homebrew-versioned-formula@1.0" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
end
|
|
let(:other_version) do
|
|
formula "homebrew-versioned-formula" do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-2.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
end
|
|
let(:keg) do
|
|
instance_double(Keg, linked?: false)
|
|
end
|
|
|
|
before do
|
|
allow(Formula).to receive(:clear_cache)
|
|
allow(Cask::Caskroom).to receive(:path).and_return(Pathname("/tmp/nonexistent-caskroom"))
|
|
allow(versioned_formula).to receive_messages(link_overwrite_formulae: [other_version],
|
|
any_version_installed?: false)
|
|
allow(other_version).to receive(:any_version_installed?).and_return(true)
|
|
end
|
|
|
|
it "only optlinks when default linking is disabled by an installed sibling" do
|
|
installer = described_class.new(versioned_formula)
|
|
|
|
expect(installer.link_keg).to be false
|
|
expect(Homebrew::Unlink).not_to receive(:unlink_link_overwrite_formulae)
|
|
expect(keg).to receive(:optlink).with(verbose: false, overwrite: false)
|
|
expect(keg).not_to receive(:link)
|
|
|
|
installer.link(keg)
|
|
end
|
|
|
|
it "unlinks siblings before linking when explicitly requested" do
|
|
installer = described_class.new(versioned_formula, link_keg: true)
|
|
|
|
expect(installer.link_keg).to be true
|
|
expect(Homebrew::Unlink).to receive(:unlink_link_overwrite_formulae).with(versioned_formula,
|
|
verbose: false).ordered
|
|
expect(keg).to receive(:link).with(verbose: false, overwrite: false).ordered
|
|
|
|
installer.link(keg)
|
|
end
|
|
end
|
|
|
|
describe "#link_manual_command_warning" do
|
|
let(:base_name) { "homebrew-versioned-formula" }
|
|
let(:formula_name) { "#{base_name}@1.0" }
|
|
let(:keg_only_formula) do
|
|
formula formula_name do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
keg_only :versioned_formula
|
|
end
|
|
end
|
|
|
|
it "explains why a versioned formula was installed but not linked" do
|
|
unversioned_formula = formula base_name do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo-1.0"
|
|
end
|
|
allow(unversioned_formula).to receive_messages(any_version_installed?: true, linked?: true)
|
|
allow(keg_only_formula).to receive_messages(any_version_installed?: false, linked?: false,
|
|
link_overwrite_formulae: [unversioned_formula])
|
|
|
|
installer = described_class.new(keg_only_formula, installed_on_request: true)
|
|
|
|
expect(installer.link_manual_command_warning).to eq <<~EOS
|
|
#{formula_name} was installed but not linked because #{base_name} is already linked.
|
|
To link this version, run:
|
|
brew link #{formula_name}
|
|
EOS
|
|
end
|
|
end
|
|
|
|
describe "#check_install_sanity" do
|
|
it "raises on direct cyclic dependency" do
|
|
ENV["HOMEBREW_DEVELOPER"] = "1"
|
|
|
|
dep_name = "homebrew-test-cyclic"
|
|
dep_path = CoreTap.instance.new_formula_path(dep_name)
|
|
dep_path.write <<~RUBY
|
|
class #{Formulary.class_s(dep_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
depends_on "#{dep_name}"
|
|
end
|
|
RUBY
|
|
f = Formulary.factory(dep_name)
|
|
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.check_install_sanity
|
|
end.to raise_error(CannotInstallFormulaError)
|
|
end
|
|
|
|
it "raises on indirect cyclic dependency" do
|
|
ENV["HOMEBREW_DEVELOPER"] = "1"
|
|
|
|
formula1_name = "homebrew-test-formula1"
|
|
formula2_name = "homebrew-test-formula2"
|
|
formula1_path = CoreTap.instance.new_formula_path(formula1_name)
|
|
formula1_path.write <<~RUBY
|
|
class #{Formulary.class_s(formula1_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
depends_on "#{formula2_name}"
|
|
end
|
|
RUBY
|
|
formula1 = Formulary.factory(formula1_name)
|
|
|
|
formula2_path = CoreTap.instance.new_formula_path(formula2_name)
|
|
formula2_path.write <<~RUBY
|
|
class #{Formulary.class_s(formula2_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
depends_on "#{formula1_name}"
|
|
end
|
|
RUBY
|
|
|
|
fi = described_class.new(formula1)
|
|
|
|
expect do
|
|
fi.check_install_sanity
|
|
end.to raise_error(CannotInstallFormulaError)
|
|
end
|
|
|
|
it "raises on pinned dependency" do
|
|
dep_name = "homebrew-test-dependency"
|
|
dep_path = CoreTap.instance.new_formula_path(dep_name)
|
|
dep_path.write <<~RUBY
|
|
class #{Formulary.class_s(dep_name)} < Formula
|
|
url "foo"
|
|
version "0.2"
|
|
end
|
|
RUBY
|
|
|
|
dependency = Formulary.factory(dep_name)
|
|
|
|
dependent = formula do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo"
|
|
version "0.5"
|
|
depends_on dependency.name.to_s
|
|
end
|
|
|
|
(dependency.prefix("0.1")/"bin"/"a").mkpath
|
|
HOMEBREW_PINNED_KEGS.mkpath
|
|
FileUtils.ln_s dependency.prefix("0.1"), HOMEBREW_PINNED_KEGS/dep_name
|
|
|
|
dependency_keg = Keg.new(dependency.prefix("0.1"))
|
|
dependency_keg.link
|
|
|
|
expect(dependency_keg).to be_linked
|
|
expect(dependency).to be_pinned
|
|
|
|
fi = described_class.new(dependent)
|
|
|
|
expect do
|
|
fi.check_install_sanity
|
|
end.to raise_error(CannotInstallFormulaError)
|
|
end
|
|
end
|
|
|
|
describe "#forbidden_license_check" do
|
|
it "raises on forbidden license on formula" do
|
|
ENV["HOMEBREW_FORBIDDEN_LICENSES"] = "AGPL-3.0"
|
|
|
|
f_name = "homebrew-forbidden-license"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
license "AGPL-3.0"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_license_check
|
|
end.to raise_error(CannotInstallFormulaError, /#{f_name}'s licenses are all forbidden/)
|
|
end
|
|
|
|
it "raises on forbidden license on formula with contact instructions" do
|
|
ENV["HOMEBREW_FORBIDDEN_LICENSES"] = "AGPL-3.0"
|
|
ENV["HOMEBREW_FORBIDDEN_OWNER"] = owner = "your dog"
|
|
ENV["HOMEBREW_FORBIDDEN_OWNER_CONTACT"] = contact = "Woof loudly to get this unblocked."
|
|
|
|
f_name = "homebrew-forbidden-license"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
license "AGPL-3.0"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_license_check
|
|
end.to raise_error(CannotInstallFormulaError, /#{owner}.+\n#{contact}/m)
|
|
end
|
|
|
|
it "raises on forbidden license on dependency" do
|
|
ENV["HOMEBREW_FORBIDDEN_LICENSES"] = "GPL-3.0"
|
|
|
|
dep_name = "homebrew-forbidden-dependency-license"
|
|
dep_path = CoreTap.instance.new_formula_path(dep_name)
|
|
dep_path.write <<~RUBY
|
|
class #{Formulary.class_s(dep_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
license "GPL-3.0"
|
|
end
|
|
RUBY
|
|
|
|
f_name = "homebrew-forbidden-dependent-license"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
depends_on "#{dep_name}"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_license_check
|
|
end.to raise_error(CannotInstallFormulaError, /dependency on #{dep_name} where all/)
|
|
end
|
|
|
|
it "raises on forbidden symbol license on formula" do
|
|
ENV["HOMEBREW_FORBIDDEN_LICENSES"] = "public_domain"
|
|
|
|
f_name = "homebrew-forbidden-license"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
license :public_domain
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_license_check
|
|
end.to raise_error(CannotInstallFormulaError, /#{f_name}'s licenses are all forbidden/)
|
|
end
|
|
end
|
|
|
|
describe "#forbidden_tap_check" do
|
|
before do
|
|
allow(Tap).to receive_messages(allowed_taps: allowed_taps_set, forbidden_taps: forbidden_taps_set)
|
|
allow(Homebrew::Trust).to receive(:trusted_tap?).and_return(true)
|
|
end
|
|
|
|
let(:homebrew_forbidden) { Tap.fetch("homebrew/forbidden") }
|
|
let(:allowed_third_party) { Tap.fetch("nothomebrew/allowed") }
|
|
let(:disallowed_third_party) { Tap.fetch("nothomebrew/notallowed") }
|
|
let(:allowed_taps_set) { [allowed_third_party.name] }
|
|
let(:forbidden_taps_set) { [homebrew_forbidden.name] }
|
|
|
|
it "raises on forbidden tap on formula" do
|
|
f_tap = homebrew_forbidden
|
|
f_name = "homebrew-forbidden-tap"
|
|
f_path = homebrew_forbidden.new_formula_path(f_name)
|
|
f_path.parent.mkpath
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory("#{f_tap}/#{f_name}")
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_tap_check
|
|
end.to raise_error(CannotInstallFormulaError, /has the tap #{f_tap}/)
|
|
ensure
|
|
FileUtils.rm_r(f_path.parent.parent)
|
|
end
|
|
|
|
it "raises on not allowed third-party tap on formula" do
|
|
f_tap = disallowed_third_party
|
|
f_name = "homebrew-not-allowed-tap"
|
|
f_path = disallowed_third_party.new_formula_path(f_name)
|
|
f_path.parent.mkpath
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory("#{f_tap}/#{f_name}")
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_tap_check
|
|
end.to raise_error(CannotInstallFormulaError, /has the tap #{f_tap}/)
|
|
ensure
|
|
FileUtils.rm_r(f_path.parent.parent.parent)
|
|
end
|
|
|
|
it "does not raise on allowed tap on formula" do
|
|
f_tap = allowed_third_party
|
|
f_name = "homebrew-allowed-tap"
|
|
f_path = allowed_third_party.new_formula_path(f_name)
|
|
f_path.parent.mkpath
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory("#{f_tap}/#{f_name}")
|
|
fi = described_class.new(f)
|
|
|
|
expect { fi.forbidden_tap_check }.not_to raise_error
|
|
ensure
|
|
FileUtils.rm_r(f_path.parent.parent.parent)
|
|
end
|
|
|
|
it "raises on forbidden tap on dependency" do
|
|
dep_tap = homebrew_forbidden
|
|
dep_name = "homebrew-forbidden-dependency-tap"
|
|
dep_path = homebrew_forbidden.new_formula_path(dep_name)
|
|
dep_path.parent.mkpath
|
|
dep_path.write <<~RUBY
|
|
class #{Formulary.class_s(dep_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f_name = "homebrew-forbidden-dependent-tap"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
depends_on "#{dep_name}"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_tap_check
|
|
end.to raise_error(CannotInstallFormulaError, /from the #{dep_tap} tap but/)
|
|
ensure
|
|
FileUtils.rm_r(dep_path.parent.parent)
|
|
end
|
|
end
|
|
|
|
describe "#forbidden_formula_check" do
|
|
it "raises on forbidden formula" do
|
|
ENV["HOMEBREW_FORBIDDEN_FORMULAE"] = f_name = "homebrew-forbidden-formula"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_formula_check
|
|
end.to raise_error(CannotInstallFormulaError, /#{f_name} was forbidden/)
|
|
end
|
|
|
|
it "raises on forbidden dependency" do
|
|
ENV["HOMEBREW_FORBIDDEN_FORMULAE"] = dep_name = "homebrew-forbidden-dependency-formula"
|
|
dep_path = CoreTap.instance.new_formula_path(dep_name)
|
|
dep_path.write <<~RUBY
|
|
class #{Formulary.class_s(dep_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f_name = "homebrew-forbidden-dependent-formula"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
depends_on "#{dep_name}"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
fi = described_class.new(f)
|
|
|
|
expect do
|
|
fi.forbidden_formula_check
|
|
end.to raise_error(CannotInstallFormulaError, /#{dep_name} formula was forbidden/)
|
|
end
|
|
end
|
|
|
|
describe "#prelude_fetch" do
|
|
context "with an API-loaded bottled formula" do
|
|
let(:deno_formula) do
|
|
formula("deno") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/deno-2.7.11.tar.gz"
|
|
end
|
|
end
|
|
let(:formula_struct) do
|
|
Homebrew::API::FormulaStruct.new(
|
|
bottle_checksums: [
|
|
{
|
|
cellar: :any_skip_relocation,
|
|
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97",
|
|
},
|
|
],
|
|
bottle_present: true,
|
|
desc: "deno",
|
|
homepage: "https://brew.sh",
|
|
license: "MIT",
|
|
ruby_source_checksum: "abc123",
|
|
stable_present: true,
|
|
stable_version: "2.7.11",
|
|
)
|
|
end
|
|
let(:installer) do
|
|
installer = described_class.new(deno_formula, ignore_deps: true)
|
|
installer.download_queue = instance_double(Homebrew::DownloadQueue)
|
|
installer
|
|
end
|
|
|
|
before do
|
|
allow(deno_formula).to receive_messages(
|
|
bottle_tag?: true,
|
|
core_formula?: true,
|
|
loaded_from_internal_api?: true,
|
|
pour_bottle?: true,
|
|
)
|
|
allow(Homebrew::API::Internal).to receive(:formula_struct).with("deno").and_return(formula_struct)
|
|
end
|
|
|
|
it "uses API bottle metadata to enqueue the manifest and bottle" do
|
|
expect(deno_formula).not_to receive(:bottle_for_tag)
|
|
expect(deno_formula).not_to receive(:bottle)
|
|
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest))
|
|
expect(installer.download_queue).to receive(:enqueue)
|
|
.with(an_instance_of(Bottle), check_attestation: false, stage: true)
|
|
|
|
installer.prelude_fetch
|
|
end
|
|
|
|
it "enqueues only the bottle manifest when fetching metadata" do
|
|
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest))
|
|
|
|
installer.prelude_fetch(metadata_only: true)
|
|
end
|
|
|
|
it "enqueues the bottle without repeating metadata work after a metadata-only run" do
|
|
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest)).once
|
|
|
|
installer.prelude_fetch(metadata_only: true)
|
|
|
|
expect(installer.download_queue).to receive(:enqueue)
|
|
.with(an_instance_of(Bottle), check_attestation: false, stage: true)
|
|
|
|
installer.prelude_fetch
|
|
end
|
|
end
|
|
|
|
it "does not repeat source download prelude work" do
|
|
f = formula("homebrew-prelude-fetch-once") do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "https://brew.sh/prelude-fetch-once-0.1.tar.gz"
|
|
end
|
|
allow(f).to receive(:loaded_from_api?).and_return(true)
|
|
fi = described_class.new(f, ignore_deps: true)
|
|
fi.download_queue = instance_double(Homebrew::DownloadQueue)
|
|
|
|
expect(Homebrew::API::Formula).to receive(:source_download)
|
|
.with(f, download_queue: fi.download_queue, enqueue: true)
|
|
.once
|
|
|
|
fi.prelude_fetch
|
|
fi.prelude_fetch
|
|
end
|
|
|
|
it "raises on forbidden formula tap before fetching the source from the API" do
|
|
homebrew_forbidden = Tap.fetch("homebrew/forbidden")
|
|
allow(Tap).to receive_messages(allowed_taps: [], forbidden_taps: [homebrew_forbidden.name])
|
|
f_name = "homebrew-forbidden-fail-fast-tap"
|
|
f_path = homebrew_forbidden.new_formula_path(f_name)
|
|
f_path.parent.mkpath
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory("#{homebrew_forbidden}/#{f_name}")
|
|
allow(f).to receive(:loaded_from_api?).and_return(true)
|
|
fi = described_class.new(f)
|
|
|
|
expect(Homebrew::API::Formula).not_to receive(:source_download)
|
|
|
|
expect { fi.prelude_fetch }.to raise_error(CannotInstallFormulaError, /has the tap #{homebrew_forbidden}/)
|
|
ensure
|
|
FileUtils.rm_r(f_path.parent.parent)
|
|
end
|
|
|
|
it "raises on forbidden formula before fetching the source from the API" do
|
|
ENV["HOMEBREW_FORBIDDEN_FORMULAE"] = f_name = "homebrew-forbidden-fail-fast-formula"
|
|
f_path = CoreTap.instance.new_formula_path(f_name)
|
|
f_path.write <<~RUBY
|
|
class #{Formulary.class_s(f_name)} < Formula
|
|
url "foo"
|
|
version "0.1"
|
|
end
|
|
RUBY
|
|
|
|
f = Formulary.factory(f_name)
|
|
allow(f).to receive(:loaded_from_api?).and_return(true)
|
|
fi = described_class.new(f)
|
|
|
|
expect(Homebrew::API::Formula).not_to receive(:source_download)
|
|
|
|
expect { fi.prelude_fetch }.to raise_error(CannotInstallFormulaError, /was forbidden/)
|
|
end
|
|
end
|
|
|
|
specify "install fails with BuildError when a system() call fails" do
|
|
ENV["HOMEBREW_TEST_NO_EXIT_CLEANUP"] = "1"
|
|
ENV["FAILBALL_BUILD_ERROR"] = "1"
|
|
|
|
expect do
|
|
temporary_install(Failball.new)
|
|
end.to raise_error(BuildError)
|
|
end
|
|
|
|
specify "install fails with a RuntimeError when #install raises" do
|
|
ENV["HOMEBREW_TEST_NO_EXIT_CLEANUP"] = "1"
|
|
|
|
expect do
|
|
temporary_install(Failball.new)
|
|
end.to raise_error(RuntimeError)
|
|
end
|
|
|
|
describe "#caveats" do
|
|
subject(:formula_installer) { described_class.new(Testball.new) }
|
|
|
|
it "shows audit problems if HOMEBREW_DEVELOPER is set" do
|
|
ENV["HOMEBREW_DEVELOPER"] = "1"
|
|
with_env(HOMEBREW_NO_INSTALL_FROM_API: "1") do
|
|
formula_installer.fetch
|
|
formula_installer.install
|
|
end
|
|
expect(formula_installer).to receive(:audit_installed).and_call_original
|
|
formula_installer.caveats
|
|
end
|
|
end
|
|
|
|
describe "#install_service" do
|
|
it "works if service is set" do
|
|
formula = Testball.new
|
|
service = Homebrew::Service.new(formula)
|
|
launchd_service_path = formula.launchd_service_path
|
|
service_path = formula.systemd_service_path
|
|
formula.opt_prefix.mkpath
|
|
|
|
expect(formula).to receive(:service?).and_return(true)
|
|
expect(formula).to receive(:service).at_least(:once).and_return(service)
|
|
expect(formula).to receive(:launchd_service_path).and_call_original
|
|
expect(formula).to receive(:systemd_service_path).and_call_original
|
|
|
|
expect(service).to receive(:timed?).and_return(false)
|
|
expect(service).to receive(:command?).and_return(true)
|
|
expect(service).to receive(:to_plist).and_return("plist")
|
|
expect(service).to receive(:to_systemd_unit).and_return("unit")
|
|
|
|
installer = described_class.new(formula)
|
|
expect do
|
|
installer.install_service
|
|
end.not_to output(/Error: Failed to install service files/).to_stderr
|
|
|
|
expect(launchd_service_path).to exist
|
|
expect(service_path).to exist
|
|
end
|
|
|
|
it "works if timed service is set" do
|
|
formula = Testball.new
|
|
service = Homebrew::Service.new(formula)
|
|
launchd_service_path = formula.launchd_service_path
|
|
service_path = formula.systemd_service_path
|
|
timer_path = formula.systemd_timer_path
|
|
formula.opt_prefix.mkpath
|
|
|
|
expect(formula).to receive(:service?).and_return(true)
|
|
expect(formula).to receive(:service).at_least(:once).and_return(service)
|
|
expect(formula).to receive(:launchd_service_path).and_call_original
|
|
expect(formula).to receive(:systemd_service_path).and_call_original
|
|
expect(formula).to receive(:systemd_timer_path).and_call_original
|
|
|
|
expect(service).to receive(:timed?).and_return(true)
|
|
expect(service).to receive(:command?).and_return(true)
|
|
expect(service).to receive(:to_plist).and_return("plist")
|
|
expect(service).to receive(:to_systemd_unit).and_return("unit")
|
|
expect(service).to receive(:to_systemd_timer).and_return("timer")
|
|
|
|
installer = described_class.new(formula)
|
|
expect do
|
|
installer.install_service
|
|
end.not_to output(/Error: Failed to install service files/).to_stderr
|
|
|
|
expect(launchd_service_path).to exist
|
|
expect(service_path).to exist
|
|
expect(timer_path).to exist
|
|
end
|
|
|
|
it "returns without definition" do
|
|
formula = Testball.new
|
|
path = formula.launchd_service_path
|
|
formula.opt_prefix.mkpath
|
|
|
|
expect(formula).to receive(:service?).and_return(nil)
|
|
expect(formula).not_to receive(:launchd_service_path)
|
|
|
|
installer = described_class.new(formula)
|
|
expect do
|
|
installer.install_service
|
|
end.not_to output(/Error: Failed to install service files/).to_stderr
|
|
|
|
expect(path).not_to exist
|
|
end
|
|
end
|
|
|
|
describe "#build" do
|
|
it "attempts source download when formula is loaded from API" do
|
|
formula = Testball.new
|
|
allow(formula).to receive(:loaded_from_api?).and_return(true)
|
|
|
|
source_formula = Testball.new
|
|
allow(source_formula).to receive(:loaded_from_api?).and_return(false)
|
|
|
|
expect(Homebrew::API::Formula).to receive(:source_download_formula)
|
|
.with(formula)
|
|
.and_return(source_formula)
|
|
|
|
installer = described_class.new(formula)
|
|
|
|
# Stub out the actual build subprocess since we only care about the guard
|
|
allow(installer).to receive(:build_argv).and_return([])
|
|
allow(Utils).to receive(:safe_fork)
|
|
allow(source_formula).to receive_messages(logs: mktmpdir, update_head_version: nil, prefix: mktmpdir,
|
|
network_access_allowed?: true)
|
|
allow(Keg).to receive(:new).and_return(instance_double(Keg, empty_installation?: false))
|
|
|
|
installer.build
|
|
|
|
expect(installer.formula).to eq(source_formula)
|
|
end
|
|
|
|
it "raises when formula is loaded from API and source download fails" do
|
|
formula = Testball.new
|
|
allow(formula).to receive(:loaded_from_api?).and_return(true)
|
|
|
|
expect(Homebrew::API::Formula).to receive(:source_download_formula)
|
|
.with(formula)
|
|
.and_raise(CannotInstallFormulaError, "source code not found")
|
|
|
|
installer = described_class.new(formula)
|
|
|
|
expect do
|
|
installer.build
|
|
end.to raise_error(CannotInstallFormulaError, /source code not found/)
|
|
end
|
|
|
|
it "exposes local formula and trust paths to the sandbox" do
|
|
formula_path = mktmpdir/"homebrew-local-formula.rb"
|
|
FileUtils.touch formula_path
|
|
formula = formula("homebrew-local-formula", path: formula_path) do
|
|
T.bind(self, T.class_of(Formula))
|
|
url "foo"
|
|
version "1.0"
|
|
end
|
|
installer = described_class.new(formula)
|
|
sandbox = instance_double(Sandbox)
|
|
|
|
allow(installer).to receive(:build_argv).and_return([])
|
|
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
|
|
allow(sandbox).to receive_messages(record_log: nil, allow_read_if_exists: nil, allow_write_temp_and_cache: nil,
|
|
allow_write_log: nil, allow_cvs: nil, allow_fossil: nil,
|
|
allow_write_xcode: nil, allow_write_cellar: nil, deny_read_home: nil,
|
|
run: nil)
|
|
allow(formula).to receive_messages(logs: mktmpdir, update_head_version: nil, prefix: mktmpdir,
|
|
network_access_allowed?: true)
|
|
allow(Keg).to receive(:new).and_return(instance_double(Keg, empty_installation?: false))
|
|
|
|
expect(sandbox).to receive(:allow_read_if_exists).with(path: formula_path).ordered
|
|
expect(sandbox).to receive(:allow_read_if_exists).with(path: Homebrew::Trust.trust_file).ordered
|
|
|
|
with_env(HOMEBREW_REQUIRE_TAP_TRUST: "1") do
|
|
installer.build
|
|
end
|
|
end
|
|
end
|
|
end
|