brew/Library/Homebrew/test/formulary_spec.rb
Mike McQuaid bbdea29a0f
Deprecate installing casks/formulae from paths.
We've already disabled installing casks/formulae from URLs and we
regularly tell people not to install from paths so let's just deprecate
this behaviour entirely.

Even Homebrew developers do not need to work this way.
2024-09-26 20:25:07 +01:00

780 lines
26 KiB
Ruby

# frozen_string_literal: true
require "formula"
require "formula_installer"
require "utils/bottles"
RSpec.describe Formulary do
let(:formula_name) { "testball_bottle" }
let(:formula_path) { CoreTap.instance.new_formula_path(formula_name) }
let(:formula_content) do
<<~RUBY
class #{described_class.class_s(formula_name)} < Formula
url "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz"
sha256 TESTBALL_SHA256
bottle do
root_url "file://#{bottle_dir}"
sha256 cellar: :any_skip_relocation, #{Utils::Bottles.tag}: "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
end
def install
prefix.install "bin"
prefix.install "libexec"
end
end
RUBY
end
let(:bottle_dir) { Pathname.new("#{TEST_FIXTURE_DIR}/bottles") }
let(:bottle) { bottle_dir/"testball_bottle-0.1.#{Utils::Bottles.tag}.bottle.tar.gz" }
describe "::class_s" do
it "replaces '+' with 'x'" do
expect(described_class.class_s("foo++")).to eq("Fooxx")
end
it "converts a string with dots to PascalCase" do
expect(described_class.class_s("shell.fm")).to eq("ShellFm")
end
it "converts a string with hyphens to PascalCase" do
expect(described_class.class_s("pkg-config")).to eq("PkgConfig")
end
it "converts a string with a single letter separated by a hyphen to PascalCase" do
expect(described_class.class_s("s-lang")).to eq("SLang")
end
it "converts a string with underscores to PascalCase" do
expect(described_class.class_s("foo_bar")).to eq("FooBar")
end
it "replaces '@' with 'AT'" do
expect(described_class.class_s("openssl@1.1")).to eq("OpensslAT11")
end
end
describe "::factory" do
context "without the API" do
before do
formula_path.dirname.mkpath
formula_path.write formula_content
end
it "returns a Formula" do
expect(described_class.factory(formula_name)).to be_a(Formula)
end
it "returns a Formula when given a fully qualified name" do
expect(described_class.factory("homebrew/core/#{formula_name}")).to be_a(Formula)
end
it "raises an error if the Formula cannot be found" do
expect do
described_class.factory("not_existed_formula")
end.to raise_error(FormulaUnavailableError)
end
it "raises an error if ref is nil" do
expect do
described_class.factory(nil)
end.to raise_error(TypeError)
end
context "with sharded Formula directory" do
let(:formula_name) { "testball_sharded" }
let(:formula_path) do
core_tap = CoreTap.instance
(core_tap.formula_dir/formula_name[0]).mkpath
core_tap.new_formula_path(formula_name)
end
it "returns a Formula" do
expect(described_class.factory(formula_name)).to be_a(Formula)
end
it "returns a Formula when given a fully qualified name" do
expect(described_class.factory("homebrew/core/#{formula_name}")).to be_a(Formula)
end
end
context "when the Formula has the wrong class" do
let(:formula_name) { "giraffe" }
let(:formula_content) do
<<~RUBY
class Wrong#{described_class.class_s(formula_name)} < Formula
end
RUBY
end
it "raises an error" do
expect do
described_class.factory(formula_name)
end.to raise_error(TapFormulaClassUnavailableError)
end
end
it "returns a Formula when given a path" do
expect(described_class.factory(formula_path)).to be_a(Formula)
end
it "errors when given a path but paths are disabled" do
ENV["HOMEBREW_FORBID_PACKAGES_FROM_PATHS"] = "1"
FileUtils.cp formula_path, HOMEBREW_TEMP
temp_formula_path = HOMEBREW_TEMP/formula_path.basename
expect do
described_class.factory(temp_formula_path)
ensure
temp_formula_path.unlink
end.to raise_error(FormulaUnavailableError)
end
it "errors when given a URL but paths are disabled" do
ENV["HOMEBREW_FORBID_PACKAGES_FROM_PATHS"] = "1"
expect do
described_class.factory("file://#{formula_path}")
end.to raise_error(FormulaUnavailableError)
end
context "when given a bottle" do
subject(:formula) { described_class.factory(bottle) }
it "returns a Formula" do
expect(formula).to be_a(Formula)
end
it "calling #local_bottle_path on the returned Formula returns the bottle path" do
expect(formula.local_bottle_path).to eq(bottle.realpath)
end
end
context "when given an alias" do
subject(:formula) { described_class.factory("foo") }
let(:alias_dir) { CoreTap.instance.alias_dir }
let(:alias_path) { alias_dir/"foo" }
before do
alias_dir.mkpath
FileUtils.ln_s formula_path, alias_path
end
it "returns a Formula" do
expect(formula).to be_a(Formula)
end
it "calling #alias_path on the returned Formula returns the alias path" do
expect(formula.alias_path).to eq(alias_path)
end
end
context "with installed Formula" do
before do
# don't try to load/fetch gcc/glibc
allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)
end
let(:installed_formula) { described_class.factory(formula_path) }
let(:installer) { FormulaInstaller.new(installed_formula) }
it "returns a Formula when given a rack" do
installer.fetch
installer.install
f = described_class.from_rack(installed_formula.rack)
expect(f).to be_a(Formula)
end
it "returns a Formula when given a Keg" do
installer.fetch
installer.install
keg = Keg.new(installed_formula.prefix)
f = described_class.from_keg(keg)
expect(f).to be_a(Formula)
end
end
context "when migrating from a Tap" do
let(:tap) { Tap.fetch("homebrew", "foo") }
let(:another_tap) { Tap.fetch("homebrew", "bar") }
let(:tap_migrations_path) { tap.path/"tap_migrations.json" }
let(:another_tap_formula_path) { another_tap.path/"Formula/#{formula_name}.rb" }
before do
tap.path.mkpath
another_tap_formula_path.dirname.mkpath
another_tap_formula_path.write formula_content
end
after do
FileUtils.rm_rf tap.path
FileUtils.rm_rf another_tap.path
end
it "returns a Formula that has gone through a tap migration into homebrew/core" do
tap_migrations_path.write <<~EOS
{
"#{formula_name}": "homebrew/core"
}
EOS
formula = described_class.factory("#{tap}/#{formula_name}")
expect(formula).to be_a(Formula)
expect(formula.tap).to eq(CoreTap.instance)
expect(formula.path).to eq(formula_path)
end
it "returns a Formula that has gone through a tap migration into another tap" do
tap_migrations_path.write <<~EOS
{
"#{formula_name}": "#{another_tap}"
}
EOS
formula = described_class.factory("#{tap}/#{formula_name}")
expect(formula).to be_a(Formula)
expect(formula.tap).to eq(another_tap)
expect(formula.path).to eq(another_tap_formula_path)
end
end
context "when loading from Tap" do
let(:tap) { Tap.fetch("homebrew", "foo") }
let(:another_tap) { Tap.fetch("homebrew", "bar") }
let(:formula_path) { tap.path/"Formula/#{formula_name}.rb" }
let(:alias_name) { "bar" }
let(:alias_dir) { tap.alias_dir }
let(:alias_path) { alias_dir/alias_name }
before do
alias_dir.mkpath
FileUtils.ln_s formula_path, alias_path
end
it "returns a Formula when given a name" do
expect(described_class.factory(formula_name)).to be_a(Formula)
end
it "returns a Formula from an Alias path" do
expect(described_class.factory(alias_name)).to be_a(Formula)
end
it "returns a Formula from a fully qualified Alias path" do
expect(described_class.factory("#{tap.name}/#{alias_name}")).to be_a(Formula)
end
it "raises an error when the Formula cannot be found" do
expect do
described_class.factory("#{tap}/not_existed_formula")
end.to raise_error(TapFormulaUnavailableError)
end
it "returns a Formula when given a fully qualified name" do
expect(described_class.factory("#{tap}/#{formula_name}")).to be_a(Formula)
end
it "raises an error if a Formula is in multiple Taps" do
(another_tap.path/"Formula").mkpath
(another_tap.path/"Formula/#{formula_name}.rb").write formula_content
expect do
described_class.factory(formula_name)
end.to raise_error(TapFormulaAmbiguityError)
end
end
end
context "with the API" do
def formula_json_contents(extra_items = {})
{
formula_name => {
"desc" => "testball",
"homepage" => "https://example.com",
"license" => "MIT",
"revision" => 0,
"version_scheme" => 0,
"versions" => { "stable" => "0.1" },
"urls" => {
"stable" => {
"url" => "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz",
"tag" => nil,
"revision" => nil,
},
},
"bottle" => {
"stable" => {
"rebuild" => 0,
"root_url" => "file://#{bottle_dir}",
"files" => {
Utils::Bottles.tag.to_s => {
"cellar" => ":any",
"url" => "file://#{bottle_dir}/#{formula_name}",
"sha256" => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97",
},
},
},
},
"keg_only_reason" => {
"reason" => ":provided_by_macos",
"explanation" => "",
},
"build_dependencies" => ["build_dep"],
"dependencies" => ["dep"],
"test_dependencies" => ["test_dep"],
"recommended_dependencies" => ["recommended_dep"],
"optional_dependencies" => ["optional_dep"],
"uses_from_macos" => ["uses_from_macos_dep"],
"requirements" => [
{
"name" => "xcode",
"cask" => nil,
"download" => nil,
"version" => "1.0",
"contexts" => ["build"],
},
],
"conflicts_with" => ["conflicting_formula"],
"conflicts_with_reasons" => ["it does"],
"link_overwrite" => ["bin/abc"],
"caveats" => "example caveat string\n/$HOME\n$HOMEBREW_PREFIX",
"service" => {
"name" => { macos: "custom.launchd.name", linux: "custom.systemd.name" },
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
"run_type" => "immediate",
"working_dir" => "/$HOME",
},
"ruby_source_path" => "Formula/#{formula_name}.rb",
"ruby_source_checksum" => { "sha256" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ" },
}.merge(extra_items),
}
end
let(:deprecate_json) do
{
"deprecation_date" => "2022-06-15",
"deprecation_reason" => "repo_archived",
}
end
let(:disable_json) do
{
"disable_date" => "2022-06-15",
"disable_reason" => "requires something else",
}
end
let(:variations_json) do
{
"variations" => {
Utils::Bottles.tag.to_s => {
"dependencies" => ["dep", "variations_dep"],
},
},
}
end
let(:older_macos_variations_json) do
{
"variations" => {
Utils::Bottles.tag.to_s => {
"dependencies" => ["uses_from_macos_dep"],
},
},
}
end
let(:linux_variations_json) do
{
"variations" => {
"x86_64_linux" => {
"dependencies" => ["dep", "uses_from_macos_dep"],
},
},
}
end
before do
ENV.delete("HOMEBREW_NO_INSTALL_FROM_API")
# avoid unnecessary network calls
allow(Homebrew::API::Formula).to receive_messages(all_aliases: {}, all_renames: {})
allow(CoreTap.instance).to receive(:tap_migrations).and_return({})
allow(CoreCaskTap.instance).to receive(:tap_migrations).and_return({})
# don't try to load/fetch gcc/glibc
allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)
end
it "returns a Formula when given a name" do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.keg_only_reason.reason).to eq :provided_by_macos
expect(formula.declared_deps.count).to eq 6
if OS.mac?
expect(formula.deps.count).to eq 5
else
expect(formula.deps.count).to eq 6
end
expect(formula.requirements.count).to eq 1
req = formula.requirements.first
expect(req).to be_an_instance_of XcodeRequirement
expect(req.version).to eq "1.0"
expect(req.tags).to eq [:build]
expect(formula.conflicts.map(&:name)).to include "conflicting_formula"
expect(formula.conflicts.map(&:reason)).to include "it does"
expect(formula.class.link_overwrite_paths).to include "bin/abc"
expect(formula.caveats).to eq "example caveat string\n#{Dir.home}\n#{HOMEBREW_PREFIX}"
expect(formula).to be_a_service
expect(formula.service.command).to eq(["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"])
expect(formula.service.run_type).to eq(:immediate)
expect(formula.service.working_dir).to eq(Dir.home)
expect(formula.plist_name).to eq("custom.launchd.name")
expect(formula.service_name).to eq("custom.systemd.name")
expect(formula.ruby_source_checksum.hexdigest).to eq("abcdefghijklmnopqrstuvwxyz")
expect do
formula.install
end.to raise_error("Cannot build from source from abstract formula.")
end
it "returns a deprecated Formula when given a name" do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(deprecate_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.deprecated?).to be true
expect do
formula.install
end.to raise_error("Cannot build from source from abstract formula.")
end
it "returns a disabled Formula when given a name" do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(disable_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.disabled?).to be true
expect do
formula.install
end.to raise_error("Cannot build from source from abstract formula.")
end
it "returns a Formula with variations when given a name", :needs_macos do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(variations_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.declared_deps.count).to eq 7
expect(formula.deps.count).to eq 6
expect(formula.deps.map(&:name).include?("variations_dep")).to be true
expect(formula.deps.map(&:name).include?("uses_from_macos_dep")).to be false
end
it "returns a Formula without duplicated deps and uses_from_macos with variations on Linux", :needs_linux do
allow(Homebrew::API::Formula)
.to receive(:all_formulae).and_return formula_json_contents(linux_variations_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.declared_deps.count).to eq 6
expect(formula.deps.count).to eq 6
expect(formula.deps.map(&:name).include?("uses_from_macos_dep")).to be true
end
it "returns a Formula with the correct uses_from_macos dep on older macOS", :needs_macos do
allow(Homebrew::API::Formula)
.to receive(:all_formulae).and_return formula_json_contents(older_macos_variations_json)
formula = described_class.factory(formula_name)
expect(formula).to be_a(Formula)
expect(formula.declared_deps.count).to eq 6
expect(formula.deps.count).to eq 5
expect(formula.deps.map(&:name).include?("uses_from_macos_dep")).to be true
end
context "with core tap migration renames" do
let(:foo_tap) { Tap.fetch("homebrew", "foo") }
before do
allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents
foo_tap.path.mkpath
end
after do
FileUtils.rm_rf foo_tap.path
end
it "returns the tap migration rename by old formula_name" do
old_formula_name = "#{formula_name}-old"
(foo_tap.path/"tap_migrations.json").write <<~JSON
{ "#{old_formula_name}": "homebrew/core/#{formula_name}" }
JSON
loader = described_class::FromNameLoader.try_new(old_formula_name)
expect(loader).to be_a(described_class::FromAPILoader)
expect(loader.name).to eq formula_name
expect(loader.path).not_to exist
end
it "returns the tap migration rename by old full name" do
old_formula_name = "#{formula_name}-old"
(foo_tap.path/"tap_migrations.json").write <<~JSON
{ "#{old_formula_name}": "homebrew/core/#{formula_name}" }
JSON
loader = described_class::FromTapLoader.try_new("#{foo_tap}/#{old_formula_name}")
expect(loader).to be_a(described_class::FromAPILoader)
expect(loader.name).to eq formula_name
expect(loader.path).not_to exist
end
end
end
context "when passed a URL" do
it "raises an error when given an https URL" do
expect do
described_class.factory("https://brew.sh/foo.rb")
end.to raise_error(MethodDeprecatedError)
end
it "raises an error when given a bottle URL" do
expect do
described_class.factory("https://brew.sh/foo-1.0.arm64_catalina.bottle.tar.gz")
end.to raise_error(MethodDeprecatedError)
end
it "raises an error when given an ftp URL" do
expect do
described_class.factory("ftp://brew.sh/foo.rb")
end.to raise_error(MethodDeprecatedError)
end
it "raises an error when given an sftp URL" do
expect do
described_class.factory("sftp://brew.sh/foo.rb")
end.to raise_error(MethodDeprecatedError)
end
it "raises an error when given a file URL" do
expect do
described_class.factory("file://#{TEST_FIXTURE_DIR}/testball.rb")
end.to raise_error(MethodDeprecatedError)
end
end
context "when passed ref with spaces" do
it "raises a FormulaUnavailableError error" do
expect do
described_class.factory("foo bar")
end.to raise_error(FormulaUnavailableError)
end
end
end
specify "::from_contents" do
expect(described_class.from_contents(formula_name, formula_path, formula_content)).to be_a(Formula)
end
describe "::to_rack" do
alias_matcher :exist, :be_exist
let(:rack_path) { HOMEBREW_CELLAR/formula_name }
context "when the Rack does not exist" do
it "returns the Rack" do
expect(described_class.to_rack(formula_name)).to eq(rack_path)
end
end
context "when the Rack exists" do
before do
rack_path.mkpath
end
it "returns the Rack" do
expect(described_class.to_rack(formula_name)).to eq(rack_path)
end
end
it "raises an error if the Formula is not available" do
expect do
described_class.to_rack("a/b/#{formula_name}")
end.to raise_error(TapFormulaUnavailableError)
end
end
describe "::core_path" do
it "returns the path to a Formula in the core tap" do
name = "foo-bar"
expect(described_class.core_path(name))
.to eq(Pathname.new("#{HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-core/Formula/#{name}.rb"))
end
end
describe "::convert_to_string_or_symbol" do
it "returns the original string if it doesn't start with a colon" do
expect(described_class.convert_to_string_or_symbol("foo")).to eq "foo"
end
it "returns a symbol if the original string starts with a colon" do
expect(described_class.convert_to_string_or_symbol(":foo")).to eq :foo
end
end
describe "::loader_for" do
context "when given a relative path with two slashes" do
it "returns a `FromPathLoader`" do
mktmpdir.cd do
FileUtils.mkdir "Formula"
FileUtils.touch "Formula/gcc.rb"
expect(described_class.loader_for("./Formula/gcc.rb")).to be_a Formulary::FromPathLoader
end
end
end
context "when given a tapped name" do
it "returns a `FromTapLoader`" do
expect(described_class.loader_for("homebrew/core/gcc")).to be_a Formulary::FromTapLoader
end
end
context "when not using the API" do
before do
ENV["HOMEBREW_NO_INSTALL_FROM_API"] = "1"
end
context "when a formula is migrated" do
let(:token) { "foo" }
let(:core_tap) { CoreTap.instance }
let(:core_cask_tap) { CoreCaskTap.instance }
let(:tap_migrations) do
{
token => new_tap.name,
}
end
before do
old_tap.path.mkpath
new_tap.path.mkpath
(old_tap.path/"tap_migrations.json").write tap_migrations.to_json
end
context "to a cask in the default tap" do
let(:old_tap) { core_tap }
let(:new_tap) { core_cask_tap }
let(:cask_file) { new_tap.cask_dir/"#{token}.rb" }
before do
new_tap.cask_dir.mkpath
FileUtils.touch cask_file
end
it "warn only once" do
expect do
described_class.loader_for(token)
end.to output(
a_string_including("Warning: Formula #{token} was renamed to #{new_tap}/#{token}.").once,
).to_stderr
end
end
context "to the default tap" do
let(:old_tap) { core_cask_tap }
let(:new_tap) { core_tap }
let(:formula_file) { new_tap.formula_dir/"#{token}.rb" }
before do
new_tap.formula_dir.mkpath
FileUtils.touch formula_file
end
it "does not warn when loading the short token" do
expect do
described_class.loader_for(token)
end.not_to output.to_stderr
end
it "does not warn when loading the full token in the default tap" do
expect do
described_class.loader_for("#{new_tap}/#{token}")
end.not_to output.to_stderr
end
it "warns when loading the full token in the old tap" do
expect do
described_class.loader_for("#{old_tap}/#{token}")
end.to output(
a_string_including("Formula #{old_tap}/#{token} was renamed to #{token}.").once,
).to_stderr
end
# FIXME
# context "when there is an infinite tap migration loop" do
# before do
# (new_tap.path/"tap_migrations.json").write({
# token => old_tap.name,
# }.to_json)
# end
#
# it "stops recursing" do
# expect do
# described_class.loader_for("#{new_tap}/#{token}")
# end.not_to output.to_stderr
# end
# end
end
context "to a third-party tap" do
let(:old_tap) { Tap.fetch("another", "foo") }
let(:new_tap) { Tap.fetch("another", "bar") }
let(:formula_file) { new_tap.formula_dir/"#{token}.rb" }
before do
new_tap.formula_dir.mkpath
FileUtils.touch formula_file
end
after do
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"another"
end
# FIXME
# It would be preferable not to print a warning when installing with the short token
it "warns when loading the short token" do
expect do
described_class.loader_for(token)
end.to output(
a_string_including("Formula #{old_tap}/#{token} was renamed to #{new_tap}/#{token}.").once,
).to_stderr
end
it "does not warn when loading the full token in the new tap" do
expect do
described_class.loader_for("#{new_tap}/#{token}")
end.not_to output.to_stderr
end
it "warns when loading the full token in the old tap" do
expect do
described_class.loader_for("#{old_tap}/#{token}")
end.to output(
a_string_including("Formula #{old_tap}/#{token} was renamed to #{new_tap}/#{token}.").once,
).to_stderr
end
end
end
end
end
end