diff --git a/Library/Homebrew/language/node.rb b/Library/Homebrew/language/node.rb index b5bb25389b..5974e3bc25 100644 --- a/Library/Homebrew/language/node.rb +++ b/Library/Homebrew/language/node.rb @@ -85,5 +85,35 @@ module Language --#{npm_cache_config} ] end + + # Mixin module for {Formula} adding shebang rewrite features. + module Shebang + module_function + + # A regex to match potential shebang permutations. + NODE_SHEBANG_REGEX = %r{^#! ?/usr/bin/(?:env )?node( |$)}.freeze + + # The length of the longest shebang matching `SHEBANG_REGEX`. + NODE_SHEBANG_MAX_LENGTH = "#! /usr/bin/env node ".length + + # @private + sig { params(node_path: T.any(String, Pathname)).returns(Utils::Shebang::RewriteInfo) } + def node_shebang_rewrite_info(node_path) + Utils::Shebang::RewriteInfo.new( + NODE_SHEBANG_REGEX, + NODE_SHEBANG_MAX_LENGTH, + "#{node_path}\\1", + ) + end + + sig { params(formula: T.untyped).returns(Utils::Shebang::RewriteInfo) } + def detected_node_shebang(formula = self) + node_deps = formula.deps.map(&:name).grep(/^node(@.+)?$/) + raise ShebangDetectionError.new("Node", "formula does not depend on Node") if node_deps.empty? + raise ShebangDetectionError.new("Node", "formula has multiple Node dependencies") if node_deps.length > 1 + + node_shebang_rewrite_info(Formula[node_deps.first].opt_bin/"node") + end + end end end diff --git a/Library/Homebrew/language/node.rbi b/Library/Homebrew/language/node.rbi new file mode 100644 index 0000000000..15faf4a5c6 --- /dev/null +++ b/Library/Homebrew/language/node.rbi @@ -0,0 +1,9 @@ +# typed: strict + +module Language + module Node + module Shebang + include Kernel + end + end +end diff --git a/Library/Homebrew/test/language/node/shebang_spec.rb b/Library/Homebrew/test/language/node/shebang_spec.rb new file mode 100644 index 0000000000..e691f66976 --- /dev/null +++ b/Library/Homebrew/test/language/node/shebang_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "language/node" +require "utils/shebang" + +describe Language::Node::Shebang do + let(:file) { Tempfile.new("node-shebang") } + let(:f) do + f = {} + + f[:node18] = formula "node@18" do + url "https://brew.sh/node-18.0.0.tgz" + end + + f[:versioned_node_dep] = formula "foo" do + url "https://brew.sh/foo-1.0.tgz" + + depends_on "node@18" + 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 "node" + depends_on "node@18" + end + + f + end + + before do + file.write <<~EOS + #!/usr/bin/env node + a + b + c + EOS + file.flush + end + + after { file.unlink } + + describe "#detected_node_shebang" do + it "can be used to replace Node shebangs" do + allow(Formulary).to receive(:factory) + allow(Formulary).to receive(:factory).with(f[:node18].name).and_return(f[:node18]) + Utils::Shebang.rewrite_shebang described_class.detected_node_shebang(f[:versioned_node_dep]), file.path + + expect(File.read(file)).to eq <<~EOS + #!#{HOMEBREW_PREFIX/"opt/node@18/bin/node"} + a + b + c + EOS + end + + it "errors if formula doesn't depend on node" do + expect { Utils::Shebang.rewrite_shebang described_class.detected_node_shebang(f[:no_deps]), file.path } + .to raise_error(ShebangDetectionError, "Cannot detect Node shebang: formula does not depend on Node.") + end + + it "errors if formula depends on more than one node" do + expect { Utils::Shebang.rewrite_shebang described_class.detected_node_shebang(f[:multiple_deps]), file.path } + .to raise_error(ShebangDetectionError, "Cannot detect Node shebang: formula has multiple Node dependencies.") + end + end +end