Module: Datadog::LibdatadogExtconfHelpers

Defined in:
ext/libdatadog_extconf_helpers.rb

Overview

Contains a bunch of shared helpers that get used during building of extensions that link to libdatadog

Note: Specs for this file currently live in spec/datadog/profiling/native_extension_helpers_spec.rb.

Constant Summary collapse

LIBDATADOG_VERSION =

Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with “bundle exec” and thus may see multiple libdatadog versions. See github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.

'~> 29.0.0.1.0'

Class Method Summary collapse

Class Method Details

.configure_libdatadog(extconf_folder:, libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder, gem_dir: Gem.dir, logger: Logging) ⇒ Object

Directly configures mkmf to link against libdatadog by setting $INCFLAGS, $LDFLAGS, and $libs.

This replaces the previous use of mkmf’s ‘pkg_config(“datadog_profiling_with_rpath”)`, removing the need for the pkg-config system tool to be installed.

The extconf_folder argument should be the __dir__ of the calling extconf.rb, used to compute relative rpaths. The logger argument is the mkmf Logging module, dependency-injected to make testing easier. rubocop:disable Style/GlobalVars



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'ext/libdatadog_extconf_helpers.rb', line 115

def self.configure_libdatadog(
  extconf_folder:,
  libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder,
  gem_dir: Gem.dir,
  logger: Logging
)
  return unless libdatadog_pkgconfig_folder

  # The lib and include folders are at a fixed relative path from pkgconfig_folder
  libdir = "#{libdatadog_pkgconfig_folder}/../../lib"
  includedir = "#{libdatadog_pkgconfig_folder}/../../include"

  # Set mkmf global variables
  $INCFLAGS << " -I#{includedir}"
  $LDFLAGS << " -L#{libdir} -Wl,-rpath,#{libdir}"
  $libs << " -ldatadog_profiling"

  # Add extra relative rpaths using $ORIGIN to handle environments where gems are moved after installation.
  # The excessive escaping is needed to get these special characters through Make and the shell untouched.
  extra_relative_rpaths = [
    libdatadog_folder_relative_to_native_lib_folder(
      extconf_folder: extconf_folder,
      libdatadog_pkgconfig_folder: libdatadog_pkgconfig_folder,
    ),
    *libdatadog_folder_relative_to_ruby_extensions_folders(
      gem_dir: gem_dir,
      libdatadog_pkgconfig_folder: libdatadog_pkgconfig_folder,
    ),
  ]
  extra_relative_rpaths.each { |folder| $LDFLAGS << " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{folder}" }

  logger.message("linking with libdatadog (include=#{includedir}, lib=#{libdir})\n")
  logger.message("[datadog] $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")

  true
end

.libdatadog_folder_relative_to_native_lib_folder(extconf_folder:, libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder) ⇒ Object

Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and libdatadog are moved after the extension gets compiled.

Because the libdatadog native library is installed on a non-standard system path, in order for it to be found by the system dynamic linker (e.g. what takes care of dlopen(), which is used to load native extensions), we need to add a “runpath” – a list of folders to search for libdatadog.

This runpath gets hardcoded at native library linking time. You can look at it using the readelf tool in Linux: e.g. ‘readelf -d datadog_profiling_native_extension.2.7.3_x86_64-linux.so`.

In older versions of the datadog gem, we only set as runpath an absolute path to libdatadog. (This gets set automatically by the call to configure_libdatadog in extconf.rb). This worked fine as long as libdatadog was NOT moved from the folder it was present at datadog gem installation/linking time.

Unfortunately, environments such as Heroku and AWS Elastic Beanstalk move gems around in the filesystem after installation. Thus, the profiling native extension could not be loaded in these environments (see github.com/DataDog/dd-trace-rb/issues/2067) because libdatadog could not be found.

To workaround this issue, this method computes the relative path between the folder where native extensions are going to be installed and the folder where libdatadog is installed, and returns it to be set as an additional runpath. (Yes, you can set multiple runpath folders to be searched).

This way, if both gems are moved together (and it turns out that they are in these environments), the relative path can still be traversed to find libdatadog.

This is incredibly awful, and it’s kinda bizarre how it’s not possible to just find these paths at runtime and set them correctly; rather than needing to set stuff at linking-time and then praying to $deity that weird moves don’t happen.

As a curiosity, LD_LIBRARY_PATH can be used to influence the folders that get searched but **CANNOT BE SET DYNAMICALLY**, e.g. it needs to be set at the start of the process (Ruby VM) and thus it’s not something we could setup when doing a require.



49
50
51
52
53
54
55
56
57
58
59
# File 'ext/libdatadog_extconf_helpers.rb', line 49

def self.libdatadog_folder_relative_to_native_lib_folder(
  extconf_folder:,
  libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder
)
  return unless libdatadog_pkgconfig_folder

  native_lib_folder = "#{extconf_folder}/../../lib/"
  libdatadog_lib_folder = "#{libdatadog_pkgconfig_folder}/../"

  Pathname.new(libdatadog_lib_folder).relative_path_from(Pathname.new(native_lib_folder)).to_s
end

.libdatadog_folder_relative_to_ruby_extensions_folders(gem_dir: Gem.dir, libdatadog_pkgconfig_folder: Libdatadog.pkgconfig_folder) ⇒ Object

In 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 github.com/DataDog/dd-trace-rb/issues/2067 / 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?

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.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'ext/libdatadog_extconf_helpers.rb', line 87

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

.load_libdatadog_or_get_issueObject

Note: This helper is currently only used in the libdatadog_api/extconf.rb BUT still lives here to enable testing.



166
167
168
169
170
171
172
173
174
175
176
# File 'ext/libdatadog_extconf_helpers.rb', line 166

def self.load_libdatadog_or_get_issue
  try_loading_libdatadog do |exception|
    return "There was an error loading `libdatadog`: #{exception.class} #{exception.message}"
  end

  unless Libdatadog.pkgconfig_folder
    "The `libdatadog` gem installed on your system is missing binaries for your platform variant. " \
      "Your platform: " \
      "`#{Libdatadog.current_platform}`; available binaries: `#{Libdatadog.available_binaries.join("`, `")}`"
  end
end

.try_loading_libdatadogObject

rubocop:enable Style/GlobalVars



153
154
155
156
157
158
159
160
161
162
163
# File 'ext/libdatadog_extconf_helpers.rb', line 153

def self.try_loading_libdatadog
  gem 'libdatadog', LIBDATADOG_VERSION
  require 'libdatadog'
  nil
rescue Exception => e # rubocop:disable Lint/RescueException
  if block_given?
    yield e
  else
    e
  end
end