diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5af3cb3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @DataDog/ruby-guild diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3793ba2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,22 @@ +name: Publish +on: workflow_dispatch + +concurrency: "publish" # Only one publish job at a time + +jobs: + publish-ruby: + name: Build and push gem to RubyGems.org + runs-on: ubuntu-24.04 + environment: "rubygems.org" # see: https://github.com/DataDog/libdatadog-rb/settings/environments + permissions: + id-token: write # Required for trusted publishing, see https://github.com/rubygems/release-gem + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up Ruby + - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # 1.290.0 + with: + ruby-version: "ruby" + bundler-cache: true + - name: Install dependencies + run: bundle install + - uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cc305ea --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Test +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + strategy: + fail-fast: false + matrix: + platform: + - os: linux + cpu: x86_64 + base: ubuntu-24.04 # always x86_64-linux-gnu + - os: linux + cpu: aarch64 + base: ubuntu-24.04-arm # always aarch64-linux-gnu + - os: darwin + cpu: arm64 + base: macos-15 # always arm64-darwin + ruby: + - version: "2.5" + - version: "2.6" + - version: "2.7" + - version: "3.0" + - version: "3.1" + - version: "3.2" + - version: "3.3" + - version: "3.4" + - version: "4.0" + exclude: + # Ruby 2.5 is not available on arm64-darwin + - platform: + os: darwin + ruby: + version: "2.5" + name: Ruby ${{ matrix.ruby.version }} (${{ matrix.platform.cpu }}-${{ matrix.platform.os }}) + runs-on: ${{ matrix.platform.base }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # 1.290.0 + with: + ruby-version: ${{ matrix.ruby.version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - run: bundle exec rake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25b961 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status + +/Gemfile.lock + +vendor/ diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000..01a5af7 --- /dev/null +++ b/.standard.yml @@ -0,0 +1,3 @@ +# For available configuration options, see: +# https://github.com/testdouble/standard +ruby_version: 2.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2545bfb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,152 @@ +# Contributing + +Community contributions to `libdatadog-rb` are welcome 😃! See below for some basic guidelines. + +## Want to request a new feature? + +Many great ideas for new features come from the community, and we'd be happy to consider yours! + +To share your request, you can [open a Github issue](https://github.com/datadog/libdatadog-rb/issues/new) with the details +about what you'd like to see. At a minimum, please provide: + +* The goal of the new feature +* A description of how it might be used or behave +* Links to any important resources (e.g. GitHub repos, websites, screenshots, specifications, diagrams) + +Additionally, if you can, include: + +* A description of how it could be accomplished +* Code snippets that might demonstrate its use or implementation +* Screenshots or mockups that visually demonstrate the feature +* Links to similar features that would serve as a good comparison +* (Any other details that would be useful for implementing this feature!) + +## Found a bug? + +For any urgent matters (such as outages) or issues concerning the Datadog service or UI, contact our support team via +https://docs.datadoghq.com/help/ for direct, faster assistance. + +You can submit bug reports concerning `libdatadog-rb` by +[opening a Github issue](https://github.com/datadog/libdatadog-rb/issues/new). At a minimum, please provide: + +* A description of the problem +* Steps to reproduce +* Expected behavior +* Actual behavior +* Errors or warnings received +* Any details you can share about your configuration + +If at all possible, also provide: + +* Logs (from the library/profiler/application/agent) or other diagnostics +* Screenshots, links, or other visual aids that are publicly accessible +* Code sample or test that reproduces the problem +* An explanation of what causes the bug and/or how it can be fixed + +Reports that include rich detail are better, and ones with code that reproduce the bug are best. + +## Have a patch? + +We welcome code contributions to the library, which you can +[submit as a pull request](https://github.com/datadog/libdatadog-rb/pull/new/main). +To create a pull request: + +1. **Fork the repository** from +2. **Make any changes** for your patch +3. **Write tests** that demonstrate how the feature works or how the bug is fixed +4. **Update any documentation** especially for new features. +5. **Submit the pull request** from your fork back to the latest revision of the `main` branch on + + +The pull request will be run through our CI pipeline, and a project member will review the changes with you. +At a minimum, to be accepted and merged, pull requests must: + +* Have a stated goal and detailed description of the changes made +* Include thorough test coverage and documentation, where applicable +* Pass all tests and code quality checks (linting/coverage/benchmarks) on CI +* Receive at least one approval from a project member with push permissions + +We also recommend that you share in your description: + +* Any motivations or intent for the contribution +* Links to any issues/pull requests it might be related to +* Links to any webpages or other external resources that might be related to the change +* Screenshots, code samples, or other visual aids that demonstrate the changes or how they are implemented +* Benchmarks if the feature is anticipated to have performance implications +* Any limitations, constraints or risks that are important to consider + +If at any point you have a question or need assistance with your pull request, feel free to mention a project member! +We're always happy to help contributors with their pull requests. + +## Commit Message Guidelines + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for commit messages and pull request titles. +This format helps us automatically generate changelogs and determine semantic versioning. + +### Format + +Commit messages and PR titles should follow this structure: + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Common Types + +- **feat**: Code that adds features to the end user +- **fix**: A bug fix +- **docs**: Documentation changes only +- **style**: Code style changes (formatting, missing semicolons, etc.) that don't affect functionality +- **refactor**: Code changes that neither fix a bug nor add a feature. Removing a public interface is considered a refactor and should be marked with `!`. +- **perf**: Performance improvements +- **test**: Adding or updating tests +- **build**: Changes to the build system or external dependencies +- **ci**: Changes to CI configuration files and scripts +- **chore**: Other changes that don't modify src or test files + +### Scope (Optional) + +The scope provides additional context about which part of the codebase is affected: + +``` +feat(crashtracker): add signal handler for SIGSEGV +fix(profiling): correct memory leak in stack unwinding +docs(readme): update installation instructions +``` + +### Breaking Changes + +Breaking changes should be indicated by a `!` after the type/scope: + +``` +feat!: remove deprecated API endpoint +``` + +### Examples + +Good commit messages: +- `feat: add support for custom metadata tags` +- `fix(profiling): resolve deadlock in thread sampling` +- `docs: add examples for exception tracking` +- `chore: update dependencies to latest versions` +- `test(crashtracker): add integration tests for signal handling` + +Poor commit messages: +- `update code` (not descriptive, missing type) +- `Fixed bug` (missing type format, not descriptive) +- `WIP` (not meaningful) + +### Pull Request Titles + +When your pull request is merged, all commits will be squashed into a single commit. The PR title will become the final +commit message, so it's important that it accurately describes your changes.For that reason your pull request title must +follow the conventional commit format described above. Our CI pipeline will automatically validate the PR title and fail +if it doesn't comply with the format. You can update the PR title at any time to fix any validation issues. + +## Final word + +Many thanks to all of our contributors, and looking forward to seeing you on Github! :tada: diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..72da36b --- /dev/null +++ b/Gemfile @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "rake", ">= 12.0", "< 14" +gem "rspec", "~> 3.10" +gem "standard", "~> 1.7", ">= 1.7.2" unless RUBY_VERSION < "2.6" +gem "http", "~> 5.0" +gem "pry" +gem "pry-byebug" unless RUBY_VERSION > "3.1" +gem "rubygems-await" unless RUBY_VERSION < "3.1" +gem "irb" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5f8fd63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +## License + +This work is dual-licensed under Apache 2.0 or BSD3. +You may select, at your option, one of the above-listed licenses. + +`SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause` diff --git a/LICENSE-3rdparty.yml b/LICENSE-3rdparty.yml new file mode 100644 index 0000000..f5598ad --- /dev/null +++ b/LICENSE-3rdparty.yml @@ -0,0 +1,2 @@ +root_name: libdatadog-rb +third_party_libraries: [] diff --git a/LICENSE.Apache b/LICENSE.Apache new file mode 100644 index 0000000..bff56b5 --- /dev/null +++ b/LICENSE.Apache @@ -0,0 +1,200 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Datadog, Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.BSD3 b/LICENSE.BSD3 new file mode 100644 index 0000000..2335182 --- /dev/null +++ b/LICENSE.BSD3 @@ -0,0 +1,24 @@ +Copyright (c) 2016, Datadog +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Datadog nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..fee9bb3 --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +Datadog libdatadog-rb +Copyright 2021-2022 Datadog, Inc. + +This product includes software developed at Datadog (). diff --git a/README.md b/README.md index 7c12a39..6804e78 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,37 @@ -# `libdatadog-rb` +# libdatadog Ruby gem -`libdatadog-rb` packages the [`libdatadog` Rust library](https://github.com/DataDog/libdatadog) for Ruby as a [gem published on rubygems.org](https://rubygems.org/gems/libdatadog). +`libdatadog` provides a shared library containing common code used in the implementation of Datadog's libraries, +including [Continuous Profilers](https://docs.datadoghq.com/tracing/profiler/). -## Installation +**NOTE**: If you're building a new Datadog library/profiler or want to contribute to Datadog's existing tools, you've come to the +right place! +Otherwise, this is possibly not the droid you were looking for. -`libdatadog` is typically pulled in automatically by the `datadog` gem. +## Development -## Usage +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. -Direct usage is discouraged as its API is considered internal to Datadog. +### Testing packaging locally + +You can use `bundle exec rake package` to generate packages locally without publishing them. + +TIP: If the test that checks for permissions ("gem release process ... sets the right permissions on the gem files"), fails you +may need to run `umask 0022 && bundle exec rake package` so that the generated packages have the correct permissions. + +## Releasing a new version to rubygems.org + +Note: No Ruby needed to run this! It all runs in CI! + +1. [ ] Locate the new libdatadog release on GitHub: +2. [ ] Update the `LIB_GITHUB_RELEASES` section of the `Rakefile` with the hashes from the new version +3. [ ] In the file: + - [ ] Update `LIB_VERSION` with the new version. Example: Setting "25.0.0" results in the first part of the string "25.0.0.1.0.x" + - [ ] (OPTIONAL) Update `GEM_PRERELEASE_VERSION` with a prerelease descriptor. This is only needed if you want to do a prerelease. Example: Setting ".beta" results in "25.0.0.1.0.beta". +4. [ ] Commit change, open PR, get it merged +5. [ ] Trigger the "Publish" workflow in +6. [ ] Verify that release shows up correctly on: + +## Contributing + +See . diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..1b739ec --- /dev/null +++ b/Rakefile @@ -0,0 +1,236 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" +require "standard/rake" unless RUBY_VERSION < "2.6" + +require "fileutils" +require "http" unless RUBY_VERSION < "2.5" +require "pry" +require "rubygems/package" + +RSpec::Core::RakeTask.new(:spec) + +# Note: When packaging rc releases and the like, you may need to set this differently from LIB_VERSION +LIB_VERSION_TO_PACKAGE = Libdatadog::LIB_VERSION + +unless LIB_VERSION_TO_PACKAGE.start_with?(Libdatadog::LIB_VERSION) + raise "`LIB_VERSION_TO_PACKAGE` setting in (#{LIB_VERSION_TO_PACKAGE}) does not match " \ + "`LIB_VERSION` setting in (#{Libdatadog::LIB_VERSION})" +end + +LIB_GITHUB_RELEASES = [ + { + file: "libdatadog-aarch64-alpine-linux-musl.tar.gz", + sha256: "0b557141a3accc301d5ffa73d4fe1a90c84b7ceaad45b96c4b573f69170520a7", + ruby_platform: "aarch64-linux-musl" + }, + { + file: "libdatadog-aarch64-unknown-linux-gnu.tar.gz", + sha256: "a61b00551d8e2e2fbc590be08a620c2d6d610c360d94ce8bd4a0283c9ceb3db3", + ruby_platform: "aarch64-linux" + }, + { + file: "libdatadog-x86_64-alpine-linux-musl.tar.gz", + sha256: "5cbde7937d1661cc0483f7cea0c6ec3d0c7bd1540fc4bbe57f9a0a35296c579d", + ruby_platform: "x86_64-linux-musl" + }, + { + file: "libdatadog-x86_64-unknown-linux-gnu.tar.gz", + sha256: "617831c7fb9d0d9e01aa1bc232f89c6a21482d9cfefba4dcc527e8561128eafb", + ruby_platform: "x86_64-linux" + }, + { + file: "libdatadog-aarch64-apple-darwin.tar.gz", + sha256: "298346e13092c057bb2515da5b45e56a34d7a0357589f91e4bb65a375aa23656", + ruby_platform: "arm64-darwin" + } +] + +task default: [ + :spec, + (:standard unless RUBY_VERSION < "2.6") +].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/libdatadog/releases/download/v#{LIB_VERSION_TO_PACKAGE}/#{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 + + # Fix file permissions after extraction + puts "Fixing file permissions in #{target_directory}" + Helpers.fix_file_permissions(target_directory) + end +end + +desc "Package lib downloaded releases as gems" +task package: [ + :spec, + (:"standard:fix" unless RUBY_VERSION < "2.6"), + :extract +] do + gemspec = eval(File.read("libdatadog.gemspec"), nil, "libdatadog.gemspec") # standard:disable Security/Eval + FileUtils.mkdir_p("pkg") + + # Fallback package with all binaries + # This package will get used by (1) platforms that have no matching `ruby_platform` or (2) that have set + # "BUNDLE_FORCE_RUBY_PLATFORM" (or its equivalent via code) to avoid precompiled gems. + # In a previous version of libdatadog, this package had no binaries, but that could mean that we broke customers in case (2). + # For customers in case (1), this package is a no-op, and dd-trace-rb will correctly detect and warn that + # there are no valid binaries for the platform. + Helpers.package_for(gemspec, ruby_platform: nil, files: Helpers.files_for("x86_64-linux", "x86_64-linux-musl", "aarch64-linux", "aarch64-linux-musl", "arm64-darwin")) + + # 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 + Helpers.package_for(gemspec, ruby_platform: "x86_64-linux", files: Helpers.files_for("x86_64-linux", "x86_64-linux-musl")) + Helpers.package_for(gemspec, ruby_platform: "aarch64-linux", files: Helpers.files_for("aarch64-linux", "aarch64-linux-musl")) + + # macOS package (Apple Silicon) + Helpers.package_for(gemspec, ruby_platform: "arm64-darwin", files: Helpers.files_for("arm64-darwin")) +end + +Rake::Task["package"].enhance { Rake::Task["spec_validate_permissions"].execute } + +task :spec_validate_permissions do + require "rspec" + RSpec.world.reset # If any other tests ran before, flushes them + ret = RSpec::Core::Runner.run(["spec/gem_packaging.rb"]) + raise "Release tests failed! See error output above." if ret != 0 +end + +desc "Release all packaged gems" +task push_to_rubygems: [ + :package, + :"release:guard_clean" +] do + [ + "gem push pkg/libdatadog-#{Libdatadog::VERSION}.gem", + "gem push pkg/libdatadog-#{Libdatadog::VERSION}-x86_64-linux.gem", + "gem push pkg/libdatadog-#{Libdatadog::VERSION}-aarch64-linux.gem", + "gem push pkg/libdatadog-#{Libdatadog::VERSION}-arm64-darwin.gem" + ].each do |command| + puts "Running: #{command}" + abort unless system(command) + end +end + +module Helpers + # Files that should have executable permissions (755) in the gem + # Note: .so for Linux, .dylib for macOS + EXECUTABLE_FILES = ["libdatadog-crashtracking-receiver", "libdatadog_profiling.so", "libdatadog_profiling.dylib"].freeze + + def self.each_github_release_variant + LIB_GITHUB_RELEASES.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/libdatadog-#{Libdatadog::LIB_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_for(gemspec, ruby_platform:, files:) + target_gemspec = gemspec.dup + target_gemspec.files += files + target_gemspec.platform = ruby_platform if ruby_platform + + puts "Building with ruby_platform=#{ruby_platform.inspect} 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.fix_file_permissions(directory) + Dir.glob("#{directory}/**/*").each do |path| + next unless File.file?(path) + + filename = File.basename(path) + current_permissions = File.stat(path).mode & 0o777 + + if EXECUTABLE_FILES.include?(filename) + # Should be executable (755), fix if not + if current_permissions != 0o755 + puts "Fixing permissions for #{filename}: #{current_permissions.to_s(8)} -> 755" + FileUtils.chmod(0o755, path) + end + elsif current_permissions != 0o644 + # Should be non-executable (644), fix if not + puts "Fixing permissions for #{filename}: #{current_permissions.to_s(8)} -> 644" + FileUtils.chmod(0o644, path) + end + end + end + + def self.files_for( + *included_platforms, + excluded_files: [ + "datadog_profiling.pc", # we use the datadog_profiling_with_rpath.pc variant + "libdatadog_profiling.a", "datadog_profiling-static.pc", # We don't use the static library + "libdatadog_profiling.debug", # We don't include debug info + "DatadogConfig.cmake" # We don't compile using cmake + ] + ) + files = [] + + each_github_release_variant do |ruby_platform:, target_directory:, target_file:, **_| + next unless included_platforms.include?(ruby_platform) + + downloaded_release_tarball = target_file + + files += + Dir.glob("#{target_directory}/**/*") + .select { |path| File.file?(path) } + .reject { |path| path == downloaded_release_tarball } + .reject { |path| excluded_files.include?(File.basename(path)) } + end + + files + end +end + +Rake::Task["build"].clear +task(:build) { raise "Build task is disabled, use package instead" } + +Rake::Task["release"].clear +task(:release) { Rake::Task["push_to_rubygems"].invoke } diff --git a/lib/libdatadog.rb b/lib/libdatadog.rb new file mode 100644 index 0000000..aeaf36b --- /dev/null +++ b/lib/libdatadog.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require_relative "libdatadog/version" + +module Libdatadog + # This should only be used for debugging/logging + def self.available_binaries + File.directory?(vendor_directory) ? (Dir.entries(vendor_directory) - [".", ".."]) : [] + end + + def self.pkgconfig_folder(pkgconfig_file_name = "datadog_profiling_with_rpath.pc") + current_platform = self.current_platform + + return unless available_binaries.include?(current_platform) + + pkgconfig_file = Dir.glob("#{vendor_directory}/#{current_platform}/**/#{pkgconfig_file_name}").first + + return unless pkgconfig_file + + File.absolute_path(File.dirname(pkgconfig_file)) + end + + private_class_method def self.vendor_directory + ENV["LIBDATADOG_VENDOR_OVERRIDE"] || "#{__dir__}/../vendor/libdatadog-#{Libdatadog::LIB_VERSION}/" + end + + def self.current_platform + platform = Gem::Platform.local.to_s + + if platform.end_with?("-gnu") + # In some cases on Linux with glibc the platform includes a -gnu suffix. We normalize it to not have the suffix. + # + # Note: This should be platform = platform.delete_suffix("-gnu") but it doesn't work on legacy Rubies; once + # dd-trace-rb 2.0 is out we can simplify this. + # + platform = platform[0..-5] + end + + # Normalize macOS/Darwin platform strings by stripping the version number. + # e.g., "arm64-darwin-24" -> "arm64-darwin", "x86_64-darwin-19" -> "x86_64-darwin" + if platform.include?("darwin") + platform = platform.gsub(/-darwin-?\d*$/, "-darwin") + end + + if RbConfig::CONFIG["arch"].include?("-musl") && !platform.include?("-musl") + # Fix/workaround for https://github.com/datadog/dd-trace-rb/issues/2222 + # + # Old versions of rubygems (for instance 3.0.3) don't properly detect alternative libc implementations on Linux; + # in particular for our case, they don't detect musl. (For reference, Rubies older than 2.7 may have shipped with + # an affected version of rubygems). + # In such cases, we fall back to use RbConfig::CONFIG['arch'] instead. + # + # Why not use RbConfig::CONFIG['arch'] always? Because Gem::Platform.local.to_s does some normalization that seemed + # useful in the past, although part of it got removed in https://github.com/rubygems/rubygems/pull/5852. + # For now we only add this workaround in a specific situation where we actually know it is wrong, but in the + # future it may be worth re-evaluating if we should move away from `Gem::Platform` altogether. + # + # See also https://github.com/rubygems/rubygems/pull/2922 and https://github.com/rubygems/rubygems/pull/4082 + + RbConfig::CONFIG["arch"] + else + platform + end + end + + def self.path_to_crashtracking_receiver_binary + pkgconfig_folder = self.pkgconfig_folder + + return unless pkgconfig_folder + + File.absolute_path("#{pkgconfig_folder}/../../bin/libdatadog-crashtracking-receiver") + end + + def self.ld_library_path + pkgconfig_folder = self.pkgconfig_folder + + return unless pkgconfig_folder + + File.absolute_path("#{pkgconfig_folder}/../") + end +end diff --git a/lib/libdatadog/version.rb b/lib/libdatadog/version.rb new file mode 100644 index 0000000..fbd1154 --- /dev/null +++ b/lib/libdatadog/version.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Libdatadog + # Current libdatadog version + LIB_VERSION = "28.0.2" + + GEM_MAJOR_VERSION = "1" + GEM_MINOR_VERSION = "0" + GEM_PRERELEASE_VERSION = "" # remember to include dot prefix, if needed! + private_constant :GEM_MAJOR_VERSION, :GEM_MINOR_VERSION, :GEM_PRERELEASE_VERSION + + # The gem version scheme is lib_version.gem_major.gem_minor[.prerelease]. + # This allows a version constraint such as ~> 0.2.0.1.0 in the consumer (dd-trace-rb), in essence pinning libdatadog 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 diff --git a/libdatadog.gemspec b/libdatadog.gemspec new file mode 100644 index 0000000..c478ec3 --- /dev/null +++ b/libdatadog.gemspec @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "libdatadog/version" + +Gem::Specification.new do |spec| + spec.name = "libdatadog" + spec.version = Libdatadog::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 = + "libdatadog is a Rust-based utility library for Datadog's ddtrace gem." + spec.homepage = "https://docs.datadoghq.com/tracing/" + spec.license = "Apache-2.0" + spec.required_ruby_version = ">= 2.5.0" + + spec.metadata["allowed_push_host"] = "https://rubygems.org" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/datadog/libdatadog-rb" + + # 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|publish)/|\.(?:git|travis|circleci)|appveyor)}) + end + .reject do |f| + [".rspec", ".standard.yml", "Rakefile", "docker-compose.yml", "Gemfile", "README.md"].include?(f) + end + .reject { |f| f.end_with?(".tar.gz") } + end + spec.require_paths = ["lib"] +end diff --git a/spec/gem_packaging.rb b/spec/gem_packaging.rb new file mode 100644 index 0000000..86c4601 --- /dev/null +++ b/spec/gem_packaging.rb @@ -0,0 +1,51 @@ +# Note: This file does not end with _spec on purpose, it should only be run after packaging, e.g. with `rake spec_validate_permissions` + +require "rubygems" +require "rubygems/package" +require "rubygems/package/tar_reader" +require "libdatadog" +require "zlib" + +RSpec.describe "gem release process (after packaging)" do + let(:gem_version) { Libdatadog::VERSION } + let(:packaged_gem_file) { "pkg/libdatadog-#{gem_version}.gem" } + + it "sets the right permissions on the .gem files" do + gem_files = Dir.glob("pkg/*.gem") + expect(gem_files).to include(packaged_gem_file) + + gem_files.each do |gem_file| + Gem::Package::TarReader.new(File.open(gem_file)) do |tar| + data = tar.find { |entry| entry.header.name == "data.tar.gz" } + + Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(data.read))) do |data_tar| + data_tar.each do |entry| + filename = entry.header.name.split("/").last + octal_permissions = entry.header.mode.to_s(8)[-3..-1] + + expected_permissions = Helpers::EXECUTABLE_FILES.include?(filename) ? "755" : "644" + + expect(octal_permissions).to eq(expected_permissions), + "Unexpected permissions for #{filename} inside #{gem_file} (got #{octal_permissions}, " \ + "expected #{expected_permissions})" + end + end + end + end + end + + it "prefixes all public symbols in .so files" do + so_files = Dir.glob("vendor/libdatadog-#{Libdatadog::LIB_VERSION}/**/*.so") + expect(so_files.size).to be 4 + + so_files.each do |so_file| + raw_symbols = `nm -D --defined-only #{so_file}` + + symbols = raw_symbols.split("\n").map { |symbol| symbol.split(" ").last.downcase }.sort + expect(symbols.size).to be > 20 # Quick sanity check + expect(symbols).to all( + start_with("ddog_").or(start_with("blaze_")) + ) + end + end +end diff --git a/spec/libdatadog_spec.rb b/spec/libdatadog_spec.rb new file mode 100644 index 0000000..2e7e686 --- /dev/null +++ b/spec/libdatadog_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require "tmpdir" +require "fileutils" + +RSpec.describe Libdatadog do + describe "version constants" do + it "has a version number" do + expect(Libdatadog::VERSION).to_not be nil + end + + it "has an upstream libdatadog version number" do + expect(Libdatadog::LIB_VERSION).to_not be nil + end + end + + describe "binary helper methods" do + let(:temporary_directory) { Dir.mktmpdir } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with("LIBDATADOG_VENDOR_OVERRIDE").and_return(temporary_directory) + end + + after do + begin + FileUtils.remove_dir(temporary_directory) + rescue Errno::ENOENT, Errno::ENOTDIR + # Do nothing, it's ok + end + end + + shared_examples_for "libdatadog not in usable state" do + describe ".pkgconfig_folder" do + it { expect(Libdatadog.pkgconfig_folder).to be nil } + end + + describe ".path_to_crashtracking_receiver_binary" do + it { expect(Libdatadog.path_to_crashtracking_receiver_binary).to be nil } + end + + describe ".ld_library_path" do + it { expect(Libdatadog.ld_library_path).to be nil } + end + end + + context "when no binaries are available in the vendor directory" do + describe ".available_binaries" do + it { expect(Libdatadog.available_binaries).to be_empty } + end + + it_behaves_like "libdatadog not in usable state" + end + + context "when vendor directory does not exist" do + let(:temporary_directory) { "does/not/exist" } + + describe ".available_binaries" do + it { expect(Libdatadog.available_binaries).to be_empty } + end + + it_behaves_like "libdatadog not in usable state" + end + + context "when binaries are available in the vendor directory" do + before do + Dir.mkdir("#{temporary_directory}/386-freedos") + Dir.mkdir("#{temporary_directory}/mipsel-linux") + end + + describe ".available_binaries" do + it { expect(Libdatadog.available_binaries).to contain_exactly("386-freedos", "mipsel-linux") } + end + + context "for the current platform" do + let(:current_platform) do + platform = Gem::Platform.local.to_s + platform.include?("darwin") ? "arm64-darwin" : platform + end + let(:pkgconfig_folder) { "#{temporary_directory}/#{current_platform}/some/folder/containing/the/lib/pkgconfig" } + + before do + create_dummy_pkgconfig_file(pkgconfig_folder) + end + + def create_dummy_pkgconfig_file(pkgconfig_folder) + begin + FileUtils.mkdir_p(pkgconfig_folder) + rescue Errno::EEXIST + # No problem, a few specs try to create the same folder + end + + File.open("#{pkgconfig_folder}/datadog_profiling_with_rpath.pc", "w+") {} + end + + describe ".pkgconfig_folder" do + it "returns the folder containing the pkgconfig file" do + expect(Libdatadog.pkgconfig_folder).to eq pkgconfig_folder + end + end + + context "when `RbConfig::CONFIG[\"arch\"]` indicates we're on musl libc, but `Gem::Platform.local.to_s` does not detect it" do + # Fix for https://github.com/DataDog/dd-trace-rb/issues/2222 + + before do + allow(RbConfig::CONFIG).to receive(:[]).and_call_original + allow(RbConfig::CONFIG).to receive(:[]).with("arch").and_return("x86_64-linux-musl") + allow(Gem::Platform).to receive(:local).and_return("x86_64-linux") + + ["x86_64-linux", "x86_64-linux-musl"].each do |arch| + create_dummy_pkgconfig_file("#{temporary_directory}/#{arch}/some/folder/containing/the/pkgconfig/file") + end + end + + it "returns the folder containing the pkgconfig file for the musl variant" do + expect(Libdatadog.pkgconfig_folder).to eq "#{temporary_directory}/x86_64-linux-musl/some/folder/containing/the/pkgconfig/file" + end + end + + context "when platform ends with -gnu" do + let(:pkgconfig_folder) { "#{temporary_directory}/aarch64-linux/some/folder/containing/the/pkgconfig/file" } + + before do + allow(Gem::Platform).to receive(:local).and_return(Gem::Platform.new("aarch64-linux-gnu")) + end + + it "chops off the -gnu suffix and returns the folder containing the pkgconfig file for the non-gnu variant" do + expect(Libdatadog.pkgconfig_folder).to eq pkgconfig_folder + end + end + + describe ".path_to_crashtracking_receiver_binary" do + it "returns the full path to the crashtracking_receiver_binary" do + expect(Libdatadog.path_to_crashtracking_receiver_binary).to eq( + "#{temporary_directory}/#{current_platform}/some/folder/containing/the/bin/libdatadog-crashtracking-receiver" + ) + end + end + + describe ".ld_library_path" do + it "returns the full path to the libdatadog lib directory" do + expect(Libdatadog.ld_library_path).to eq( + "#{temporary_directory}/#{current_platform}/some/folder/containing/the/lib" + ) + end + end + end + + context "but not for the current platform" do + it_behaves_like "libdatadog not in usable state" + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..6557307 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "pry" + +require "libdatadog" + +# This file was generated by the `rspec --init` command + manually tweaked. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + # This will default to `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end