diff --git a/ext/datadog_profiling_native_extension/extconf.rb b/ext/datadog_profiling_native_extension/extconf.rb index 5ea5741733e..9a1a7718ca6 100644 --- a/ext/datadog_profiling_native_extension/extconf.rb +++ b/ext/datadog_profiling_native_extension/extconf.rb @@ -211,12 +211,14 @@ def add_compiler_flag(flag) skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported::COMPILER_ATOMIC_MISSING) end -# See comments on the helper method being used for why we need to additionally set this. +# See comments on the helper methods being used for why we need to additionally set this. # The extremely excessive escaping around ORIGIN below seems to be correct and was determined after a lot of # experimentation. We need to get these special characters across a lot of tools untouched... -$LDFLAGS += \ - ' -Wl,-rpath,$$$\\\\{ORIGIN\\}/' \ - "#{Datadog::Profiling::NativeExtensionHelpers.libdatadog_folder_relative_to_native_lib_folder}" +extra_relative_rpaths = [ + Datadog::Profiling::NativeExtensionHelpers.libdatadog_folder_relative_to_native_lib_folder, + *Datadog::Profiling::NativeExtensionHelpers.libdatadog_folder_relative_to_ruby_extensions_folders, +] +extra_relative_rpaths.each { |folder| $LDFLAGS += " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{folder.to_str}" } Logging.message("[datadog] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n") # Tag the native extension library with the Ruby version and Ruby platform. diff --git a/ext/datadog_profiling_native_extension/native_extension_helpers.rb b/ext/datadog_profiling_native_extension/native_extension_helpers.rb index 135a5407ebc..6033e6e6bb2 100644 --- a/ext/datadog_profiling_native_extension/native_extension_helpers.rb +++ b/ext/datadog_profiling_native_extension/native_extension_helpers.rb @@ -67,6 +67,52 @@ def self.libdatadog_folder_relative_to_native_lib_folder( Pathname.new(libdatadog_lib_folder).relative_path_from(Pathname.new(profiling_native_lib_folder)).to_s end + # In https://github.com/DataDog/dd-trace-rb/pull/3582 we got a report of a customer for which the native extension + # only got installed into the extensions folder. + # + # But then this fix was not enough to fully get them moving because then they started to see the issue from + # https://github.com/DataDog/dd-trace-rb/issues/2067 / https://github.com/DataDog/dd-trace-rb/pull/2125 : + # + # > Profiling was requested but is not supported, profiling disabled: There was an error loading the profiling + # > native extension due to 'RuntimeError Failure to load datadog_profiling_native_extension.3.2.2_x86_64-linux + # > due to libdatadog_profiling.so: cannot open shared object file: No such file or directory + # + # The problem is that when loading the native extension from the extensions directory, the relative rpath we add + # with the #libdatadog_folder_relative_to_native_lib_folder helper above is not correct, we need to add a relative + # rpath to the extensions directory. + # + # So how do we find the full path where the native extension is placed? + # * From https://github.com/ruby/ruby/blob/83f02d42e0a3c39661dc99c049ab9a70ff227d5b/lib/bundler/runtime.rb#L166 + # `extension_dirs = Dir["#{Gem.dir}/extensions/*/*/*"] + Dir["#{Gem.dir}/bundler/gems/extensions/*/*/*"]` + # we get that's in one of two fixed subdirectories of `Gem.dir` + # * From https://github.com/ruby/ruby/blob/83f02d42e0a3c39661dc99c049ab9a70ff227d5b/lib/rubygems/basic_specification.rb#L111-L115 + # we get the structure of the subdirectory (platform/extension_api_version/gem_and_version) + # + # Thus, `Gem.dir` of `/var/app/current/vendor/bundle/ruby/3.2.0` becomes (for instance) + # `/var/app/current/vendor/bundle/ruby/3.2.0/extensions/x86_64-linux/3.2.0/datadog-2.0.0/` or + # `/var/app/current/vendor/bundle/ruby/3.2.0/bundler/gems/extensions/x86_64-linux/3.2.0/datadog-2.0.0/` + # + # We then compute the relative path between these folders and the libdatadog folder, and use that as a relative path. + def self.libdatadog_folder_relative_to_ruby_extensions_folders( + gem_dir: Gem.dir, + libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder + ) + return unless libdatadog_pkgconfig_folder + + # For the purposes of calculating a folder relative to the other, we don't actually NEED to fill in the + # platform, extension_api_version and gem version. We're basically just after how many folders it is deep from + # the Gem.dir. + expected_ruby_extensions_folders = [ + "#{gem_dir}/extensions/platform/extension_api_version/datadog_version/", + "#{gem_dir}/bundler/gems/extensions/platform/extension_api_version/datadog_version/", + ] + libdatadog_lib_folder = "#{libdatadog_pkgconfig_folder}/../" + + expected_ruby_extensions_folders.map do |folder| + Pathname.new(libdatadog_lib_folder).relative_path_from(Pathname.new(folder)).to_s + end + end + # Used to check if profiler is supported, including user-visible clear messages explaining why their # system may not be supported. module Supported diff --git a/spec/datadog/profiling/native_extension_helpers_spec.rb b/spec/datadog/profiling/native_extension_helpers_spec.rb index 3387f0fa2a6..a2342dcee70 100644 --- a/spec/datadog/profiling/native_extension_helpers_spec.rb +++ b/spec/datadog/profiling/native_extension_helpers_spec.rb @@ -33,6 +33,46 @@ end end + describe '.libdatadog_folder_relative_to_ruby_extensions_folders' do + context 'when libdatadog is available' do + before do + skip_if_profiling_not_supported(self) + if PlatformHelpers.mac? && Libdatadog.pkgconfig_folder.nil? && ENV['LIBDATADOG_VENDOR_OVERRIDE'].nil? + raise 'You have a libdatadog setup without macOS support. Did you forget to set LIBDATADOG_VENDOR_OVERRIDE?' + end + end + + it 'returns a relative path to libdatadog folder from the ruby extensions folders' do + extensions_relative, bundler_extensions_relative = + described_class.libdatadog_folder_relative_to_ruby_extensions_folders + + libdatadog_extension = RbConfig::CONFIG['SOEXT'] || raise('Missing SOEXT for current platform') + libdatadog = "libdatadog_profiling.#{libdatadog_extension}" + + expect(extensions_relative).to start_with('../') + expect(bundler_extensions_relative).to start_with('../') + + extensions_full = + "#{Gem.dir}/extensions/platform/extension_api_version/datadog_version/#{extensions_relative}/#{libdatadog}" + bundler_extensions_full = + "#{Gem.dir}/bundler/gems/extensions/platform/extension_api_version/datadog_version/" \ + "#{bundler_extensions_relative}/#{libdatadog}" + + expect(File.exist?(Pathname.new(extensions_full).cleanpath.to_s)) + .to be(true), "Libdatadog not available in expected path: #{extensions_full.inspect}" + expect(File.exist?(Pathname.new(bundler_extensions_full).cleanpath.to_s)) + .to be(true), "Libdatadog not available in expected path: #{bundler_extensions_full.inspect}" + end + end + + context 'when libdatadog is unsupported' do + it do + expect(described_class.libdatadog_folder_relative_to_ruby_extensions_folders(libdatadog_pkgconfig_folder: nil)).to be nil + end + end + end + + describe '::LIBDATADOG_VERSION' do it 'must match the version restriction set on the gemspec' do # This test is expected to break when the libdatadog version on the .gemspec is updated but we forget to update