diff --git a/.github/actions/build-ruby-layer/action.yml b/.github/actions/build-ruby-layer/action.yml index 0fc7f4597f..be5b3073a9 100644 --- a/.github/actions/build-ruby-layer/action.yml +++ b/.github/actions/build-ruby-layer/action.yml @@ -12,17 +12,21 @@ outputs: runs: using: composite steps: - - name: Build Ruby layer - shell: bash - working-directory: ruby/src - run: ./build.sh - - name: Save Ruby OpenTelemetry SDK version id: version shell: bash working-directory: ruby/src run: | - unzip -q build/opentelemetry-ruby-layer.zip - export GEM_PATH=$PWD/ruby/gems/3.3.0/ - SDK_VERSION=$(ruby -e 'require "opentelemetry-sdk"; puts OpenTelemetry::SDK::VERSION') + SDK_VERSION=$(sed -n 's/^ opentelemetry-sdk (\([^)]*\))$/\1/p' otel/layer/Gemfile.lock) echo "component-version=$SDK_VERSION" >> "$GITHUB_OUTPUT" + + # The layer also contains arm64 gems, built in emulated arm64 stages. + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + with: + platforms: arm64 + + - name: Build Ruby layer + shell: bash + working-directory: ruby/src + run: ./build.sh diff --git a/.github/workflows/ci-ruby.yml b/.github/workflows/ci-ruby.yml index 07789afbd6..fda37f53b3 100644 --- a/.github/workflows/ci-ruby.yml +++ b/.github/workflows/ci-ruby.yml @@ -5,12 +5,14 @@ on: paths: - 'ruby/**' - '.github/workflows/ci-ruby.yml' + - '.github/actions/build-ruby-layer/**' branches: - main pull_request: paths: - 'ruby/**' - '.github/workflows/ci-ruby.yml' + - '.github/actions/build-ruby-layer/**' branches: - main @@ -25,6 +27,4 @@ jobs: with: persist-credentials: false - name: Build Ruby layer - shell: bash - working-directory: ruby/src - run: ./build.sh + uses: $/.github/actions/build-ruby-layer diff --git a/integration-tests/tests/layer.test.ts b/integration-tests/tests/layer.test.ts index a962a7f55a..c7c2492fe4 100644 --- a/integration-tests/tests/layer.test.ts +++ b/integration-tests/tests/layer.test.ts @@ -24,6 +24,7 @@ describe(`${language} Lambda layer`, () => { }), ); expect(response.StatusCode).toBe(200); + expect(response.FunctionError).toBeUndefined(); const payload = JSON.parse(new TextDecoder().decode(response.Payload)); const body = JSON.parse(payload.body); diff --git a/ruby/README.md b/ruby/README.md index 4ba42eb049..53bd14d297 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -3,7 +3,6 @@ Scripts and files used to build AWS Lambda Layers for running OpenTelemetry on AWS Lambda for Ruby. **Requirement** -* Ruby 3.3.0/3.4.0 * [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) * [Go](https://go.dev/doc/install) @@ -17,7 +16,7 @@ Scripts and files used to build AWS Lambda Layers for running OpenTelemetry on A ./build.sh ``` -Layer is stored in `src/build` folder +Layer is stored in `src/build`. It contains gems for Ruby 3.3 and 3.4 on x86_64 and arm64. Only Docker is needed; building the non-native architecture needs QEMU binfmt (e.g. `docker run --privileged --rm tonistiigi/binfmt --install all`). **Default GEM_PATH** diff --git a/ruby/src/otel/Dockerfile b/ruby/src/otel/Dockerfile index d6e1a993dc..cb66ba0594 100644 --- a/ruby/src/otel/Dockerfile +++ b/ruby/src/otel/Dockerfile @@ -1,60 +1,46 @@ -FROM ubuntu:26.04@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7 - -RUN mkdir /build - -COPY . /build - -RUN apt-get update && apt-get install -y git curl libssl-dev libreadline-dev zlib1g-dev autoconf bison build-essential libyaml-dev libreadline-dev libncurses5-dev libffi-dev libgdbm-dev zip - -RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ - && git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build \ - && git clone https://github.com/rbenv/rbenv-default-gems.git ~/.rbenv/plugins/rbenv-default-gems \ - && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.profile \ - && echo 'eval "$(rbenv init -)"' >> ~/.profile \ - && echo 'eval "$(rbenv init -)"' >> ~/.bashrc \ - && echo 'bundler' > ~/.rbenv/default-gems - -RUN echo 'alias be="bundle exec"' >> ~/.bashrc -RUN echo 'alias be="bundle exec"' >> ~/.profile - -# install rubies to build our gem against Gemfile -RUN . ~/.profile \ - && cd /root/.rbenv/plugins/ruby-build && git pull && cd - \ - && rbenv install 3.3.0 \ - && rbenv install 3.4.0 - -WORKDIR /build/layer - -RUN . ~/.profile && rbenv local 3.3.0 && bundle install -RUN . ~/.profile && rbenv local 3.4.0 && bundle install - -WORKDIR /root/.rbenv/versions/3.3.0/lib/ruby/gems/ -RUN zip -r gems-3.3.0.zip 3.3.0/ - -WORKDIR /root/.rbenv/versions/3.4.0/lib/ruby/gems/ - -# rbenv install 3.4.0 get 3.4.0+1/, so need to change back to 3.4.0+1 -RUN mv 3.4.0+1/ 3.4.0/ -RUN set -e && \ - dir=$(find /root/.rbenv/versions/3.4.0/lib/ruby/gems/ -type d -name '3.4.0+1' | head -n 1) && \ - target=$(echo "$dir" | sed 's/3\.4\.0+1/3.4.0/') && \ - mv "$dir" "$target" -RUN zip -r gems-3.4.0.zip 3.4.0/ - -# copy gems to /build/ruby/gems for zipping -RUN mkdir /build/ruby && mkdir /build/ruby/gems -WORKDIR /build/ruby/gems -RUN cp /root/.rbenv/versions/3.3.0/lib/ruby/gems/gems-3.3.0.zip . && unzip gems-3.3.0.zip && rm gems-3.3.0.zip -RUN cp /root/.rbenv/versions/3.4.0/lib/ruby/gems/gems-3.4.0.zip . && unzip gems-3.4.0.zip && rm gems-3.4.0.zip -RUN ls -al /build/ruby/gems - -# rm gem cache -RUN rm /root/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/cache/* \ - && rm /root/.rbenv/versions/3.4.0/lib/ruby/gems/3.4.0/cache/* - -# zip all the gems +# Builds one layer with gems for Ruby 3.3 and 3.4 on both x86_64 and arm64. +# RubyGems only activates gem specs whose platform matches the running +# machine, so both architectures' gems can live side by side in +# ruby/gems/. The four install stages below are independent, so +# BuildKit runs them in parallel (arm64 via QEMU on amd64 hosts and vice versa). + +FROM --platform=linux/amd64 public.ecr.aws/sam/build-ruby3.3:latest@sha256:0e8b401558ba3d59722ea169d0da854631136a23eded32109d6a8ed2345ab0ee AS ruby3.3-amd64 +COPY layer/Gemfile layer/Gemfile.lock install-gems.sh /build/ +RUN /build/install-gems.sh + +FROM --platform=linux/arm64 public.ecr.aws/sam/build-ruby3.3:latest@sha256:0e8b401558ba3d59722ea169d0da854631136a23eded32109d6a8ed2345ab0ee AS ruby3.3-arm64 +COPY layer/Gemfile layer/Gemfile.lock install-gems.sh /build/ +RUN /build/install-gems.sh + +FROM --platform=linux/amd64 public.ecr.aws/sam/build-ruby3.4:latest@sha256:a208c01413754ea473357ffdc850fab3cdadf6dac776a0566242464dba2aa02b AS ruby3.4-amd64 +COPY layer/Gemfile layer/Gemfile.lock install-gems.sh /build/ +RUN /build/install-gems.sh + +FROM --platform=linux/arm64 public.ecr.aws/sam/build-ruby3.4:latest@sha256:a208c01413754ea473357ffdc850fab3cdadf6dac776a0566242464dba2aa02b AS ruby3.4-arm64 +COPY layer/Gemfile layer/Gemfile.lock install-gems.sh /build/ +RUN /build/install-gems.sh + +# Final stage only merges and zips, so it runs on the host's native platform. +FROM public.ecr.aws/sam/build-ruby3.3:latest@sha256:0e8b401558ba3d59722ea169d0da854631136a23eded32109d6a8ed2345ab0ee WORKDIR /build -RUN cp layer/otel-handler . && cp layer/wrapper.rb . -RUN zip -r opentelemetry-ruby-layer.zip ruby/ otel-handler wrapper.rb - -CMD cp /build/opentelemetry-ruby-layer.zip /out/opentelemetry-ruby-layer.zip +COPY --from=ruby3.3-amd64 /gems/ruby/ amd64/ +COPY --from=ruby3.4-amd64 /gems/ruby/ amd64/ +COPY --from=ruby3.3-arm64 /gems/ruby/ arm64/ +COPY --from=ruby3.4-arm64 /gems/ruby/ arm64/ + +# Paths that exist for both architectures must be identical, otherwise one +# would overwrite the other in the merged layer. Fail if a (future) native +# gem brings such a clash back. +RUN if diff -rq amd64 arm64 2>&1 | grep -v '^Only in '; then \ + echo "ERROR: amd64 and arm64 gem trees clash on the paths above" >&2; exit 1; \ + fi + +RUN mkdir -p ruby/gems \ + && cp -a amd64/. ruby/gems/ \ + && cp -a arm64/. ruby/gems/ + +COPY layer/otel-handler ./ +COPY layer/wrapper.rb ./ +RUN zip -qr opentelemetry-ruby-layer.zip ruby/ otel-handler wrapper.rb + +CMD ["cp", "/build/opentelemetry-ruby-layer.zip", "/out/opentelemetry-ruby-layer.zip"] diff --git a/ruby/src/otel/install-gems.sh b/ruby/src/otel/install-gems.sh new file mode 100755 index 0000000000..7df52de396 --- /dev/null +++ b/ruby/src/otel/install-gems.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Installs the layer's gems for the Ruby version and architecture of the +# current build image into /gems/ruby/, keeping only what ships. +set -eu + +# Without this RubyGems also copies compiled extensions (e.g. bigdecimal.so) +# into gems//lib/, an arch-agnostic path that would clash between +# amd64 and arm64. The copy under extensions/-linux/ is enough. +echo "install_extension_in_lib: false" > ~/.gemrc + +# BUNDLE_VERSION=system: use the image's Bundler instead of installing the +# lockfile's "BUNDLED WITH" version into the layer. +cd /build +BUNDLE_PATH=/gems BUNDLE_FROZEN=true BUNDLE_VERSION=system \ + bundle install --jobs "$(nproc)" + +# Drop download cache, docs, build leftovers, binstubs and build logs. +cd /gems/ruby/*/ +rm -rf cache doc build_info bin plugins gems/*/ext +find extensions \( -name mkmf.log -o -name gem_make.out \) -delete