Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
## [Unreleased]


## [v299] - 2025-04-07

Comment on lines +6 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha interesting changelog :D


## [v298] - 2025-04-03

- Added observability metrics (https://github.com/heroku/heroku-buildpack-ruby/pull/1569)

## [v297] - 2025-03-26

- Ruby 3.1.7 and 3.2.8 is now available


## [v296] - 2025-03-21

- Bundler `1.x` usage error is downgraded to a warning. This warning will be moved to an error once `heroku-20` is Sunset (https://github.com/heroku/heroku-buildpack-ruby/pull/1565)
Expand Down Expand Up @@ -1655,7 +1661,9 @@ Bugfixes:
* Change gem detection to use lockfile parser
* use `$RACK_ENV` when thin is detected for rack apps

[unreleased]: https://github.com/heroku/heroku-buildpack-ruby/compare/v297...main
[unreleased]: https://github.com/heroku/heroku-buildpack-ruby/compare/v299...main
[v299]: https://github.com/heroku/heroku-buildpack-ruby/compare/v298...v299
[v298]: https://github.com/heroku/heroku-buildpack-ruby/compare/v297...v298
[v297]: https://github.com/heroku/heroku-buildpack-ruby/compare/v296...v297
[v296]: https://github.com/heroku/heroku-buildpack-ruby/compare/v295...v296
[v295]: https://github.com/heroku/heroku-buildpack-ruby/compare/v294...v295
Expand Down
13 changes: 13 additions & 0 deletions bin/report
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -euo pipefail

CACHE_DIR="${2}"
REPORT_FILE="${CACHE_DIR}/.scalingo/ruby/build_report.yml"

# Whilst the release file is always written by the buildpack, some apps use
# third-party slug cleaner buildpacks to remove this and other files, so we
# cannot assume it still exists by the time the release step runs.
if [[ -f "${REPORT_FILE}" ]]; then
cat "${REPORT_FILE}"
fi
7 changes: 7 additions & 0 deletions bin/support/ruby_compile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ $stdout.sync = true
$:.unshift File.expand_path("../../../lib", __FILE__)
require "language_pack"
require "language_pack/shell_helpers"
HerokuBuildReport.set_global(
# Coupled with `bin/report`
path: Pathname(ARGV[1])
.join(".scalingo")
.join("ruby")
.join("build_report.yml")
).tap(&:clear!)

begin
LanguagePack::ShellHelpers.initialize_env(ARGV[2])
Expand Down
73 changes: 73 additions & 0 deletions lib/heroku_build_report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require 'yaml'
require 'pathname'

# Observability reporting for builds
#
# Example usage:
#
# HerokuBuildReport::GLOBAL.capture(
# "ruby_version" => "3.4.2"
# )
module HerokuBuildReport
# Accumulates data in memory and writes it to the specified path in YAML format
#
# Writes data to disk on every capture. Later `bin/report` emits the disk contents
class YamlReport
attr_reader :data

def initialize(path: )
@path = Pathname(path).expand_path
@path.dirname.mkpath
FileUtils.touch(@path)
@data = {}
end

def clear!
@data.clear
@path.write("")
end

def complex_object?(value)
value.to_yaml.match?(/!ruby\/object:/)
end

def capture(metrics = {})
metrics.each do |(key, value)|
return if key.nil? || key.to_s.strip.empty?

key = key&.strip
raise "Key cannot be empty (#{key.inspect} => #{value})" if key.nil? || key.empty?

# Don't serialize complex values by accident
if complex_object?(value)
value = value.to_s
end

@data["#{key}"] = value
end

@path.write(@data.to_yaml)
end
end

# Current load order of the various "language packs"
def self.set_global(path: )
YamlReport.new(path: path).tap { |report|
# Silence warning about setting a constant
begin
old_verbose = $VERBOSE
$VERBOSE = nil
const_set(:GLOBAL, report)
ensure
$VERBOSE = old_verbose
end
}
end

# Stores data in memory only, does not persist to disk
def self.dev_null
YamlReport.new(path: "/dev/null")
end

GLOBAL = self.dev_null # Changed via `set_global`
end
2 changes: 2 additions & 0 deletions lib/language_pack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def self.detect(*args)
$:.unshift File.expand_path("../../vendor", __FILE__)
$:.unshift File.expand_path("..", __FILE__)

require 'heroku_build_report'

require 'language_pack/shell_helpers'
require "language_pack/helpers/plugin_installer"
require "language_pack/helpers/stale_file_cleaner"
Expand Down
1 change: 1 addition & 0 deletions lib/language_pack/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def initialize(build_path, cache_path = nil)
@id = Digest::SHA1.hexdigest("#{Time.now.to_f}-#{rand(1000000)}")[0..10]
@fetchers = {:buildpack => LanguagePack::Fetcher.new(VENDOR_URL) }
@arch = get_arch
@report = HerokuBuildReport::GLOBAL

Dir.chdir build_path
end
Expand Down
29 changes: 22 additions & 7 deletions lib/language_pack/helpers/bundler_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,18 @@ class LanguagePack::Helpers::BundlerWrapper
end
end

def self.detect_bundler_version(contents: )
version_match = contents.match(BUNDLED_WITH_REGEX)
if version_match
major = version_match[:major]
minor = version_match[:minor]
def self.detect_bundler_version(contents: , bundled_with: contents.match(BUNDLED_WITH_REGEX))
if bundled_with
major = bundled_with[:major]
minor = bundled_with[:minor]
version = BLESSED_BUNDLER_VERSIONS["#{major}.#{minor}"]
version
else
DEFAULT_VERSION
end
end

BUNDLED_WITH_REGEX = /^BUNDLED WITH$(\r?\n) (?<major>\d+)\.(?<minor>\d+)\.\d+/m
BUNDLED_WITH_REGEX = /^BUNDLED WITH$(\r?\n) (?<version>(?<major>\d+)\.(?<minor>\d+)\.\d+)/m

class GemfileParseError < BuildpackError
def initialize(error)
Expand Down Expand Up @@ -105,12 +104,28 @@ def initialize(version_hash, major_minor)
attr_reader :bundler_path

def initialize(options = {})
@report = options[:report] || HerokuBuildReport::GLOBAL
@bundler_tmp = Pathname.new(Dir.mktmpdir)
@fetcher = options[:fetcher] || LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL) # coupling
@gemfile_path = options[:gemfile_path] || Pathname.new("./Gemfile")
@gemfile_lock_path = Pathname.new("#{@gemfile_path}.lock")

@version = self.class.detect_bundler_version(contents: @gemfile_lock_path.read(mode: "rt"))
contents = @gemfile_lock_path.read(mode: "rt")
bundled_with = contents.match(BUNDLED_WITH_REGEX)
@report.capture(
"bundled_with" => bundled_with&.[]("version") || "empty"
)
@version = self.class.detect_bundler_version(
contents: contents,
bundled_with: bundled_with
)
parts = @version.split(".")
@report.capture(
"bundler_version_installed" => @version,
"bundler_major" => parts&.shift,
"bundler_minor" => parts&.shift,
"bundler_patch" => parts&.shift
)
@dir_name = "bundler-#{@version}"

@bundler_path = options[:bundler_path] || @bundler_tmp.join(@dir_name)
Expand Down
11 changes: 10 additions & 1 deletion lib/language_pack/installers/heroku_ruby_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class LanguagePack::Installers::HerokuRubyInstaller
include LanguagePack::ShellHelpers
attr_reader :fetcher

def initialize(stack: , multi_arch_stacks: , arch: )
def initialize(stack: , multi_arch_stacks: , arch: , report: HerokuBuildReport::GLOBAL)
@report = report
if multi_arch_stacks.include?(stack)
@fetcher = LanguagePack::Fetcher.new(BASE_URL, stack: stack, arch: arch)
else
Expand All @@ -19,6 +20,14 @@ def initialize(stack: , multi_arch_stacks: , arch: )
end

def install(ruby_version, install_dir)
@report.capture(
"ruby.version" => ruby_version.ruby_version,
"ruby.engine" => ruby_version.engine,
"ruby.engine.version" => ruby_version.engine_version,
"ruby.major" => ruby_version.major,
"ruby.minor" => ruby_version.minor,
"ruby.patch" => ruby_version.patch,
)
fetch_unpack(ruby_version, install_dir)
setup_binstubs(install_dir)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/language_pack/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ def compile
install_binaries
run_assets_precompile_rake_task
end
@report.capture(
"railties_version" => bundler.gem_version('railties'),
"rack_version" => bundler.gem_version('rack')
)
config_detect
best_practice_warnings
warn_outdated_ruby
Expand Down
49 changes: 33 additions & 16 deletions lib/language_pack/ruby_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ def initialize(output = "")
BOOTSTRAP_VERSION_NUMBER = "3.1.6".freeze
DEFAULT_VERSION_NUMBER = "3.3.7".freeze
DEFAULT_VERSION = "ruby-#{DEFAULT_VERSION_NUMBER}".freeze
LEGACY_VERSION_NUMBER = "1.9.2".freeze
LEGACY_VERSION = "ruby-#{LEGACY_VERSION_NUMBER}".freeze
RUBY_VERSION_REGEX = %r{
(?<ruby_version>\d+\.\d+\.\d+){0}
(?<patchlevel>p-?\d+){0}
Expand All @@ -26,7 +24,25 @@ def initialize(output = "")
ruby-\g<ruby_version>(-\g<patchlevel>)?(-\g<engine>-\g<engine_version>)?
}x

attr_reader :set, :version, :version_without_patchlevel, :patchlevel, :engine, :ruby_version, :engine_version

# `version` is the bundler output like `ruby-3.4.2`
attr_reader :version,
# `set` is either `:gemfile` when the app specified a version or `nil` when using
# the default version
:set,
# `version_without_patchlevel` removes any `-p<number>` as they're not significant
# effectively this is `version_for_download`
:version_without_patchlevel,
# `patchlevel` is the `-p<number>` or is empty
:patchlevel,
# `engine` is `:ruby` or `:jruby`
:engine,
# `ruby_version` is `<major>.<minor>.<patch>` extracted from `version`
:ruby_version,
# `engine_version` is the Jruby version or for MRI it is the same as `ruby_version`
# i.e. `<major>.<minor>.<patch>`
:engine_version

include LanguagePack::ShellHelpers

def initialize(bundler_output, app = {})
Expand Down Expand Up @@ -74,7 +90,7 @@ def rake_is_vendored?
end

def default?
@version == none
!set
end

# determine if we're using jruby
Expand All @@ -98,6 +114,18 @@ def vendored_bundler?
false
end

def major
@ruby_version.split(".")[0].to_i
end

def minor
@ruby_version.split(".")[1].to_i
end

def patch
@ruby_version.split(".")[2].to_i
end

# Returns the next logical version in the minor series
# for example if the current ruby version is
# `ruby-2.3.1` then then `next_logical_version(1)`
Expand Down Expand Up @@ -126,21 +154,10 @@ def next_major_version(increment = 1)
end

private

def none
if @app[:is_new]
DEFAULT_VERSION
elsif @app[:last_version]
@app[:last_version]
else
LEGACY_VERSION
end
end

def set_version
if @bundler_output.empty?
@set = false
@version = none
@version = @app[:last_version] || DEFAULT_VERSION
else
@set = :gemfile
@version = @bundler_output
Expand Down
2 changes: 1 addition & 1 deletion lib/language_pack/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module LanguagePack
class LanguagePack::Base
BUILDPACK_VERSION = "v297"
BUILDPACK_VERSION = "v299"
end
end
1 change: 0 additions & 1 deletion spec/fixtures/invalid_encoding.log

This file was deleted.

Empty file.
2 changes: 0 additions & 2 deletions spec/fixtures/windows_lockfile/Gemfile.lock

This file was deleted.

28 changes: 0 additions & 28 deletions spec/hatchet/bugs_spec.rb

This file was deleted.

14 changes: 0 additions & 14 deletions spec/hatchet/buildpack_spec.rb

This file was deleted.

Loading