Skip to content
This repository has been archived by the owner on Jun 28, 2022. It is now read-only.

[PROF-4779] Publish libddprof as a Ruby gem on rubygems.org #16

Merged
merged 11 commits into from
Feb 3, 2022
Merged
15 changes: 15 additions & 0 deletions ruby/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

# rspec failure tracking
.rspec_status

gems.locked

vendor/
3 changes: 3 additions & 0 deletions ruby/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
3 changes: 3 additions & 0 deletions ruby/.standard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# For available configuration options, see:
# https://github.com/testdouble/standard
ruby_version: 2.1
31 changes: 31 additions & 0 deletions ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# libddprof Ruby gem

`libddprof` provides a shared library containing common code used in the implementation of Datadog's
[Continuous Profilers](https://docs.datadoghq.com/tracing/profiler/).

**NOTE**: If you're building a new profiler or want to contribute to Datadog's existing profilers, you've come to the
right place!
Otherwise, this is possibly not the droid you were looking for.

## Development

Run `bundle exec rake` to run the tests and the style autofixer.
You can also run `bundle exec pry` for an interactive prompt that will allow you to experiment.

## Releasing a new version to rubygems.org

Note: No Ruby needed to run this! It all runs inside docker :)

Note: Publishing new releases to rubygems.org can only be done by Datadog employees.

1. [ ] Locate the new libddprof release on GitHub: <https://github.com/DataDog/libddprof/releases>
2. [ ] Update the `LIB_GITHUB_RELEASES` section of the <Rakefile> with the new version
3. [ ] Update the <lib/libddprof/version.rb> file with the `LIB_VERSION` and `VERSION` to use
4. [ ] Commit change, open PR, get it merged
5. [ ] Release by running `docker-compose run push_to_rubygems`.
(When asked for rubygems credentials, check your local friendly Datadog 1Password.)
6. [ ] Verify that release shows up correctly on: <https://rubygems.org/gems/libddprof>

## Contributing

See <../CONTRIBUTING.md>.
151 changes: 151 additions & 0 deletions ruby/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "standard/rake" unless RUBY_VERSION < "2.5"

require "fileutils"
require "http"
require "pry"
require "rubygems/package"

RSpec::Core::RakeTask.new(:spec)

LIB_GITHUB_RELEASES = {
"0.2.0" => [
{
file: "libddprof-x86_64-unknown-linux-gnu.tar.gz",
sha256: "cba0f24074d44781d7252b912faff50d330957e84a8f40a172a8138e81001f27",
ruby_platform: "x86_64-linux"
},
{
file: "libddprof-x86_64-alpine-linux-musl.tar.gz",
sha256: "d519a6241d78260522624b8e79e98502510f11d5d9551f5f80fc1134e95fa146",
ruby_platform: "x86_64-linux-musl"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pedantic, but will anyone be confused by seeing libddprof-x86_64-alpine-linux-musl on their non-Alpine musl system? As far as I know, the liddprof artifacts don't care. In fact, I wouldn't be surprised if the musl artifact works perfectly fine on glibc systems.

(irrelevant comment is irrelevant, sorry)

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a good question!

Ideally we would upload each ruby_platform variant separately to rubygems.org, and Ruby would pick the right one. But there's a bunch of bugs around that and even if the bugs were fixed, we support really old Ruby versions that wouldn't be running the fixed versions.

So actually we're shipping both variants inside the same release on rubygems.org, and then the consumer will pick the right one:

def self.pkgconfig_folder
  current_platform = Gem::Platform.local.to_s

  return unless available_binaries.include?(current_platform)

  pkgconfig_file = 
    Dir.glob("#{vendor_directory}/#{current_platform}/**/ddprof_ffi.pc").first # <--- magic happens here

But TL;DR I don't think this will confuse customers because they will only get libddprof through dd-trace-rb, which will use the code above to pick the right one and ignore the other.

}
]
# Add more versions here
}

task default: [
:spec,
(:'standard:fix' unless RUBY_VERSION < "2.5")
].compact

desc "Download lib release from github"
task :fetch do
Helpers.each_github_release_variant do |file:, sha256:, target_directory:, target_file:, **_|
target_url = "https://github.com/DataDog/libddprof/releases/download/v#{Libddprof::LIB_VERSION}/#{file}"

if File.exist?(target_file)
target_file_hash = Digest::SHA256.hexdigest(File.read(target_file))

if target_file_hash == sha256
puts "Found #{target_file} matching the expected sha256, skipping download"
next
else
puts "Found #{target_file} with hash (#{target_file_hash}) BUT IT DID NOT MATCH THE EXPECTED sha256 (#{sha256}), downloading it again..."
end
end

puts "Going to download #{target_url} into #{target_file}"

File.open(target_file, "wb") do |file|
HTTP.follow.get(target_url).body.each { |chunk| file.write(chunk) }
end

if Digest::SHA256.hexdigest(File.read(target_file)) == sha256
puts "Success!"
else
raise "Downloaded file is corrupt, does not match expected sha256"
end
end
end

desc "Extract lib downloaded releases"
task extract: [:fetch] do
Helpers.each_github_release_variant do |target_directory:, target_file:, **_|
puts "Extracting #{target_file}"
File.open(target_file, "rb") do |file|
Gem::Package.new("").extract_tar_gz(file, target_directory)
end
end
end

desc "Package lib downloaded releases as gems"
task package: [:spec, :'standard:fix', :extract] do
gemspec = eval(File.read("libddprof.gemspec"), nil, "libddprof.gemspec") # standard:disable Security/Eval
FileUtils.mkdir_p("pkg")

Helpers.package_without_binaries(gemspec)
Helpers.package_linux_x86_64(gemspec)
end

desc "Release all packaged gems"
task push_to_rubygems: [
Copy link

Choose a reason for hiding this comment

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

Seems uncanny to have the _to_rubygems suffix. Is there a conflicting push task preexisting? We could also name it push:all, WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

While a bit uncommon, I like that it exactly matches the name of the docker invocation that calls it (docker-compose run push_to_rubygems), which was why I adopted this naming.

:package,
:'release:guard_clean'
] do
system("gem signout") # make sure there are no existing credentials in use

system("gem push pkg/libddprof-#{Libddprof::VERSION}.gem")
system("gem push pkg/libddprof-#{Libddprof::VERSION}-x86_64-linux.gem")

system("gem signout") # leave no credentials behind
end

module Helpers
def self.each_github_release_variant(version: Libddprof::LIB_VERSION)
LIB_GITHUB_RELEASES.fetch(version).each do |variant|
file = variant.fetch(:file)
sha256 = variant.fetch(:sha256)
ruby_platform = variant.fetch(:ruby_platform)

# These two are so common that we just centralize them here
target_directory = "vendor/libddprof-#{version}/#{ruby_platform}"
target_file = "#{target_directory}/#{file}"

FileUtils.mkdir_p(target_directory)

yield(file: file, sha256: sha256, ruby_platform: ruby_platform, target_directory: target_directory, target_file: target_file)
end
end

def self.package_without_binaries(gemspec)
target_gemspec = gemspec.dup

puts "Building a variant without binaries including:"
pp target_gemspec.files

package = Gem::Package.build(target_gemspec)
FileUtils.mv(package, "pkg")
puts("-" * 80)
end

def self.package_linux_x86_64(gemspec)
# We include both glibc and musl variants in the same binary gem to avoid the issues
# documented in https://github.com/rubygems/rubygems/issues/3174
target_gemspec = gemspec.dup
target_gemspec.files += files_for("x86_64-linux", "x86_64-linux-musl")
target_gemspec.platform = "x86_64-linux"

puts "Building for x86_64-linux including: (this can take a while)"
pp target_gemspec.files

package = Gem::Package.build(target_gemspec)
FileUtils.mv(package, "pkg")
puts("-" * 80)
end

def self.files_for(*included_platforms, version: Libddprof::LIB_VERSION)
files = []

each_github_release_variant(version: version) do |ruby_platform:, target_directory:, target_file:, **_|
next unless included_platforms.include?(ruby_platform)

files += Dir.glob("#{target_directory}/**/*").select { |path| File.file?(path) } - [target_file]
end

files
end
end
14 changes: 14 additions & 0 deletions ruby/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: '3.2'
services:
push_to_rubygems:
image: ruby:3.1
platform: linux/x86_64
stdin_open: true
tty: true
command: bash -c 'cd /libddprof/ruby && bundle install && bundle exec rake push_to_rubygems'
volumes:
- ..:/libddprof
- bundle-3.1:/usr/local/bundle

volumes:
bundle-3.1:
13 changes: 13 additions & 0 deletions ruby/gems.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

source "https://rubygems.org"

# Specify your gem's dependencies in libddprof.gemspec
gemspec

gem "rake", "~> 13.0"
gem "rspec", "~> 3.10"
gem "standard", "~> 1.3" unless RUBY_VERSION < "2.5"
gem "http", "~> 5.0"
gem "pry"
gem "pry-byebug"
31 changes: 31 additions & 0 deletions ruby/lib/libddprof.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require_relative "libddprof/version"

module Libddprof
# Does this libddprof release include any binaries?
def self.binaries?
available_binaries.any?
end

# This should only be used for debugging/logging
def self.available_binaries
File.directory?(vendor_directory) ? Dir.children(vendor_directory) : []
end

def self.pkgconfig_folder
current_platform = Gem::Platform.local.to_s

return unless available_binaries.include?(current_platform)

pkgconfig_file = Dir.glob("#{vendor_directory}/#{current_platform}/**/ddprof_ffi.pc").first

return unless pkgconfig_file

File.absolute_path(File.dirname(pkgconfig_file))
end

private_class_method def self.vendor_directory
ENV["LIBDDPROF_VENDOR_OVERRIDE"] || "#{__dir__}/../vendor/libddprof-#{Libddprof::LIB_VERSION}/"
end
end
17 changes: 17 additions & 0 deletions ruby/lib/libddprof/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Libddprof
# Current libddprof version
LIB_VERSION = "0.2.0"

GEM_MAJOR_VERSION = "1"
GEM_MINOR_VERSION = "0"
GEM_PRERELEASE_VERSION = ".beta3"
private_constant :GEM_MAJOR_VERSION, :GEM_MINOR_VERSION, :GEM_PRERELEASE_VERSION

# The gem version scheme is lib_version.gem_major.gem_minor[.prerelease].
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, this makes sense.

# This allows a version constraint such as ~> 0.2.0.1.0 in the consumer (ddtrace), in essence pinning the libddprof to
# a specific version like = 0.2.0, but still allow a) introduction of a gem-level breaking change by bumping gem_major
# and b) allow to push automatically picked up bugfixes by bumping gem_minor.
VERSION = "#{LIB_VERSION}.#{GEM_MAJOR_VERSION}.#{GEM_MINOR_VERSION}#{GEM_PRERELEASE_VERSION}"
end
36 changes: 36 additions & 0 deletions ruby/libddprof.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "libddprof/version"

Gem::Specification.new do |spec|
spec.name = "libddprof"
spec.version = Libddprof::VERSION
spec.authors = ["Datadog, Inc."]
spec.email = ["dev@datadoghq.com"]

spec.summary = "Library of common code used by Datadog Continuous Profiler for Ruby"
spec.description =
"libddprof contains implementation bits used by Datadog's ddtrace gem as part of its Continuous Profiler feature."
spec.homepage = "https://docs.datadoghq.com/tracing/profiler/"
spec.license = "Apache-2.0"
spec.required_ruby_version = ">= 2.1.0"

spec.metadata["allowed_push_host"] = "https://rubygems.org"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/DataDog/libddprof/tree/main/ruby"

# Require releases on rubygems.org to be coming from multi-factor-auth-authenticated accounts
spec.metadata["rubygems_mfa_required"] = "true"

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject do |f|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
end
end
spec.require_paths = ["lib"]
end
Loading