Skip to content

Commit

Permalink
[close #990] Warn on bad shebang line
Browse files Browse the repository at this point in the history
This article explains binstubs and "shebang" lines: https://devcenter.heroku.com/articles/bad-ruby-binstub-shebang. A relatively common problem when generating a binstub is for it to contain a bad shebang line. Here's an example:

```
#!/usr/bin/env ruby2.5
```

If you've got a binstub with that shebang line in your project and are running on Heroku 18 then your app will be forced to use `ruby2.5` binary no matter what version of Ruby you've specified in the `Gemfile`. This causes very odd behavior and weird, difficult to debug bundler errors. 

This PR does not fix the problem, but will at least warn anyone with a version number in their ruby shebang line.

This behavior was originally:


-    Introduced: #586
-    Rolled back: #623
 

It was rolled back because it would falsely claim that `#!/usr/bin/env bash` was incorrect also it only protected against a limited set of binstubs.
  • Loading branch information
schneems committed Jun 10, 2020
1 parent 30184b6 commit 6499ce8
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* A bug in 2.6.0, 2.6.1, 2.6.3 require a Ruby upgrade, a warning has been added (https://github.com/heroku/heroku-buildpack-ruby/pull/1015)
* The spring library is now disabled by setting the enviornment variable DISABLE_SPRING=1 (https://github.com/heroku/heroku-buildpack-ruby/pull/1017)
* Warn when a bad "shebang" line in a binstub is detected (https://github.com/heroku/heroku-buildpack-ruby/pull/1014)
* Default node version now 12.16.2, yarn is 1.22.4 (https://github.com/heroku/heroku-buildpack-ruby/pull/986)
* Gracefully handle unrecognised stacks ([#982](https://github.com/heroku/heroku-buildpack-ruby/pull/982))

Expand Down
64 changes: 64 additions & 0 deletions lib/language_pack/helpers/binstub_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

# This class is designed to check for binstubs for validity
#
# Example:
#
# check = LanguagePack::Helpers::BinstubCheck.new(Dir.pwd, self)
# check.call
class LanguagePack::Helpers::BinstubCheck
attr_reader :bad_binstubs

def initialize(app_root_dir:, warn_object: )
@bin_dir = Pathname.new(app_root_dir).join("bin")
@warn_object = warn_object
@bad_binstubs = []
end

def call
return unless @bin_dir.directory?

@bin_dir.entries.each do |basename|
binstub = @bin_dir.join(basename)
next unless binstub.file?

shebang = binstub.open(&:readline)

if shebang.match?(/^#!\s*\/usr\/bin\/env\s*ruby(\d.*)$/) # https://rubular.com/r/ozbNEPVInc3sSN
@bad_binstubs << binstub
end
rescue EOFError
end

warn unless @bad_binstubs.empty?
end

private def warn
message = <<~EOM
Improperly formatted binstubs detected in your project
The following file(s) have appear to contain a problematic "shebang" line
#{@bad_binstubs.map {|binstub| " - bin/#{binstub.basename}" }.join("\n")}
For example bin/#{@bad_binstubs.first.basename} has the shebang line:
```
#{@bad_binstubs.first.open(&:readline).chomp}
```
It should be:
```
#!/usr/bin/env ruby
```
A malformed shebang line may cause your program to crash.
For more information about binstubs and "shebang" lines see:
https://devcenter.heroku.com/articles/bad-ruby-binstub-shebang
EOM

@warn_object.warn(message, inline: true)
end
end
18 changes: 17 additions & 1 deletion lib/language_pack/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "language_pack/helpers/yarn_installer"
require "language_pack/helpers/jvm_installer"
require "language_pack/helpers/layer"
require "language_pack/helpers/binstub_check"
require "language_pack/version"

# base Ruby Language Pack. This is for any base ruby app.
Expand Down Expand Up @@ -96,6 +97,7 @@ def compile
Dir.chdir(build_path)
remove_vendor_bundle
warn_bundler_upgrade
warn_bad_binstubs
install_ruby(slug_vendor_ruby, build_ruby_path)
install_jvm
setup_language_pack_environment(ruby_layer_path: File.expand_path("."), gem_layer_path: File.expand_path("."))
Expand All @@ -121,10 +123,11 @@ def compile
raise e
end


def build
new_app?
remove_vendor_bundle

warn_bad_binstubs
ruby_layer = Layer.new(@layer_dir, "ruby", launch: true)
install_ruby("#{ruby_layer.path}/#{slug_vendor_ruby}")
ruby_layer.metadata[:version] = ruby_version.version
Expand Down Expand Up @@ -174,6 +177,19 @@ def config_detect

private

# A bad shebang line looks like this:
#
# ```
# #!/usr/bin/env ruby2.5
# ```
#
# Since `ruby2.5` is not a valid binary name
#
def warn_bad_binstubs
check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: Dir.pwd, warn_object: self)
check.call
end

def default_malloc_arena_max?
return true if @metadata.exists?("default_malloc_arena_max")
return @metadata.touch("default_malloc_arena_max") if new_app?
Expand Down
1 change: 1 addition & 0 deletions lib/language_pack/test/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def compile
new_app?
Dir.chdir(build_path)
remove_vendor_bundle
warn_bad_binstubs
install_ruby(slug_vendor_ruby, build_ruby_path)
install_jvm
setup_language_pack_environment(ruby_layer_path: File.expand_path("."), gem_layer_path: File.expand_path("."))
Expand Down
43 changes: 43 additions & 0 deletions spec/helpers/binstub_check_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require_relative "../spec_helper.rb"

describe LanguagePack::Helpers::BinstubCheck do
it "doesn't error on empty directories" do
Dir.mktmpdir do |dir|
warn_obj = Object.new
check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: dir, warn_object: warn_obj)
check.call
end
end

it "checks binstubs and finds bad ones" do
Dir.mktmpdir do |dir|
bin_dir = Pathname.new(dir).join("bin")
bin_dir.mkpath

# Bad binstub
bin_dir.join("bad_binstub_example").write(<<~EOM)
#!/usr/bin/env ruby2.5
nothing else matters
EOM

# Good binstub
bin_dir.join("good_binstub_example").write(<<~EOM)
#!/usr/bin/env bash
nothing else matters
EOM
bin_dir.join("good_binstub_example_two").write("#!/usr/bin/env ruby")

warn_obj = Object.new
def warn_obj.warn(*args, **kwargs); @msg = args.first; end
def warn_obj.msg; @msg; end

check = LanguagePack::Helpers::BinstubCheck.new(app_root_dir: dir, warn_object: warn_obj)
check.call

expect(check.bad_binstubs.count).to eq(1)
expect(warn_obj.msg).to include("bin/bad_binstub_example")
end
end
end

0 comments on commit 6499ce8

Please sign in to comment.