diff --git a/Library/Homebrew/compat.rb b/Library/Homebrew/compat.rb index b82a492f60..9a2cf29459 100644 --- a/Library/Homebrew/compat.rb +++ b/Library/Homebrew/compat.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require "compat/cask/dsl/version" +require "compat/language/python" require "compat/requirements/macos_requirement" require "compat/formula" diff --git a/Library/Homebrew/compat/language/python.rb b/Library/Homebrew/compat/language/python.rb new file mode 100644 index 0000000000..54784465ba --- /dev/null +++ b/Library/Homebrew/compat/language/python.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Language + module Python + class << self + module Compat + def rewrite_python_shebang(python_path) + Pathname.pwd.find do |f| + Utils::Shebang.rewrite_shebang(Shebang.python_shebang_rewrite_info(python_path), f) + end + end + end + + prepend Compat + end + end +end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 1df01f60a6..4da0e79b41 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -6,6 +6,7 @@ require "lock_file" require "formula_pin" require "hardware" require "utils/bottles" +require "utils/shebang" require "utils/shell" require "build_environment" require "build_options" @@ -50,6 +51,7 @@ require "find" class Formula include FileUtils include Utils::Inreplace + include Utils::Shebang include Utils::Shell extend Enumerable extend Forwardable diff --git a/Library/Homebrew/language/perl.rb b/Library/Homebrew/language/perl.rb new file mode 100644 index 0000000000..7368b36ee6 --- /dev/null +++ b/Library/Homebrew/language/perl.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Language + module Perl + module Shebang + module_function + + def detected_perl_shebang(formula = self) + perl_path = if formula.uses_from_macos_elements&.include? "perl" + "/usr/bin/perl" + elsif formula.deps.map(&:name).include? "perl" + Formula["perl"].opt_bin/"perl" + else + raise "Cannot detect Perl shebang: formula does not depend on Perl." + end + + Utils::Shebang::RewriteInfo.new( + %r{^#! ?/usr/bin/(env )?perl$}, + 20, # the length of "#! /usr/bin/env perl" + perl_path, + ) + end + end + end +end diff --git a/Library/Homebrew/language/python.rb b/Library/Homebrew/language/python.rb index 5ec93a8a06..caaa6cb6a4 100644 --- a/Library/Homebrew/language/python.rb +++ b/Library/Homebrew/language/python.rb @@ -87,14 +87,26 @@ module Language ] end - def self.rewrite_python_shebang(python_path) - regex = %r{^#! ?/usr/bin/(env )?python([23](\.\d{1,2})?)?$} - maximum_regex_length = 28 # the length of "#! /usr/bin/env pythonx.yyy$" - Pathname(".").find do |f| - next unless f.file? - next unless regex.match?(f.read(maximum_regex_length)) + # Mixin module for {Formula} adding shebang rewrite features. + module Shebang + module_function - Utils::Inreplace.inreplace f.to_s, regex, "#!#{python_path}" + # @private + def python_shebang_rewrite_info(python_path) + Utils::Shebang::RewriteInfo.new( + %r{^#! ?/usr/bin/(env )?python([23](\.\d{1,2})?)?$}, + 28, # the length of "#! /usr/bin/env pythonx.yyy$" + python_path, + ) + end + + def detected_python_shebang(formula = self) + python_deps = formula.deps.map(&:name).grep(/^python(@.*)?$/) + + raise "Cannot detect Python shebang: formula does not depend on Python." if python_deps.empty? + raise "Cannot detect Python shebang: formula has multiple Python dependencies." if python_deps.length > 1 + + python_shebang_rewrite_info(Formula[python_deps.first].opt_bin/"python3") end end diff --git a/Library/Homebrew/test/language/perl/shebang_spec.rb b/Library/Homebrew/test/language/perl/shebang_spec.rb new file mode 100644 index 0000000000..5a59f162b7 --- /dev/null +++ b/Library/Homebrew/test/language/perl/shebang_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "language/perl" +require "utils/shebang" + +describe Language::Perl::Shebang do + let(:file) { Tempfile.new("perl-shebang") } + let(:perl_f) do + formula "perl" do + url "https://brew.sh/perl-1.0.tgz" + end + end + let(:f) do + formula "foo" do + url "https://brew.sh/foo-1.0.tgz" + + uses_from_macos "perl" + end + end + + before do + file.write <<~EOS + #!/usr/bin/env perl + a + b + c + EOS + file.flush + end + + after { file.unlink } + + describe "#detected_perl_shebang" do + it "can be used to replace Perl shebangs" do + allow(Formulary).to receive(:factory).with(perl_f.name).and_return(perl_f) + Utils::Shebang.rewrite_shebang described_class.detected_perl_shebang(f), file + + expected_shebang = if OS.mac? + "/usr/bin/perl" + else + HOMEBREW_PREFIX/"opt/perl/bin/perl" + end + + expect(File.read(file)).to eq <<~EOS + #!#{expected_shebang} + a + b + c + EOS + end + end +end diff --git a/Library/Homebrew/test/language/python_spec.rb b/Library/Homebrew/test/language/python_spec.rb index a474e04da6..174373d999 100644 --- a/Library/Homebrew/test/language/python_spec.rb +++ b/Library/Homebrew/test/language/python_spec.rb @@ -2,6 +2,7 @@ require "language/python" require "resource" +require "utils/shebang" describe Language::Python, :needs_python do describe "#major_minor_version" do @@ -32,6 +33,48 @@ describe Language::Python, :needs_python do end end +describe Language::Python::Shebang do + let(:file) { Tempfile.new("python-shebang") } + let(:python_f) do + formula "python" do + url "https://brew.sh/python-1.0.tgz" + end + end + let(:f) do + formula "foo" do + url "https://brew.sh/foo-1.0.tgz" + + depends_on "python" + end + end + + before do + file.write <<~EOS + #!/usr/bin/env python3 + a + b + c + EOS + file.flush + end + + after { file.unlink } + + describe "#detected_python_shebang" do + it "can be used to replace Python shebangs" do + expect(Formulary).to receive(:factory).with(python_f.name).and_return(python_f) + Utils::Shebang.rewrite_shebang described_class.detected_python_shebang(f), file + + expect(File.read(file)).to eq <<~EOS + #!#{HOMEBREW_PREFIX}/opt/python/bin/python3 + a + b + c + EOS + end + end +end + describe Language::Python::Virtualenv::Virtualenv do subject { described_class.new(formula, dir, "python") } diff --git a/Library/Homebrew/test/resource_spec.rb b/Library/Homebrew/test/resource_spec.rb index ec21741f08..fd4e591014 100644 --- a/Library/Homebrew/test/resource_spec.rb +++ b/Library/Homebrew/test/resource_spec.rb @@ -157,7 +157,7 @@ describe Resource do end specify "#verify_download_integrity_mismatch" do - fn = double(file?: true) + fn = double(file?: true, basename: "foo") checksum = subject.sha256(TEST_SHA256) expect(fn).to receive(:verify_checksum).with(checksum) diff --git a/Library/Homebrew/utils/shebang.rb b/Library/Homebrew/utils/shebang.rb new file mode 100644 index 0000000000..eda6eec190 --- /dev/null +++ b/Library/Homebrew/utils/shebang.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Utils + module Shebang + module_function + + class RewriteInfo + attr_reader :regex, :max_length, :replacement + + def initialize(regex, max_length, replacement) + @regex = regex + @max_length = max_length + @replacement = replacement + end + end + + def rewrite_shebang(rewrite_info, *paths) + paths.each do |f| + f = Pathname(f) + next unless f.file? + next unless rewrite_info.regex.match?(f.read(rewrite_info.max_length)) + + Utils::Inreplace.inreplace f.to_s, rewrite_info.regex, "#!#{rewrite_info.replacement}" + end + end + end +end