diff --git a/Library/Homebrew/.rubocop.yml b/Library/Homebrew/.rubocop.yml index 1815ed3974..f79308777f 100644 --- a/Library/Homebrew/.rubocop.yml +++ b/Library/Homebrew/.rubocop.yml @@ -41,6 +41,7 @@ Style/Documentation: - language/java.rb - language/node.rb - language/perl.rb + - language/php.rb - language/python.rb - livecheck/strategy/apache.rb - livecheck/strategy/bitbucket.rb diff --git a/Library/Homebrew/language/php.rb b/Library/Homebrew/language/php.rb new file mode 100644 index 0000000000..c8ec368022 --- /dev/null +++ b/Library/Homebrew/language/php.rb @@ -0,0 +1,43 @@ +# typed: strict +# frozen_string_literal: true + +module Language + # Helper functions for PHP formulae. + # + # @api public + module PHP + # Helper module for replacing `php` shebangs. + module Shebang + extend T::Helpers + + requires_ancestor { Formula } + + module_function + + # A regex to match potential shebang permutations. + PHP_SHEBANG_REGEX = %r{^#! ?(?:/usr/bin/(?:env )?)?php( |$)} + + # The length of the longest shebang matching `SHEBANG_REGEX`. + PHP_SHEBANG_MAX_LENGTH = T.let("#! /usr/bin/env php ".length, Integer) + + # @private + sig { params(php_path: T.any(String, Pathname)).returns(Utils::Shebang::RewriteInfo) } + def php_shebang_rewrite_info(php_path) + Utils::Shebang::RewriteInfo.new( + PHP_SHEBANG_REGEX, + PHP_SHEBANG_MAX_LENGTH, + "#{php_path}\\1", + ) + end + + sig { params(formula: Formula).returns(Utils::Shebang::RewriteInfo) } + def detected_php_shebang(formula = T.cast(self, Formula)) + php_deps = formula.deps.select(&:required?).map(&:name).grep(/^php(@.+)?$/) + raise ShebangDetectionError.new("PHP", "formula does not depend on PHP") if php_deps.empty? + raise ShebangDetectionError.new("PHP", "formula has multiple PHP dependencies") if php_deps.length > 1 + + php_shebang_rewrite_info(Formula[php_deps.first].opt_bin/"php") + end + end + end +end diff --git a/Library/Homebrew/test/language/php/shebang_spec.rb b/Library/Homebrew/test/language/php/shebang_spec.rb new file mode 100644 index 0000000000..47589e0b61 --- /dev/null +++ b/Library/Homebrew/test/language/php/shebang_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require "language/php" +require "utils/shebang" + +RSpec.describe Language::PHP::Shebang do + let(:file) { Tempfile.new("php-shebang") } + let(:broken_file) { Tempfile.new("php-shebang") } + let(:f) do + f = {} + + f[:php81] = formula "php@8.1" do + url "https://brew.sh/node-18.0.0.tgz" + end + + f[:versioned_php_dep] = formula "foo" do + url "https://brew.sh/foo-1.0.tgz" + + depends_on "php@8.1" + end + + f[:no_deps] = formula "foo" do + url "https://brew.sh/foo-1.0.tgz" + end + + f[:multiple_deps] = formula "foo" do + url "https://brew.sh/foo-1.0.tgz" + + depends_on "php" + depends_on "php@8.1" + end + + f + end + + before do + file.write <<~EOS + #!/usr/bin/env php + a + b + c + EOS + file.flush + + broken_file.write <<~EOS + #!php + a + b + c + EOS + broken_file.flush + end + + after { [file, broken_file].each(&:unlink) } + + describe "#detected_php_shebang" do + it "can be used to replace PHP shebangs" do + allow(Formulary).to receive(:factory).with(f[:php81].name).and_return(f[:php81]) + Utils::Shebang.rewrite_shebang described_class.detected_php_shebang(f[:versioned_php_dep]), file.path + + expect(File.read(file)).to eq <<~EOS + #!#{HOMEBREW_PREFIX/"opt/php@8.1/bin/php"} + a + b + c + EOS + end + + it "can fix broken shebang like `#!php`" do + allow(Formulary).to receive(:factory).with(f[:php81].name).and_return(f[:php81]) + Utils::Shebang.rewrite_shebang described_class.detected_php_shebang(f[:versioned_php_dep]), broken_file.path + + expect(File.read(broken_file)).to eq <<~EOS + #!#{HOMEBREW_PREFIX/"opt/php@8.1/bin/php"} + a + b + c + EOS + end + + it "errors if formula doesn't depend on PHP" do + expect { Utils::Shebang.rewrite_shebang described_class.detected_php_shebang(f[:no_deps]), file.path } + .to raise_error(ShebangDetectionError, "Cannot detect PHP shebang: formula does not depend on PHP.") + end + + it "errors if formula depends on more than one php" do + expect { Utils::Shebang.rewrite_shebang described_class.detected_php_shebang(f[:multiple_deps]), file.path } + .to raise_error(ShebangDetectionError, "Cannot detect PHP shebang: formula has multiple PHP dependencies.") + end + end +end