Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
adaaeba
Enable tests for vendored mode and deployment mode
lloeki Nov 3, 2025
110152a
Unguard deployment mode and vendored mode
lloeki Nov 3, 2025
b1bcb28
Run tests using `RUBYOPT` and `bundle exec`
lloeki Nov 3, 2025
85bd7e7
Protect forwarder against RUBYOPT
lloeki Nov 3, 2025
ee7d1db
Handle forwarder failure
lloeki Nov 3, 2025
9c449ac
Add `rubygems` path info to context
lloeki Nov 3, 2025
3ba522c
Pass (and log) context to injector
lloeki Nov 3, 2025
1861266
Improve error and fatal logging
lloeki Nov 3, 2025
61300e7
Add state output information to stubs
lloeki Nov 3, 2025
3d0795c
Memoize status
lloeki Nov 3, 2025
5f3c577
Set up break through vendored mode
lloeki Nov 3, 2025
7a3b14d
Break through deployment mode
lloeki Nov 3, 2025
2cc77b4
Enforce RUBYOPT order
lloeki Nov 3, 2025
5239c52
Disable `bundler` autoloading from `rubygems`
lloeki Nov 3, 2025
864ad3a
Enable injector second stage
lloeki Nov 3, 2025
073ee35
Wrap `run` output for readability
lloeki Nov 4, 2025
5deda6e
Transform select values for test filtering
lloeki Nov 4, 2025
50c4b67
Group tests by execution context commonality
lloeki Nov 4, 2025
baddf02
Improve test output clarity
lloeki Nov 4, 2025
ab19c8c
Handle exception in test case
lloeki Nov 4, 2025
683b8bc
Remove dead comments
lloeki Nov 4, 2025
4ccba72
Ensure IO thread completion
lloeki Nov 4, 2025
676f2d3
Run test unbundled when bundle is unlocked
lloeki Nov 4, 2025
d9c54ac
Fix testing on Ruby 1.8
lloeki Nov 4, 2025
b94ee03
Pass test case env to `bundle install`
lloeki Nov 6, 2025
b60a06e
Persist `/bundle` across runs
lloeki Nov 6, 2025
f8b3521
Add `vendored` fixture
lloeki Nov 6, 2025
9a8a169
Guard against manually specified vendored path
lloeki Nov 6, 2025
1ab0649
Use gemfile+lockfile from context
lloeki Nov 6, 2025
f29a590
Fix exception logging
lloeki Nov 6, 2025
ec01210
Use proper vendored bundle path from context
lloeki Nov 6, 2025
2997137
Add missing ruby 3.5 test coverage
lloeki Nov 6, 2025
d832620
Fix Ruby 3.5.0
lloeki Nov 6, 2025
66e0e40
Require necessary `Bundler::CLI` when patching
lloeki Nov 6, 2025
1e79de6
Allow log level control
lloeki Nov 6, 2025
26fbb5a
Update packages
lloeki Nov 7, 2025
0611cb4
Package `did_you_mean` for Ruby 2.6
lloeki Nov 14, 2025
6a1e55d
Pin Docker version
lloeki Nov 14, 2025
2068529
Merge remote-tracking branch 'origin/main' into lloeki/deployment-mode
lloeki Dec 16, 2025
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
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ jobs:
- name: Set up Docker with containerd
uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0
with:
version: v28.5.1
daemon-config: |
{
"features": {
Expand All @@ -122,6 +123,7 @@ jobs:
run: ruby test/bin/test.rb --filter engine:${{ matrix.ruby.engine }} --filter version:${{ matrix.ruby.version }}
env:
DOCKER_BUILDKIT: 1
DD_INTERNAL_RUBY_INJECTOR_LOG_LEVEL: DEBUG

complete:
name: Test (complete)
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
/Gemfile.lock

# transient files
/test/tmp/
/test/packages/
/tmp/
/test/tmp/
/test/packages/*/*/*/
!/test/packages/*/*/*/Gemfile
!/test/packages/*/*/*/Gemfile.lock

# tooling
/.ruby-lsp/
Expand Down
34 changes: 34 additions & 0 deletions src/mod/bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def status

{
:rubygems => Gem::VERSION,
:gem_path => Gem.path,
:version => Bundler::VERSION,
:simulate_version => Bundler.settings[:simulate_version],
:gemfile => (Bundler.default_gemfile rescue nil),
Expand All @@ -21,6 +22,8 @@ def status
:install_path => (Bundler.install_path rescue nil),
:app_config_path => (Bundler.app_config_path rescue nil),
:settings => {
:gem_home => ENV['GEM_HOME'],
:gem_path => ENV['GEM_PATH'],
:gemfile => Bundler.settings[:gemfile],
:deployment => Bundler.settings[:deployment],
:frozen => Bundler.settings[:frozen],
Expand All @@ -30,6 +33,37 @@ def status
}
end

def patch!
require!

mod = Module.new do
def [](name)

if name == :deployment
return false
end

super
end
end

::Bundler::Settings.prepend mod

require 'bundler/cli'
require 'bundler/cli/exec'

mod = Module.new do
def kernel_exec(*args)
ENV['RUBYOPT'] = ENV['RUBYOPT'].gsub(%r{^(.*)(?:\s+|^)(-r(\s*)\S+/injector\.rb)(.*)$}, '\2 \1 \3')
ENV.delete('BUNDLER_SETUP')

super
end
end

::Bundler::CLI::Exec.prepend mod
end

private

def require!
Expand Down
2 changes: 1 addition & 1 deletion src/mod/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def primitive(val)
private :primitive

def status
{
@status ||= {
:ruby => {
:version => RUBY.version,
:api_version => RUBY.api_version,
Expand Down
12 changes: 4 additions & 8 deletions src/mod/guard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,12 @@ def call(status)
result << { :name => 'bundler.locked', :reason => 'bundler.unlocked' }
end

if !status[:fs][:writable]
result << { :name => 'fs.writable', :reason => 'fs.readonly' }
end

if status[:bundler][:deployment]
result << { :name => 'bundler.deployment', :reason => 'bundler.deployment' }
if status[:bundler][:settings][:path]
result << { :name => 'bundler.path', :reason => 'bundler.vendored' }
end

if !status[:bundler][:use_system_gems]
result << { :name => 'bundler.use_system_gems', :reason => 'bundler.vendored' }
if !status[:fs][:writable]
result << { :name => 'fs.writable', :reason => 'fs.readonly' }
end

result unless result.empty?
Expand Down
34 changes: 24 additions & 10 deletions src/mod/injector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ def inject(gemfile_path, lockfile_path)
end

class << self
def call
LOG.info { 'injector:call' }
def call(context)
LOG.info { "injector:call context:#{context}" }

# TODO: check if nested injection (maybe very early too)
# TODO: check if injection already performed

# TODO: extract to a package module
package_basepath = ENV['DD_INTERNAL_RUBY_INJECTOR_BASEPATH'] || File.expand_path(File.join(File.dirname(__FILE__), '..'))
package_gem_home = ENV['DD_INTERNAL_RUBY_INJECTOR_GEM_HOME'] || File.join(package_basepath, RUBY.api_version)
package_gem_home = ENV['DD_INTERNAL_RUBY_INJECTOR_GEM_HOME'] || File.join(package_basepath, 'ruby', RUBY.api_version)
package_lockfile = ENV['DD_INTERNAL_RUBY_INJECTOR_LOCKFILE'] || File.join(package_gem_home, 'Gemfile.lock')

# TODO: capture stdout+stderr
Expand All @@ -76,13 +76,12 @@ def call

BUNDLER.send(:require!)

# TODO: these are in context
# pinpoint app gemfile and lockfile
app_gemfile = Bundler.default_gemfile
app_lockfile = Bundler.default_lockfile
app_gemfile = context[:bundler][:gemfile]
app_lockfile = context[:bundler][:lockfile]

# determine output paths
out = File.join(app_gemfile.dirname)
out = File.dirname(app_gemfile)

# TODO: this should work, unless the app's Gemfile has relative references...
# if File.writable?(File.join(out, 'tmp'))
Expand Down Expand Up @@ -118,12 +117,27 @@ def call

ENV['DD_INTERNAL_RUBY_INJECTOR'] = 'false'

return [nil, err] if err
if err
LOG.debug { "error: #{err}"}
return [nil, err]
end

return [false, nil] unless gemfile

Gem.paths = { 'GEM_PATH' => "#{package_gem_home}:#{ENV['GEM_PATH']}" }
ENV['GEM_PATH'] = Gem.path.join(File::PATH_SEPARATOR)
if context[:bundler][:deployment]
app_bundle_path = context[:bundler][:bundle_path]

ENV['DD_INTERNAL_RUBY_INJECTOR_PATCH'] = "mode=deployment,path=#{package_gem_home}:#{app_bundle_path}"
Gem.paths = { 'GEM_PATH' => "#{package_gem_home}:#{app_bundle_path}" }
ENV['GEM_PATH'] = Gem.path.join(File::PATH_SEPARATOR)
ENV['GEM_HOME'] = app_bundle_path

BUNDLER.patch!
else
Gem.paths = { 'GEM_PATH' => "#{package_gem_home}:#{ENV['GEM_PATH']}" }
ENV['GEM_PATH'] = Gem.path.join(File::PATH_SEPARATOR)
end

ENV['BUNDLE_GEMFILE'] = gemfile

[true, nil]
Expand Down
9 changes: 8 additions & 1 deletion src/mod/log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ def progname
end

def level
UNKNOWN
@level ||= case ENV['DD_INTERNAL_RUBY_INJECTOR_LOG_LEVEL']
when 'DEBUG', 0 then DEBUG
when 'INFO', 1 then INFO
when 'WARN', 2 then WARN
when 'ERROR', 3 then ERROR
when 'FATAL', 4 then FATAL
else UNKNOWN
end
end

def add(severity, message = nil, progname = nil)
Expand Down
16 changes: 13 additions & 3 deletions src/mod/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,27 @@
injector = import 'injector'

if ENV['DD_INTERNAL_RUBY_INJECTOR'] == 'false'
log.info { 'inject:skip' }
if ENV['DD_INTERNAL_RUBY_INJECTOR_PATCH']
log.info { 'inject:patch' }

bundler = import 'bundler'

bundler.patch!
else
log.info { 'inject:skip' }
end

telemetry.emit([
{ :name => 'library_entrypoint.complete', :tags => ["reason:internal.skip"] },
], { :result => report.cached })
else
begin
# TODO: pass args, e.g context, location, etc...
injected, err = injector.call
injected, err = injector.call(context.status)

if err
log.info { "inject:error err:#{err.inspect}" }

telemetry.emit([
{ :name => 'library_entrypoint.error', :tags => ["reason:#{err}"] },
], { :result => report.errored(err) })
Expand All @@ -62,7 +72,7 @@
], { :result => report.completed(injected) })
end
rescue StandardError => e
log.info { 'inject:error' }
log.info { "inject:fatal exc:#{e.class.name},#{e.message.inspect},#{e.backtrace.first.inspect}" }

telemetry.emit([
{ :name => 'library_entrypoint.error', :tags => ["reason:exc.fatal"] },
Expand Down
6 changes: 2 additions & 4 deletions src/mod/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ def self.platform
end

def self.api_version
# TODO: handle '+0' suffix appearing in preview versions
require 'rbconfig' unless defined?(RbConfig)

major, minor, = RUBY_VERSION.split('.')

"#{major}.#{minor}.0"
RbConfig::CONFIG['ruby_version']
end
2 changes: 2 additions & 0 deletions src/mod/telemetry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def emit(*args)

rd, wr = IO.pipe

opt = ENV.delete('RUBYOPT')
pid = PROCESS.spawn(forwarder, 'library_entrypoint', { :in => rd, [:out, :err] => '/dev/null' })
ENV['RUBYOPT'] = opt if opt

wr.write(payload(pid, version, result, points))
wr.flush
Expand Down
Loading
Loading