Repository navigation
Portable builds in CI - #4239
cataphract wants to merge 32 commits into
Conversation
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 8 Pipeline jobs failed
ℹ️ InfoNo other issues found (see more)🧪 All tests passed 🎯 Code Coverage (details) Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: b0ebefa | Docs | View more details | Give us feedback! |
Benchmarks [ tracer ]Benchmark execution time: 2026-09-30 10:47:14 Comparing candidate commit 31a3cfb in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 194 metrics, 0 unstable metrics.
|
43a8a23 to
a38379b
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Fetch generated bridge sources in downstream AppSec jobs and preserve glibc backtraces in musl-built portable tracer extensions.
The portable-bundle rework stopped producing the per-API tarballs
(dd-library-php-X-{arch}-linux-gnu-20230831.tar.gz and friends), and
datadog-setup.php dropped the logic that downloaded them. Every install
therefore fetched the full bundle, which is about 1.5 GB per
architecture, instead of a ~50 MB subset. With ext-curl the download is
bounded by a 50 s CURLOPT_TIMEOUT, which that size cannot meet on most
connections, and the file_get_contents() fallback would read the whole
archive into memory.
Bring the per-version tarballs back, for Linux and Windows, but name
them after the portable platform (x86_64-linux-20230831.tar.gz) rather
than after a libc: the binaries are identical for glibc and musl, so a
libc-specific pair would only duplicate the release assets. They are
cut from the already assembled full bundle instead of being staged
separately, so their contents cannot drift from it.
datadog-setup.php regains the previous behaviour: when all selected
binaries share one PHP API and configuration it asks for the
per-version tarball, and falls back to the full bundle on a 404.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Portable builds are compiled against musl and look up backtrace() and backtrace_symbols() as weak symbols, so on a musl system the SIGSEGV handler has nothing to print. Setting datadog.log_backtrace there was silently ignored, or, with crashtracking on (the default), reported as a conflict with the crashtracker, which is not the reason it does nothing. Check availability first and say so explicitly, then carry on as if the setting were off. That also lets the crashtracker conflict be decided in one place: the portable-build change had added a second copy of that check ahead of the original one. The new test only runs on Alpine; tracer .phpt jobs run on glibc images, so it is exercised by local Alpine builds, which take the same weak-import path as the portable build. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The SIGSEGV handler is also installed for health metrics, where it sends
datadog.tracer.uncaught_exceptions{class:sigsegv}. It is skipped while
the crashtracker is installed, since it neither saves nor chains to the
crashtracker's handler. In that case the warning blamed
datadog.log_backtrace, even when the user had only enabled health
metrics.
The two settings are not really exclusive: the heartbeat metric is
still sent, and the crashtracker sends its own crash report. Say
exactly what is lost instead. Letting both run by installing ddtrace's
handler first, so the crashtracker chains to it, was considered and
rejected: it would bring back a metric the crash report already covers,
at the cost of running the handler's backtrace() calls, which are not
async-signal-safe, on every crash.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The trampoline that pins PHP's VM frame and instruction registers around the nested handler call is file-scope inline assembly. It switched to .text with a bare directive, behind the compiler's back: GCC tracks the current section itself and only emits a directive when it thinks one is needed, so anything it emitted next could land in .text instead of where it belonged. Use .pushsection/.popsection so the compiler's section state is left as it was. On aarch64 the trampoline also saved x30 without building a frame record. The CFI was correct, so gdb and libunwind were unaffected, but frame-pointer walkers (perf --call-graph fp and similar) followed the handler's saved x29 straight past zai_interceptor_declare_jit_handler. Save x29/x30 as a pair and point x29 at it, keeping x27/x28 in the same 32-byte frame. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fpm produces an invalid apk when the after-install script's size falls in a range ending at 7680 bytes (jordansissel/fpm#1866). The file carries a comment about this, but nothing enforced it, and dropping the Alpine-specific extension naming shrank the script from 7751 to 7571 bytes, into the failing range. The portable-build change padded the comment back up; the margin is now about 250 bytes, and the next edit that trims the script would fail both APK jobs with an unrelated-looking tar error. Check the size in the .apk.% target before running fpm, so the failure names the cause, and make the comment state the threshold and point at the check. The check is limited to apk because deb, rpm and tar.gz packaging are not affected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Release artifacts are now built in the portable musl image, and the CentOS 7 build images this series removed are no longer produced. A few things still existed only to accommodate them: fallback definitions of HWCAP2_BTI and PROT_BTI for CentOS 7's kernel headers in the loader's telemetry reaper, the X-centos7 services in docker-compose.yml, and dockerh's translation of --php variants for centos-7 images. Both the portable image and bookworm-11 define the BTI constants; the reaper compiles with branch protection enabled against either. dockerh keeps its BASH_ENV redirect, which other images still need, with the comment no longer pointing at centos-7 and devtoolset-7. CentOS 7 as a runtime target is untouched: it still has to load the portable binaries, so the package verification and randomized-test jobs that run on it stay, as do runtime workarounds for old glibc such as calling gettid through syscall(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The oldest supported glibc is 2.17 (CentOS 7), per the loader's requirements.json and check_glibc_version.sh, and nothing in CI verifies on CentOS 6 any more. What was left of it: - The CentOS 6 targets, compose services and verify_rpm_centos6.sh in dockerfiles/verify_packages, reachable from no job. - dockerfiles/packaging/Makefile, removed as a whole rather than just its CentOS 6 targets: every target in it points at services or Dockerfiles that no longer exist (jessie, stretch, PHP 5.x), and nothing invokes it. fpm_packaging/ next to it is still used. - Two comments that gave CentOS 6 as the reason for code that is still needed for other reasons. The empty Expect header in coms.c remains necessary because CURLOPT_EXPECT_100_TIMEOUT_MS needs curl 7.36 and CentOS 7 ships 7.29; the stack-sample _Static_assert stays out of the header because the header is included from C++ in tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Removing the CentOS 7 toolchain took the image definitions in dockerfiles/ci/centos/7 with it, but those images are not only a toolchain: the randomized tests run the shipped portable binaries in datadog/dd-trace-ci:php-randomizedtests-centos7-*, which tests/randomized/docker/centos.Dockerfile builds on top of php-X.Y_centos-7. That is the only coverage of the release artifacts under real web servers on glibc 2.17, the oldest glibc we support. The published images keep that working for now, but without their sources and the ci-images jobs nobody could rebuild them to pick up a new PHP patch release or fix the base. Bring both back unchanged from master. php-profiling.Dockerfile stays deleted: nothing in the compose file builds it, and it only served profiler builds on CentOS 7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CentOS 7 CI image now exists only as a runtime environment: the randomized tests run the shipped portable binaries on top of it. It still carried everything needed to compile ddtrace itself on CentOS 7, which nothing does any more: LLVM/clang/lld built from source (the libclang for bindgen, and the bulk of the image's build time), the Rust toolchain with rust-src, Catch2 for the C component tests that no longer run on this image, the securebits.h stand-in that only ddtrace's config.m4 needed, and librdkafka-devel. What the image still needs stays: devtoolset-7, the autotools and the from-source libraries used to build PHP itself, CMake for libzip, and the compilers and -devel packages pecl needs in the randomized image. postgresql-devel looked removable but is still used by PHP 7.0-7.3's pdo_pgsql. Built on amd64 with PHP 8.3 and the randomized image on top; it behaves as the published image does (same extensions loaded, same web server configs valid), while php-8.3_centos-7 shrinks from 5.0 to 2.6 GB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The base image enables devtoolset-7 through BASH_ENV, but Docker runs RUN steps with /bin/sh, and bash ignores BASH_ENV in that mode. pecl therefore compiled the extensions with CentOS 7's GCC 4.8. That went unnoticed until redis 6.3.0, which no longer compiles with it, so the Dockerfile could no longer be rebuilt at all; the published image from master fails the same way. Pinning redis would fix today's failure but leave the next extension release to break the same way. Running the steps under bash puts them on the compiler the base image intends, which is also what the PHP builds in that image already use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Portable builds link libdatadog's spawn trampoline against musl, and its PT_INTERP named musl's loader, so on glibc hosts every spawn that went through the trampoline failed with ENOENT. The default direct exec avoided it, but DD_SPAWN_WORKER_USE_EXEC and DD_SIDECAR_DISABLE_DIRECT_EXEC both select the trampoline: the sidecar never started, and every request logged "Failed starting AppSec in sidecar". The randomized-test images set DD_SPAWN_WORKER_USE_EXEC, which is how it surfaced on CentOS 7. Pick up the spawn_worker change that rewrites the trampoline's interpreter to the running process's loader at spawn time, rather than steering the sidecar away from the trampoline: that keeps both variables working, on musl and glibc alike. spawn_worker keeps it behind the opt-in trampoline-host-loader feature, since only a trampoline linked against musl with glibc-compatible library names needs it; the portable sidecar build enables it through a datadog-php feature of the same name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every randomized scenario, on master too, failed its prepare step waiting for Elasticsearch, and the jobs still passed because analyze runs in after_script. The base services (Elasticsearch, MySQL, the test agent, ...) never started: run-base-docker-compose.sh only runs docker-compose up while their network is missing, and since the move to GitLab the job created that network itself beforehand. It did so to paper over a race: the base services were just another prerequisite of the scenarios Makefile's test target, so make --jobs started the scenarios alongside them, and a scenario's `docker-compose up` failed on the not-yet-created external network. Start the base services before any scenario instead, shut them down afterwards whatever the outcome, and drop the network creation. Once they did start, Elasticsearch 7.17.4 exited at once on cgroup v2 hosts (a JDK NullPointerException in its cgroup detection); 7.17.28 starts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the base services up, two thirds of the randomized web requests
returned 500 and the long-running CLI script died on its first
message: the app requests "httpbin/get?...", which Guzzle rejects
("URI must include a scheme and host") since 6.5.8. curl accepted the
scheme-less URL, so only the Guzzle snippet broke. The same happens
with master's bundle; it went unnoticed because the scenarios never
got this far.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The randomized tests ran analyze in after_script, whose failures GitLab ignores, so the jobs passed while every scenario failed: that is how the missing base services, the Elasticsearch crash and the broken Guzzle URL all went unnoticed. Run it as part of the script, and keep the results as artifacts on failure too, since that is when they are needed. The no-asan jobs also generated scenarios for the buster images, whose PHP is a debug-zts-asan build that the regular bundle does not ship, so the installer refused every one of them. Restrict those jobs to centos7, where the bundle's NTS extension loads. The asan jobs have no such platform yet: their debug-zts bundle is built on bookworm, CentOS 7 has no debug-zts PHP and buster's glibc is too old. They will fail until bookworm-based randomized images exist. Both job families stay out of the merge gate (flaky-jobs.txt), so this makes the failures visible without blocking merges. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Disabled test_c runs fail whenever a test's output matches "Segmentation fault", which is how they catch crashes. The warning added for health metrics with the crashtracker began "Segmentation faults will not be reported ...", and the dogstatsd tests that enable health metrics print it, so those runs failed on PHP 8.0, 8.4 and 8.5 without anything having crashed. Name the signal instead. Narrowing the grep would have weakened a crash check to accommodate a log message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
31a3cfb to
9601f9d
Compare
|
I'm skeptical about this PR for two reasons First, I’m not convinced our current test coverage validates the build change. Two examples from a spot check:
These two checks therefore do not validate the portability and performance of the binaries we would ship. I know other jobs consume the portable artifacts. Second (and that is my biggest concernt), this moves libc compatibility assumptions from the standard toolchain into a compatibility layer we need to maintain and validate. For exmaple on ARM64, I would prefer to keep relying on Rust’s supported target/libc pairing rather than take on the ongoing responsibility of validating the standard library and our dependencies against a different libc. |
Build PHPT, Rust, correctness, sanitizer, and benchmark inputs with the portable musl-to-glibc toolchain. Patch project and build-std libc layouts on ARM64. Verify pthread_attr_t bounds with an ASAN interposer on glibc.
Ensure fresh ARM64 CI checkouts have the parent directory required for the patched Cargo sources.
libdatadog now derives the listener PID from the current process. Drop the stale PID arguments while retaining explicit PIDs for worker connections.
Skip the pre-8.2 x86_64 musl ZTS JIT cases whose PHP runtime has broken TLS access. Exclude PHP 7.4 ARM64 language tests whose stock release image has an incorrect double-to-long configure result.
Exclude old intl expectations that depend on undefined float-to-int behavior outside x86. Exclude PHP 7.2 bundled Oniguruma tests that crash in stock ARM64 NTS and ZTS runtimes.
|
@realFlowControl I believe I've addresses most of your point, though the more abstract one, ofc remains. Please see also https://chonk.us1.prod.dog/v2/get/chonk-uploads-prod/gustavo.lopes/9d9b5f3e-4b01-4d60-884e-19bb327fffc6 for the summary of the job changes |
Benchmarks [ profiler ]Benchmark execution time: 2026-10-01 15:12:41 Comparing candidate commit b0ebefa in PR branch Found 0 performance improvements and 1 performance regressions! Performance is the same for 25 metrics, 10 unstable metrics.
|
The trampoline executable embeds the program interpreter of the libc it was linked against. dd-trace-php portable builds link it against musl with glibc-compatible library names, so the binary can otherwise run on both libcs. On glibc hosts, however, execve() fails because its musl PT_INTERP path does not exist. Only portable builds need this adaptation. Gate it behind the opt-in `trampoline-host-loader` feature so other spawn_worker consumers keep the trampoline and build behavior unchanged. No interpreter path exists on both libcs. With the feature enabled, the build script relinks the trampoline with its default interpreter padded with leading slashes to a fixed 256 bytes. The padded path is equivalent, so an unpatched trampoline behaves as before. When writing the trampoline out, the spawner replaces it with the running process PT_INTERP, which is always the right loader for the host. Invoking the trampoline through that loader would avoid patching, but comes with its own set of complications. Tested with a portable dd-trace-php build: the sidecar starts through the trampoline on Alpine, Debian bookworm (glibc 2.36), and CentOS 7 (glibc 2.17), where it previously panicked on glibc. See also DataDog/dd-trace-php#4239 Co-authored-by: gustavo.lopes <gustavo.lopes@datadoghq.com>
Description
Every Linux artifact used to be built twice: once against glibc on the old CentOS 7 images, and once against musl on Alpine. That doubled the compile jobs for each component and PHP version, and kept an ageing toolchain alive only to get an old enough glibc.
This PR builds each extension, the sidecar library and the loader once per architecture, against musl, and links them in a way that loads in both glibc and musl processes. The build scripts refuse any output that picks up a glibc symbol version. The builds now run in the parent pipeline, so the test, appsec, profiler and packaging child pipelines all use the binaries that actually ship, instead of rebuilding their own.
A few runtime fixes came with it, because the musl build changed assumptions the code had been relying on implicitly:
datadog.log_backtracenow warns when they're unavailable instead of being silently ignored.The CentOS 7 images are no longer needed and are removed.
With one binary per architecture, the release bundles no longer carry a libc in their name. That is the one change users may notice:
Release asset names changed
Linux builds are now portable across glibc and musl, so the separate
-linux-gnuand-linux-muslrelease assets are replaced by one archive per architecture:dd-library-php-<version>-<arch>-linux-gnu.tar.gzand…-linux-musl.tar.gz→dd-library-php-<version>-<arch>-linux.tar.gz…-linux-gnu-<API>[-zts|-debug].tar.gzand…-linux-musl-<API>[-zts].tar.gz→…-linux-<API>[-zts|-debug].tar.gzdatadog-setup.phpalready uses the new names. Scripts that download these archives directly must be updated.Summary of job changes
Reviewer checklist