diff --git a/.dockerignore b/.dockerignore index 31003e476..b838d623a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,5 @@ -.scratch/ .git/ build/ -build-san/ _build/ -deps/moxygen/.git/ -deps/moxygen/moxygen/ -deps/moxygen/standalone/ install/ +.scratch/ diff --git a/.github/actions/setup-build/action.yml b/.github/actions/setup-build/action.yml new file mode 100644 index 000000000..9ef037864 --- /dev/null +++ b/.github/actions/setup-build/action.yml @@ -0,0 +1,92 @@ +name: Set up moqx build +description: > + Install system dependencies and restore the ccache + dependency-download + caches shared by every moqx build job. Run right after actions/checkout. + +inputs: + ccache-key: + description: > + ccache cache-key namespace — a short slug, distinct per build flavor so + lanes with different flags don't share a cache (e.g. "linux", "asan", + "conformance", "microbench"). Must be a valid actions/cache key fragment: + no commas, so never a human-readable display name. + required: true + +runs: + using: composite + steps: + # install-system-deps.sh elevates its own package-manager calls, so the same + # invocation serves both runner families. + - name: Install system dependencies + shell: bash + run: | + scripts/install-system-deps.sh + if [[ "$(uname)" == "Darwin" ]]; then + brew install coreutils + echo "/opt/homebrew/opt/coreutils/libexec/gnubin" >> "$GITHUB_PATH" + fi + + # The moqx CMakeLists routes the compiler through ccache automatically when + # it is installed (install-system-deps.sh installs it). + - name: Cache ccache + uses: actions/cache@v5 + with: + path: ~/.cache/ccache + key: ccache-${{ runner.os }}-${{ inputs.ccache-key }}-${{ github.sha }} + restore-keys: | + ccache-${{ runner.os }}-${{ inputs.ccache-key }}- + + # The prebuilt tarball is named for the host's *distro* release, so the key + # needs that granularity: on runner.os alone a bookworm self-hosted box and + # an ubuntu-22.04 runner collide, and the loser re-downloads every run. + - name: Derive the dependency-cache platform key + id: depkey + shell: bash + run: | + if [[ "$(uname)" == "Darwin" ]]; then + plat="macos-$(sw_vers -productVersion | cut -d. -f1)" + else + plat="$(. /etc/os-release && echo "${ID}-${VERSION_ID}")" + fi + echo "plat=${plat}-$(uname -m)" >> "$GITHUB_OUTPUT" + + # One shared fetch cache for CPM sources and the prebuilt moxygen install, + # ~680 MB of downloads per configure otherwise, content-keyed on the pins. + - name: Cache dependency downloads + uses: actions/cache@v5 + with: + path: ~/.cache/moqx + key: deps-${{ steps.depkey.outputs.plat }}-${{ hashFiles('cmake/dependencies.cmake') }} + # actions/cache saves only at job end, so a pin bump misses the exact key + # in all ~11 concurrent jobs. The prefix starts them from the previous + # tree and fetches the delta. + restore-keys: | + deps-${{ steps.depkey.outputs.plat }}- + + # Restoring by prefix carries every previously pinned rev's install tree + # along (~320 MB each), so drop the unusable ones before the save step + # re-uploads them. The CPM clones accumulate too, but are far smaller. + - name: Prune superseded prebuilt installs + shell: bash + run: | + cache="$HOME/.cache/moqx" + [ -d "$cache" ] || exit 0 + rev="$(cmake -DPIN=MOXYGEN_REV -P cmake/print-pin.cmake)" + for d in "$cache"/moxygen-*; do + case "${d##*/}" in + "moxygen-${rev:0:12}-"*) ;; # this run's install + moxygen-????????????-*) rm -rf "$d" ;; # a superseded pin's + esac + done + rm -rf "$cache/downloads" + + - name: Route the dependency and ccache dirs through the caches + shell: bash + run: | + # The root only; cmake/DepsCache.cmake puts the CPM clones under it. + echo "MOQX_DEPS_CACHE=$HOME/.cache/moqx" >> "$GITHUB_ENV" + # Pin ccache's dir to the cached path; its own default is platform-dependent. + echo "CCACHE_DIR=$HOME/.cache/ccache" >> "$GITHUB_ENV" + # GITHUB_TOKEN stays off $GITHUB_ENV, which would hand it to every later + # step in the job, ctest among them — and ctest runs the relay and the + # shell integration tests. The configure step sets it for its one API read. diff --git a/.github/workflows/auto-merge-moxygen.yml b/.github/workflows/auto-merge-moxygen.yml index 996d5bcfa..2c794f7ea 100644 --- a/.github/workflows/auto-merge-moxygen.yml +++ b/.github/workflows/auto-merge-moxygen.yml @@ -79,7 +79,7 @@ jobs: - name: Generate app token if: github.event.workflow_run.conclusion == 'success' id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index cfad07e13..0e85249cc 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -6,7 +6,7 @@ name: ci main # Job graph: # # check-format ──────────────────────────────────────────────────────────────────┐ -# build (linux, asan debug) ── publish (docker+smoke) ── release ────────────────┼── notify +# build (linux, asan) ── publish (docker+smoke) ── release ──────────────────────┼── notify # microbenchmark (linux, macos) ────────────────────────────────────────────────-┘ on: @@ -33,103 +33,82 @@ jobs: check-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install clang-format run: pip install clang-format==19.1.7 - name: Check formatting - run: bash scripts/format.sh --check + run: bash scripts/dev/format.sh --check build: needs: [check-format] strategy: fail-fast: false matrix: + # `key` is the machine-readable lane id (ccache/cache keys, option + # gating); `name` is display-only, safe to rename. The build dir is + # always build/ (the presets' binaryDir). include: - name: linux + key: linux preset: default - build_dir: build runner: ubuntu-22.04 # arm64 is validated by the publish-side docker build (publish arm64 # entry below); skipped here to avoid the ubuntu-runner ↔ bookworm-tarball # glog ABI mismatch. arm64 issues surface at the publish step. - name: macos + key: macos preset: default - build_dir: build runner: macos-15 - - name: asan debug + # ASan on moqx's own TUs, over the uninstrumented prebuilt folly, for a + # fast signal; sanitizers.yml builds the instrumented stack. + # RelWithDebInfo: the prebuilt is NDEBUG and folly's kIsDebug is ABI. + - name: asan (moqx TUs, prebuilt deps) + key: asan preset: san - build_dir: build-san + extra_cmake: -DCMAKE_BUILD_TYPE=RelWithDebInfo + # The san preset demands an instrumented moxygen; this lane knowingly + # takes the uninstrumented one. Without it configure.sh refuses. + uninstrumented_deps: true + leak_check: true runner: [self-hosted, linode] name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} + # Ceiling for the from-source fallback, not the normal runtime. + # Matched to sanitizers.yml, the only measured from-source budget we have. + timeout-minutes: 180 steps: - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.OMOQ_APP_ID }} - private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} + - uses: actions/checkout@v5 - - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build with: - submodules: recursive - - - name: Install system dependencies - run: | - if [[ "$(uname)" == "Darwin" ]]; then - deps/moxygen/standalone/install-system-deps.sh - brew install coreutils - echo "/opt/homebrew/opt/coreutils/libexec/gnubin" >> "$GITHUB_PATH" - else - sudo deps/moxygen/standalone/install-system-deps.sh - fi + ccache-key: ${{ matrix.key }} - - name: Setup dependencies + # The trilogy, not raw cmake: configure.sh picks the moxygen and build.sh + # resolves a job count, where Ninja's own nproc + 2 default OOMs the + # sanitizer lane. See BUILD.md#build. + - name: Build env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - MOQX_PLATFORM: ${{ matrix.platform || '' }} + GITHUB_TOKEN: ${{ github.token }} + MOQX_ALLOW_UNINSTRUMENTED_DEPS: ${{ matrix.uninstrumented_deps && '1' || '' }} run: | - # Three-mode setup resolution: - # 1. .moxygen-release file (release branches): use pinned tag. - # 2. Submodule SHA reachable from moxygen main: tarball mode via - # snapshot-latest. Robust to drift between moxygen merges and - # moqx's daily moxygen-sync. - # 3. Submodule SHA NOT reachable from main (dev iterating against - # an unreleased moxygen feature branch): source build. Slow - # but supports cross-repo iteration without requiring a - # published moxygen tarball. - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi - - - name: Build - run: bash scripts/build.sh --profile ${{ matrix.preset }} --build-dir ${{ matrix.build_dir }} + scripts/configure.sh ${{ matrix.preset }} --moxygen prebuilt-with-fallback ${{ matrix.extra_cmake }} + scripts/build.sh ${{ matrix.preset }} + # test.sh, not raw ctest: it resolves the --parallel the suite needs (the + # shell integration tests carry unique ports so they can share a run). - name: Test env: - ASAN_OPTIONS: ${{ matrix.name == 'asan debug' && 'detect_leaks=1:abort_on_error=1' || '' }} - run: bash scripts/build.sh test --build-dir ${{ matrix.build_dir }} -- --output-junit test-results.xml + ASAN_OPTIONS: ${{ matrix.leak_check && 'detect_leaks=1:abort_on_error=1' || '' }} + run: scripts/test.sh ${{ matrix.preset }} --output-junit test-results.xml - name: Publish test results - uses: dorny/test-reporter@v1.9.1 + uses: dorny/test-reporter@v3 if: success() || failure() with: name: "test (${{ matrix.name }})" - path: ${{ matrix.build_dir }}/test-results.xml + path: build/${{ matrix.preset }}/test-results.xml reporter: java-junit fail-on-empty: ${{ job.status == 'success' && 'true' || 'false' }} # Only list failing tests in the Check Run summary. Listing all @@ -178,47 +157,26 @@ jobs: stack: "pico" name: conformance (${{ matrix.name }}) runs-on: ubuntu-22.04 + # Ceiling for the from-source fallback; see the build job. + timeout-minutes: 180 steps: - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.OMOQ_APP_ID }} - private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} + - uses: actions/checkout@v5 - - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build with: - submodules: recursive - - - name: Install system dependencies - run: sudo deps/moxygen/standalone/install-system-deps.sh + ccache-key: conformance - - name: Setup dependencies + # Tests off: this job only drives the moqx binary; the ~25 gtest + # executables would link the heavy static stack for nothing. + - name: Build env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_TOKEN: ${{ github.token }} run: | - # Three-mode setup resolution (matches build job). - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi - - - name: Build - run: bash scripts/build.sh + scripts/configure.sh default --moxygen prebuilt-with-fallback -DMOQX_BUILD_TESTS=OFF + scripts/build.sh default - name: Run conformance tests - run: bash test/test_conformance.sh ./build/moqx ${{ matrix.versions }} ${{ matrix.transport }} ${{ matrix.stack }} + run: bash test/test_conformance.sh ./build/default/moqx ${{ matrix.versions }} ${{ matrix.transport }} ${{ matrix.stack }} # ════════════════════════════════════════════════════════════════════════════ # Microbenchmark: run in-process micro-benchmarks (independent of build/publish pipeline) @@ -233,64 +191,32 @@ jobs: - name: linux runner: ubuntu-22.04 - name: macos - # Pinned to macos-15 to match the moxygen publish runner / tarball - # name (moxygen-macos-15-arm64.tar.gz), downloaded here with - # --no-fallback. See ci-pr.yml microbenchmark job for the full rationale. + # Pinned to the moxygen publish runner: releases ship + # moxygen-macos-15-arm64.tar.gz and no macos-26 one, so macos-latest + # 404s. Keep in lockstep with the build job above. runner: macos-15 name: microbenchmark (${{ matrix.name }}) runs-on: ${{ matrix.runner }} + # Ceiling for the from-source fallback; see the build job. + timeout-minutes: 180 steps: - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.OMOQ_APP_ID }} - private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} + - uses: actions/checkout@v5 - - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build with: - submodules: recursive + ccache-key: microbench - - name: Install system dependencies - run: | - if [[ "$(uname)" == "Darwin" ]]; then - deps/moxygen/standalone/install-system-deps.sh - else - sudo deps/moxygen/standalone/install-system-deps.sh - fi - - - name: Setup dependencies + - name: Build microbenchmarks env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_TOKEN: ${{ github.token }} run: | - # Three-mode setup resolution (mirrors the build/conformance jobs): - # 1. .moxygen-release file (release branches): use pinned tag. - # 2. Submodule SHA reachable from moxygen main: snapshot tarball - # (avoids a slow from-source dep build when moxygen has advanced - # past the local pin — the common case under the daily sync). - # 3. Submodule SHA diverged from main: source build. - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi - - - name: Build microbenchmarks - run: bash scripts/build.sh --benchmark + scripts/configure.sh default --moxygen prebuilt-with-fallback \ + -DMOQX_BUILD_BENCHMARKS=ON -DMOQX_BUILD_TESTS=OFF + scripts/build.sh default - name: Run microbenchmarks run: | - ./build/benchmark/moqx_benchmark \ + ./build/default/benchmark/moqx_benchmark \ --bm_json_verbose=microbench-results.json \ | tee microbench-output.txt @@ -307,7 +233,7 @@ jobs: - name: Upload microbenchmark artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: microbench-results-${{ matrix.name }} path: | @@ -332,41 +258,23 @@ jobs: platform: bookworm-arm64 name: publish (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} + # The image's moxygen stage can fall back to compiling folly; see the + # build job. Without this the default is 6 h. + timeout-minutes: 180 steps: - name: Generate app token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: - submodules: recursive - # Full history + tags so `git describe --tags` can version the image - # (bare tag on release commits, tag-distance-sha on snapshots). + # Full history + tags so `git describe --tags` can version the + # image (bare tag on release commits, tag-distance-sha otherwise). fetch-depth: 0 - - name: Download bookworm moxygen tarball - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - MOQX_PLATFORM: ${{ matrix.platform }} - run: | - # Release branches pin a specific moxygen release tag via - # .moxygen-release. Main uses snapshot-latest via --use-latest. - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "Using pinned moxygen release tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - bash scripts/build.sh setup --use-latest --no-fallback - fi - - - name: Stage tarball for Docker build - run: | - mkdir -p .docker-deps - cp -a .scratch/moxygen-install .docker-deps/moxygen - - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin @@ -393,12 +301,24 @@ jobs: echo "version=$V" >> "$GITHUB_OUTPUT" echo "==> build version: $V" + # The docker-container driver is what makes --cache-to work at all; the + # default driver has no cache exporter. + - uses: docker/setup-buildx-action@v4 + + # Caches the pin-keyed moxygen stage (docker/Dockerfile), so a source-only + # push reuses the prefix. Registry, not type=gha: that backend shares one + # 10 GB budget with the ccache and dependency caches, and mode=max evicts them. - name: Build Docker image + env: + GITHUB_TOKEN: ${{ github.token }} run: | IMAGE="ghcr.io/${{ github.repository }}" ARCH="${{ matrix.arch }}" - docker build -f docker/Dockerfile \ + docker buildx build -f docker/Dockerfile --target relay --load \ + --secret id=github_token,env=GITHUB_TOKEN \ --build-arg MOQX_VERSION_STRING="${{ steps.version.outputs.version }}" \ + --cache-from "type=registry,ref=${IMAGE}/buildcache:${ARCH}" \ + --cache-to "type=registry,ref=${IMAGE}/buildcache:${ARCH},mode=max" \ -t "${IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" \ -t "${IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" \ . @@ -429,11 +349,19 @@ jobs: docker stop moqx-smoke && docker rm moqx-smoke echo "==> Smoke test passed" + # Same Dockerfile, same moxygen stage — so the client binary and the relay + # can never come from different moxygen builds. No --cache-to: the relay + # build above already exported that stage. - name: Build interop client image + env: + GITHUB_TOKEN: ${{ github.token }} run: | + IMAGE="ghcr.io/${{ github.repository }}" CLIENT_IMAGE="ghcr.io/${{ github.repository_owner }}/moqx-interop-client" ARCH="${{ matrix.arch }}" - docker build -f docker/Dockerfile.interop-client \ + docker buildx build -f docker/Dockerfile --target interop-client --load \ + --secret id=github_token,env=GITHUB_TOKEN \ + --cache-from "type=registry,ref=${IMAGE}/buildcache:${ARCH}" \ -t "${CLIENT_IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" \ -t "${CLIENT_IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" \ . @@ -471,7 +399,7 @@ jobs: echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT" - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.package.outputs.artifact }} path: ${{ steps.package.outputs.artifact }} @@ -550,10 +478,10 @@ jobs: outputs: tag: ${{ steps.publish.outputs.tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: "*.tar.gz" path: artifacts/ @@ -609,7 +537,7 @@ jobs: RELAY_PORT: 4433 ADMIN_PORT: 8000 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Ensure DNS A record env: diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 4f5a0eb94..9d19d68b7 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -18,99 +18,85 @@ jobs: check-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install clang-format run: pip install clang-format==19.1.7 - name: Check formatting - run: bash scripts/format.sh --check + run: bash scripts/dev/format.sh --check build: needs: [check-format] strategy: fail-fast: false matrix: + # `key` is the lane id (ccache keys, option gating); `name` renders into + # the status-check name. Renaming a lane branch protection requires stops + # that check reporting — update the ruleset in the same change. include: - name: linux + key: linux preset: default - build_dir: build runner: ubuntu-22.04 # arm64 is validated by the publish-side docker build (ci-main publish # arm64 entry); skipped here to avoid the ubuntu-runner ↔ bookworm-tarball # glog ABI mismatch. arm64 regressions surface at main-push time. - name: macos + key: macos preset: default - build_dir: build runner: macos-15 - - name: asan debug + # ASan on moqx's own TUs, over the uninstrumented prebuilt folly, for a + # fast PR signal; sanitizers.yml builds the instrumented stack. + # RelWithDebInfo: the prebuilt is NDEBUG and folly's kIsDebug is ABI. + - name: asan (moqx TUs, prebuilt deps) + key: asan preset: san - build_dir: build-san + extra_cmake: -DCMAKE_BUILD_TYPE=RelWithDebInfo + # The san preset demands an instrumented moxygen; this lane knowingly + # takes the uninstrumented one. Without it configure.sh refuses. + uninstrumented_deps: true + leak_check: true runner: [self-hosted, linode] name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} + # Ceiling for the from-source fallback, not the normal runtime (~28 min). + # Matched to sanitizers.yml, the only measured from-source budget we have. + timeout-minutes: 180 steps: - - uses: actions/checkout@v4 - with: - submodules: recursive + - uses: actions/checkout@v5 - - name: Install system dependencies - run: | - if [[ "$(uname)" == "Darwin" ]]; then - deps/moxygen/standalone/install-system-deps.sh - brew install coreutils - echo "/opt/homebrew/opt/coreutils/libexec/gnubin" >> "$GITHUB_PATH" - else - sudo deps/moxygen/standalone/install-system-deps.sh - fi + - uses: ./.github/actions/setup-build + with: + ccache-key: ${{ matrix.key }} - - name: Setup dependencies + # The trilogy, not raw cmake: configure.sh picks the moxygen and build.sh + # resolves a job count, where Ninja's own nproc + 2 default OOMs the + # sanitizer lane. See BUILD.md#build. + - name: Build env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MOQX_PLATFORM: ${{ matrix.platform || '' }} + GITHUB_TOKEN: ${{ github.token }} + MOQX_ALLOW_UNINSTRUMENTED_DEPS: ${{ matrix.uninstrumented_deps && '1' || '' }} run: | - # Three-mode setup resolution: - # 1. .moxygen-release file (release branches): use pinned tag. - # 2. Submodule SHA reachable from moxygen main: tarball mode via - # snapshot-latest. Robust to drift between moxygen merges and - # moqx's daily moxygen-sync. - # 3. Submodule SHA NOT reachable from main (dev iterating against - # an unreleased moxygen feature branch): source build. Slow - # but supports cross-repo iteration without requiring a - # published moxygen tarball. - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi - - - name: Build - run: bash scripts/build.sh --profile ${{ matrix.preset }} --build-dir ${{ matrix.build_dir }} + scripts/configure.sh ${{ matrix.preset }} --moxygen prebuilt-with-fallback ${{ matrix.extra_cmake }} + scripts/build.sh ${{ matrix.preset }} + # test.sh, not raw ctest: it resolves the --parallel the suite needs (the + # shell integration tests carry unique ports so they can share a run). - name: Test env: - ASAN_OPTIONS: ${{ matrix.name == 'asan debug' && 'detect_leaks=1:abort_on_error=1' || '' }} - run: bash scripts/build.sh test --build-dir ${{ matrix.build_dir }} -- --output-junit test-results.xml + ASAN_OPTIONS: ${{ matrix.leak_check && 'detect_leaks=1:abort_on_error=1' || '' }} + run: scripts/test.sh ${{ matrix.preset }} --output-junit test-results.xml # dorny/test-reporter writes a Check Run, which needs 'checks: write' on # GITHUB_TOKEN. Fork-PR tokens are read-only regardless of workflow perms, # so skip on cross-repo PRs. test-log output above still shows pass/fail. - name: Publish test results - uses: dorny/test-reporter@v1.9.1 + uses: dorny/test-reporter@v3 if: ${{ (success() || failure()) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} with: name: "test (${{ matrix.name }})" - path: ${{ matrix.build_dir }}/test-results.xml + path: build/${{ matrix.preset }}/test-results.xml reporter: java-junit fail-on-empty: ${{ job.status == 'success' && 'true' || 'false' }} # Only list failing tests in the Check Run summary. Listing all @@ -159,47 +145,26 @@ jobs: stack: "pico" name: conformance (${{ matrix.name }}) runs-on: ubuntu-22.04 + # Ceiling for the from-source fallback; see the build job. + timeout-minutes: 180 steps: - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.OMOQ_APP_ID }} - private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} + - uses: actions/checkout@v5 - - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build with: - submodules: recursive + ccache-key: conformance - - name: Install system dependencies - run: sudo deps/moxygen/standalone/install-system-deps.sh - - - name: Setup dependencies + # Tests off: this job only drives the moqx binary; the ~25 gtest + # executables would link the heavy static stack for nothing. + - name: Build env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_TOKEN: ${{ github.token }} run: | - # Three-mode setup resolution (matches build job). - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi - - - name: Build - run: bash scripts/build.sh + scripts/configure.sh default --moxygen prebuilt-with-fallback -DMOQX_BUILD_TESTS=OFF + scripts/build.sh default - name: Run conformance tests - run: bash test/test_conformance.sh ./build/moqx ${{ matrix.versions }} ${{ matrix.transport }} ${{ matrix.stack }} + run: bash test/test_conformance.sh ./build/default/moqx ${{ matrix.versions }} ${{ matrix.transport }} ${{ matrix.stack }} microbenchmark: needs: [check-format] @@ -210,66 +175,32 @@ jobs: - name: linux runner: ubuntu-22.04 - name: macos - # Pinned to match the moxygen publish runner: snapshot-latest ships - # moxygen-macos-15-arm64.tar.gz, and this job downloads it with - # --no-fallback. macos-latest now resolves to macOS 26 (gradual - # rollout), which requests a macos-26 tarball that 404s. Keep in - # lockstep with the build job above (also macos-15). + # Pinned to the moxygen publish runner: releases ship + # moxygen-macos-15-arm64.tar.gz and no macos-26 one, so macos-latest + # 404s. Keep in lockstep with the build job above. runner: macos-15 name: microbenchmark (${{ matrix.name }}) runs-on: ${{ matrix.runner }} + # Ceiling for the from-source fallback; see the build job. + timeout-minutes: 180 steps: - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.OMOQ_APP_ID }} - private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} + - uses: actions/checkout@v5 - - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build with: - submodules: recursive - - - name: Install system dependencies - run: | - if [[ "$(uname)" == "Darwin" ]]; then - deps/moxygen/standalone/install-system-deps.sh - else - sudo deps/moxygen/standalone/install-system-deps.sh - fi + ccache-key: microbench - - name: Setup dependencies + - name: Build microbenchmarks env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_TOKEN: ${{ github.token }} run: | - # Three-mode setup resolution (mirrors the build/conformance jobs): - # 1. .moxygen-release file (release branches): use pinned tag. - # 2. Submodule SHA reachable from moxygen main: snapshot tarball - # (avoids a slow from-source dep build when moxygen has advanced - # past the local pin — the common case under the daily sync). - # 3. Submodule SHA diverged from main: source build. - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi - - - name: Build microbenchmarks - run: bash scripts/build.sh --benchmark + scripts/configure.sh default --moxygen prebuilt-with-fallback \ + -DMOQX_BUILD_BENCHMARKS=ON -DMOQX_BUILD_TESTS=OFF + scripts/build.sh default - name: Run microbenchmarks run: | - ./build/benchmark/moqx_benchmark \ + ./build/default/benchmark/moqx_benchmark \ --bm_json_verbose=microbench-results.json \ | tee microbench-output.txt @@ -286,7 +217,7 @@ jobs: - name: Upload microbenchmark artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: microbench-results-${{ matrix.name }} path: | diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml index 0edd77631..d8d9b1904 100644 --- a/.github/workflows/deploy-relay.yml +++ b/.github/workflows/deploy-relay.yml @@ -51,7 +51,7 @@ jobs: deploy: runs-on: [self-hosted, linode] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Compute deployment target id: target diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index c4caa0db9..349a3d9f8 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -1,15 +1,8 @@ name: dev build -# On-demand RelWithDebInfo dev kit. Manually dispatched against any branch -# (typically a PR head with a moxygen submodule pointing at a feature/PR SHA). -# Uses the same three-mode dependency resolution as ci-pr.yml's build job, so -# source-build kicks in automatically when the moxygen submodule diverges from -# moxygen/main — no cross-repo coordination needed beyond bumping the submodule -# on the moqx branch. -# -# Produces a single ubuntu-22.04 amd64 tarball uploaded via -# actions/upload-artifact (90-day retention, requires GH login to fetch via -# `gh run download`). No tests, no Docker, no release, no GHCR push. +# On-demand dev kit: source-builds moxygen at the pinned MOXYGEN_REV via the +# superbuild (works for any rev, incl. an unreleased SHA with no prebuilt), then +# packages moqx as one ubuntu-22.04 amd64 tarball (90-day artifact). See BUILD.md. on: workflow_dispatch: @@ -25,55 +18,33 @@ jobs: build: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - name: Install system dependencies - run: sudo deps/moxygen/standalone/install-system-deps.sh + - uses: actions/checkout@v5 - - name: Setup dependencies - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Three-mode setup resolution (mirrors ci-pr.yml build job): - # 1. .moxygen-release file (release branches): use pinned tag. - # 2. Submodule SHA reachable from moxygen main: tarball mode via - # snapshot-latest. - # 3. Submodule SHA NOT reachable from main: source build. This is - # the typical dev/PR case where the moxygen submodule points at - # a feature-branch SHA. - if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - echo "==> Pinned moxygen tag: $MOQX_MOXYGEN_RELEASE_TAG" - bash scripts/build.sh setup --no-fallback - else - SUB_SHA=$(git -C deps/moxygen rev-parse HEAD) - AHEAD=$(gh api "repos/openmoq/moxygen/compare/main...$SUB_SHA" --jq .ahead_by 2>/dev/null || echo "1") - if [ "$AHEAD" = "0" ]; then - echo "==> moxygen submodule $SUB_SHA on main → snapshot tarball" - bash scripts/build.sh setup --use-latest --no-fallback - else - echo "==> moxygen submodule $SUB_SHA $AHEAD commits diverged from main → source build" - bash scripts/build.sh setup - fi - fi + - uses: ./.github/actions/setup-build + with: + ccache-key: dev-build + # From source: setup builds the moxygen prefix and configures moqx against + # it; build.sh compiles. - name: Build - run: bash scripts/build.sh --build-dir build + run: | + scripts/configure.sh --moxygen from-source + scripts/build.sh + # The artifact links system glog/gflags/&c. — run it on a box that has run + # install-system-deps.sh (it is a dev kit, not a self-contained release). - name: Package id: package run: | SHORT="${GITHUB_SHA:0:7}" ARTIFACT="moqx-ubuntu-22.04-amd64-${SHORT}.tar.gz" - cmake --install build --prefix dist + cmake --install build/default --prefix dist tar czf "$ARTIFACT" -C dist . ls -la "$ARTIFACT" echo "artifact=$ARTIFACT" >> "$GITHUB_OUTPUT" - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.package.outputs.artifact }} path: ${{ steps.package.outputs.artifact }} diff --git a/.github/workflows/moxygen-sync.yml b/.github/workflows/moxygen-sync.yml index 923c33413..aa0bc9fc4 100644 --- a/.github/workflows/moxygen-sync.yml +++ b/.github/workflows/moxygen-sync.yml @@ -1,6 +1,7 @@ name: moxygen sync -# Updates deps/moxygen submodule to latest moxygen main, creates a PR. +# Updates the pinned MOXYGEN_REV in cmake/dependencies.cmake to latest +# openmoq/moxygen main, creates a PR. # Schedule-driven (daily cron) plus workflow_dispatch for ad-hoc syncs. # repository_dispatch retained for non-sync flows (e.g., manual hand-merged # moxygen PRs that need immediate propagation); the routine cron-cascade @@ -16,7 +17,7 @@ on: # 03:23 picoquic upstream-sync (private-octopus → openmoq/picoquic) # 04:23 moxygen upstream-sync (facebookexperimental → openmoq/moxygen) # 04:37 moxygen picoquic-pin sync (openmoq/picoquic → moxygen picoquic-rev.txt) - # 08:23 moqx moxygen-submodule sync (this workflow — openmoq/moxygen → moqx deps/moxygen) + # 08:23 moqx moxygen-rev sync (this workflow — openmoq/moxygen → moqx cmake/dependencies.cmake MOXYGEN_REV) # The 3h46m gap before this stage gives moxygen sync PRs (upstream + picoquic-pin) # time to run CI and auto-merge into openmoq/moxygen main before we pull it. - cron: '23 8 * * *' @@ -35,21 +36,15 @@ jobs: # ── Generate GitHub App token (bot identity for PRs) ── - name: Generate app token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: main - submodules: true token: ${{ steps.app-token.outputs.token }} - # Full history required so we can check out arbitrary moxygen - # SHAs inside the submodule. Shallow (default depth=1) creates - # shallow submodule clones, which fail with "unable to read - # tree" when checking out a SHA whose tree isn't present. - fetch-depth: 0 # ══════════════════════════════════════════════════════════ # PHASE A: Check for blocking sync PR @@ -101,7 +96,7 @@ jobs: PR_URL="${{ github.server_url }}/${{ github.repository }}/pull/${PR_NUM}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" SUBJECT="[moqx] moxygen sync paused — PR #${PR_NUM} pending" - BODY="Moxygen submodule sync blocked by open PR.\n\nPR: ${PR_URL}\nBranch: ${PR_REF}\nRun: ${RUN_URL}\n\nResolve or merge the existing PR, then re-run manually." + BODY="Moxygen rev sync blocked by open PR.\n\nPR: ${PR_URL}\nBranch: ${PR_REF}\nRun: ${RUN_URL}\n\nResolve or merge the existing PR, then re-run manually." aws ses send-email \ --from "noreply@ci.openmoq.org" \ --destination '{"ToAddresses":["github-notifications@openmoq.org"]}' \ @@ -111,32 +106,45 @@ jobs: }" # ══════════════════════════════════════════════════════════ - # PHASE B: Update submodule and create PR + # PHASE B: Bump MOXYGEN_REV and create PR # ══════════════════════════════════════════════════════════ - name: Determine target SHA if: steps.blocking.outputs.blocked == 'false' id: target + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + # Attacker-controllable on a repository_dispatch payload. Bound to an + # env var so it reaches the shell as data, never spliced into script + # text; the 40-hex check below is a value check, not an injection guard. + CLIENT_PAYLOAD_SHA: ${{ github.event.client_payload.sha }} run: | - CURRENT_SHA=$(git -C deps/moxygen rev-parse HEAD) + CURRENT_SHA=$(cmake -DPIN=MOXYGEN_REV -P cmake/print-pin.cmake) echo "current_sha=${CURRENT_SHA}" >> "$GITHUB_OUTPUT" if [ "${{ github.event_name }}" = "repository_dispatch" ]; then - TARGET_SHA="${{ github.event.client_payload.sha }}" + TARGET_SHA="$CLIENT_PAYLOAD_SHA" else - # Manual trigger: fetch latest moxygen main - git -C deps/moxygen fetch origin main - TARGET_SHA=$(git -C deps/moxygen rev-parse origin/main) + # Manual/scheduled trigger: latest openmoq/moxygen main + TARGET_SHA=$(gh api repos/openmoq/moxygen/commits/main --jq '.sha') + fi + + # Guard: TARGET_SHA is spliced into cmake/dependencies.cmake by sed + # below; a non-40-hex value would corrupt the pin (empty payload) or + # break the sed. + if ! printf '%s' "$TARGET_SHA" | grep -qE '^[0-9a-fA-F]{40}$'; then + echo "::error::Invalid moxygen target SHA: '${TARGET_SHA}' (expected 40-hex)" + exit 1 fi echo "target_sha=${TARGET_SHA}" >> "$GITHUB_OUTPUT" echo "short_sha=${TARGET_SHA:0:7}" >> "$GITHUB_OUTPUT" if [ "$CURRENT_SHA" = "$TARGET_SHA" ]; then - echo "Submodule already at ${TARGET_SHA:0:7}. Nothing to do." + echo "MOXYGEN_REV already at ${TARGET_SHA:0:7}. Nothing to do." echo "needs_update=false" >> "$GITHUB_OUTPUT" else - echo "Will update submodule: ${CURRENT_SHA:0:7} → ${TARGET_SHA:0:7}" + echo "Will update MOXYGEN_REV: ${CURRENT_SHA:0:7} → ${TARGET_SHA:0:7}" echo "needs_update=true" >> "$GITHUB_OUTPUT" fi @@ -145,6 +153,9 @@ jobs: id: new_pr env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + # Untrusted on repository_dispatch; kept out of the script text (only + # interpolated into the PR body as data below). + CLIENT_PAYLOAD_RUN_ID: ${{ github.event.client_payload.run_id }} run: | TARGET_SHA="${{ steps.target.outputs.target_sha }}" SHORT_SHA="${{ steps.target.outputs.short_sha }}" @@ -171,20 +182,24 @@ jobs: git checkout -b "$BRANCH" - # Update submodule to target SHA - git -C deps/moxygen fetch origin - git -C deps/moxygen checkout "$TARGET_SHA" - # --force: deps/moxygen has ignore=all in .gitmodules, and git >= 2.54 - # skips ignored submodules even on explicit add - git add --force deps/moxygen - git commit -m "sync: update moxygen submodule to ${SHORT_SHA}" + # The regex assumes the one-line `set(MOXYGEN_REV "<40-hex>")` shape. + # Verify the write landed, so a reformat of that file fails here rather + # than as a puzzling empty commit. + sed -i -E "s/(MOXYGEN_REV \")[0-9a-fA-F]{40}(\")/\1${TARGET_SHA}\2/" \ + cmake/dependencies.cmake + if ! grep -q "MOXYGEN_REV \"${TARGET_SHA}\"" cmake/dependencies.cmake; then + echo "::error::MOXYGEN_REV bump did not apply — cmake/dependencies.cmake no longer matches 'MOXYGEN_REV \"<40-hex>\"'" + exit 1 + fi + git add cmake/dependencies.cmake + git commit -m "sync: update moxygen to ${SHORT_SHA}" git push -u origin "$BRANCH" echo "Created branch $BRANCH" # Determine trigger source for PR body if [ "${{ github.event_name }}" = "repository_dispatch" ]; then - TRIGGER="Triggered by moxygen \`ci main\` run #${{ github.event.client_payload.run_id }}." + TRIGGER="Triggered by moxygen \`ci main\` run #${CLIENT_PAYLOAD_RUN_ID}." else TRIGGER="Triggered manually." fi @@ -192,7 +207,7 @@ jobs: PR_NUM=$(gh api repos/${{ github.repository }}/pulls \ -f title="sync: moxygen ${SHORT_SHA}" \ -f body="$(cat <> "$GITHUB_OUTPUT" echo "short=${SHA:0:7}" >> "$GITHUB_OUTPUT" - - name: Install system dependencies - run: sudo deps/moxygen/standalone/install-system-deps.sh - - - name: Setup dependencies - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: bash scripts/build.sh setup + - uses: ./.github/actions/setup-build + with: + ccache-key: perf-test + # Tests off: this job only ships the moqx binary to the perf VMs. - name: Build moqx - run: bash scripts/build.sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + scripts/configure.sh default --moxygen prebuilt-with-fallback -DMOQX_BUILD_TESTS=OFF + scripts/build.sh default - name: Configure SSH for perf VMs env: @@ -145,7 +147,7 @@ jobs: FILE="run-${{ steps.sha.outputs.short }}.json" # Fallbacks: schedule events carry no `inputs`, so apply the canonical # nightly load when invoked by cron. - bash scripts/perf-test-ci.sh \ + bash scripts/perf/perf-test-ci.sh \ --duration "${{ inputs.duration || '120' }}" \ --subscriber-max "${{ inputs.subscribers || '600' }}" \ --ramp 150 \ @@ -227,13 +229,13 @@ jobs: # Picked up by actions/deploy-pages in the deploy job (gated to the # nightly schedule). Dispatch / called runs still upload it so reviewers # can download + inspect without publishing. - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: perf-out - name: Upload workflow artifacts (always retained) if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: perf-results path: | @@ -250,7 +252,7 @@ jobs: env: FILE: ${{ steps.collect.outputs.run-file }} run: | - python3 scripts/perf-compare.py \ + python3 scripts/perf/perf-compare.py \ --current "$FILE" \ --data-dir data \ --window 10 \ @@ -294,12 +296,12 @@ jobs: # the personal actor GitHub attributes scheduled runs to. Keeps the # deployments list owned by the bot, consistent with the rest of the org # automation. Requires the App installation to have Pages: write. - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@v3 id: app-token with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 with: token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 000000000..ca4ab2bb3 --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,48 @@ +name: sanitizers + +# Instrumented from-source sanitizer builds: the folly/mvfst/proxygen stack is +# compiled WITH the sanitizer, unlike the per-PR "asan" lane. Heavy, so it runs +# nightly, or on dispatch against a branch to vet a PR before merge. + +on: + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + sanitizer: + strategy: + fail-fast: false + matrix: + profile: [san, tsan] + name: ${{ matrix.profile }} (from-source) + runs-on: [self-hosted, linode] + timeout-minutes: 180 + steps: + - uses: actions/checkout@v5 + + - uses: ./.github/actions/setup-build + with: + ccache-key: ${{ matrix.profile }}-from-source + + # Builds the instrumented moxygen prefix via the superbuild, then moqx. + - name: Build (instrumented, from source) + run: | + scripts/configure.sh ${{ matrix.profile }} --moxygen from-source + scripts/build.sh ${{ matrix.profile }} + + - name: Test + env: + ASAN_OPTIONS: ${{ matrix.profile == 'san' && 'detect_leaks=1:abort_on_error=1' || '' }} + TSAN_OPTIONS: ${{ matrix.profile == 'tsan' && 'halt_on_error=1' || '' }} + # Instrumented binaries run 5-15x slower; keep ctest parallelism modest + # so timing-sensitive integration tests don't flake under CPU pressure. + MOQX_TEST_JOBS: 4 + run: scripts/test.sh ${{ matrix.profile }} diff --git a/.github/workflows/version-release.yml b/.github/workflows/version-release.yml index 0aadec0cf..c7b7b3dc7 100644 --- a/.github/workflows/version-release.yml +++ b/.github/workflows/version-release.yml @@ -24,17 +24,14 @@ name: version release # # Inputs: # version required. semver, e.g. 1.2.3 -# moxygen_release optional. moxygen tag to consume (e.g. v1.2.3). -# When absent, falls back to moxygen `snapshot-latest`. -# The fallback is fine for everyday tagging but won't -# produce portable tarballs (snapshot-latest doesn't -# carry them — portable only ships in tagged moxygen -# releases). +# moxygen_release optional cross-check, not a selector. The moxygen consumed +# is always the one MOXYGEN_REV pins — pass the tag you expect +# that pin to resolve to and the run fails early if it doesn't. # -# Behavior matrix: -# moxygen_release present + has portable assets → portable matrix runs -# moxygen_release present + no portable assets → portable skipped (no failure) -# moxygen_release absent → portable skipped (no failure) +# Behavior matrix, on the moxygen release MOXYGEN_REV resolves to: +# tagged v* release (carries portable assets) → portable matrix runs +# rolling snapshot-* release (does not) → portable skipped (no failure) +# no tag at the pin → validate fails, nothing tagged # # Snapshot source — derived from dispatched ref: # dispatched on main → consume snapshot-latest @@ -53,7 +50,7 @@ on: required: true type: string moxygen_release: - description: 'moxygen release tag (optional, e.g. v1.2.3). Default: moxygen snapshot-latest.' + description: 'Expected moxygen release tag (optional, e.g. v1.2.3) — checked against what MOXYGEN_REV resolves to.' required: false type: string @@ -83,7 +80,7 @@ jobs: moxygen_ref: ${{ steps.v.outputs.moxygen_ref }} has_portable: ${{ steps.v.outputs.has_portable }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -121,40 +118,31 @@ jobs: exit 1 fi - # Moxygen reference: explicit input or snapshot-latest fallback. - MOX_REF="${{ inputs.moxygen_release }}" - if [ -z "$MOX_REF" ]; then - # Branch-aware default: from main, use GitHub's "latest" tagged - # release (highest non-prerelease semver across all branches). - # From release/vM.m, use the latest non-prerelease v M.m.* tag — - # so a hotfix dispatch on a release line picks the matching - # moxygen patch line, not whatever's newest globally. - if [ "$BRANCH" = "main" ]; then - MOX_REF=$(gh release view --repo openmoq/moxygen --json tagName --jq .tagName 2>/dev/null || true) - if [ -z "$MOX_REF" ]; then - echo "Error: no tagged moxygen release found." >&2 - exit 1 - fi - echo "==> No moxygen_release input — defaulting to GitHub latest: $MOX_REF" - else - LABEL="${BRANCH#release/v}" - MOX_REF=$(gh release list --repo openmoq/moxygen --limit 50 \ - --json tagName,isPrerelease \ - --jq ".[] | select(.isPrerelease == false) | select(.tagName | startswith(\"v$LABEL.\")) | .tagName" \ - | head -1) - if [ -z "$MOX_REF" ]; then - echo "Error: no v${LABEL}.* non-prerelease moxygen release found for this release branch line." >&2 - exit 1 - fi - echo "==> No moxygen_release input — defaulting to latest v${LABEL}.* tag: $MOX_REF" - fi - else + # Resolve the moxygen tag exactly the way the build will. Anything else + # (moxygen's newest release, say) disagrees with MOXYGEN_REV, fails + # both matrix legs at configure, and strands the release as a draft. + MOX_PIN=$(cmake -DPIN=MOXYGEN_REV -P cmake/print-pin.cmake) + if ! MOX_REF=$(cmake -P cmake/print-release-tag.cmake); then + echo "Error: no published moxygen release points at the pinned MOXYGEN_REV" >&2 + echo " $MOX_PIN" >&2 + echo "Bump the pin on $BRANCH to a published rev before cutting a release." >&2 + exit 1 + fi + echo "==> MOXYGEN_REV $MOX_PIN resolves to moxygen release $MOX_REF" + + # Disagreement can only mean the operator meant to bump MOXYGEN_REV + # first — the input cannot move the build off the pin. + INPUT_REF="${{ inputs.moxygen_release }}" + if [ -n "$INPUT_REF" ]; then # Accept bare semver (0.1.4) or v-prefixed (v0.1.4); normalize. - if [[ "$MOX_REF" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then - MOX_REF="v$MOX_REF" - echo "==> Normalized bare semver to $MOX_REF" + if [[ "$INPUT_REF" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + INPUT_REF="v$INPUT_REF" + fi + if [ "$INPUT_REF" != "$MOX_REF" ]; then + echo "Error: moxygen_release '$INPUT_REF' is not the release MOXYGEN_REV resolves to ('$MOX_REF')." >&2 + echo "Bump MOXYGEN_REV on $BRANCH instead — the build verifies the tag against the pin." >&2 + exit 1 fi - echo "==> Pinning to moxygen release: $MOX_REF" fi # Verify moxygen reference exists; on failure, list recent releases. @@ -200,12 +188,12 @@ jobs: steps: - name: Generate app token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Create release (no assets yet) env: @@ -280,57 +268,43 @@ jobs: steps: - name: Generate app token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} - - uses: actions/checkout@v4 - with: - # recursive: deps/catapult carries nested submodules (libcbor, - # doctest, spdlog, nlohmann_json) that the top-level build needs. - submodules: recursive + - uses: actions/checkout@v5 - name: Install system dependencies run: | - # Match docker/Dockerfile's build-stage apt list — picks up - # libdwarf-dev / libbrotli-dev that moxygen's install-system-deps - # doesn't install but proxygen needs. - sudo apt-get update + # Base library set via the shared installer, so a new moqx dependency + # reaches the release build on its own. Only extras below: libdwarf + # (prebuilt proxygen symbolizer), xxhash (folly), ca-certificates. + scripts/install-system-deps.sh sudo apt-get install -y --no-install-recommends \ - build-essential ninja-build cmake git ca-certificates pkg-config \ - libssl-dev libunwind-dev libgoogle-glog-dev libgflags-dev \ - libdouble-conversion-dev libevent-dev libsodium-dev libzstd-dev \ - libboost-all-dev libfmt-dev zlib1g-dev libc-ares-dev libdwarf-dev \ - libbrotli-dev libgtest-dev libgmock-dev libxxhash-dev + ca-certificates libdwarf-dev libxxhash-dev - - name: Download moxygen portable tarball + - name: Configure env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} MOQX_PLATFORM: ${{ matrix.platform }} MOX_REF: ${{ needs.validate.outputs.moxygen_ref }} + # FetchMoxygenPrebuilt reads the releases API. Anonymous is 60/hour/IP, + # shared across runner egress, and a 403 here fails the release build. + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: | - if [ "$MOX_REF" = "snapshot-latest" ]; then - bash scripts/build.sh setup --use-latest --no-fallback - else - export MOQX_MOXYGEN_RELEASE_TAG="$MOX_REF" - bash scripts/build.sh setup --no-fallback - fi - - - name: Configure - run: | - # CMAKE_INSTALL_RPATH = $ORIGIN/../lib makes the binary look - # for bundled .so files next to itself before the system search - # path. Combined with the bundle step below this produces a - # self-contained tarball that runs on noble / RHEL 9 / AL2023 - # without needing distro-specific packages installed. - # Pass the tag explicitly: the checkout is shallow and the tag may - # not exist yet, so git describe would yield a bare sha. + # Strict prebuilt, no from-source fallback: a versioned artifact ships a + # published, digest-verified moxygen (docs/ci-architecture.md). MOQX_PLATFORM + # picks the tarball, a non-snapshot ref its release tag (BUILD.md#build). + TAG_ARG=() + [ "$MOX_REF" != "snapshot-latest" ] && TAG_ARG=(-DMOXYGEN_RELEASE_TAG="$MOX_REF") + # $ORIGIN/../lib plus the bundle step below make the tarball run on + # noble / RHEL 9 / AL2023 unaided. The version is passed in because the + # checkout is shallow, where git describe would yield a bare sha. cmake -S . -B _build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DMOQX_VERSION_STRING="${{ needs.validate.outputs.tag }}" \ - -DCMAKE_PREFIX_PATH="$(cat .scratch/cmake_prefix_path.txt)" \ - -DCMAKE_MODULE_PATH="${GITHUB_WORKSPACE}/cmake;${GITHUB_WORKSPACE}/deps/moxygen/build/fbcode_builder/CMake" \ + -DMOQX_BUILD_TESTS=OFF \ + "${TAG_ARG[@]}" \ -DCMAKE_C_FLAGS="${{ matrix.cxx_flags }}" \ -DCMAKE_CXX_FLAGS="${{ matrix.cxx_flags }}" \ -DCMAKE_INSTALL_RPATH='$ORIGIN/../lib' \ @@ -409,7 +383,7 @@ jobs: steps: - name: Generate app token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} @@ -474,7 +448,7 @@ jobs: steps: - name: Generate app token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OMOQ_APP_ID }} private-key: ${{ secrets.OMOQ_APP_PRIV_KEY }} diff --git a/.gitignore b/.gitignore index c13cb2ac3..f8768e01a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ # Build outputs /build/ -/build-*/ /_build/ /cmake-build-*/ /CMakeFiles/ CMakeCache.txt +/CMakeUserPresets.json cmake_install.cmake install_manifest.txt compile_commands.json @@ -20,9 +20,9 @@ DartConfiguration.tcl # CPack _CPack_Packages/ -# Dependency scratch dirs +# Local dependency staging: /.scratch/ holds the from-source superbuild's +# moxygen install. /.scratch/ -/.docker-deps/ # OS .DS_Store diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 995a75418..000000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "deps/moxygen"] - path = deps/moxygen - url = git@github.com:openmoq/moxygen.git - branch = main - # "all": pointer drift never shows in status/diff, so bulk git add can't - # sweep an accidental pin bump into a feature commit. Intentional bumps - # (sync PRs) stage with `git add --force deps/moxygen` (git >= 2.54 - # skips ignored submodules even on explicit add). - ignore = all -[submodule "deps/catapult"] - path = deps/catapult - url = https://github.com/Quicr/catapult.git diff --git a/BUILD.md b/BUILD.md index 543f22285..d189970eb 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,219 +1,185 @@ # Building moqx -The Quick Start is in the top-level [README](README.md#quick-start). This -document is the detailed build reference: the procedure annotated with -what each step does, plus prereqs, dependency modes, profiles, Docker, -formatting, and CI. - -## Supported Platforms - -| Platform | Status | Notes | -|----------|--------|-------| -| Ubuntu 22.04 (Jammy) | Tested in CI | Primary dev/CI platform | -| Debian 12 (Bookworm) | Tested (Docker build) | Docker image base | -| Ubuntu/Debian derivatives (Mint, Pop!_OS, ...) | Supported | Detected via `ID_LIKE`, mapped to the Ubuntu/Debian base artifacts; override with `MOQX_PLATFORM` if detection is wrong | -| Fedora / RHEL | TBD | `install-system-deps.sh` has dnf support; not yet CI-tested | -| macOS (Homebrew) | TBD | `install-system-deps.sh` has brew support; not yet CI-tested | - -## Build Procedure - -Six steps. Each links to its detail section. - -1. **Clone and init the moxygen submodule.** - `git clone … && cd moqx && git submodule update --init --recursive` — - the submodule pins the exact moxygen commit the build will use - (see [Dependency Modes](#dependency-modes)). -2. **Ensure CMake 3.22+** is on `PATH`. All current targets ship a - new-enough version: Ubuntu 22.04+, Debian 12+, recent macOS Homebrew. - `build.sh` aborts early if cmake is missing or too old. -3. **Install system libraries.** Required in *both* dependency modes — - see the system-libraries bullet in [Prerequisites](#prerequisites) - for why. One-liner: - `sudo deps/moxygen/standalone/install-system-deps.sh`. -4. **Stage moxygen.** `./scripts/build.sh setup` — defaults to - `--from-release` (downloads the prebuilt tarball, ~1 min); falls back - to source build if unavailable. See [Dependency Modes](#dependency-modes) - for `--from-source`, `--moxygen-dir`, SHA pinning, etc. -5. **Build moqx.** `./scripts/build.sh` — configures and builds. - See [Build Profiles](#build-profiles) for `default` vs `san`. -6. **Run tests.** `./scripts/build.sh test` — 77 tests, sub-second. - -If you'd rather build in a clean container, skip to -[Building from a Fresh Ubuntu Docker](#building-from-a-fresh-ubuntu-docker-reproducible-build). +A standard CMake preset build. moqx links +[moxygen](https://github.com/openmoq/moxygen) as an installed package; everything +else is fetched at configure time by +[CPM](https://github.com/cpm-cmake/CPM.cmake). Pinned revisions live in +[/cmake/dependencies.cmake](/cmake/dependencies.cmake). ## Prerequisites -- **CMake 3.22+** — required by moqx top-level `CMakeLists.txt`. All - current targets ship a new-enough version out of the box: Ubuntu - 22.04+, Debian 12+, recent macOS Homebrew. `build.sh` enforces this - and aborts early if cmake is missing or too old (override with - `MOQX_SKIP_CMAKE_CHECK=1`). -- Ninja, C++20 compiler (GCC 11+ / Clang 14+). -- `curl` for downloading the moxygen release tarball - ([`setup-deps-tarball.sh`](scripts/setup-deps-tarball.sh)). No `gh` CLI is - required. Set `GITHUB_TOKEN`/`GH_TOKEN` only if you hit api.github.com's - unauthenticated rate limit. -- **System libraries** — required in **both** dependency modes: - libssl, libfmt, libglog, libgflags, libdouble-conversion, libevent, - libsodium, libzstd, libboost, c-ares, libunwind, zlib, gperf. The - release tarball ships folly/fizz/wangle/mvfst/proxygen statically, but - moxygen's CMake config does `find_dependency(fmt, Glog, ...)` and folly - itself transitively wants OpenSSL/Boost. Install all with: - ```bash - sudo deps/moxygen/standalone/install-system-deps.sh - ``` - (`build.sh setup`'s system-dep check only fires when falling back to a - source build — it does not preempt the build-step failure if libs are - missing.) - -### Installing CMake - -moqx requires CMake 3.22+. All current targets ship a new-enough version -out of the box: - -**Ubuntu 22.04+ / Debian 12+:** +- **CMake 3.23+**, **Ninja**, a **C++20 compiler** (GCC 11+ / Clang 14+). + [`scripts/install-system-deps.sh`](/scripts/install-system-deps.sh) fetches a + current CMake from PyPI when the distro's is older (e.g. Ubuntu 22.04 ships + 3.22, but reflect-cpp needs 3.23). +- **System libraries** — [`scripts/install-system-deps.sh`](/scripts/install-system-deps.sh) + (apt/dnf/brew), or install by hand: OpenSSL, boost, glog, gflags, + double-conversion, libevent, sodium, zstd, fmt, c-ares, libunwind, zlib, brotli, + gperf. Both modes need them: folly & co. resolve them from the system. +- **ccache** is used automatically when it's on `PATH`. + +## How dependencies work + +moqx doesn't compile moxygen in-tree; it `find_package`es an **installed** moxygen +and builds against it — into `build/` (`build/default`, `build/san`, …). +The only choice is where that install comes from: + +- **Prebuilt with fallback** — the prebuilt when one is published, else the source + build. The default choice, and what CI uses: moxygen decides what it publishes, + so this is the only setting that always yields a build. +- **Prebuilt** — download the published tarball for the pinned `MOXYGEN_REV` and + your platform. Fast, and errors rather than compiling when there is none — take + it when a slow build is worse for you than a failure. +- **From source** — compile moxygen into an install prefix, then build moqx + against it. Works for any revision or platform, and lets you develop moxygen and + moqx together. + +The moqx build is identical whichever it is; only the prefix differs. + +moxygen is itself a superbuild: its `standalone/` tree compiles Meta's +folly/fizz/wangle/mvfst/proxygen plus the +[openmoq/picoquic](https://github.com/openmoq/picoquic) fork. Those revisions are +pinned inside moxygen, in its +[`build/deps/github_hashes/`](https://github.com/openmoq/moxygen/tree/main/build/deps/github_hashes), +not here. moqx's [/cmake/dependencies.cmake](/cmake/dependencies.cmake) pins the CPM +ones: moxygen, catapult, reflect-cpp, yaml-cpp. + +## Build + +Three scripts, each taking the profile (`default` | `san` | `tsan`, default +`default`) as its first argument, each shown below beside its raw cmake: + +- [`scripts/configure.sh`](/scripts/configure.sh) — picks where moxygen comes from, configures + `build/` from scratch. Run once per profile; that dir's CMake cache is + the only state. +- [`scripts/build.sh`](/scripts/build.sh) — compiles. +- [`scripts/test.sh`](/scripts/test.sh) — runs the suite. + +`configure.sh` and `build.sh` take `-j N` for compile parallelism, or `MOQX_BUILD_JOBS` +in the environment. Either overrides the default in both directions: go well above the +core count to farm out to distcc, below it on a host short on RAM. + +**Prebuilt with fallback** — the default: ```bash -sudo apt-get install -y cmake -cmake --version # should show 3.22+ +scripts/configure.sh --moxygen prebuilt-with-fallback +scripts/build.sh # compile moqx +scripts/test.sh ``` +Attempts the prebuilt and builds from source if that fails for any reason, +transient ones included. -**macOS (Homebrew):** +Falling back on everything is deliberate: a slow build beats a failed one, and +re-running costs less than a guess about which failures are permanent. -```bash -brew install cmake # or `brew upgrade cmake` if already present -cmake --version -``` - -### Installing System Dependencies - -```bash -sudo deps/moxygen/standalone/install-system-deps.sh -``` +No raw-cmake equivalent: the two paths are separate CMake projects, and choosing +between them is what the wrapper is for. -### Building from a Fresh Ubuntu Docker (Reproducible Build) +`MOQX_MOXYGEN_FALLBACK=off` reduces it to `--moxygen prebuilt`, for when a bad pin +would otherwise have every CI lane compiling folly. -This is handy for verifying the build on a clean system or for contributors -who don't want to install deps on their host: +**Prebuilt:** ```bash -docker run --rm -it -v "$PWD":/src -w /src ubuntu:22.04 bash - -# Inside the container: -apt-get update && apt-get install -y cmake ninja-build sudo git curl ca-certificates -git submodule update --init --recursive -sudo deps/moxygen/standalone/install-system-deps.sh -./scripts/build.sh setup --from-source # build from source (no release artifacts available offline) -./scripts/build.sh -./scripts/build.sh test +scripts/configure.sh --moxygen prebuilt # download prebuilt moxygen, configure +scripts/build.sh # compile moqx +scripts/test.sh ``` - -## Dependency Modes - -moqx depends on moxygen (and its Meta deps: folly, fizz, wangle, mvfst, proxygen). -The `deps/moxygen` submodule pins the exact version. Two ways to get these deps: - -| Mode | Command | Time | When to use | -|------|---------|------|-------------| -| **from-release** | `build.sh setup` | ~1 min | Default -- downloads CI-built artifacts | -| **from-source** | `build.sh setup --from-source` | 15-30 min | Full control, or when artifacts unavailable | - -Both accept an optional commit SHA to override the submodule pointer: - +Raw equivalent — moqx downloads the prebuilt itself at configure time: ```bash -build.sh setup --from-release abc1234 # artifacts for specific moxygen commit -build.sh setup --from-source abc1234 # build specific commit from source +cmake --preset default && cmake --build build/default && ctest --test-dir build/default --output-on-failure ``` +A moxygen install already on `CMAKE_PREFIX_PATH` is used as-is (tried before any +download). No prebuilt for your platform → configure errors and points here; force +a published tag with `-DMOQX_PLATFORM=`. Platform tags are +`ubuntu--`, `bookworm-` (Debian and its derivatives) and +`macos--`, `` ∈ {`amd64`, `arm64`} — what is actually published +is on [openmoq/moxygen's releases](https://github.com/openmoq/moxygen/releases). -To build against a local moxygen checkout (for iterating on moxygen itself): +**From source:** ```bash -build.sh setup --from-source --moxygen-dir ~/src/moxygen -``` - -Default (no SHA or dir) uses the current submodule HEAD. -Falls back from release to source if artifacts aren't available. -Use `--no-fallback` to fail instead. Use `--clean` to wipe `.scratch/` first. - -## Build Profiles - -``` -build.sh setup [--from-release [SHA]|--from-source [SHA]] [--moxygen-dir DIR] [--no-fallback] [--clean] -build.sh [--profile default|san] [--build-dir DIR] -build.sh test [--build-dir DIR] [-- CTEST_ARGS...] +scripts/configure.sh --moxygen from-source # build moxygen -> a prefix, configure moqx against it +scripts/build.sh # compile moqx +scripts/test.sh ``` - -| Profile | Build dir | Description | -|---------|-----------|-------------| -| `default` | `build/` | RelWithDebInfo | -| `san` | `build-san/` | Debug + ASAN/UBSAN | - -## Formatting and Linting - -CI requires clang-format-19. Check before pushing: - +Raw equivalent — the [superbuild](/superbuild) builds the moxygen prefix, then the +same moqx build consumes it: ```bash -./scripts/format.sh --check # verify (dry-run) -./scripts/format.sh # fix in-place -./scripts/lint.sh build # clang-tidy (requires prior build) +cmake -S superbuild -B .scratch/moxygen-build -G Ninja # [-DCPM_moxygen_SOURCE=/path] +cmake --build .scratch/moxygen-build # -> .scratch/moxygen-build/moxygen-install +cmake --preset default -DMOQX_MOXYGEN_PREBUILT=OFF \ + -DCMAKE_PREFIX_PATH=$PWD/.scratch/moxygen-build/moxygen-install +cmake --build build/default ``` -## PR Process - -1. Create a branch, push changes -2. CI runs: format check + build/test (default + ASAN) -3. All checks must pass before merge -4. Squash-and-merge preferred for single-feature PRs - -## CI and Automation - -See [design/ci-architecture.md](design/ci-architecture.md) for the full CI pipeline: -upstream sync, submodule updates, build/publish/release, and auto-deploy. - -## Developer IDEs -### CLion +## Developing moxygen + moqx together -CLion can build moqx directly via its CMake integration. You still need the -same prerequisites as the command-line build (CMake 3.22+, system libraries, -and a staged moxygen install). The steps below wire CLion to the local -dependency tree under `.scratch/`. - -#### 1. Stage dependencies (first time, or after dep changes) - -Before CLion can configure the project, moxygen must be present at -`.scratch/moxygen-install`. Run: +Point `configure.sh` at a local moxygen checkout **once**, then iterate with +two builds: ```bash -./scripts/build.sh setup --from-source +scripts/configure.sh --moxygen from-source --moxygen-dir ~/src/moxygen # once +# edit ~/src/moxygen/… then: +cmake --build .scratch/moxygen-build # recompile + reinstall moxygen (incremental) +scripts/build.sh # relink moqx against the refreshed install ``` -This installs moxygen and its Meta dependencies into `.scratch/moxygen-install`. -It takes 15–30 minutes on a first run. +Only your changed moxygen files recompile, and moqx relinks against the refreshed +prefix. `--moxygen-dir` is the only flag you need: `configure.sh` passes both the +prefix and the matching find-modules to the moqx configure. By hand that is two +flags — `-DCMAKE_PREFIX_PATH` for the libraries **and** +`-DCPM_moxygen_SOURCE=/path` for moxygen's MODULE-mode find-modules. -You only need to re-run this when **dependencies change** — for example, after -updating the `deps/moxygen` submodule or when `.scratch/` has been cleaned. -Day-to-day moqx source edits do not require re-running setup. +Develop a local catapult instead with `-DCPM_catapult_SOURCE=/path` on the moqx +build — it compiles in-tree, no prefix needed. -#### 2. Configure CMake in CLion +## Sanitizers -Open the moqx project root in CLion, then go to -**Settings → Build, Execution, Deployment → CMake** and edit the **Debug** -profile. Add these CMake options: - -``` --DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_PREFIX_PATH=$CMakeProjectDir$/.scratch/moxygen-install -DGFLAGS_SHARED=ON +```bash +scripts/configure.sh san --moxygen from-source # or tsan +scripts/build.sh san +scripts/test.sh san ``` -`$CMakeProjectDir$` is a CLion macro that expands to the project root, so -CMake can find the staged moxygen package config. - -If you configure before running setup, CMake will fail because -`.scratch/moxygen-install` does not exist yet — that is expected. - -#### 3. Reload and build - -After setup completes, reload the CMake project in CLion -(**Tools → CMake → Reload CMake Project**, or the reload button in the CMake -tool window). Configuration should succeed and you can build and run targets -from the IDE as usual. +Sanitizers must instrument the dependencies too, so `san --moxygen from-source` +builds an instrumented moxygen as well; moqx lands in `build/san` (or +`build/tsan`). + +`prebuilt` and `prebuilt-with-fallback` are refused for these profiles, since +neither can produce an instrumented moxygen. `MOQX_ALLOW_UNINSTRUMENTED_DEPS=1` +overrides that to sanitize moqx's own TUs only — what the per-PR asan lane does. +Its fallback builds the uninstrumented stack too, so a missing prebuilt cannot +quietly promote that lane to a full instrumented build. + +Instrumented TUs peak over 2 GB each, enough for the core count to OOM the compiler +on a smaller host, so these profiles derate the default job count by free RAM. `-j` and +`MOQX_BUILD_JOBS` still win outright. + +## Custom presets + +Profiles are CMake presets. Add your own in `CMakeUserPresets.json` +(gitignored), inherit `default` (that keeps `binaryDir` at `build/`, +which the scripts rely on), and the three scripts accept its name: +`scripts/configure.sh my-preset --moxygen prebuilt-with-fallback && scripts/build.sh my-preset`. +A preset that enables `MOQX_ENABLE_SANITIZERS` or `MOQX_ENABLE_TSAN` gets a +matching instrumented moxygen from `--moxygen from-source` — derived from the +preset's own cache variables, overridable with the `MOQX_MOXYGEN_PROFILE` env +var. Other presets build/link the default-flag moxygen, and `MOQX_MOXYGEN_PROFILE` +naming an instrumented one there is refused: moqx would carry no sanitizer flags +of its own, leaving the interceptors undefined at link. + +## Docker, formatting, IDE, CI + +- **Docker** — [`docker/Dockerfile`](/docker/Dockerfile) builds via the same + `cmake --preset` flow, with targets `relay` (default) and `interop-client`. Its + `moxygen` stage resolves the prefix the same way `prebuilt-with-fallback` does, + and is keyed on the pin rather than on `src/` so a source change reuses it. +- **Format / lint** (CI requires clang-format-19) — + [`scripts/dev/format.sh`](/scripts/dev/format.sh) `[--check]`, + [`scripts/dev/lint.sh`](/scripts/dev/lint.sh) `build/default`. +- **CLion** — point its CMake profile at `cmake --preset default`; for from-source, + run `scripts/configure.sh --moxygen from-source` first, then add + `-DMOQX_MOXYGEN_PREBUILT=OFF -DCMAKE_PREFIX_PATH=` to the profile. +- **CI / automation** — [/docs/ci-architecture.md](/docs/ci-architecture.md). diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f3d200a2..9fd714414 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22) +cmake_minimum_required(VERSION 3.23) # reflect-cpp requires >= 3.23 project(moqx VERSION 0.1.0 LANGUAGES CXX) @@ -6,6 +6,19 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +# scripts/dev/lint.sh reads compile_commands.json from the build dir. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Route the compiler through ccache when it is installed, without a toolchain edit. +if(NOT DEFINED CMAKE_CXX_COMPILER_LAUNCHER) + find_program(CCACHE_PROGRAM ccache) + if(CCACHE_PROGRAM) + set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + message(STATUS "ccache: enabled (${CCACHE_PROGRAM})") + endif() +endif() + # Default to RelWithDebInfo for reproducible dev builds. if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE) @@ -18,6 +31,9 @@ option(MOQX_BUILD_TESTS "Build tests" ON) option(MOQX_BUILD_BENCHMARKS "Build benchmarks" OFF) option(MOQX_ENABLE_SANITIZERS "Enable ASAN/UBSAN (non-Release)" OFF) option(MOQX_ENABLE_TSAN "Enable TSan (non-Release)" OFF) +option(MOQX_MOXYGEN_PREBUILT + "Auto-download a prebuilt moxygen install when none is found on CMAKE_PREFIX_PATH" + ON) if(MOQX_ENABLE_SANITIZERS AND MOQX_ENABLE_TSAN) message(FATAL_ERROR "MOQX_ENABLE_SANITIZERS and MOQX_ENABLE_TSAN are mutually exclusive") @@ -32,23 +48,52 @@ option(MOQX_ENABLE_BPF_STEERING "Attach a classic BPF reuseport filter to steer QUIC packets to the correct worker (Linux only)" ${_moqx_bpf_default}) +include(cmake/SanitizerFlags.cmake) + if(MOQX_ENABLE_SANITIZERS AND NOT CMAKE_BUILD_TYPE STREQUAL "Release") - add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) - add_link_options(-fsanitize=address,undefined) + add_compile_options(${MOQX_ASAN_FLAGS}) + add_link_options(${MOQX_ASAN_FLAGS}) endif() if(MOQX_ENABLE_TSAN AND NOT CMAKE_BUILD_TYPE STREQUAL "Release") - add_compile_options(-fsanitize=thread -fno-omit-frame-pointer) - add_link_options(-fsanitize=thread) + add_compile_options(${MOQX_TSAN_FLAGS}) + add_link_options(${MOQX_TSAN_FLAGS}) endif() -# Prefer static libraries for all transitive deps (from-release mode). -# For from-source builds, build.sh overrides this to ON (system gflags is shared-only). -set(GFLAGS_SHARED OFF CACHE BOOL "") +include(cmake/CheckSystemDeps.cmake) + +# GFLAGS_SHARED=OFF makes moxygen's FindGflags steer glog at a second, static +# libgflags.a. Both copies load and gflags' startup self-check aborts on the +# duplicate registration, at test startup, with nothing naming the cause. +# FORCE, so a -DGFLAGS_SHARED=OFF on the command line cannot reintroduce it. +set(GFLAGS_SHARED ON CACHE BOOL "" FORCE) + +# cmake/ carries Findc-ares.cmake, the sole resolver for proxygen's +# find_dependency(c-ares): some distros ship no c-ares CONFIG package, and +# moxygen's fbcode_builder only answers the different module name "Cares". +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Some fetched deps declare pre-3.5 minimums that CMake >= 4 refuses to +# configure (yaml-cpp 0.8.0: 3.4; catapult's bundled libcbor/doctest: 3.5). +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) -list(APPEND CMAKE_MODULE_PATH - "${PROJECT_SOURCE_DIR}/deps/moxygen/build/fbcode_builder/CMake" +# MOQX_DEPS_CACHE + CPM_SOURCE_CACHE, shared with the superbuild. +include(cmake/DepsCache.cmake) + +include(cmake/CPM.cmake) +include(cmake/dependencies.cmake) + +# --- moxygen (openmoq fork) --------------------------------------------------- +# The SOURCE is fetched even in prebuilt mode: the installed folly/moxygen configs +# resolve Glog/Sodium/&c. in MODULE mode and ship no Find*.cmake, so moxygen's +# fbcode_builder find-modules have to be on the module path. See BUILD.md#how-dependencies-work. +CPMAddPackage( + NAME moxygen + GITHUB_REPOSITORY ${MOXYGEN_REPOSITORY} + GIT_TAG ${MOXYGEN_REV} + DOWNLOAD_ONLY YES ) +list(APPEND CMAKE_MODULE_PATH "${moxygen_SOURCE_DIR}/build/fbcode_builder/CMake") # Folly's config calls FindBoost which was removed in CMake 3.30 (CMP0167). # Folly itself works fine with Boost's own config-mode package; silence the @@ -57,16 +102,82 @@ if(POLICY CMP0167) cmake_policy(SET CMP0167 OLD) endif() -find_package(moxygen REQUIRED) -find_package(OpenSSL REQUIRED) +# An install already on CMAKE_PREFIX_PATH wins, e.g. one from the superbuild. +# Otherwise MOQX_MOXYGEN_PREBUILT decides: ON downloads the prebuilt for +# MOXYGEN_REV, OFF demands a prefix. See BUILD.md#how-dependencies-work. +find_package(moxygen QUIET CONFIG) +if(NOT moxygen_FOUND AND MOQX_MOXYGEN_PREBUILT) + if(MOQX_ENABLE_SANITIZERS OR MOQX_ENABLE_TSAN) + message(WARNING + "Sanitizers are enabled but moxygen resolves to the UNINSTRUMENTED " + "prebuilt — sanitizer coverage is limited to moqx's own TUs. For full " + "coverage build an instrumented moxygen: " + "scripts/configure.sh san|tsan --moxygen from-source (see BUILD.md).") + endif() + include(cmake/FetchMoxygenPrebuilt.cmake) + find_package(moxygen REQUIRED CONFIG) +elseif(NOT moxygen_FOUND) + message(FATAL_ERROR + "moxygen not found and MOQX_MOXYGEN_PREBUILT=OFF.\n" + "Build moxygen from source and point moqx at it:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh\n" + "(or pass -DCMAKE_PREFIX_PATH=; see BUILD.md).") +endif() +message(STATUS "moxygen: using install at ${moxygen_DIR}") + +# find_package caches moxygen_DIR and the folly/fizz/… tree with it, so a build +# dir binds to the moxygen it first resolved and a pin bump cannot take effect in +# place. +set(_moqx_moxygen_id "${MOXYGEN_REV}:${moxygen_DIR}") +if(DEFINED MOQX_RESOLVED_MOXYGEN AND NOT "${MOQX_RESOLVED_MOXYGEN}" STREQUAL "${_moqx_moxygen_id}") + get_filename_component(_moqx_profile "${CMAKE_BINARY_DIR}" NAME) + if(MOQX_MOXYGEN_PREBUILT) + set(_moqx_moxygen "prebuilt") + else() + set(_moqx_moxygen "from-source") + endif() + message(FATAL_ERROR + "MOXYGEN_REV (or the resolved moxygen install) changed since this build " + "dir was configured; cached find_package results would mix the two.\n" + "Reconfigure from scratch — this discards the build dir, so moqx recompiles " + "in full (a moxygen bump invalidates every moqx TU anyway; ccache and the " + "dependency cache survive):\n" + " scripts/configure.sh ${_moqx_profile} --moxygen ${_moqx_moxygen}\n" + "(raw cmake: delete the build dir first).") +endif() +set(MOQX_RESOLVED_MOXYGEN "${_moqx_moxygen_id}" CACHE INTERNAL + "moxygen identity this build dir resolved against") + +# ABI-crossing folly inlines branch on kIsDebug (= !NDEBUG), so moqx and its +# moxygen must agree on NDEBUG or they violate ODR. Only Debug leaves it +# undefined, and only the superbuild's san/tsan profiles build a Debug moxygen. +if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT MOQX_ALLOW_ABI_SKEW + AND ((NOT MOQX_ENABLE_SANITIZERS AND NOT MOQX_ENABLE_TSAN) OR MOQX_MOXYGEN_IS_PREBUILT)) + message(FATAL_ERROR + "moqx is a Debug build but the moxygen it links is release-flavored (NDEBUG); " + "folly's kIsDebug differs across the ABI boundary (ODR/UB). Use a " + "RelWithDebInfo/Release build type (for sanitizers on the prebuilt, add " + "-DCMAKE_BUILD_TYPE=RelWithDebInfo), or build an instrumented moxygen with " + "scripts/configure.sh --moxygen from-source. Pass -DMOQX_ALLOW_ABI_SKEW=ON " + "to build anyway.") +endif() -include(cmake/CPM.cmake) +# moxygen's sample binaries (moqclient/…) live in /bin, which integration +# tests reach through MOQBIN. moxygen_DIR is /lib/cmake/moxygen. +get_filename_component(MOXYGEN_BIN_DIR "${moxygen_DIR}/../../../bin" ABSOLUTE) + +# Test and perf scripts run outside ctest source this instead of re-deriving +# MOQBIN from CMakeCache.txt. Regenerated on every configure. +file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/moqx-tools.env" + CONTENT "MOQBIN='${MOXYGEN_BIN_DIR}'\n") + +find_package(OpenSSL REQUIRED) # --- yaml-cpp (CPM) --- CPMAddPackage( NAME yaml-cpp GITHUB_REPOSITORY jbeder/yaml-cpp - GIT_TAG 0.8.0 + GIT_TAG ${YAMLCPP_VERSION} OPTIONS "YAML_CPP_BUILD_TESTS OFF" "YAML_CPP_BUILD_TOOLS OFF" @@ -74,33 +185,46 @@ CPMAddPackage( "CMAKE_WARN_DEPRECATED OFF" ) -# --- reflect-cpp (FetchContent, needs yaml-cpp available) --- +# --- reflect-cpp (CPM, needs yaml-cpp available) --- CPMAddPackage( NAME reflectcpp GITHUB_REPOSITORY getml/reflect-cpp - GIT_TAG v0.18.0 + GIT_TAG ${REFLECTCPP_VERSION} OPTIONS "REFLECTCPP_YAML ON" "REFLECTCPP_BUILD_TESTS OFF" ) # --- XLOG category rooting --- -# Rewrite __FILE__ for moqx sources so folly XLOG categories render as moqx.* -# (e.g. moqx.MoqxRelay) instead of src.* — sources live under src/, but the -# user-facing logging API expects "moqx" as the top-level category. The -# FOLLY_XLOG_STRIP_PREFIXES define handles paths the prefix-map doesn't touch -# (notably generated headers under the build dir). Placed here so subsequent -# moqx_* targets pick it up; yaml-cpp / reflectcpp above were declared before -# this point and are unaffected. +# Sources live under src/, but folly XLOG categories should read moqx.MoqxRelay +# rather than src.MoqxRelay. FOLLY_XLOG_STRIP_PREFIXES covers what the +# prefix-map misses, notably generated headers. Must precede the moqx_* targets. add_compile_options(-fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}/src=moqx) add_compile_definitions( "FOLLY_XLOG_STRIP_PREFIXES=\"${CMAKE_SOURCE_DIR}:${CMAKE_BINARY_DIR}\"" ) # --- catapult (CAT/CWT/MOQT auth implementation) --- +# Nested deps (external/libcbor, …) arrive through FetchContent's recursive +# submodule clone. Develop against a local checkout with -DCPM_catapult_SOURCE, +# which must have its own submodules initialised. +CPMAddPackage( + NAME catapult + GITHUB_REPOSITORY ${CATAPULT_REPOSITORY} + GIT_TAG ${CATAPULT_REV} + EXCLUDE_FROM_ALL YES + OPTIONS + "ENABLE_LOGGING OFF" + "CATAPULT_ENABLE_JSON OFF" +) -set(ENABLE_LOGGING OFF CACHE BOOL "" FORCE) -set(CATAPULT_ENABLE_JSON OFF CACHE BOOL "" FORCE) -add_subdirectory(deps/catapult EXCLUDE_FROM_ALL) +# Strict warnings for first-party code; linked PRIVATE per target so nothing +# propagates to consumers. +# -Wmissing-field-initializers off: GCC fires it on C++20 designated initializers +# that omit members carrying default member initializers — the idiom throughout +# src/ and test/, and correct. Clang does not warn there. +add_library(moqx_warnings INTERFACE) +target_compile_options(moqx_warnings INTERFACE + -Wall -Wextra -Wpedantic -Wno-missing-field-initializers) # --- Cache library (forked from moxygen) --- @@ -124,7 +248,7 @@ target_link_libraries(moqx_cache PUBLIC Folly::folly_logging_logging ) -target_compile_options(moqx_cache PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx_cache PRIVATE moqx_warnings) # --- HMAC key derivation (shared by the relay's verifier and the issuer) --- @@ -139,7 +263,7 @@ target_include_directories(moqx_hmac_key target_link_libraries(moqx_hmac_key PUBLIC OpenSSL::Crypto) -target_compile_options(moqx_hmac_key PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx_hmac_key PRIVATE moqx_warnings) # --- Core library --- @@ -204,7 +328,7 @@ target_link_libraries(moqx_core PUBLIC mvfst::mvfst_logging_file_qlogger ) -target_compile_options(moqx_core PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx_core PRIVATE moqx_warnings) # PUBLIC so dependents can include moqx/Version.h. target_link_libraries(moqx_core PUBLIC moqx_version) @@ -224,9 +348,8 @@ target_link_libraries(moqx_config INTERFACE ) # --- Config loader (YAML parsing + validation + resolution + init) --- -# Application-level: ParsedConfig rfl structs, loadConfig(), resolveConfig(), -# handleConfigSubcommand(). Links rfl/yaml-cpp; consumers that only need -# resolved Config types can depend on moqx_config alone. +# Pulls in rfl/yaml-cpp. Consumers that only need the resolved Config types can +# depend on moqx_config alone. add_library(moqx_config_loader STATIC src/config/Loader.cpp @@ -254,7 +377,7 @@ target_link_libraries(moqx_config_loader PRIVATE Folly::folly_ssl_openssl_cert_utils ) -target_compile_options(moqx_config_loader PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx_config_loader PRIVATE moqx_warnings) # --- Main executable --- @@ -290,7 +413,7 @@ target_link_libraries(moqx_issuer_lib PUBLIC moxygen::moxygen_moq_types ) -target_compile_options(moqx_issuer_lib PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx_issuer_lib PRIVATE moqx_warnings) # --- Standalone moqx-issuer CLI --- @@ -307,7 +430,7 @@ target_link_libraries(moqx-issuer PRIVATE gflags ) -target_compile_options(moqx-issuer PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx-issuer PRIVATE moqx_warnings) # --- Tests --- @@ -320,9 +443,7 @@ if(MOQX_BUILD_BENCHMARKS) add_subdirectory(benchmark) endif() -include(${PROJECT_SOURCE_DIR}/cmake/Lint.cmake) - -install(TARGETS moqx moqx-issuer moqx_core moqx_config_loader) +install(TARGETS moqx moqx-issuer) # Identifies an unpacked tarball without running the binary. install(FILES ${CMAKE_BINARY_DIR}/VERSION DESTINATION .) diff --git a/CMakePresets.json b/CMakePresets.json index a2a855550..70f0fa4bf 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,67 +1,32 @@ { "version": 3, - "cmakeMinimumRequired": { - "major": 3, - "minor": 20, - "patch": 0 - }, "configurePresets": [ { "name": "default", "displayName": "Default (Ninja, RelWithDebInfo)", "generator": "Ninja", - "binaryDir": "${sourceDir}/build", + "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo", - "CMAKE_FIND_LIBRARY_SUFFIXES": ".a", - "CMAKE_MODULE_PATH": "${sourceDir}/cmake", - "CMAKE_POLICY_VERSION_MINIMUM": "3.5" + "CMAKE_BUILD_TYPE": "RelWithDebInfo" } }, { "name": "san", + "inherits": "default", "displayName": "Sanitizers (ASAN/UBSAN)", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build-san", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "MOQX_ENABLE_SANITIZERS": "ON", - "CMAKE_MODULE_PATH": "${sourceDir}/cmake", - "CMAKE_POLICY_VERSION_MINIMUM": "3.5" + "MOQX_ENABLE_SANITIZERS": "ON" } }, { "name": "tsan", + "inherits": "default", "displayName": "ThreadSanitizer (TSan)", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build-tsan", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "MOQX_ENABLE_TSAN": "ON", - "CMAKE_MODULE_PATH": "${sourceDir}/cmake", - "CMAKE_POLICY_VERSION_MINIMUM": "3.5" + "MOQX_ENABLE_TSAN": "ON" } } - ], - "buildPresets": [ - { - "name": "default", - "configurePreset": "default" - }, - { - "name": "san", - "configurePreset": "san" - }, - { - "name": "tsan", - "configurePreset": "tsan" - } - ], - "testPresets": [ - { - "name": "default", - "configurePreset": "default", - "output": {"outputOnFailure": true} - } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 024dc91b0..af9058983 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ be patient, reciprocate. ## CI -- `ci pr` — format, build (linux + asan debug), tests. Must pass before merge. +- `ci pr` — format, build (linux + asan), tests. Must pass before merge. - `ci main` — publish / release / deploy on push to `main` and `release/*`. See [docs/ci-architecture.md](docs/ci-architecture.md). diff --git a/README.md b/README.md index 2bf11439f..ff8e93229 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![ci main](https://github.com/openmoq/moqx/actions/workflows/ci-main.yml/badge.svg)](https://github.com/openmoq/moqx/actions/workflows/ci-main.yml) [![Latest release](https://img.shields.io/github/v/release/openmoq/moqx?display_name=tag&sort=semver&logo=github)](https://github.com/openmoq/moqx/releases/latest) -[![License](https://img.shields.io/github/license/openmoq/moqx)](LICENSE) +[![License](https://img.shields.io/github/license/openmoq/moqx)](/LICENSE) [![Last commit](https://img.shields.io/github/last-commit/openmoq/moqx)](https://github.com/openmoq/moqx/commits/main) [![Open issues](https://img.shields.io/github/issues/openmoq/moqx)](https://github.com/openmoq/moqx/issues) [![Open PRs](https://img.shields.io/github/issues-pr/openmoq/moqx)](https://github.com/openmoq/moqx/pulls) @@ -22,66 +22,55 @@ The OpenMOQ Relay — a MoQT relay server based on ## Architecture -For the underlying moxygen library architecture (session model, data plane, -threading, transport abstraction), see -[deps/moxygen/ARCHITECTURE.md](deps/moxygen/ARCHITECTURE.md). +`MoqxRelay` is a hard fork of moxygen's +[`MoQRelay`](https://github.com/openmoq/moxygen/blob/main/moxygen/relay/MoQRelay.h), +so the relay core can evolve independently while the lower-level moxygen pieces +stay libraries: -`MoqxRelay` is a hard fork of moxygen's `MoQRelay`. We copy the relay core into -moqx so we can evolve it independently (threading model, custom cache miss -handling, chained caches, etc.) while still using moxygen's lower-level -building blocks as libraries: +- **MoQForwarder** — fan-out engine +- **MoqxCache** — object cache +- **MoQSession / MoQServer / MoQRelaySession** — session/server infrastructure. -- **MoQForwarder** — fan-out engine, used as-is from moxygen for now. May need - to fork in the future to accommodate threading model differences. -- **MoqxCache** — object cache, hard-forked from moxygen. Customizable for moqx-specific functionality. - and chained cache support may be upstreamed to openmoq/moxygen or maintained - in our fork. -- **MoQSession / MoQServer / MoQRelaySession** — session and server - infrastructure, used as libraries. +`MoqxRelayServer` extends `MoQServer` to wire `MoqxRelay` in as the +publish/subscribe handler. For moxygen's own architecture, see its +[ARCHITECTURE.md](https://github.com/openmoq/moxygen/blob/main/ARCHITECTURE.md). -`MoqxRelayServer` extends `MoQServer` to wire up `MoqxRelay` as the publish/subscribe -handler and create `MoQRelaySession` instances for incoming connections. +## Quick Start -## Documentation +Standard CMake preset build (CMake 3.23+, C++20, Ninja). -- [docs/metrics.md](docs/metrics.md) — Prometheus metrics reference +```bash +scripts/install-system-deps.sh # toolchain + system libs +scripts/configure.sh --moxygen prebuilt-with-fallback # get a moxygen, configure +scripts/build.sh # cmake --build build/default +scripts/test.sh # ctest over build/default +``` -## Design Documents +`build.sh` and `test.sh` are thin wrappers that add a job count derived from +cores and free RAM ([/scripts/lib/jobs.sh](/scripts/lib/jobs.sh)). -- [design/ci-architecture.md](design/ci-architecture.md) — CI pipelines, upstream sync, auto-deploy -- [design/configuration.md](design/configuration.md) — relay config file reference -- [design/gummy-bear.md](design/gummy-bear.md) — cache and forwarding design -- [design/hot-reloading.md](design/hot-reloading.md) — hot config reload -- [design/miss-handler.md](design/miss-handler.md) — cache miss handling +Profiles (`default` | `san` | `tsan`) are each script's first argument and map to +`build/`. `--moxygen` picks where moxygen comes from: -## Quick Start +| Goal | Command | +|------|---------| +| Build it | `scripts/configure.sh --moxygen prebuilt-with-fallback && scripts/build.sh` | +| Download only, never compile moxygen | `scripts/configure.sh --moxygen prebuilt && scripts/build.sh` | +| Compile moxygen / any rev or platform | `scripts/configure.sh --moxygen from-source && scripts/build.sh` | +| Local moxygen checkout | `scripts/configure.sh --moxygen from-source --moxygen-dir /path && scripts/build.sh` | -> **Prerequisite: CMake 3.22+ is required.** All current targets ship a -> new-enough version out of the box: Ubuntu 22.04+, Debian 12+, recent -> macOS Homebrew. Verify with `cmake --version`. `build.sh` aborts early -> if cmake is missing or too old (override with `MOQX_SKIP_CMAKE_CHECK=1` -> if you know what you're doing). +The three modes, the raw-cmake equivalents, and how to pick: +[/BUILD.md](/BUILD.md#how-dependencies-work). -```bash -git clone https://github.com/openmoq/moqx.git && cd moqx -git submodule update --init --recursive -sudo deps/moxygen/standalone/install-system-deps.sh # system libs (both modes) +Pins live in [/cmake/dependencies.cmake](/cmake/dependencies.cmake). -./scripts/build.sh setup # download prebuilt deps (~1 min) -./scripts/build.sh # build -./scripts/build.sh test # test -``` +`ccache` is used automatically when it is on `PATH`. -System libraries are needed in **both** dependency modes — the moxygen -tarball ships folly/fizz/mvfst/proxygen statically, but its CMake config -still does `find_dependency(fmt, Glog, ...)` and folly itself transitively -needs OpenSSL/Boost. `build.sh setup`'s system-dep check only fires when -falling back to source, but the build step needs the libs regardless. +## Docs -See [BUILD.md](BUILD.md) for full build and test instructions (dependency -modes, sanitizer profiles, Docker), and [RUNNING.md](RUNNING.md) for relay -operations. +- Build [/BUILD.md](/BUILD.md) · Run [/RUNNING.md](/RUNNING.md) · Metrics [/docs/metrics.md](/docs/metrics.md) +- Design: [/design/](/design) ## License -Apache 2.0 — see [LICENSE](LICENSE). +Apache 2.0 — see [/LICENSE](/LICENSE). diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 7aa423d30..64cd6347e 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -19,4 +19,4 @@ target_link_libraries(moqx_benchmark PRIVATE Folly::follybenchmark ) -target_compile_options(moqx_benchmark PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries(moqx_benchmark PRIVATE moqx_warnings) diff --git a/cmake/CheckSystemDeps.cmake b/cmake/CheckSystemDeps.cmake new file mode 100644 index 000000000..a16cf9f76 --- /dev/null +++ b/cmake/CheckSystemDeps.cmake @@ -0,0 +1,81 @@ +# CheckSystemDeps.cmake — name the missing system -dev packages up front, instead +# of leaving them to surface as "Could NOT find ..." deep inside folly's config. +# +# Linux only; macOS/brew and non-standard prefixes are left to find_package. +# Skip with -DMOQX_SKIP_SYSTEM_DEP_CHECK=ON. + +# The Boost components folly's config find_package()s. Set before the early +# return: superbuild/CMakeLists.txt reads this list even when the check is +# skipped. +set(MOQX_BOOST_COMPONENTS context filesystem program_options regex thread) + +if(MOQX_SKIP_SYSTEM_DEP_CHECK OR NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + return() +endif() + +# "
||" — needed in both dependency modes, since +# folly resolves these from the system even under a prebuilt moxygen. A new +# library goes here and in scripts/install-system-deps.sh. +set(_moqx_reqs + "openssl/ssl.h|libssl-dev|openssl-devel" + "gflags/gflags.h|libgflags-dev|gflags-devel" + "glog/logging.h|libgoogle-glog-dev|glog-devel" + "double-conversion/double-conversion.h|libdouble-conversion-dev|double-conversion-devel" + "event2/event.h|libevent-dev|libevent-devel" + "sodium.h|libsodium-dev|libsodium-devel" + "zstd.h|libzstd-dev|libzstd-devel" + "boost/version.hpp|libboost-dev|boost-devel" + # folly's config find_dependency(ZLIB)s and proxygen's find_dependency(c-ares)s; + # both resolve from the system, the latter via cmake/Findc-ares.cmake. fmt does + # not belong here — moxygen builds and installs it into the prefix. + "zlib.h|zlib1g-dev|zlib-devel" + "ares.h|libc-ares-dev|c-ares-devel" +) + +set(_moqx_missing "") +foreach(_req IN LISTS _moqx_reqs) + string(REPLACE "|" ";" _parts "${_req}") + list(GET _parts 0 _hdr) + list(GET _parts 1 _apt) + list(GET _parts 2 _dnf) + string(MAKE_C_IDENTIFIER "sysdep_${_hdr}" _key) + find_path(${_key} NAMES "${_hdr}") + if(NOT ${_key}) + list(APPEND _moqx_missing " ${_hdr} (Debian: ${_apt} / Fedora: ${_dnf})") + endif() + # Don't cache the result: a stale NOTFOUND would survive installing the dep. + unset(${_key} CACHE) +endforeach() + +# Debian's libboost-dev ships headers only, and folly find_package()s each +# component separately. The probe goes through the compiler driver because this +# file also runs in the language-less superbuild, where find_library is blind. +find_program(_moqx_probe_cxx NAMES $ENV{CXX} c++ g++ clang++) +if(_moqx_probe_cxx) + foreach(_comp IN LISTS MOQX_BOOST_COMPONENTS) + set(_found FALSE) + foreach(_ext so a) + execute_process(COMMAND "${_moqx_probe_cxx}" -print-file-name=libboost_${_comp}.${_ext} + OUTPUT_VARIABLE _loc OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(_loc MATCHES "^/") + set(_found TRUE) + break() + endif() + endforeach() + if(NOT _found) + string(REPLACE "_" "-" _pkg "${_comp}") + list(APPEND _moqx_missing + " libboost_${_comp} (Debian: libboost-${_pkg}-dev / Fedora: boost-devel)") + endif() + endforeach() +endif() +unset(_moqx_probe_cxx CACHE) + +if(_moqx_missing) + string(REPLACE ";" "\n" _moqx_missing_str "${_moqx_missing}") + message(FATAL_ERROR + "Missing system dependencies — these dev packages were not found:\n" + "${_moqx_missing_str}\n\n" + "Install them with: scripts/install-system-deps.sh\n" + "(or bypass this check with -DMOQX_SKIP_SYSTEM_DEP_CHECK=ON).") +endif() diff --git a/cmake/DepsCache.cmake b/cmake/DepsCache.cmake new file mode 100644 index 000000000..a4a36b7bb --- /dev/null +++ b/cmake/DepsCache.cmake @@ -0,0 +1,34 @@ +# DepsCache.cmake — one root for everything moqx downloads. +# +# /cpm CPM's source clones (CPM_SOURCE_CACHE) +# /moxygen-- extracted prebuilt installs (FetchMoxygenPrebuilt.cmake) +# +# Root precedence: -DMOQX_DEPS_CACHE, $MOQX_DEPS_CACHE, $HOME/.cache/moqx, else a +# directory in the build tree (a container with no HOME). Relocating the root +# moves both halves; an explicit CPM_SOURCE_CACHE still wins for the clones alone. +# +# Included by CMakeLists.txt, superbuild/CMakeLists.txt and +# cmake/FetchMoxygenPrebuilt.cmake. The superbuild and the moqx build have to +# land on the same clones, or a from-source moqx compiles against different +# source than the moxygen prefix it links. + +include_guard(GLOBAL) + +if(NOT DEFINED MOQX_DEPS_CACHE OR MOQX_DEPS_CACHE STREQUAL "") + if(NOT "$ENV{MOQX_DEPS_CACHE}" STREQUAL "") + set(_moqx_deps_cache "$ENV{MOQX_DEPS_CACHE}") + elseif(NOT "$ENV{HOME}" STREQUAL "") + set(_moqx_deps_cache "$ENV{HOME}/.cache/moqx") + else() + set(_moqx_deps_cache "${CMAKE_BINARY_DIR}/deps-cache") + endif() + set(MOQX_DEPS_CACHE "${_moqx_deps_cache}" CACHE PATH + "Root for moqx's dependency downloads (CPM sources + prebuilt installs)") +endif() + +# This must be the CACHE entry: CPM declares its own, which drops a normal +# variable of the same name and switches the cache off on the first configure. +if(NOT DEFINED CACHE{CPM_SOURCE_CACHE} AND "$ENV{CPM_SOURCE_CACHE}" STREQUAL "") + set(CPM_SOURCE_CACHE "${MOQX_DEPS_CACHE}/cpm" CACHE PATH + "Directory to download CPM dependencies") +endif() diff --git a/cmake/FetchMoxygenPrebuilt.cmake b/cmake/FetchMoxygenPrebuilt.cmake new file mode 100644 index 000000000..e85c14b2c --- /dev/null +++ b/cmake/FetchMoxygenPrebuilt.cmake @@ -0,0 +1,370 @@ +# FetchMoxygenPrebuilt.cmake — resolve MOXYGEN_REV to a published moxygen release, +# download its prebuilt install for this platform, and append that to +# CMAKE_PREFIX_PATH for the find_package(moxygen CONFIG) that follows. +# +# The prebuilt is a pure function of MOXYGEN_REV: +# 1. moqx_moxygen_release_tag() (MoxygenRelease.cmake) maps the rev to a tag. +# 2. One GitHub API read of that release returns both the commit it was built +# from and every asset's digest, so a tag that repoints mid-fetch cannot +# deliver wrong-rev binaries. The digest is a consistency check, not tamper +# resistance — it shares its origin with the asset. +# 3. The download uses the browser_download_url from that same response. +# +# No tag at the rev, no release on the tag, or no asset for this platform is a +# hard error pointing at the superbuild. See BUILD.md#how-dependencies-work. +# +# Included by CMakeLists.txt, after cmake/dependencies.cmake. +# +# Knobs: +# MOQX_PLATFORM override the auto-detected platform tag +# MOXYGEN_RELEASE_TAG release tag to fetch from (declared in +# MoxygenRelease.cmake; default: resolve it from +# MOXYGEN_REV). Either way the release's commit is +# verified against the pin. +# MOQX_DEPS_CACHE cache root, variable or env — see cmake/DepsCache.cmake +# GITHUB_TOKEN/GH_TOKEN env only — authenticate the release read + +include("${CMAKE_CURRENT_LIST_DIR}/DepsCache.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/MoxygenRelease.cmake") + +# --- platform tag: the in moxygen-.tar.gz --------------- +function(_moqx_detect_platform OUT_VAR) + if(DEFINED MOQX_PLATFORM AND NOT MOQX_PLATFORM STREQUAL "") + set(${OUT_VAR} "${MOQX_PLATFORM}" PARENT_SCOPE) + return() + endif() + if(DEFINED ENV{MOQX_PLATFORM} AND NOT "$ENV{MOQX_PLATFORM}" STREQUAL "") + set(${OUT_VAR} "$ENV{MOQX_PLATFORM}" PARENT_SCOPE) + return() + endif() + + # CMAKE_HOST_SYSTEM_PROCESSOR comes from project()/enable_language(), so it is + # empty under `cmake -P`. uname -m is CMake's own probe, so script mode and + # project mode agree; disagreeing seeds a directory the build never looks at. + set(_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}") + if(_arch STREQUAL "") + execute_process(COMMAND uname -m + OUTPUT_VARIABLE _arch OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + endif() + if(_arch STREQUAL "x86_64" OR _arch STREQUAL "AMD64") + set(_arch "amd64") + elseif(_arch STREQUAL "aarch64" OR _arch STREQUAL "arm64") + set(_arch "arm64") + endif() + + # CMAKE_HOST_APPLE, not APPLE: this picks the tarball for the machine doing the + # download, and APPLE describes the target and is unset in script mode. + if(CMAKE_HOST_APPLE) + execute_process(COMMAND sw_vers -productVersion + OUTPUT_VARIABLE _ver OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + string(REGEX MATCH "^[0-9]+" _major "${_ver}") + if(_major STREQUAL "") + set(${OUT_VAR} "" PARENT_SCOPE) # sw_vers failed -> clean "unsupported" error + else() + # Real arch, not hardcoded arm64: an Intel Mac must get the clean + # "no prebuilt" error, not a successful arm64 download that fails at link. + set(${OUT_VAR} "macos-${_major}-${_arch}" PARENT_SCOPE) + endif() + return() + endif() + + if(NOT EXISTS "/etc/os-release") + set(${OUT_VAR} "" PARENT_SCOPE) + return() + endif() + # Read the few fields we need out of /etc/os-release. + foreach(_field ID VERSION_ID ID_LIKE UBUNTU_CODENAME) + file(STRINGS "/etc/os-release" _line REGEX "^${_field}=") + string(REGEX REPLACE "^${_field}=" "" _line "${_line}") + string(REGEX REPLACE "\"" "" _line "${_line}") + set(_osr_${_field} "${_line}") + endforeach() + + if(_osr_ID STREQUAL "ubuntu") + set(${OUT_VAR} "ubuntu-${_osr_VERSION_ID}-${_arch}" PARENT_SCOPE) + elseif(_osr_ID STREQUAL "debian") + set(${OUT_VAR} "bookworm-${_arch}" PARENT_SCOPE) + elseif(_osr_ID_LIKE MATCHES "ubuntu") + if(_osr_UBUNTU_CODENAME STREQUAL "jammy") + set(${OUT_VAR} "ubuntu-22.04-${_arch}" PARENT_SCOPE) + elseif(_osr_UBUNTU_CODENAME STREQUAL "noble") + set(${OUT_VAR} "ubuntu-24.04-${_arch}" PARENT_SCOPE) + elseif(EXISTS "/etc/upstream-release/lsb-release") + # Derivative with a codename we don't map (or none): Mint/Pop/... record + # their Ubuntu base release here. + file(STRINGS "/etc/upstream-release/lsb-release" _rel REGEX "^DISTRIB_RELEASE=") + string(REGEX REPLACE "^DISTRIB_RELEASE=" "" _rel "${_rel}") + string(REGEX REPLACE "\"" "" _rel "${_rel}") + if(_rel MATCHES "^[0-9.]+$") + set(${OUT_VAR} "ubuntu-${_rel}-${_arch}" PARENT_SCOPE) + else() + set(${OUT_VAR} "" PARENT_SCOPE) + endif() + else() + set(${OUT_VAR} "" PARENT_SCOPE) + endif() + elseif(_osr_ID_LIKE MATCHES "debian") + set(${OUT_VAR} "bookworm-${_arch}" PARENT_SCOPE) + else() + set(${OUT_VAR} "" PARENT_SCOPE) + endif() +endfunction() + +_moqx_detect_platform(_moqx_platform) + +# --- cache location (per rev + platform) ------------------------------------- +# Beside CPM_SOURCE_CACHE under the shared root rather than inside it: these +# extracted install prefixes are not CPM sources, and clearing CPM's cache must +# not take them along. Root resolution: cmake/DepsCache.cmake. +set(_cache_root "${MOQX_DEPS_CACHE}") +string(SUBSTRING "${MOXYGEN_REV}" 0 12 _rev_short) +set(_install_dir "${_cache_root}/moxygen-${_rev_short}-${_moqx_platform}") + +# Two cold-cache configures sharing this root would corrupt each other's install, +# since the reuse-check and the download/extract below write the same paths. +# GUARD PROCESS auto-releases when a FATAL_ERROR ends the process. +file(MAKE_DIRECTORY "${_cache_root}") +file(LOCK "${_cache_root}/.moxygen-fetch.lock" GUARD PROCESS TIMEOUT 600 + RESULT_VARIABLE _fetch_lock) +if(_fetch_lock AND NOT _fetch_lock STREQUAL "0") + message(FATAL_ERROR + "moxygen: could not acquire the prebuilt cache lock at ${_cache_root} (${_fetch_lock})") +endif() + +# --- reuse if already present, else resolve + download ----------------------- +# The .moqx-pin-rev marker records the rev the tree was installed for, and reuse +# needs a match, so a pin bump can never be served a stale entry. A hit needs no +# network. +set(_pin_marker "${_install_dir}/.moqx-pin-rev") +set(_cached_pin "") +if(EXISTS "${_pin_marker}") + file(READ "${_pin_marker}" _cached_pin) + string(STRIP "${_cached_pin}" _cached_pin) +endif() +if(EXISTS "${_install_dir}/lib/cmake/moxygen/moxygen-config.cmake" + AND _cached_pin STREQUAL "${MOXYGEN_REV}") + message(STATUS "moxygen: reusing cached prebuilt at ${_install_dir}") +else() + if(_moqx_platform STREQUAL "") + message(FATAL_ERROR + "moxygen: no prebuilt platform tag for this host. Build from source:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh\n" + "(force a published platform with -DMOQX_PLATFORM=; see BUILD.md).") + endif() + + # --- resolve MOXYGEN_REV -> release tag ------------------------------------ + # Honors MOXYGEN_RELEASE_TAG; the release read below verifies the commit + # against the pin either way, so a wrong tag fails before any download. + moqx_moxygen_release_tag(_rel_tag) + + # --- read the release: its commit and its asset digests, in one response --- + set(_api_hdrs + HTTPHEADER "Accept: application/vnd.github+json" + HTTPHEADER "X-GitHub-Api-Version: 2022-11-28") + # A token buys nothing but rate limit here — the repo is public. Anonymous is + # 60 requests/hour/IP, which shared CI egress IPs can exhaust on their own. + set(_api_token "$ENV{GITHUB_TOKEN}") + if(_api_token STREQUAL "") + set(_api_token "$ENV{GH_TOKEN}") + endif() + if(NOT _api_token STREQUAL "") + list(APPEND _api_hdrs HTTPHEADER "Authorization: Bearer ${_api_token}") + endif() + + # One fixed filename, not one per tag: the whole fetch runs under the cache + # lock, and a tag containing '/' must not turn into a path. + file(MAKE_DIRECTORY "${_cache_root}/downloads") + set(_rel_json_file "${_cache_root}/downloads/release.json") + file(DOWNLOAD + "https://api.github.com/repos/${MOXYGEN_REPOSITORY}/releases/tags/${_rel_tag}" + "${_rel_json_file}" + STATUS _api_status LOG _api_log TLS_VERIFY ON INACTIVITY_TIMEOUT 30 + ${_api_hdrs}) + list(GET _api_status 0 _api_rc) + if(NOT _api_rc EQUAL 0) + file(REMOVE "${_rel_json_file}") + list(GET _api_status 1 _api_msg) + # The status message only says "HTTP response code said error"; the code + # itself is in the response headers, which LOG captured. + string(REGEX MATCHALL "HTTP/[0-9.]+ [0-9]+" _api_codes "${_api_log}") + set(_http_code "") + if(_api_codes) + list(GET _api_codes -1 _http_code) + string(REGEX REPLACE "^.* " "" _http_code "${_http_code}") + endif() + if(_http_code STREQUAL "404") + message(FATAL_ERROR + "moxygen: tag '${_rel_tag}' has no GitHub release, so it publishes no " + "prebuilt. Build from source:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh") + elseif(_http_code STREQUAL "403" OR _http_code STREQUAL "429") + message(FATAL_ERROR + "moxygen: the GitHub API rate limit is exhausted for this IP (anonymous " + "reads get 60/hour). Export GITHUB_TOKEN (or GH_TOKEN) to authenticate the " + "read, wait for the limit to reset, or build from source:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh") + elseif(_http_code STREQUAL "401") + message(FATAL_ERROR + "moxygen: the GitHub API rejected the token in GITHUB_TOKEN/GH_TOKEN. The " + "release is public — unset it to read anonymously.") + else() + if(NOT _http_code STREQUAL "") + set(_api_msg "HTTP ${_http_code}") + endif() + message(FATAL_ERROR + "moxygen: could not read release '${_rel_tag}' from the GitHub API " + "(${_api_msg}).") + endif() + endif() + file(READ "${_rel_json_file}" _rel_json) + file(REMOVE "${_rel_json_file}") + + string(JSON _rel_commit ERROR_VARIABLE _json_err GET "${_rel_json}" target_commitish) + if(NOT _json_err STREQUAL "NOTFOUND") + message(FATAL_ERROR + "moxygen: unexpected release JSON for '${_rel_tag}' (${_json_err}).") + endif() + string(TOLOWER "${_rel_commit}" _rel_commit) + string(TOLOWER "${MOXYGEN_REV}" _pin_lc) + if(NOT _rel_commit STREQUAL _pin_lc) + message(FATAL_ERROR + "moxygen: release '${_rel_tag}' was built from\n" + " ${_rel_commit}\n" + "not the pinned\n" + " ${MOXYGEN_REV}\n" + "(the tag moved, or MOXYGEN_RELEASE_TAG disagrees with the pin). Fix the pin\n" + "in cmake/dependencies.cmake, or build from source " + "(scripts/configure.sh --moxygen from-source).") + endif() + + # --- pick this platform's asset, then download + verify it ----------------- + set(_want "moxygen-${_moqx_platform}.tar.gz") + set(_asset_url "") + set(_asset_digest "") + set(_published "") + string(JSON _asset_count ERROR_VARIABLE _json_err LENGTH "${_rel_json}" assets) + # Unchecked, the error text lands in _asset_count and the loop below is skipped, + # reporting a malformed response as a release that publishes nothing. + if(NOT _json_err STREQUAL "NOTFOUND") + message(FATAL_ERROR + "moxygen: unexpected release JSON for '${_rel_tag}' — no asset list (${_json_err}).") + endif() + if(_asset_count GREATER 0) + math(EXPR _last_asset "${_asset_count} - 1") + foreach(_i RANGE ${_last_asset}) + string(JSON _name GET "${_rel_json}" assets ${_i} name) + if(_name MATCHES "^moxygen-(.+)\\.tar\\.gz$") + list(APPEND _published "${CMAKE_MATCH_1}") + endif() + if(_name STREQUAL "${_want}") + string(JSON _asset_url GET "${_rel_json}" assets ${_i} browser_download_url) + # digest is null until GitHub finishes computing it, and older assets + # predate the field entirely. + string(JSON _digest_type ERROR_VARIABLE _dg_err TYPE "${_rel_json}" assets ${_i} digest) + if(_dg_err STREQUAL "NOTFOUND" AND _digest_type STREQUAL "STRING") + string(JSON _asset_digest GET "${_rel_json}" assets ${_i} digest) + endif() + endif() + endforeach() + endif() + + if(_asset_url STREQUAL "") + list(SORT _published) + list(JOIN _published ", " _published_str) + if(_published_str STREQUAL "") + set(_published_str "nothing") + endif() + message(FATAL_ERROR + "moxygen: release '${_rel_tag}' publishes no '${_want}'.\n" + "It publishes: ${_published_str}\n" + "Build from source:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh\n" + "(or force a published platform with -DMOQX_PLATFORM=; see BUILD.md).") + endif() + + if(_asset_digest STREQUAL "") + message(WARNING + "moxygen: release '${_rel_tag}' publishes no digest for '${_want}' — " + "downloading it unverified. GitHub computes digests asynchronously, so a " + "just-published asset can be missing one for a few minutes.") + elseif(NOT _asset_digest MATCHES "^sha256:") + message(WARNING + "moxygen: '${_want}' advertises an unsupported digest (${_asset_digest}) — " + "downloading it unverified.") + set(_asset_digest "") + endif() + + set(_dl "${_cache_root}/downloads/${_want}") + message(STATUS "moxygen: downloading prebuilt ${_asset_url}") + # INACTIVITY_TIMEOUT, not TIMEOUT: the tarball is hundreds of MB and a wall + # clock cap would punish slow links. A stall must still fail, because this runs + # under the cache lock and would block every other configure sharing the root. + file(DOWNLOAD "${_asset_url}" "${_dl}" STATUS _dl_status SHOW_PROGRESS TLS_VERIFY ON + INACTIVITY_TIMEOUT 60) + list(GET _dl_status 0 _dl_rc) + if(NOT _dl_rc EQUAL 0) + list(GET _dl_status 1 _dl_msg) + file(REMOVE "${_dl}") + message(FATAL_ERROR + "moxygen: could not download '${_want}' from release '${_rel_tag}' (${_dl_msg}).\n" + "Re-run the configure, or build from source:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh") + endif() + + # The digest came from the same read as the commit the pin was checked against, + # so a match proves these bytes belong to that one snapshot of the release. + if(NOT _asset_digest STREQUAL "") + string(REGEX REPLACE "^sha256:" "" _want_sha "${_asset_digest}") + string(TOLOWER "${_want_sha}" _want_sha) + file(SHA256 "${_dl}" _got_sha) + string(TOLOWER "${_got_sha}" _got_sha) + if(NOT _got_sha STREQUAL _want_sha) + file(REMOVE "${_dl}") + message(FATAL_ERROR + "moxygen: '${_want}' does not match the digest release '${_rel_tag}' advertises\n" + " got ${_got_sha}\n" + " expected ${_want_sha}\n" + "The release's assets changed while it downloaded. Re-run the configure.") + endif() + endif() + + # Extract atomically: into a temp dir, then rename into place. + set(_tmp "${_install_dir}.tmp") + file(REMOVE_RECURSE "${_tmp}") + file(MAKE_DIRECTORY "${_tmp}") + file(ARCHIVE_EXTRACT INPUT "${_dl}" DESTINATION "${_tmp}") + + # The platform mapping is name-based (every Debian maps to bookworm), so run + # one shipped binary to catch a host that cannot load it. rc 127 or a + # non-numeric rc means it never ran; any other code proves it loaded. + if(EXISTS "${_tmp}/bin/moqtest_client") + execute_process(COMMAND "${_tmp}/bin/moqtest_client" --help + RESULT_VARIABLE _probe_rc OUTPUT_QUIET ERROR_QUIET) + if(_probe_rc STREQUAL "127" OR NOT _probe_rc MATCHES "^[0-9]+$") + file(REMOVE_RECURSE "${_tmp}") + file(REMOVE "${_dl}") + message(FATAL_ERROR + "moxygen: the '${_moqx_platform}' prebuilt does not run on this host " + "(bin/moqtest_client failed to load: ${_probe_rc}) — likely a system-library " + "mismatch with the platform it was built for. Build from source instead:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh") + endif() + endif() + + file(WRITE "${_tmp}/.moqx-pin-rev" "${MOXYGEN_REV}\n") + file(REMOVE_RECURSE "${_install_dir}") + file(RENAME "${_tmp}" "${_install_dir}") + # The extracted tree is the artifact; keeping the archive beside it doubles + # what a cache of this root has to carry. + file(REMOVE "${_dl}") + message(STATUS + "moxygen: installed prebuilt '${_rel_tag}' (rev ${_rev_short}...) to ${_install_dir}") +endif() + +file(LOCK "${_cache_root}/.moxygen-fetch.lock" RELEASE) + +list(APPEND CMAKE_PREFIX_PATH "${_install_dir}") +# Persist that this build dir's moxygen is the (NDEBUG) prebuilt: the ABI-skew +# guard in CMakeLists.txt keys on it. +set(MOQX_MOXYGEN_IS_PREBUILT TRUE CACHE INTERNAL + "this build dir resolved moxygen to the release-flavored prebuilt") diff --git a/cmake/Lint.cmake b/cmake/Lint.cmake deleted file mode 100644 index dd6f744d5..000000000 --- a/cmake/Lint.cmake +++ /dev/null @@ -1,34 +0,0 @@ -# Lint/format targets -find_program(CLANG_FORMAT clang-format) -find_program(CLANG_TIDY clang-tidy) - -file(GLOB_RECURSE MOQX_LINT_SOURCES - CONFIGURE_DEPENDS - ${PROJECT_SOURCE_DIR}/src/*.h - ${PROJECT_SOURCE_DIR}/src/*.hpp - ${PROJECT_SOURCE_DIR}/src/*.cc - ${PROJECT_SOURCE_DIR}/src/*.cpp - ${PROJECT_SOURCE_DIR}/src/*.cxx - ${PROJECT_SOURCE_DIR}/test/*.cc - ${PROJECT_SOURCE_DIR}/test/*.cpp - ${PROJECT_SOURCE_DIR}/test/*.cxx - ${PROJECT_SOURCE_DIR}/tools/*.cc - ${PROJECT_SOURCE_DIR}/tools/*.cpp - ${PROJECT_SOURCE_DIR}/tools/*.cxx -) - -if(CLANG_FORMAT) - add_custom_target(format - COMMAND ${CLANG_FORMAT} -i ${MOQX_LINT_SOURCES} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - COMMENT "Running clang-format" - ) -endif() - -if(CLANG_TIDY) - add_custom_target(lint - COMMAND ${CLANG_TIDY} -p ${CMAKE_BINARY_DIR} ${MOQX_LINT_SOURCES} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - COMMENT "Running clang-tidy" - ) -endif() diff --git a/cmake/MoxygenRelease.cmake b/cmake/MoxygenRelease.cmake new file mode 100644 index 000000000..94b63f06d --- /dev/null +++ b/cmake/MoxygenRelease.cmake @@ -0,0 +1,96 @@ +# MoxygenRelease.cmake — map the pinned MOXYGEN_REV to the moxygen release that +# publishes its prebuilt tarballs. +# +# Function definitions only, no side effects, so the project build and a +# `cmake -P` script can both include it and resolve a pin identically. +# +# Requires cmake/dependencies.cmake first (MOXYGEN_REPOSITORY, MOXYGEN_REV). + +# Empty means resolve the tag from MOXYGEN_REV; release branches pin it for a +# stable source (docs/release.md). Declared here, not at the fetch, so every +# consumer of moqx_moxygen_release_tag() resolves the same tag — the release +# workflow reads it through cmake/print-release-tag.cmake. +# +# Guarded rather than plain set(CACHE): a plain one erases a normal variable of +# the same name under CMP0126 OLD, which is what a `set(MOXYGEN_RELEASE_TAG …)` +# in cmake/dependencies.cmake is. +if(NOT DEFINED MOXYGEN_RELEASE_TAG) + set(MOXYGEN_RELEASE_TAG "" + CACHE STRING "moxygen release tag to fetch the prebuilt from (empty = auto-resolve from MOXYGEN_REV)") +endif() + +# Run `git ls-remote --tags` and return the tag names pointing at MOXYGEN_REV +# (annotated tags matched via their peeled ^{} line). +function(moqx_moxygen_tags_at_pin OUT_VAR) + # Script mode has no find_package(Git) behind it, unlike the project build. + if(NOT DEFINED GIT_EXECUTABLE OR GIT_EXECUTABLE STREQUAL "") + find_package(Git QUIET) + endif() + if(NOT GIT_EXECUTABLE) + message(FATAL_ERROR + "moxygen: git is required to resolve the release for MOXYGEN_REV") + endif() + execute_process( + COMMAND "${GIT_EXECUTABLE}" ls-remote --tags "https://github.com/${MOXYGEN_REPOSITORY}" + OUTPUT_VARIABLE _ls_out RESULT_VARIABLE _ls_rc ERROR_VARIABLE _ls_err) + if(NOT _ls_rc EQUAL 0) + message(FATAL_ERROR + "moxygen: 'git ls-remote https://github.com/${MOXYGEN_REPOSITORY}' failed: ${_ls_err}") + endif() + string(REGEX REPLACE "\r?\n" ";" _ls_lines "${_ls_out}") + # Both sides lowercased: ls-remote prints lowercase, a hand-edited pin need not + # be, and a case mismatch here reports a published rev as having no release. + string(TOLOWER "${MOXYGEN_REV}" _pin_lc) + set(_matched "") + foreach(_line IN LISTS _ls_lines) + if(_line STREQUAL "") + continue() + endif() + string(REGEX MATCH "^[0-9a-fA-F]+" _sha "${_line}") + string(TOLOWER "${_sha}" _sha) + if(_sha STREQUAL "${_pin_lc}" AND _line MATCHES "refs/tags/(.+)$") + set(_t "${CMAKE_MATCH_1}") + string(REGEX REPLACE "\\^\\{\\}$" "" _t "${_t}") # peel annotated-tag suffix + list(APPEND _matched "${_t}") + endif() + endforeach() + list(REMOVE_DUPLICATES _matched) + set(${OUT_VAR} "${_matched}" PARENT_SCOPE) +endfunction() + +# Return the release tag whose assets belong to MOXYGEN_REV. GitHub keeps no +# commit->release index, so this asks the remote which tags point at the pin, and +# prefers an immutable v* release over a mutable snapshot-*. +function(moqx_moxygen_release_tag OUT_VAR) + # An explicit tag wins and needs no remote read; the fetch verifies the + # release's commit against the pin either way. + if(NOT MOXYGEN_RELEASE_TAG STREQUAL "") + set(${OUT_VAR} "${MOXYGEN_RELEASE_TAG}" PARENT_SCOPE) + return() + endif() + moqx_moxygen_tags_at_pin(_matched) + set(_tag "") + foreach(_t IN LISTS _matched) + if(NOT _t MATCHES "^snapshot") + set(_tag "${_t}") + break() + endif() + endforeach() + if(_tag STREQUAL "" AND _matched) + list(GET _matched 0 _tag) + endif() + if(_tag STREQUAL "") + message(FATAL_ERROR + "moxygen: no tag on ${MOXYGEN_REPOSITORY} points at the pinned MOXYGEN_REV\n" + " ${MOXYGEN_REV}\n" + "so no release publishes a prebuilt for it and there is nothing to fetch.\n" + "Expected when the rev's only tag was a rolling snapshot-* that has since\n" + "moved on. -DMOXYGEN_RELEASE_TAG cannot rescue it: that release's assets\n" + "moved with the tag.\n" + "\n" + "Build moxygen from source instead:\n" + " scripts/configure.sh --moxygen from-source && scripts/build.sh\n" + "or bump the pin in cmake/dependencies.cmake to a currently published rev.") + endif() + set(${OUT_VAR} "${_tag}" PARENT_SCOPE) +endfunction() diff --git a/cmake/SanitizerFlags.cmake b/cmake/SanitizerFlags.cmake new file mode 100644 index 000000000..71cc1071f --- /dev/null +++ b/cmake/SanitizerFlags.cmake @@ -0,0 +1,9 @@ +# Sanitizer flags shared by the moqx build and the superbuild's moxygen san/tsan +# profiles. The two link together, so their instrumentation has to match. +set(MOQX_ASAN_FLAGS -fsanitize=address,undefined -fno-omit-frame-pointer) +set(MOQX_TSAN_FLAGS -fsanitize=thread -fno-omit-frame-pointer) + +# The dependency stack gets ASan without UBSan: folly's static_assert on syscall +# addresses (NetOps.cpp) is not constant under -fsanitize=function, which +# `,undefined` pulls in. ASan's ABI still matches across the boundary. +set(MOQX_ASAN_DEPS_FLAGS -fsanitize=address -fno-omit-frame-pointer) diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake new file mode 100644 index 000000000..2a9e1f07b --- /dev/null +++ b/cmake/dependencies.cmake @@ -0,0 +1,25 @@ +# Single source of truth for the revisions moqx fetches via CPM. One pin per +# line — a bump is then a one-line diff. +# +# Shell consumers read pins through cmake, never by regexing this file: +# cmake -DPIN=MOXYGEN_REV -P cmake/print-pin.cmake +# +# Override for local development without editing this file: +# -DCPM_moxygen_SOURCE=/path/to/local/moxygen +# -DCPM_catapult_SOURCE=/path/to/local/catapult +# +# The Meta stack (folly/fizz/wangle/mvfst/proxygen/picoquic) is NOT pinned here — +# moxygen owns those revisions in its build/deps/github_hashes/. + +# Plain set(), not CACHE: this file must always win, so a pin bump takes effect +# on the next reconfigure of an existing build dir. A cached pin would silently +# shadow the file's value. +set(MOXYGEN_REPOSITORY "openmoq/moxygen") +set(MOXYGEN_REV "d034b571e5886642b1909d5de701df603709ebb3") + +set(CATAPULT_REPOSITORY "Quicr/catapult") +set(CATAPULT_REV "2bbf479fe2e65e425624316d335443a8c0fc0507") + +# Release tags, not shas — these projects publish stable tagged releases. +set(YAMLCPP_VERSION "0.8.0") +set(REFLECTCPP_VERSION "v0.18.0") diff --git a/cmake/fetch-moxygen-prebuilt.cmake b/cmake/fetch-moxygen-prebuilt.cmake new file mode 100644 index 000000000..efff4602d --- /dev/null +++ b/cmake/fetch-moxygen-prebuilt.cmake @@ -0,0 +1,24 @@ +# fetch-moxygen-prebuilt.cmake — put the prebuilt moxygen install for the pinned +# MOXYGEN_REV in the dependency cache, with no moqx configure around it: +# +# cmake -DOUT= [-DMOQX_PLATFORM=] -P cmake/fetch-moxygen-prebuilt.cmake +# +# OUT file to write the install prefix to. stdout carries download +# progress, so the path needs a channel of its own. +# MOQX_PLATFORM override the auto-detected platform tag +# +# Exits non-zero when the pin has no published prebuilt for this platform, so the +# caller can pair it with the superbuild the way `scripts/configure.sh --moxygen +# prebuilt-with-fallback` does. docker/Dockerfile is why this exists standalone: +# its moxygen layer must not depend on src/, or every source change would rebuild +# the whole folly stack. +# +# cmake_minimum_required matters here — a -P script without one runs under +# old-policy defaults, and the included files are dense with if(x STREQUAL "…"). +cmake_minimum_required(VERSION 3.23) +if(NOT DEFINED OUT) + message(FATAL_ERROR "usage: cmake -DOUT= -P cmake/fetch-moxygen-prebuilt.cmake") +endif() +include("${CMAKE_CURRENT_LIST_DIR}/dependencies.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/FetchMoxygenPrebuilt.cmake") +file(WRITE "${OUT}" "${_install_dir}") diff --git a/cmake/print-pin.cmake b/cmake/print-pin.cmake new file mode 100644 index 000000000..b23d1e331 --- /dev/null +++ b/cmake/print-pin.cmake @@ -0,0 +1,18 @@ +# print-pin.cmake — print one pin from dependencies.cmake on stdout, so shell +# consumers read pins through CMake instead of regexing the file: +# +# cmake -DPIN=MOXYGEN_REV -P cmake/print-pin.cmake +# +# message() writes to stderr in script mode; -E echo is the stdout channel. +# +# cmake_minimum_required matters here — a -P script without one runs under +# old-policy defaults, which change how set() and if() behave. +cmake_minimum_required(VERSION 3.23) +include("${CMAKE_CURRENT_LIST_DIR}/dependencies.cmake") +if(NOT DEFINED PIN) + message(FATAL_ERROR "usage: cmake -DPIN= -P cmake/print-pin.cmake") +endif() +if(NOT DEFINED ${PIN}) + message(FATAL_ERROR "print-pin: '${PIN}' is not set in cmake/dependencies.cmake") +endif() +execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "${${PIN}}") diff --git a/cmake/print-release-tag.cmake b/cmake/print-release-tag.cmake new file mode 100644 index 000000000..839a439ee --- /dev/null +++ b/cmake/print-release-tag.cmake @@ -0,0 +1,16 @@ +# print-release-tag.cmake — print the moxygen release tag the pinned MOXYGEN_REV +# resolves to: +# +# cmake -P cmake/print-release-tag.cmake +# +# Hits the network (git ls-remote) unless MOXYGEN_RELEASE_TAG pins the answer; +# no tag at the pin exits non-zero. +# message() writes to stderr in script mode; -E echo is the stdout channel. +# +# cmake_minimum_required matters here — a -P script without one runs under +# old-policy defaults, which change how set() and if() behave. +cmake_minimum_required(VERSION 3.23) +include("${CMAKE_CURRENT_LIST_DIR}/dependencies.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/MoxygenRelease.cmake") +moqx_moxygen_release_tag(_tag) +execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "${_tag}") diff --git a/config.example.yaml b/config.example.yaml index 897e50687..083872f26 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,5 +1,5 @@ # moqx relay configuration -# See: moqx dump-config-schema | scripts/gen-config-reference.sh +# See: moqx dump-config-schema | scripts/dev/config-schema-to-markdown.sh listeners: - name: main diff --git a/deps/catapult b/deps/catapult deleted file mode 160000 index 2bbf479fe..000000000 --- a/deps/catapult +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2bbf479fe2e65e425624316d335443a8c0fc0507 diff --git a/deps/moxygen b/deps/moxygen deleted file mode 160000 index d034b571e..000000000 --- a/deps/moxygen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d034b571e5886642b1909d5de701df603709ebb3 diff --git a/docker/Dockerfile b/docker/Dockerfile index 9acc4abb3..8a54b4c35 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,38 +1,89 @@ -# ── Build stage: compile moqx inside bookworm against moxygen tarball ─── -FROM debian:bookworm AS builder - -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential cmake ninja-build git ca-certificates \ - libssl-dev libunwind-dev libgoogle-glog-dev libgflags-dev \ - libdouble-conversion-dev libevent-dev libsodium-dev libzstd-dev \ - libboost-all-dev libfmt-dev zlib1g-dev libc-ares-dev libdwarf-dev libaio-dev \ - && rm -rf /var/lib/apt/lists/* +# Targets: `relay` (default) and `interop-client`. Both consume the one moxygen +# prefix built by the `moxygen` stage, so the two images can never disagree about +# which moxygen they carry. +# +# ── moxygen prefix ─────────────────────────────────────────────────────── +# Keyed on the pin and the build system, never on src/, so a source-only commit +# reuses this layer and only a MOXYGEN_REV bump pays for it. That is the whole +# reason moqx's sources are copied in the stage below and not this one. +FROM debian:bookworm AS moxygen WORKDIR /src -# Moxygen tarball (pre-extracted bookworm build) -COPY .docker-deps/moxygen /opt/moxygen +# System libraries through the shared installer, so a new moqx dependency needs +# no second package list here. Image-only extras after it: ca-certificates for +# the configure-time downloads, libdwarf/libaio for the prebuilt proxygen link. +COPY scripts/install-system-deps.sh scripts/ +RUN scripts/install-system-deps.sh \ + && apt-get install -y --no-install-recommends \ + ca-certificates libdwarf-dev libaio-dev \ + && rm -rf /var/lib/apt/lists/* -# MoQX source — only what cmake needs -COPY CMakeLists.txt CMakePresets.json ./ COPY cmake/ cmake/ -COPY src/ src/ -COPY deps/catapult/ deps/catapult/ -COPY deps/moxygen/build/fbcode_builder/CMake deps/moxygen/build/fbcode_builder/CMake +COPY superbuild/ superbuild/ + +# The ladder `scripts/configure.sh --moxygen prebuilt-with-fallback` runs, driven +# here by the standalone fetch: configure.sh needs src/ and this stage must not. + +# The token is a secret mount, not an ARG, which image history records. Optional — +# it only buys GitHub API rate limit (cmake/FetchMoxygenPrebuilt.cmake). +RUN --mount=type=cache,target=/root/.cache/moqx \ + --mount=type=secret,id=github_token \ + set -eu; \ + export MOQX_DEPS_CACHE=/root/.cache/moqx; \ + export GITHUB_TOKEN="$(cat /run/secrets/github_token 2>/dev/null || true)"; \ + if cmake -DOUT=/tmp/prefix -P cmake/fetch-moxygen-prebuilt.cmake; then \ + echo "==> moxygen: published prebuilt for the pin"; \ + else \ + echo "==> moxygen: no prebuilt for the pin — compiling it"; \ + cmake -S superbuild -B /tmp/sb -G Ninja; \ + CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)" cmake --build /tmp/sb -j"$(nproc)"; \ + printf %s /tmp/sb/moxygen-install > /tmp/prefix; \ + fi; \ + cp -a "$(cat /tmp/prefix)" /opt/moxygen + +# ── moqx ───────────────────────────────────────────────────────────────── +FROM moxygen AS builder # The build context excludes .git, so cmake cannot derive the version here; # CI passes it in. Empty falls back to the static project version. ARG MOQX_VERSION_STRING="" -RUN cmake -S . -B _build --preset default \ - -DCMAKE_PREFIX_PATH=/opt/moxygen \ +# PREBUILT=OFF so a prefix that failed to arrive is an error here rather than a +# silent second download that ignores the stage above. +COPY CMakeLists.txt CMakePresets.json ./ +COPY src/ src/ +RUN --mount=type=cache,target=/root/.cache/moqx \ + MOQX_DEPS_CACHE=/root/.cache/moqx \ + cmake -S . -B _build --preset default \ -DMOQX_VERSION_STRING="${MOQX_VERSION_STRING}" \ - -DBUILD_TESTING=OFF -DMOQX_BUILD_TESTS=OFF \ + -DMOQX_BUILD_TESTS=OFF \ + -DMOQX_MOXYGEN_PREBUILT=OFF \ + -DCMAKE_PREFIX_PATH=/opt/moxygen \ && cmake --build _build -j$(nproc) \ && cmake --install _build --prefix /install -# ── Runtime stage: minimal image with just the binary ──────────────────── -FROM debian:bookworm-slim +# ── interop client: moxygen's own binary, no moqx in the image ─────────── +# Env-driven (RELAY_URL, TESTCASE, TLS_DISABLE_VERIFY, VERBOSE) to match the +# moq-interop-runner convention, so it needs no entrypoint wrapper. +FROM debian:bookworm-slim AS interop-client + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libunwind8 libsodium23 libboost-context1.74.0 \ + libgoogle-glog0v6 libgflags2.2 libdouble-conversion3 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=moxygen /opt/moxygen/bin/moq_interop_client /usr/local/bin/moq_interop_client + +# Non-root, UID 1000 by interop-runner convention. +RUN useradd -u 1000 -m interop +USER interop + +ENTRYPOINT ["moq_interop_client"] + +# ── relay: the published moqx image. Last, so it is the default target. ── +FROM debian:bookworm-slim AS relay RUN apt-get update && apt-get install -y --no-install-recommends \ libunwind8 libsodium23 libboost-context1.74.0 \ diff --git a/docker/Dockerfile.interop-client b/docker/Dockerfile.interop-client deleted file mode 100644 index 9db9935c7..000000000 --- a/docker/Dockerfile.interop-client +++ /dev/null @@ -1,25 +0,0 @@ -# Interop client image: extracts the moq_interop_client binary from the -# pre-built moxygen tarball. No compilation required. -# -# The binary reads env vars natively (RELAY_URL, TESTCASE, TLS_DISABLE_VERIFY, -# VERBOSE) matching the moq-interop-runner convention, so no entrypoint -# wrapper is needed. -# -# Build context expects .docker-deps/moxygen/ to contain the extracted -# moxygen bookworm tarball (same staging step as the relay Dockerfile). - -FROM debian:bookworm-slim - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libunwind8 libsodium23 libboost-context1.74.0 \ - libgoogle-glog0v6 libgflags2.2 libdouble-conversion3 \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -COPY .docker-deps/moxygen/bin/moq_interop_client /usr/local/bin/moq_interop_client - -# Run as non-root (interop runner convention: UID 1000) -RUN useradd -u 1000 -m interop -USER interop - -ENTRYPOINT ["moq_interop_client"] diff --git a/scripts/docker-build.sh b/docker/docker-build.sh similarity index 100% rename from scripts/docker-build.sh rename to docker/docker-build.sh diff --git a/scripts/docker-shell.sh b/docker/docker-shell.sh similarity index 100% rename from scripts/docker-shell.sh rename to docker/docker-shell.sh diff --git a/docs/ci-architecture.md b/docs/ci-architecture.md index c516938b8..8d1689b73 100644 --- a/docs/ci-architecture.md +++ b/docs/ci-architecture.md @@ -1,7 +1,8 @@ # CI and Automation -This document describes the CI pipelines, upstream sync, submodule management, -artifact publishing, and relay deployment across the openmoq organization. +This document describes the CI pipelines, upstream sync, dependency-revision +management, artifact publishing, and relay deployment across the openmoq +organization. ## Cross-Repo Dependency @@ -12,7 +13,7 @@ artifact publishing, and relay deployment across the openmoq organization. │ ci main ──► snapshot-latest release (tarballs, all platforms) │ │ │ └─────────────────────────┼───────────────────────────────────┘ - │ submodule SHA pins which tarball + │ MOXYGEN_REV (cmake/dependencies.cmake) pins the rev ▼ ┌─────────────────────────────────────────────────────────────┐ │ moqx (application) │ @@ -23,8 +24,25 @@ artifact publishing, and relay deployment across the openmoq organization. └─────────────────────────────────────────────────────────────┘ ``` -moqx's `deps/moxygen` submodule pins a moxygen commit. `setup-deps-release.sh` -downloads the matching pre-built tarball from moxygen's release. +moqx pins a moxygen commit via `MOXYGEN_REV` in +[`cmake/dependencies.cmake`](/cmake/dependencies.cmake). At configure time CPM +fetches that moxygen source and [`cmake/FetchMoxygenPrebuilt.cmake`](/cmake/FetchMoxygenPrebuilt.cmake) +downloads the matching prebuilt install tarball from moxygen's release. + +Build and test lanes run `configure.sh --moxygen prebuilt-with-fallback`, so a pin +with no published tarball costs a slow from-source lane rather than a red one. The +modes themselves are in [/BUILD.md](/BUILD.md#how-dependencies-work). + +The images do the same, in [`docker/Dockerfile`](/docker/Dockerfile)'s `moxygen` +stage. That stage copies the pin and the build system but not `src/`, so its layer +key changes only on a `MOXYGEN_REV` bump, and a registry-backed buildx cache makes +a source-only push reuse it. + +Both image targets, `relay` and `interop-client`, take their moxygen from that one +stage, so they cannot disagree about which moxygen they carry. + +`version release` is the exception and stays strict: a versioned artifact must +ship a published, digest-verified dependency, not whatever a runner compiled. ## End-to-End Flow @@ -118,10 +136,13 @@ Promotes `snapshot-latest` artifacts to a versioned `vX.Y.Z` release (no rebuild | Job | Runner | Purpose | |-----|--------|---------| | check-format | ubuntu-latest (trixie) | clang-format-19 check | -| linux | ubuntu-22.04 | Build + test (from-release tarball) | -| asan debug | self-hosted (linode) | ASAN/UBSAN build + test | +| linux | ubuntu-22.04 | Build + test (prebuilt tarball, from-source fallback) | +| asan (moqx TUs, prebuilt deps) | self-hosted (linode) | ASan/UBSan on moqx TUs, build + test | -Format check must pass before build runs. +Format check must pass before build runs. The fully-instrumented from-source +san/tsan stacks build nightly in the `sanitizers` workflow +([sanitizers.yml](/.github/workflows/sanitizers.yml), also manually +dispatchable against any branch). ### 2. `ci main` — Build, Publish, Release, Deploy, Notify @@ -136,12 +157,12 @@ check-format + build ──► publish (Docker) ──► release ──► depl - Deploy automatically updates moqx-main.ci.openmoq.org with the new image - Notify sends Slack + email with per-job status -### 3. `moxygen sync` — Automated submodule update +### 3. `moxygen sync` — Automated dependency-revision update **Trigger:** `repository_dispatch` from moxygen + manual | **Time:** <1 min - Checks for blocking `sync-moxygen/*` PR (one at a time) -- Updates `deps/moxygen` submodule to dispatched SHA +- Bumps `MOXYGEN_REV` in `cmake/dependencies.cmake` to the dispatched SHA - Creates PR with dual identity (bot creates, PAT approves) - Notifies on block or failure @@ -218,3 +239,11 @@ All build jobs use ccache: - **~35-50%** hit rate after upstream sync (many changed files) - **~65-80%** on incremental builds (typical PR or small change) - Warm cache cuts moxygen build time roughly in half + +Build jobs also cache `~/.cache/moqx` (CPM source clones + the prebuilt moxygen +install, ~680 MB of downloads per configure otherwise), keyed on the dependency +pins — a pin bump refetches once, every other run configures offline. + +The from-source fallback prefix is not cached: it is multiple GB per lane against a +10 GB LRU budget, and `actions/cache` saves at job end, so the run that triggers the +fallback would miss anyway. ccache absorbs the repeat cost instead. diff --git a/docs/perf-tracking.md b/docs/perf-tracking.md index 2c97116c1..badbf525f 100644 --- a/docs/perf-tracking.md +++ b/docs/perf-tracking.md @@ -15,7 +15,7 @@ surfaced via: - **PR comments** — comparison table, optionally posted to a PR when a run sets the `pr` input (otherwise the same report lives in the step summary) -Primary workflow: [`.github/workflows/perf-test.yml`](../.github/workflows/perf-test.yml) +Primary workflow: [`.github/workflows/perf-test.yml`](/.github/workflows/perf-test.yml) ## Triggering @@ -169,13 +169,13 @@ The workflow stages a Pages artifact (`perf-out/`) and deploys it with | File | Purpose | |------|---------| -| `scripts/perf-test-ci.sh` | CI orchestration (deploy, run, collect) | -| `scripts/perf-results-to-json.sh` | Parse client output → JSON | -| `scripts/perf-compare.py` | Regression detection + markdown | -| `scripts/perf-test.sh` | Underlying test runner (unchanged) | -| `scripts/perf-metrics.sh` | Prometheus metrics poller (unchanged) | -| `.github/workflows/perf-test.yml` | Standalone perf workflow (run, compare, stage, deploy) | -| `status/index.html` | Dashboard shell (copied to Pages artifact root) | +| [`scripts/perf/perf-test-ci.sh`](/scripts/perf/perf-test-ci.sh) | CI orchestration (deploy, run, collect) | +| [`scripts/perf/perf-results-to-json.sh`](/scripts/perf/perf-results-to-json.sh) | Parse client output → JSON | +| [`scripts/perf/perf-compare.py`](/scripts/perf/perf-compare.py) | Regression detection + markdown | +| [`scripts/perf/perf-test.sh`](/scripts/perf/perf-test.sh) | Underlying test runner | +| [`scripts/perf/perf-metrics.sh`](/scripts/perf/perf-metrics.sh) | Prometheus metrics poller | +| [`.github/workflows/perf-test.yml`](/.github/workflows/perf-test.yml) | Standalone perf workflow (run, compare, stage, deploy) | +| [`status/index.html`](/status/index.html) | Dashboard shell (copied to Pages artifact root) | | `perf-out/perf/index.json` | Generated run manifest in Pages artifact | | `perf-out/perf/run-*.json` | Generated per-run result files | diff --git a/docs/release.md b/docs/release.md index f3d66f4a4..5e05e15e4 100644 --- a/docs/release.md +++ b/docs/release.md @@ -2,40 +2,49 @@ This document describes the moqx release branch model, how release artifacts are produced, and how to cut a new release branch. -For the underlying CI workflow reference, see [ci-architecture.md](ci-architecture.md). For day-to-day contributor workflow, see [../CONTRIBUTING.md](../CONTRIBUTING.md). +For the underlying CI workflow reference, see [ci-architecture.md](/docs/ci-architecture.md). For day-to-day contributor workflow, see [../CONTRIBUTING.md](/CONTRIBUTING.md). ## Branch Model | Branch | moxygen pin | Build target | Audience | |---|---|---|---| -| `main` | floats — uses moxygen `snapshot-latest` via `--use-latest` | `ghcr.io/openmoq/moqx:main-latest` (also `:latest` alias) + `snapshot-latest` GitHub release | Continuous integration; auto-deployed to `moqx-main.ci.openmoq.org` | -| `release/` | **pinned** to a specific moxygen tag via [`.moxygen-release`](#moxygen-release-pin) | `ghcr.io/openmoq/moqx:-latest` + `snapshot--latest` GitHub release | Demo / customer / event branches; manually deployed | +| `main` | floats — `MOXYGEN_REV` bumped daily by the sync bot | `ghcr.io/openmoq/moqx:main-latest` (also `:latest` alias) + `snapshot-latest` GitHub release | Continuous integration; auto-deployed to `moqx-main.ci.openmoq.org` | +| `release/` | **pinned** — `MOXYGEN_REV` frozen (bot doesn't touch release branches) | `ghcr.io/openmoq/moqx:-latest` + `snapshot--latest` GitHub release | Demo / customer / event branches; manually deployed | | `devops/*`, `feat/*`, etc. | follows the branch they were cut from | (no `ci main` — only `ci pr` runs on PRs) | Working branches | `main` floats forward; `release/*` pins for reproducibility. -## moxygen-release pin +## Pinning moxygen on a release branch -Release branches contain a top-level `.moxygen-release` file with a single line — the moxygen release tag (e.g. `v0.1.2`) the branch builds against. +The pin lives in [`cmake/dependencies.cmake`](/cmake/dependencies.cmake) — the +`MOXYGEN_REV` line. It is per-branch by construction: a `release/*` branch simply +commits the `MOXYGEN_REV` it should build against, and the sync bot (which only +targets `main`) never advances it. -**This file is load-bearing**; do not remove it from a release branch. +The prebuilt fetch resolves the pin to a release tag itself and verifies that +release's commit against it, so a frozen `MOXYGEN_REV` is all a release branch +needs — provided a tag stays retained at that rev. -The contract is enforced in [`.github/workflows/ci-main.yml`](../.github/workflows/ci-main.yml) and [`ci-pr.yml`](../.github/workflows/ci-pr.yml): +**Freeze on a `v*`-tagged moxygen rev.** Between moxygen releases `main` carries +revs whose only tag is the rolling `snapshot-latest`. That tag moves on the next +publish and takes the release's assets with it, and every `--moxygen prebuilt` build +of the frozen branch then fails at configure. Check a candidate with +`git ls-remote --tags https://github.com/openmoq/moxygen | grep `, or accept +that the branch is `--moxygen from-source` only. -```bash -# Release branches pin a specific moxygen release tag via -# .moxygen-release. Main uses snapshot-latest via --use-latest. -if [ -f .moxygen-release ]; then - export MOQX_MOXYGEN_RELEASE_TAG=$(cat .moxygen-release | tr -d '[:space:]') - bash scripts/build.sh setup --no-fallback -else - bash scripts/build.sh setup --no-fallback --use-latest -fi +Optionally pin the exact tag to fetch from by setting `MOXYGEN_RELEASE_TAG` in the +same file — useful when a commit carries several tags and you want a specific one: + +```cmake +set(MOXYGEN_RELEASE_TAG "") # a tag on openmoq/moxygen *releases* ``` -The `deps/moxygen` submodule must point at the same commit the tag resolves to. CI does not cross-check, but a divergence will cause the build to download the wrong tarball. +The tag must be one of [openmoq/moxygen's releases](https://github.com/openmoq/moxygen/releases) +— moqx's own `snapshot--latest` releases are a different repo. Its commit is +verified against `MOXYGEN_REV`, so an inconsistent pin fails the configure. -When merging `main` into a `release/*` branch, the `.moxygen-release` modify-vs-delete conflict resolves **in favor of keeping the file**, then bump it to match the merged submodule pin. +When merging `main` into a `release/*` branch, resolve the `cmake/dependencies.cmake` +conflict in favor of the release branch's pinned `MOXYGEN_REV`. ## Snapshot Releases @@ -67,11 +76,13 @@ To open a new demo / customer release branch: ```bash git checkout -b release/ main ``` -2. **Add the moxygen pin.** Pick the moxygen release tag your demo will run against (typically the latest `vX.Y.Z` whose hash is already on `main`): +2. **Freeze the moxygen pin.** Set `MOXYGEN_REV` (and optionally `MOXYGEN_RELEASE_TAG`) + in [`cmake/dependencies.cmake`](/cmake/dependencies.cmake) to the commit your + demo runs against — a `v*`-tagged moxygen rev, per the note above, not + necessarily the current `main` value: ```bash - echo "v0.1.2" > .moxygen-release - git add .moxygen-release - git commit -m "release/: pin moxygen v0.1.2" + # edit cmake/dependencies.cmake: MOXYGEN_REV "" + git commit -am "release/: pin moxygen " git push origin release/ ``` 3. **Verify CI.** Push triggers `ci main`. Confirm `snapshot--latest` and `ghcr.io/openmoq/moqx:-latest` were produced successfully. @@ -81,15 +92,13 @@ To open a new demo / customer release branch: When a release branch needs to absorb fixes from `main`: -1. Verify the moxygen tag you want to land on is already tagged (`vX.Y.Z`) and `main` builds against it cleanly. Pinning at a tag main has continuously CI-tested derisks the merge. -2. Open a PR from a `devops/-vX.Y.Z` working branch into the release branch: +1. Verify the moxygen commit you want to land on is one `main` has continuously CI-tested; pinning there derisks the merge. +2. Open a PR from a `devops/-` working branch into the release branch: ```bash - git checkout -b devops/-vX.Y.Z origin/release/ - git merge origin/main # resolve .moxygen-release in favor of keeping - echo "vX.Y.Z" > .moxygen-release - git add .moxygen-release && git commit --amend --no-edit - git push origin devops/-vX.Y.Z - gh pr create --base release/ --head devops/-vX.Y.Z ... + git checkout -b devops/- origin/release/ + git merge origin/main # resolve cmake/dependencies.cmake to the intended MOXYGEN_REV + git push origin devops/- + gh pr create --base release/ --head devops/- ... ``` 3. **Use a merge commit (not squash)** when merging into a release branch — preserves the upstream commits' attribution and history on the release branch. 4. (Recommended for first-time release branches) **dry-run** by pushing the merged commit as `release/-test` first, watch a full `ci main` cycle end-to-end, then delete the throwaway branch + its `snapshot--test-latest` release + Docker tags before merging the real PR. @@ -115,4 +124,4 @@ Inputs to `deploy relay` (all optional, branch-derived defaults): - `restart_only` — restart the existing image without redeploying - `verbose` — GLOG verbosity level -See [ci-architecture.md](ci-architecture.md) for the underlying workflow details. +See [ci-architecture.md](/docs/ci-architecture.md) for the underlying workflow details. diff --git a/scripts/README.md b/scripts/README.md index 363c1b48e..d41f0a03e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,12 +1,21 @@ # moqx scripts -Helper scripts for running and benchmarking a moqx relay. Run them from the -repository root. +Helper scripts for building, running and benchmarking a moqx relay. Run them +from the repository root. + +- Top level — the build lifecycle: [`configure.sh`](/scripts/configure.sh), + [`build.sh`](/scripts/build.sh), [`test.sh`](/scripts/test.sh) (see [BUILD.md](/BUILD.md)), + plus [`install-system-deps.sh`](/scripts/install-system-deps.sh) and + [`moqx-run.sh`](/scripts/moqx-run.sh) (below). +- [`perf/`](/scripts/perf) — perf testing: local + CI harnesses, metrics, comparison. +- [`dev/`](/scripts/dev) — maintenance: format/lint, upstream sync, diagnostics. +- [`lib/`](/scripts/lib) — sourced by the above, not run directly. +- Docker helpers live in [`/docker`](/docker) next to the Dockerfiles. ## Relay quickstart -Run a relay with [`moqx-run.sh`](moqx-run.sh). It fills -[`config.bench.yaml`](config.bench.yaml) (a template with sensible defaults) +Run a relay with [`moqx-run.sh`](/scripts/moqx-run.sh). It fills +[`perf/config.bench.yaml`](/scripts/perf/config.bench.yaml) (a template with sensible defaults) and serves it. Override anything with a flag or env var — you rarely need to. ### Simplest possible @@ -74,14 +83,14 @@ Full option list: `./scripts/moqx-run.sh --help`. ## Other scripts -- [`perf-test.sh`](perf-test.sh) — relay throughput / subscriber-ramp perf test - (drives the relay via `moqx-run.sh`). See `./scripts/perf-test.sh` header for +- [`perf/perf-test.sh`](/scripts/perf/perf-test.sh) — relay throughput / subscriber-ramp perf test + (drives the relay via `moqx-run.sh`). See `./scripts/perf/perf-test.sh` header for options; short flags `-s`/`-d`/`-t`/`-l`/`-j` mirror the common ones. -- [`perf-metrics.sh`](perf-metrics.sh) — generic `/metrics` poller; logs the relay's +- [`perf/perf-metrics.sh`](/scripts/perf/perf-metrics.sh) — generic `/metrics` poller; logs the relay's Prometheus metrics to a file (standalone, or via `perf-test.sh --metrics`). For a - live graphical view, open [`../tools/metrics-dashboard.html`](../tools/metrics-dashboard.html) + live graphical view, open [`../tools/metrics-dashboard.html`](/tools/metrics-dashboard.html) in a browser — a self-contained dashboard that scrapes the relay `/metrics` (plus node_exporter host metrics and moqperf client latency) once per second. See that file's header for endpoint/CORS setup. -- [`config.bench.yaml`](config.bench.yaml) — the relay config template +- [`perf/config.bench.yaml`](/scripts/perf/config.bench.yaml) — the relay config template `moqx-run.sh` renders. diff --git a/scripts/build.sh b/scripts/build.sh index ecbb87cf6..3dbe0089c 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,521 +1,60 @@ #!/usr/bin/env bash -# build.sh — Developer build script for moqx. +# build.sh — compile moqx # -# Mirrors the same build flows used by CI. Supports two dependency modes: -# --from-release — download released moxygen artifacts (~1 min) -# --from-source — build moxygen + all Meta deps from source (~15-30 min) +# Usage: build.sh [PROFILE] [CMAKE_BUILD_ARGS...] +# PROFILE: default | san | tsan, or any preset from +# CMakeUserPresets.json; the literals `setup`/`test` +# are reserved. Default: default. +# -j N | --jobs N: compile parallelism, claimed ahead of the passthrough +# CMAKE_BUILD_ARGS: everything after PROFILE goes to `cmake --build`, +# e.g. build.sh default --target moqx-issuer # -# Usage: -# ./scripts/build.sh setup [--from-release [SHA]|--from-source [SHA]] [--profile NAME] [--no-fallback] [--clean] [-j N] -# ./scripts/build.sh [--profile NAME] [--build-dir DIR] [-j N] -# ./scripts/build.sh test [--build-dir DIR] [-- CTEST_ARGS...] +# Run scripts/configure.sh [PROFILE] once per profile first. # -# First-time setup: see README "Quick Start" and BUILD.md "Prerequisites" -# (CMake 3.22+, system libs). -# -# Incremental (after source changes): -# ./scripts/build.sh # rebuilds only what changed -# -# After submodule update: -# ./scripts/build.sh setup # re-downloads or rebuilds deps -# ./scripts/build.sh - +# Env: MOQX_BUILD_JOBS is the job count when -j is absent. Both override the +# default (cores, derated by free RAM for sanitizer profiles) — set either well +# above the core count for distcc. See scripts/lib/jobs.sh. set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +die() { echo "build.sh: $*" >&2; exit 1; } +. "$ROOT/scripts/lib/jobs.sh" + +# `setup` and `test` name the sibling scripts, so they are rejected rather than +# taken for preset names. +case "${1:-}" in + setup|configure) die "configuring is done by scripts/configure.sh [PROFILE] --moxygen … (see its --help)" ;; + test) die "tests are run by scripts/test.sh [PROFILE] [CTEST_ARGS...]" ;; + -h|--help) awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "${BASH_SOURCE[0]}"; exit 0 ;; +esac -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SCRATCH="${MOQX_SCRATCH_PATH:-${PROJECT_ROOT}/.scratch}" -MOXYGEN_DIR="${PROJECT_ROOT}/deps/moxygen" - -PREFIX_PATH_FILE="${SCRATCH}/cmake_prefix_path.txt" -DEPS_MODE_FILE="${SCRATCH}/deps-mode" - -# ── Helpers ────────────────────────────────────────────────────────────────── - -die() { echo "Error: $*" >&2; exit 1; } - -# Precedence: --jobs flag, then MOQX_BUILD_JOBS, then core count. The override -# exists for distcc, which wants far more jobs than the local core count. -resolve_jobs() { - local jobs="${1:-${MOQX_BUILD_JOBS:-}}" - [[ -n "$jobs" ]] || jobs=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - [[ "$jobs" =~ ^[1-9][0-9]*$ ]] || die "invalid job count '$jobs' (expected a positive integer)" - echo "$jobs" -} - -# ── CMake version precheck ─────────────────────────────────────────────────── -# moqx top-level CMakeLists.txt requires cmake_minimum_required(VERSION 3.22). -# All current targets (Ubuntu 22.04, 24.04+, Debian 12+, recent Homebrew -# macOS) ship a new-enough cmake out of the box. Override with -# MOQX_SKIP_CMAKE_CHECK=1 if your environment uses a non-default path. -require_cmake_version() { - [[ "${MOQX_SKIP_CMAKE_CHECK:-}" == "1" ]] && return 0 - - if ! command -v cmake >/dev/null 2>&1; then - cat >&2 <<'EOF' -Error: cmake not found in PATH. -moqx requires CMake 3.22+. Install: - Ubuntu 22.04+ / Debian 12+: sudo apt-get install cmake - macOS: brew install cmake - -To bypass this check (advanced): export MOQX_SKIP_CMAKE_CHECK=1 -EOF - exit 1 - fi - - local ver major minor - ver=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\.[0-9]*\).*/\1/') - major=$(echo "$ver" | cut -d. -f1) - minor=$(echo "$ver" | cut -d. -f2) - if (( major < 3 || (major == 3 && minor < 22) )); then - cat >&2 </dev/null 2>&1 || missing+=("cmake") - command -v ninja >/dev/null 2>&1 || missing+=("ninja") - command -v git >/dev/null 2>&1 || missing+=("git") - - # Libraries — check via pkg-config where available, fall back to header probes - if command -v pkg-config >/dev/null 2>&1; then - pkg-config --exists openssl 2>/dev/null || missing+=("libssl-dev") - pkg-config --exists libglog 2>/dev/null || missing+=("libgoogle-glog-dev") - pkg-config --exists gflags 2>/dev/null || missing+=("libgflags-dev") - pkg-config --exists zlib 2>/dev/null || missing+=("zlib1g-dev") - pkg-config --exists fmt 2>/dev/null || missing+=("libfmt-dev") - pkg-config --exists libevent 2>/dev/null || missing+=("libevent-dev") - pkg-config --exists libsodium 2>/dev/null || missing+=("libsodium-dev") - pkg-config --exists libzstd 2>/dev/null || missing+=("libzstd-dev") - pkg-config --exists libcares 2>/dev/null || missing+=("libc-ares-dev") - else - warnings+=("pkg-config not found — cannot verify library dependencies") - # Fall back to header checks for the most critical ones - for hdr in openssl/ssl.h glog/logging.h gflags/gflags.h boost/version.hpp; do - if ! find /usr/include /usr/local/include -name "$(basename "$hdr")" -path "*$hdr" 2>/dev/null | grep -q .; then - missing+=("$hdr (header not found)") - fi - done - fi - - # Boost — special: pkg-config not always available, check header - if ! find /usr/include /usr/local/include -name "version.hpp" -path "*/boost/*" 2>/dev/null | grep -q .; then - if ! command -v brew >/dev/null 2>&1 || ! brew --prefix boost >/dev/null 2>&1; then - missing+=("libboost-all-dev") - fi - fi - - # Boost component libs — packaged separately on Debian/Ubuntu (headers via - # libboost-dev do NOT include them). folly's cmake config find_package()s - # each component, so a missing one fails configure with an opaque - # "boost_context-config.cmake not found" error. Probe for the dev artifacts - # (unversioned .a/.so — runtime-only packages ship just .so.N.NN.N). - # Homebrew's boost is monolithic, so this is Linux-only. - if [[ "$(uname)" == "Linux" ]]; then - local comp - for comp in context filesystem program_options regex thread; do - if ! find /usr/lib /usr/lib64 /usr/local/lib \ - \( -name "libboost_${comp}.a" -o -name "libboost_${comp}.so" \) \ - 2>/dev/null | grep -q .; then - missing+=("libboost-${comp//_/-}-dev") - fi - done - - # double-conversion — no pkg-config file on Debian/Ubuntu; probe header - if ! find /usr/include /usr/local/include -path "*double-conversion/double-conversion.h" 2>/dev/null | grep -q .; then - missing+=("libdouble-conversion-dev") - fi - fi - - # NOTE: CMake version is enforced by require_cmake_version() — kept - # separate so we can give a clean, focused error if cmake is missing or - # too old before any other dep checks run. - - for w in "${warnings[@]+"${warnings[@]}"}"; do - echo " Warning: $w" - done - - if (( ${#missing[@]} > 0 )); then - echo "" - echo "Missing system dependencies:" - for dep in "${missing[@]}"; do - echo " - $dep" - done - echo "" - - # Detect OS and suggest install command - if [[ "$(uname)" == "Darwin" ]]; then - echo "Install with:" - echo " brew install cmake ninja openssl@3 glog gflags double-conversion \\" - echo " libevent libsodium zstd boost fmt c-ares gperf" - elif [[ -f /etc/os-release ]]; then - . /etc/os-release - case "${ID:-}" in - ubuntu|debian) - echo "Install with:" - echo " sudo apt-get install -y build-essential cmake ninja-build \\" - echo " libssl-dev libunwind-dev libgoogle-glog-dev libgflags-dev \\" - echo " libdouble-conversion-dev libevent-dev libsodium-dev libzstd-dev \\" - echo " libboost-all-dev libfmt-dev zlib1g-dev libc-ares-dev gperf" - ;; - fedora|centos|rhel) - echo "Install with:" - echo " sudo dnf install -y cmake ninja-build openssl-devel glog-devel \\" - echo " gflags-devel double-conversion-devel libevent-devel libsodium-devel \\" - echo " libzstd-devel boost-devel fmt-devel zlib-devel c-ares-devel gperf" - ;; - *) - echo "See deps/moxygen/standalone/install-system-deps.sh for package list." - ;; - esac - fi - echo "" - echo "Or run: sudo deps/moxygen/standalone/install-system-deps.sh" - return 1 - fi - return 0 -} - -# ── System-dep precheck wrapper ────────────────────────────────────────────── -# System libraries are needed when actually compiling — that's the from-source -# setup path (which builds moxygen on the host) and every cmd_build invocation -# (moxygen's CMake config does find_dependency(fmt, Glog, ...) and folly -# transitively wants OpenSSL/Boost). The from-release setup path only fetches -# a tarball and does NOT need them, so callers must gate this themselves and -# not invoke it unconditionally during setup. Override with -# MOQX_SKIP_DEPS_CHECK=1 if you have these on a non-default path. -require_system_deps() { - [[ "${MOQX_SKIP_DEPS_CHECK:-}" == "1" ]] && return 0 - if ! check_system_deps; then - die "Install missing dependencies and re-run. - Quick fix: sudo deps/moxygen/standalone/install-system-deps.sh" - fi -} - -# ── Submodule check ────────────────────────────────────────────────────────── - -check_submodule() { - if [[ ! -e "$MOXYGEN_DIR/.git" ]]; then - die "deps/moxygen submodule not initialized. - Run: git submodule update --init --recursive" - fi - # catapult and its own nested submodules (libcbor, nlohmann_json, spdlog, - # doctest) are required by the top-level build. A plain 'submodule update - # --init' without --recursive (or one scoped to deps/moxygen) leaves them - # empty, which fails configure with a missing-CMakeLists error. Auto-init - # recursively so a fresh or partial clone just works. - if [[ ! -f "$PROJECT_ROOT/deps/catapult/CMakeLists.txt" ]]; then - echo "==> Initializing catapult submodule (recursive)..." - git -C "$PROJECT_ROOT" submodule update --init --recursive deps/catapult - fi -} - -# ── Checkout submodule to specific SHA ─────────────────────────────────────── - -checkout_submodule() { - local sha="$1" - echo "==> Checking out moxygen submodule at $sha..." - git -C "$MOXYGEN_DIR" fetch origin --quiet - git -C "$MOXYGEN_DIR" checkout "$sha" --quiet -} - -# ── Setup command ──────────────────────────────────────────────────────────── - -cmd_setup() { - require_cmake_version - local mode="from-release" - local profile="default" - local no_fallback=false - local use_latest=false - local clean=false - local target_sha="" - local moxygen_dir="" - local jobs="" - local tarball_ok - - while (( $# > 0 )); do - case "$1" in - --profile) profile="$2"; shift 2 ;; - --from-release) - mode="from-release"; shift - # Optional SHA argument (next arg that isn't a flag) - if (( $# > 0 )) && [[ "$1" != -* ]]; then - target_sha="$1"; shift - fi - ;; - --from-source) - mode="from-source"; shift - if (( $# > 0 )) && [[ "$1" != -* ]]; then - target_sha="$1"; shift - fi - ;; - --moxygen-dir) - [[ $# -gt 1 ]] || die "--moxygen-dir requires a path argument" - moxygen_dir="$(cd "$2" && pwd)" || die "--moxygen-dir: '$2' not found" - shift 2 - ;; - --no-fallback) no_fallback=true; shift ;; - --use-latest) use_latest=true; shift ;; - --clean) clean=true; shift ;; - -j|--jobs) [[ $# -gt 1 ]] || die "$1 requires a job count"; jobs="$2"; shift 2 ;; - -j*) jobs="${1#-j}"; shift ;; - -h|--help) usage ;; - *) die "Unknown setup option: $1" ;; - esac - done - - if [[ -n "$moxygen_dir" && -n "$target_sha" ]]; then - die "--moxygen-dir and a SHA override are mutually exclusive" - fi - - local job_count - job_count=$(resolve_jobs "$jobs") - - if [[ -n "$moxygen_dir" ]]; then - export MOQX_MOXYGEN_DIR="$moxygen_dir" - echo "Using moxygen from: $moxygen_dir" - else - check_submodule - if [[ -n "$target_sha" ]]; then - checkout_submodule "$target_sha" - fi - fi - - # System dep check is deferred until we know a source build is actually - # needed (see the from-source branch below). from-release mode downloads a - # prebuilt tarball and doesn't need host system libs at setup time. - - if $clean; then - echo "Cleaning .scratch..." - rm -rf "$SCRATCH" - fi - - mkdir -p "$SCRATCH" - - if [[ "$mode" == "from-release" ]]; then - echo "" - echo "==> Setting up dependencies (from release)..." - if $use_latest; then - tarball_ok=true - bash "$SCRIPT_DIR/setup-deps-tarball.sh" --use-latest || tarball_ok=false - else - tarball_ok=true - bash "$SCRIPT_DIR/setup-deps-tarball.sh" || tarball_ok=false - fi - if $tarball_ok; then - echo "from-release" > "$DEPS_MODE_FILE" - elif ! $use_latest && bash "$SCRIPT_DIR/setup-deps-dev-artifact.sh"; then - # Snapshot SHA didn't match the submodule pin (typical when the - # submodule points at a moxygen PR/feature branch). Try a dev-build - # actions artifact for that exact SHA before falling to a slow source - # build. Skipped when --use-latest is set since the user already - # opted out of pin matching. - echo "from-dev-artifact" > "$DEPS_MODE_FILE" - else - if $no_fallback; then - die "Release artifacts not available and --no-fallback specified." - fi - echo "" - echo "Release artifacts not available — falling back to source build..." - mode="from-source" - fi - fi - - if [[ "$mode" == "from-source" ]]; then - require_system_deps - echo "" - echo "==> Setting up dependencies (from source)..." - bash "$SCRIPT_DIR/setup-deps-standalone.sh" --profile "$profile" --jobs "$job_count" - echo "from-source" > "$DEPS_MODE_FILE" - fi - - echo "" - echo "Setup complete (mode: $(cat "$DEPS_MODE_FILE"))." - echo "Run: ./scripts/build.sh" -} - -# ── Build command (default) ────────────────────────────────────────────────── - -cmd_build() { - require_cmake_version - require_system_deps - local profile="default" - local build_dir="" - local jobs="" - - local benchmark=OFF - - while (( $# > 0 )); do - case "$1" in - --profile) profile="$2"; shift 2 ;; - --build-dir) build_dir="$2"; shift 2 ;; - --benchmark) benchmark=ON; shift ;; - -j|--jobs) [[ $# -gt 1 ]] || die "$1 requires a job count"; jobs="$2"; shift 2 ;; - -j*) jobs="${1#-j}"; shift ;; - -h|--help) usage ;; - *) die "Unknown build option: $1" ;; - esac - done - - # Default build dir from profile - if [[ -z "$build_dir" ]]; then - case "$profile" in - default) build_dir="build" ;; - san) build_dir="build-san" ;; - *) build_dir="build-${profile}" ;; - esac - fi - - # Use profile-specific prefix path if available, fall back to default - local prefix_path_file="$PREFIX_PATH_FILE" - if [[ "$profile" != "default" && -f "${SCRATCH}/cmake_prefix_path-${profile}.txt" ]]; then - prefix_path_file="${SCRATCH}/cmake_prefix_path-${profile}.txt" - fi - - if [[ ! -f "$prefix_path_file" ]]; then - die "Dependencies not set up. Run: ./scripts/build.sh setup" - fi - - check_submodule - - local prefix_path - prefix_path=$(cat "$prefix_path_file") - - local job_count - job_count=$(resolve_jobs "$jobs") - - # Map profile to cmake preset - local preset="$profile" - - # from-source builds use system libs (e.g. gflags shared-only on CentOS); - # override the preset's .a-only suffix to allow shared system libraries. - local extra_cmake_args=() - local deps_mode="" - [[ -f "$DEPS_MODE_FILE" ]] && deps_mode=$(cat "$DEPS_MODE_FILE") - if [[ "$deps_mode" == "from-source" ]]; then - extra_cmake_args+=("-DCMAKE_FIND_LIBRARY_SUFFIXES=.so;.a") - extra_cmake_args+=("-DGFLAGS_SHARED=ON") - fi - - # macOS: prefer shared gflags from brew to avoid conflict with static - # gflags symbols bundled in the moxygen tarball (see openmoq/moxygen#114). - if [[ "$(uname)" == "Darwin" ]]; then - extra_cmake_args+=("-DGFLAGS_SHARED=ON") - fi - - if [[ "$benchmark" == "ON" ]]; then - extra_cmake_args+=("-DMOQX_BUILD_BENCHMARKS=ON") - extra_cmake_args+=("-DMOQX_BUILD_TESTS=OFF") - fi - - - echo "==> Configuring (profile: $profile, build: $build_dir)..." - cmake -S "$PROJECT_ROOT" -B "$build_dir" \ - --preset "$preset" \ - "${extra_cmake_args[@]+"${extra_cmake_args[@]}"}" \ - -DCMAKE_PREFIX_PATH="$prefix_path" - - echo "==> Building ($job_count jobs)..." - cmake --build "$build_dir" -j"$job_count" - - echo "==> Build complete." -} - -# ── Test command ───────────────────────────────────────────────────────────── - -cmd_test() { - local build_dir="build" - local ctest_args=() - - while (( $# > 0 )); do - case "$1" in - --build-dir) build_dir="$2"; shift 2 ;; - --) shift; ctest_args=("$@"); break ;; - -h|--help) usage ;; - *) die "Unknown test option: $1" ;; - esac - done - - if [[ ! -d "$build_dir" ]]; then - die "Build directory '$build_dir' not found. Run: ./scripts/build.sh" - fi - - echo "==> Running tests (build: $build_dir)..." - ctest --test-dir "$build_dir" --output-on-failure "${ctest_args[@]+"${ctest_args[@]}"}" -} - -# ── Main ───────────────────────────────────────────────────────────────────── - -if (( $# == 0 )); then - cmd_build - exit 0 +profile="default" +if (($#)) && [[ "$1" != -* ]]; then profile="$1"; shift; fi + +# -j is claimed here rather than left to the passthrough, so the resolved count +# is the only one on the cmake line. +jobs="" passthrough=() +while (($#)); do + case "$1" in + -j|--jobs) (($# >= 2)) || die "$1 needs a job count"; jobs="$2"; shift 2 ;; + -j*) jobs="${1#-j}"; shift ;; + --jobs=*) jobs="${1#--jobs=}"; shift ;; + *) passthrough+=("$1"); shift ;; + esac +done + +build_dir="build/$profile" +[[ -f "$build_dir/CMakeCache.txt" ]] \ + || die "no configured build dir '$build_dir' — run first: scripts/configure.sh $profile --moxygen … (see its --help)" + +# The configured cache is the only thing that knows whether this build is +# instrumented; the profile name alone does not (custom presets, -D overrides). +sanitized="" +if grep -qE '^MOQX_ENABLE_(SANITIZERS|TSAN):BOOL=(ON|TRUE|1)$' "$build_dir/CMakeCache.txt"; then + sanitized=1 fi +jobs="$(resolve_jobs "$jobs" "$sanitized")" -case "$1" in - setup) shift; cmd_setup "$@" ;; - test) shift; cmd_test "$@" ;; - -h|--help) usage ;; - -*) cmd_build "$@" ;; - *) die "Unknown command: $1. Use: setup, test, or build options (--profile, --build-dir)" ;; -esac +set -x +cmake --build "$build_dir" -j"$jobs" ${passthrough[@]+"${passthrough[@]}"} diff --git a/scripts/configure.sh b/scripts/configure.sh index 85e2e4f99..b2d798cb8 100755 --- a/scripts/configure.sh +++ b/scripts/configure.sh @@ -1,18 +1,274 @@ #!/usr/bin/env bash +# configure.sh — bind a build profile to a moxygen and configure it. +# +# Usage: +# configure.sh [PROFILE] --moxygen (prebuilt-with-fallback|prebuilt|from-source) +# [--moxygen-dir DIR] [--clean] [-j N] [-DVAR=VALUE]... +# PROFILE: default | san | tsan, or any preset from +# CMakeUserPresets.json; inherit `default` so +# binaryDir stays build/. Default: default. +# --moxygen prebuilt-with-fallback: +# the prebuilt when one is published, else the +# source build +# --moxygen prebuilt: download the prebuilt install for the pinned +# rev; no published tarball is an error +# --moxygen from-source: build moxygen (+ the folly/… stack) from source +# --moxygen-dir DIR: with from-source, build the local checkout DIR +# (the cross-repo moxygen+moqx dev loop) +# Which to pick: BUILD.md#how-dependencies-work +# --clean: also discard this profile's from-source moxygen +# build (build/ is rebuilt from scratch +# either way; no effect unless the source build runs) +# -DVAR=VALUE: forwarded to the moqx configure. The variables +# this script owns are refused — MOQX_MOXYGEN_PREBUILT, +# CMAKE_PREFIX_PATH, CPM_moxygen_SOURCE, +# MOQX_MOXYGEN_PROFILE and MOQX_ALLOW_ABI_SKEW are +# what --moxygen, --moxygen-dir and the env vars +# below decide. For anything else, drive cmake +# directly (see BUILD.md). +# +# A build that enables MOQX_ENABLE_SANITIZERS/MOQX_ENABLE_TSAN gets a matching +# instrumented moxygen from --moxygen from-source (derived from the preset's cache +# variables and any -D for those two; override with the MOQX_MOXYGEN_PROFILE env +# var). Neither prebuilt nor prebuilt-with-fallback can deliver one, so both refuse +# a sanitizer build unless MOQX_ALLOW_UNINSTRUMENTED_DEPS=1 — and the fallback then +# builds the uninstrumented stack too, so losing the prebuilt cannot silently turn a +# short lane into a long instrumented one. +# +# Env: MOQX_BUILD_JOBS is the job count for the moxygen source build when -j is +# absent. Both override the default (cores, derated by free RAM for sanitizer +# profiles, whose coroutine-heavy TUs each peak >2 GB). See scripts/lib/jobs.sh. +# MOQX_MOXYGEN_FALLBACK=off reduces prebuilt-with-fallback to plain prebuilt — the +# lever to pull when a bad pin is making every CI lane compile folly. set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SCRATCH="${MOQX_SCRATCH_PATH:-$ROOT/.scratch}" +# The one root under which CPM's source clones and the extracted prebuilt +# installs both live; cmake/DepsCache.cmake splits it. Exported so the superbuild +# and the moqx build land on the same clones. Only the no-HOME default differs +# from CMake's own: a scripted build has $ROOT/.scratch to fall back on. +export MOQX_DEPS_CACHE="${MOQX_DEPS_CACHE:-${HOME:-$SCRATCH}/.cache/moqx}" -SCRATCH_PATH="${MOQX_SCRATCH_PATH:-${PROJECT_ROOT}/.scratch}" -PREFIX_PATH_FILE="${SCRATCH_PATH}/cmake_prefix_path.txt" +die() { echo "configure.sh: $*" >&2; exit 1; } +usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "${BASH_SOURCE[0]}"; exit "${1:-0}"; } +# CMake's truthy spelling, matched case-insensitively without ${x^^} — this +# script also runs under macOS's bash 3.2. +truthy() { case "$1" in [Oo][Nn]|[Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss]) return 0 ;; *) return 1 ;; esac; } +. "$ROOT/scripts/lib/jobs.sh" -BUILD_DIR=${1:-build} -PRESET=${2:-default} +profile="default" moxygen="" moxygen_dir="" clean=0 jobs="" passthru=() +if (($#)) && [[ "$1" != -* ]]; then profile="$1"; shift; fi +while (($#)); do + case "$1" in + --moxygen) (($# >= 2)) || die "--moxygen needs a value (prebuilt|prebuilt-with-fallback|from-source)"; moxygen="$2"; shift 2 ;; + --moxygen-dir) (($# >= 2)) || die "--moxygen-dir needs a directory"; moxygen_dir="$(cd "$2" 2>/dev/null && pwd)" || die "--moxygen-dir: '$2' not found"; shift 2 ;; + --clean) clean=1; shift ;; + -j|--jobs) (($# >= 2)) || die "$1 needs a job count"; jobs="$2"; shift 2 ;; + -j*) jobs="${1#-j}"; shift ;; + --jobs=*) jobs="${1#--jobs=}"; shift ;; + -D) (($# >= 2)) || die "-D needs VAR=VALUE"; passthru+=("-D$2"); shift 2 ;; + -D*) passthru+=("$1"); shift ;; + -h|--help) usage ;; + *) die "unknown option '$1' (see --help)" ;; + esac +done +case "$moxygen" in + prebuilt|prebuilt-with-fallback|from-source) ;; + "") die "choose where moxygen comes from: + configure.sh [PROFILE] --moxygen prebuilt-with-fallback download it, or build it when none is published — start here + configure.sh [PROFILE] --moxygen prebuilt download only; error when none is published + configure.sh [PROFILE] --moxygen from-source build it from source (any rev/platform; moxygen dev)" ;; + *) die "unknown --moxygen '$moxygen' (prebuilt-with-fallback|prebuilt|from-source)" ;; +esac +[[ -n "$moxygen_dir" && "$moxygen" != from-source ]] && die "--moxygen-dir requires --moxygen from-source" -PREFIX_ARG=() -if [[ -f "$PREFIX_PATH_FILE" ]]; then - PREFIX_ARG=("-DCMAKE_PREFIX_PATH=$(cat "$PREFIX_PATH_FILE")") +# Refusing beats last-wins: a stray -DMOQX_MOXYGEN_PREBUILT=ON would defeat the +# from-source prefix and pull an uninstrumented moxygen under a sanitizer build, +# which is the case the interlock below exists to prevent. +for arg in ${passthru[@]+"${passthru[@]}"}; do + case "$arg" in + -DMOQX_MOXYGEN_PREBUILT[:=]*) die "-DMOQX_MOXYGEN_PREBUILT is what --moxygen selects" ;; + -DCMAKE_PREFIX_PATH[:=]*) die "-DCMAKE_PREFIX_PATH is set by --moxygen/--moxygen-dir; to point at your own prefix drive cmake directly (see BUILD.md)" ;; + -DCPM_moxygen_SOURCE[:=]*) die "-DCPM_moxygen_SOURCE is what --moxygen-dir sets" ;; + -DMOQX_MOXYGEN_PROFILE[:=]*) die "MOQX_MOXYGEN_PROFILE is an env var here — it goes to the superbuild, not to moqx" ;; + -DMOQX_ALLOW_ABI_SKEW[:=]*) die "set MOQX_ALLOW_UNINSTRUMENTED_DEPS=1 instead; it covers the whole degraded combination" ;; + esac +done + +# -N resolves the preset's cache variables without configuring, so an unknown +# name dies here with cmake's list of presets. Those variables also say which +# moxygen is needed; MOQX_MOXYGEN_PROFILE overrides the derivation. +preset_info="$(cmake --preset "$profile" -N)" +# What the preset declares it needs... +# The *_src labels name whatever settled each flag, so the errors below blame the +# thing the reader has to change. +san=0 tsan=0 san_src="preset '$profile'" tsan_src="preset '$profile'" +if grep -qE 'MOQX_ENABLE_SANITIZERS(:[A-Z]+)?="?(ON|TRUE|1)"?' <<<"$preset_info"; then san=1; fi +if grep -qE 'MOQX_ENABLE_TSAN(:[A-Z]+)?="?(ON|TRUE|1)"?' <<<"$preset_info"; then tsan=1; fi +# ...and what -D asks for on top, last value winning as it does at the cmake +# line. Read here too, or a sanitizer turned on this way walks straight past the +# interlock below and links the uninstrumented moxygen. +for arg in ${passthru[@]+"${passthru[@]}"}; do + case "$arg" in + -DMOQX_ENABLE_SANITIZERS[:=]*) if truthy "${arg#*=}"; then san=1; else san=0; fi + san_src="$arg" ;; + -DMOQX_ENABLE_TSAN[:=]*) if truthy "${arg#*=}"; then tsan=1; else tsan=0; fi + tsan_src="$arg" ;; + esac +done +if ((san)); then + needs_profile="san" needs_src="$san_src" +elif ((tsan)); then + needs_profile="tsan" needs_src="$tsan_src" +else + # Nothing on: whichever -D turned one off is the reason, else the preset. + needs_profile="default" needs_src="$san_src" + if [[ "$needs_src" == preset\'* && "$tsan_src" != preset\'* ]]; then + needs_src="$tsan_src" + fi +fi +# ...versus the moxygen actually built/downloaded. A fallback build has to match +# the prebuilt it stands in for, or losing the prebuilt would quietly change what +# the lane tests. +if [[ "$moxygen" == prebuilt-with-fallback ]]; then + moxygen_profile="${MOQX_MOXYGEN_PROFILE:-default}" +else + moxygen_profile="${MOQX_MOXYGEN_PROFILE:-$needs_profile}" +fi + +# A sanitizer build loses its instrumented moxygen to a --moxygen value that cannot +# produce one, or to MOQX_MOXYGEN_PROFILE naming a different one. Neither is visible +# to the moqx build, and ASan deps under a TSan moqx link two clashing runtimes. +if [[ "$needs_profile" != default && -z "${MOQX_ALLOW_UNINSTRUMENTED_DEPS:-}" \ + && ( "$moxygen" != from-source || "$moxygen_profile" != "$needs_profile" ) ]]; then + if [[ "$moxygen" != from-source ]]; then + why="--moxygen $moxygen can only deliver the uninstrumented prebuilt" + else + why="MOQX_MOXYGEN_PROFILE=$moxygen_profile builds a different one" + fi + die "$needs_src enables sanitizers ($needs_profile) and needs a moxygen instrumented to match, + but $why — + use: configure.sh $profile --moxygen from-source with MOQX_MOXYGEN_PROFILE unset or =$needs_profile + (or set MOQX_ALLOW_UNINSTRUMENTED_DEPS=1 to knowingly link an uninstrumented moxygen)" fi +# The mirror skew, which no opt-in covers: an instrumented moxygen under a moqx +# carrying no sanitizer flags leaves the __asan_/__tsan_ interceptors undefined at +# link, and the superbuild builds those profiles Debug, which skews kIsDebug too. +if [[ "$needs_profile" == default && "$moxygen_profile" != default ]]; then + die "MOQX_MOXYGEN_PROFILE=$moxygen_profile asks for a $moxygen_profile-instrumented moxygen, but + $needs_src enables no sanitizers, so moqx itself would carry none — + use: configure.sh san|tsan --moxygen from-source (or unset MOQX_MOXYGEN_PROFILE)" +fi + +build_dir="build/$profile" +sb="$SCRATCH/moxygen-build"; [[ "$profile" != default ]] && sb+="-$profile" +if ((clean)) && [[ -d "$sb" ]]; then + rm -rf "$sb" + echo "configure.sh: removed $sb" +fi + +# One opt-in for the whole degraded combination: uninstrumented deps also imply +# the ABI skew CMakeLists refuses (Debug preset over the NDEBUG prebuilt). +common=() +[[ "$needs_profile" != default && -n "${MOQX_ALLOW_UNINSTRUMENTED_DEPS:-}" ]] \ + && common+=("-DMOQX_ALLOW_ABI_SKEW=ON") -cmake -S "$PROJECT_ROOT" -B "${BUILD_DIR}" --preset "${PRESET}" "${PREFIX_ARG[@]}" +# Fresh configure: a build dir binds to the moxygen it first resolved, so a +# clean slate is the only way to (re)bind the choice. +configure_moqx() { + rm -rf "$build_dir" + cmake --preset "$profile" "$@" \ + ${common[@]+"${common[@]}"} ${passthru[@]+"${passthru[@]}"} +} + +# Compile the moxygen prefix, then configure moqx against it. +configure_from_source() { + # Each profile gets its own superbuild dir, since a custom preset may change + # ABI-relevant flags and sharing one cannot be assumed safe. + local cfg=(cmake -S superbuild -B "$sb" -G Ninja) + # Passed even when default: a reused superbuild dir caches the profile, and a + # stale san/tsan value would rebuild an instrumented moxygen under a + # non-instrumented moqx. + cfg+=("-DMOQX_MOXYGEN_PROFILE=$moxygen_profile") + if [[ -n "$moxygen_dir" ]]; then + # Local checkout: rebuild on every configure so source edits take effect. + cfg+=("-DCPM_moxygen_SOURCE=$moxygen_dir" "-DMOQX_MOXYGEN_BUILD_ALWAYS=ON") + else + # Clear any cached local-checkout override from a previous configure. + cfg+=("-UCPM_moxygen_SOURCE" "-DMOQX_MOXYGEN_BUILD_ALWAYS=OFF") + fi + "${cfg[@]}" + # moxygen_profile, not needs_profile: it names the stack actually being + # compiled here, which is what has to fit in RAM. + local sb_sanitized="" + if [[ "$moxygen_profile" != default ]]; then sb_sanitized=1; fi + local sb_jobs; sb_jobs="$(resolve_jobs "$jobs" "$sb_sanitized")" + # The ExternalProject runs its own `cmake --build`, which honors the env var + # but not the outer -j. Without it the folly stack compiles at all cores and + # OOMs the low-RAM hosts the job count exists to protect. + CMAKE_BUILD_PARALLEL_LEVEL="$sb_jobs" cmake --build "$sb" -j"$sb_jobs" + echo "configure.sh: moxygen installed to $sb/moxygen-install" + + local from_source=("-DMOQX_MOXYGEN_PREBUILT=OFF" "-DCMAKE_PREFIX_PATH=$sb/moxygen-install") + # Local moxygen: point moqx's find-modules at the same checkout as the libs. + [[ -n "$moxygen_dir" ]] && from_source+=("-DCPM_moxygen_SOURCE=$moxygen_dir") + configure_moqx "${from_source[@]}" +} + +# Is a prebuilt published for the pin on this platform? Populates the dependency +# cache when it is, so the configure that follows needs no network. Forwarding the +# probe's own knobs keeps it answering for the platform moqx goes on to ask for. +fetch_prebuilt() { + local args=() a rc=0 out + for a in ${passthru[@]+"${passthru[@]}"}; do + case "$a" in -DMOQX_PLATFORM[:=]*|-DMOXYGEN_RELEASE_TAG[:=]*) args+=("$a") ;; esac + done + out="$(mktemp)" + cmake -DOUT="$out" ${args[@]+"${args[@]}"} -P cmake/fetch-moxygen-prebuilt.cmake || rc=$? + rm -f "$out" + return "$rc" +} + +resolved="$moxygen" +[[ "$moxygen" == prebuilt-with-fallback && "${MOQX_MOXYGEN_FALLBACK:-}" == off ]] && resolved="prebuilt" +case "$resolved" in + from-source) + configure_from_source + ;; + prebuilt) + configure_moqx -DMOQX_MOXYGEN_PREBUILT=ON + ;; + prebuilt-with-fallback) + # Ask the fetcher directly rather than read a failed moqx configure as "no + # prebuilt": a broken CMakeLists or an unrelated CPM fetch would otherwise buy + # a folly build that fails at the same place half an hour later. + if fetch_prebuilt; then + resolved="prebuilt" + configure_moqx -DMOQX_MOXYGEN_PREBUILT=ON + else + resolved="from-source" + echo "configure.sh: no moxygen prebuilt for the pin (see above) — building it from source, which is slow" >&2 + [[ -n "${GITHUB_ACTIONS:-}" ]] \ + && echo "::warning title=moxygen prebuilt unavailable::configure.sh fell back to the from-source superbuild" + configure_from_source + fi + ;; +esac + +# The fresh-slate wipe above only holds when the preset lands where we wiped. +[[ -f "$build_dir/CMakeCache.txt" ]] || die "preset '$profile' did not configure into $build_dir — + the wrapper needs binaryDir \${sourceDir}/build/\${presetName}; inherit the 'default' preset." + +# clangd searches a source file's ancestors and a literal build/ under each, never a +# named build//, so without this every translation unit gets an empty +# compilation database. Relative target survives a moved checkout; -n re-points on a +# profile switch rather than nesting inside the old target. +ln -sfn "$profile/compile_commands.json" build/compile_commands.json + +if [[ "$resolved" == "$moxygen" ]]; then + echo "configure.sh: $build_dir configured ($resolved) — compile with: scripts/build.sh $profile" +else + echo "configure.sh: $build_dir configured ($moxygen -> $resolved) — compile with: scripts/build.sh $profile" +fi diff --git a/scripts/detect-platform.sh b/scripts/detect-platform.sh deleted file mode 100755 index 9c158d2d6..000000000 --- a/scripts/detect-platform.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# detect-platform.sh — Resolve the moxygen artifact platform string for this -# host (e.g. ubuntu-22.04-amd64, bookworm-arm64, macos-15-arm64). -# -# Sourced by setup-deps-tarball.sh and setup-deps-dev-artifact.sh so both -# use identical naming; can also be executed directly to print the platform. -# -# Ubuntu derivatives (Linux Mint, Pop!_OS, elementary, ...) report their own -# ID/VERSION_ID in /etc/os-release (e.g. linuxmint/22.3), so ID alone can't -# name an artifact. For those we consult ID_LIKE and map the Ubuntu base -# release via UBUNTU_CODENAME (or /etc/upstream-release/lsb-release). -# Debian and Debian derivatives map to the bookworm artifacts, matching how -# plain Debian is handled. Override with MOQX_PLATFORM if detection is wrong. - -detect_platform() { - local os arch - os=$(uname -s) - arch=$(uname -m) - - local darch="${arch/x86_64/amd64}" - darch="${darch/aarch64/arm64}" - - if [[ "$os" == "Darwin" ]]; then - local ver - ver=$(sw_vers -productVersion | cut -d. -f1) - echo "macos-${ver}-arm64" - return 0 - fi - - if [[ "$os" != "Linux" ]]; then - echo "Error: unsupported OS: $os" >&2 - return 1 - fi - - if [[ ! -f /etc/os-release ]]; then - echo "Error: cannot detect Linux distro (no /etc/os-release)" >&2 - return 1 - fi - - local id id_like version_id ubuntu_codename - id=$(. /etc/os-release && echo "${ID:-}") - id_like=$(. /etc/os-release && echo "${ID_LIKE:-}") - version_id=$(. /etc/os-release && echo "${VERSION_ID:-}") - ubuntu_codename=$(. /etc/os-release && echo "${UBUNTU_CODENAME:-}") - - case "$id" in - ubuntu) - echo "ubuntu-${version_id}-${darch}" - return 0 - ;; - debian) - echo "bookworm-${darch}" - return 0 - ;; - esac - - # Ubuntu derivative: VERSION_ID is the derivative's own (Mint 22.3), so - # resolve the Ubuntu base release from the codename instead. - if [[ " $id_like " == *" ubuntu "* ]]; then - local base_ver="" - case "$ubuntu_codename" in - jammy) base_ver="22.04" ;; - noble) base_ver="24.04" ;; - esac - if [[ -z "$base_ver" && -f /etc/upstream-release/lsb-release ]]; then - base_ver=$(. /etc/upstream-release/lsb-release && echo "${DISTRIB_RELEASE:-}") - fi - if [[ -n "$base_ver" ]]; then - echo "ubuntu-${base_ver}-${darch}" - return 0 - fi - echo "Error: Ubuntu derivative '$id' with unrecognized base codename '${ubuntu_codename:-unset}'." >&2 - echo " Set MOQX_PLATFORM explicitly (e.g. MOQX_PLATFORM=ubuntu-22.04-${darch})." >&2 - return 1 - fi - - if [[ " $id_like " == *" debian "* ]]; then - echo "bookworm-${darch}" - return 0 - fi - - echo "Error: unsupported Linux distro: $id (ID_LIKE='${id_like}')" >&2 - echo " Set MOQX_PLATFORM explicitly (e.g. MOQX_PLATFORM=ubuntu-22.04-${darch})." >&2 - return 1 -} - -# Print the platform when executed directly (not sourced). -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - detect_platform -fi diff --git a/scripts/config-schema-to-markdown.sh b/scripts/dev/config-schema-to-markdown.sh similarity index 96% rename from scripts/config-schema-to-markdown.sh rename to scripts/dev/config-schema-to-markdown.sh index be163b941..25147db7f 100755 --- a/scripts/config-schema-to-markdown.sh +++ b/scripts/dev/config-schema-to-markdown.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Generate markdown config reference from moqx JSON schema on stdin. -# Usage: moqx dump-config-schema | scripts/gen-config-reference.sh +# Usage: moqx dump-config-schema | scripts/dev/config-schema-to-markdown.sh set -euo pipefail if ! command -v jq &>/dev/null; then diff --git a/scripts/format.sh b/scripts/dev/format.sh similarity index 95% rename from scripts/format.sh rename to scripts/dev/format.sh index 8005634f3..865c41320 100755 --- a/scripts/format.sh +++ b/scripts/dev/format.sh @@ -46,7 +46,7 @@ if [[ "${1:-}" == "--check" ]]; then ${CF_BIN} --dry-run -Werror ${FILES} || cf_exit=$? if [[ $header_errors -ne 0 ]]; then - echo "error: files missing copyright headers (run scripts/format.sh to fix)" >&2 + echo "error: files missing copyright headers (run scripts/dev/format.sh to fix)" >&2 fi if [[ $header_errors -ne 0 || $cf_exit -ne 0 ]]; then exit 1 diff --git a/scripts/dev/lint.sh b/scripts/dev/lint.sh new file mode 100755 index 000000000..54672dfee --- /dev/null +++ b/scripts/dev/lint.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# lint.sh — run clang-tidy over moqx's own translation units. +# +# Usage: lint.sh [BUILD_DIR] (default: build/default) +# +# The positional regex matches each compile_commands.json entry's source path. +# Without it, the CPM-fetched dependencies — two thirds of the entries — are +# analysed too. +# +# .clang-tidy does not load — it carries a key clang-tidy 16 removed — so this +# runs the default check set, not the one configured there. +# See https://github.com/openmoq/moqx/issues/518. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +BUILD_DIR=${1:-build/default} + +# run-clang-tidy hands the positional straight to Python's re, so the checkout +# path has to be escaped: a '+' or '(' in it fails to compile, a '.' matches wide. +ROOT_RE=$(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "$ROOT") + +run-clang-tidy -p "${BUILD_DIR}" "^${ROOT_RE}/(src|test|benchmark)/" diff --git a/scripts/log-cleanup.sh b/scripts/dev/log-cleanup.sh similarity index 100% rename from scripts/log-cleanup.sh rename to scripts/dev/log-cleanup.sh diff --git a/scripts/sync-relay.sh b/scripts/dev/sync-relay.sh similarity index 76% rename from scripts/sync-relay.sh rename to scripts/dev/sync-relay.sh index f93507cde..e972f1233 100755 --- a/scripts/sync-relay.sh +++ b/scripts/dev/sync-relay.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash # -# sync-relay.sh - Sync MoqxRelay from deps/moxygen/moxygen/relay/ +# sync-relay.sh - Sync MoqxRelay from moxygen's relay/ source # -# Usage: scripts/sync-relay.sh [--no-build] [--no-test] +# Usage: scripts/dev/sync-relay.sh [--moxygen-dir DIR] [--no-build] [--no-test] +# +# moxygen source is resolved (first match): --moxygen-dir, $CPM_moxygen_SOURCE, +# the source build/default already fetched for the pinned MOXYGEN_REV, else a +# clone of that rev. # # Transforms: # MoQRelay.h -> include/moqx/MoqxRelay.h @@ -18,24 +22,72 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -MOXYGEN_RELAY="${REPO_ROOT}/deps/moxygen/moxygen/relay" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CACHE_ROOT="${MOQX_DEPS_CACHE:-$HOME/.cache/moqx}" NO_BUILD=false NO_TEST=false -for arg in "$@"; do - case "$arg" in - --no-build) NO_BUILD=true ;; - --no-test) NO_TEST=true ;; - *) echo "Unknown argument: $arg" >&2; exit 1 ;; +MOXYGEN_DIR="${MOXYGEN_DIR:-${CPM_moxygen_SOURCE:-}}" +while (( $# > 0 )); do + case "$1" in + --moxygen-dir) + (( $# >= 2 )) || { echo "--moxygen-dir needs a directory" >&2; exit 1; } + MOXYGEN_DIR="$(cd "$2" 2>/dev/null && pwd)" \ + || { echo "--moxygen-dir: '$2' not found" >&2; exit 1; } + shift 2 ;; + --no-build) NO_BUILD=true; shift ;; + --no-test) NO_TEST=true; shift ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; esac done +# Resolve the moxygen source tree. +if [[ -z "$MOXYGEN_DIR" ]]; then + MOXYGEN_REV_PIN="$(cmake -DPIN=MOXYGEN_REV -P "$REPO_ROOT/cmake/print-pin.cmake")" + MOXYGEN_REPO="$(cmake -DPIN=MOXYGEN_REPOSITORY -P "$REPO_ROOT/cmake/print-pin.cmake")" + + # A configured build dir records where CPM put the moxygen it fetched. Reuse + # that tree rather than keeping a second full copy per pin. + CPM_SRC="$(sed -n 's|^CPM_PACKAGE_moxygen_SOURCE_DIR:INTERNAL=||p' \ + "$REPO_ROOT/build/default/CMakeCache.txt" 2>/dev/null || true)" +fi +if [[ -z "$MOXYGEN_DIR" && -d "${CPM_SRC:-/nonexistent}/moxygen/relay" ]]; then + # A build dir configured before the last pin bump still names the previous + # rev's clone, which is still on disk — syncing from it would rewrite the + # relay off the wrong revision. Fall through to the clone when it disagrees. + CPM_REV="$(git -C "$CPM_SRC" rev-parse HEAD 2>/dev/null || true)" + if [[ "$CPM_REV" == "$MOXYGEN_REV_PIN" ]]; then + MOXYGEN_DIR="$CPM_SRC" + fi +fi +if [[ -z "$MOXYGEN_DIR" ]]; then + MOXYGEN_DIR="${CACHE_ROOT}/moxygen-src-${MOXYGEN_REV_PIN:0:12}" + if [[ ! -d "$MOXYGEN_DIR/.git" ]]; then + echo "==> Cloning ${MOXYGEN_REPO}@${MOXYGEN_REV_PIN:0:12} into ${MOXYGEN_DIR}..." + # Clone into a staging dir and only publish it once the pinned rev is + # checked out: a half-finished tree left at $MOXYGEN_DIR would be treated as + # a cache hit on the next run and silently sync from the wrong revision. + stage="${MOXYGEN_DIR}.tmp.$$" + rm -rf "$stage" + trap 'rm -rf "$stage"' EXIT + git clone --filter=blob:none "https://github.com/${MOXYGEN_REPO}.git" "$stage" + git -C "$stage" checkout --detach "$MOXYGEN_REV_PIN" + mv "$stage" "$MOXYGEN_DIR" + trap - EXIT + fi + # Only the current pin's clone is ever wanted, and nothing else prunes these. + # The glob is the exact 12-hex shape this script writes. + for stale in "${CACHE_ROOT}"/moxygen-src-????????????; do + if [[ -d "$stale" && "$stale" != "$MOXYGEN_DIR" ]]; then rm -rf "$stale"; fi + done +fi +MOXYGEN_RELAY="${MOXYGEN_DIR}/moxygen/relay" + TMP_DIR="$(mktemp -d)" trap 'rm -rf "${TMP_DIR}"' EXIT -MOXYGEN_REV="$(git -C "${REPO_ROOT}/deps/moxygen" rev-parse --short HEAD 2>/dev/null || echo unknown)" -echo "==> Syncing relay files from moxygen @ ${MOXYGEN_REV}" +MOXYGEN_REV="$(git -C "${MOXYGEN_DIR}" rev-parse --short HEAD 2>/dev/null || echo unknown)" +echo "==> Syncing relay files from moxygen @ ${MOXYGEN_REV} (${MOXYGEN_DIR})" # ───────────────────────────────────────────────────────────────────────────── # Helper: replace Meta-only Apache 2.0 copyright with combined OpenMOQ header @@ -51,7 +103,8 @@ new_header = """\ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */""" @@ -211,7 +264,7 @@ echo "--> Formatting" # ───────────────────────────────────────────────────────────────────────────── if ! ${NO_BUILD}; then echo "--> Building" - "${SCRIPT_DIR}/build.sh" + "${SCRIPT_DIR}/../build.sh" fi # ───────────────────────────────────────────────────────────────────────────── @@ -219,7 +272,7 @@ fi # ───────────────────────────────────────────────────────────────────────────── if ! ${NO_TEST}; then echo "--> Testing" - "${SCRIPT_DIR}/test.sh" + "${SCRIPT_DIR}/../test.sh" fi echo "==> Done" diff --git a/scripts/test-relay-shutdown.sh b/scripts/dev/test-relay-shutdown.sh similarity index 97% rename from scripts/test-relay-shutdown.sh rename to scripts/dev/test-relay-shutdown.sh index b10f07b8d..49d2f0014 100755 --- a/scripts/test-relay-shutdown.sh +++ b/scripts/dev/test-relay-shutdown.sh @@ -1,12 +1,12 @@ #!/bin/bash # Test that relay pair shuts down cleanly (no ASAN leaks or crashes). -# Usage: ./scripts/test-relay-shutdown.sh [up_config] [down_config] +# Usage: ./scripts/dev/test-relay-shutdown.sh [up_config] [down_config] # up_config config with no upstream (default: /tmp/up.yaml, port 12345) # down_config config with upstream pointing to up (default: /tmp/down.yaml, port 12346) set -euo pipefail -BIN="${BIN:-./build-san/moqx}" +BIN="${BIN:-./build/san/moqx}" UP_CFG="${1:-/tmp/up.yaml}" DOWN_CFG="${2:-/tmp/down.yaml}" SHUTDOWN_TIMEOUT="${SHUTDOWN_TIMEOUT:-10}" # seconds to wait for clean exit before declaring a hang diff --git a/scripts/install-system-deps.sh b/scripts/install-system-deps.sh new file mode 100755 index 000000000..c498f3003 --- /dev/null +++ b/scripts/install-system-deps.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# install-system-deps.sh — install the system libraries moqx needs. +# +# Required in BOTH dependency modes: the prebuilt moxygen install ships +# folly/fizz/mvfst/proxygen statically, but its CMake config still resolves +# fmt/glog/gflags/... and folly transitively needs OpenSSL/Boost at link time. +# +# Optional helper — install the equivalents by hand if you prefer. +# +# Run it plain: `scripts/install-system-deps.sh`. It elevates the package-manager +# calls itself, so it works as root (containers), as a sudo-capable user, and on +# macOS — where Homebrew refuses to run under sudo at all. +# +# The -dev packages here overlap cmake/CheckSystemDeps.cmake. Kept in sync by +# hand: this script installs cmake, so it cannot read that list back. +set -e + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + if command -v sudo >/dev/null 2>&1; then + SUDO=sudo + elif [ "$(uname)" != "Darwin" ]; then + echo "Installing packages needs root: re-run as root or install sudo." >&2 + exit 1 + fi +fi + +# reflect-cpp (a moqx dependency) needs CMake >= 3.23, newer than the 3.22 that +# e.g. Ubuntu 22.04 ships. Install a current CMake from PyPI when the distro's is +# too old, rather than requiring users to add a third-party apt repo. +ensure_recent_cmake() { + local have min=3.23 + have="$(cmake --version 2>/dev/null | sed -nE 's/.*version ([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/p' | head -1)" + # Already >= min? (portable version compare — works on apt and dnf distros) + if [ -n "$have" ] && [ "$(printf '%s\n%s\n' "$min" "$have" | sort -V | head -1)" = "$min" ]; then + return + fi + # Pin to the 3.x series: CMake 4.x drops compatibility shims the Meta-stack + # (folly/proxygen/…) source build still relies on. + echo "CMake ${have:-not found} is older than $min; installing a current CMake 3.x from PyPI..." + if ! command -v pip3 >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get install -y python3-pip + elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y python3-pip; fi + fi + # --break-system-packages: PEP-668 distros refuse system-wide pip installs + # otherwise; older pips don't know the flag, hence the fallback. + $SUDO pip3 install --upgrade 'cmake<4' --break-system-packages 2>/dev/null \ + || $SUDO pip3 install --upgrade 'cmake<4' + hash -r + echo "Using $(cmake --version | head -1)" +} + +install_ubuntu() { + echo "Installing dependencies for Ubuntu/Debian..." + $SUDO apt-get update + $SUDO apt-get install -y \ + build-essential cmake ninja-build git pkg-config ccache \ + libssl-dev libunwind-dev libgoogle-glog-dev libgflags-dev \ + libdouble-conversion-dev libevent-dev libsodium-dev libzstd-dev \ + libboost-dev libboost-context-dev libboost-filesystem-dev \ + libboost-program-options-dev libboost-regex-dev libboost-thread-dev \ + libfmt-dev zlib1g-dev libc-ares-dev libbrotli-dev python3 gperf \ + jq curl + ensure_recent_cmake +} + +install_fedora() { + echo "Installing dependencies for Fedora/CentOS/RHEL..." + $SUDO dnf install -y \ + gcc gcc-c++ make cmake git pkgconf-pkg-config ccache \ + openssl-devel libunwind-devel glog-devel gflags-devel \ + double-conversion-devel libevent-devel libsodium-devel libzstd-devel \ + boost-devel fmt-devel zlib-devel c-ares-devel brotli-devel python3 gperf \ + jq curl + if ! command -v ninja &>/dev/null; then + $SUDO dnf install -y ninja-build 2>/dev/null || \ + echo "WARNING: install ninja manually (pip install ninja)." + fi + ensure_recent_cmake # RHEL/CentOS 8 ship cmake 3.20 (< 3.23) +} + +install_macos() { + echo "Installing dependencies for macOS..." + brew install \ + cmake ninja ccache openssl@3 glog gflags double-conversion \ + libevent libsodium zstd boost fmt c-ares gperf brotli jq + # Homebrew ships CMake 4.x; the from-source moxygen build needs the 3.x + # series (same reason as the Linux pin above). Prebuilt-mode builds are fine. + case "$(cmake --version 2>/dev/null | sed -nE 's/.*version ([0-9]+).*/\1/p' | head -1)" in + 4*) echo "WARNING: CMake 4.x detected — from-source moxygen builds need CMake 3.x:" + echo " pip3 install 'cmake<4' (and ensure it precedes brew's on PATH)" ;; + esac +} + +if [[ "$(uname)" == "Darwin" ]]; then + install_macos +elif [[ -f /etc/os-release ]]; then + . /etc/os-release + case "$ID" in + ubuntu|debian) install_ubuntu ;; + fedora|centos|rhel) install_fedora ;; + *) + case "$ID_LIKE" in + *ubuntu*|*debian*) install_ubuntu ;; + *fedora*|*rhel*) install_fedora ;; + *) echo "Unsupported distro: $ID — install deps manually (see BUILD.md)"; exit 1 ;; + esac ;; + esac +else + echo "Unsupported operating system"; exit 1 +fi + +echo +echo "Done. Build moqx with: scripts/configure.sh --moxygen prebuilt-with-fallback && scripts/build.sh" diff --git a/scripts/lib/jobs.sh b/scripts/lib/jobs.sh new file mode 100644 index 000000000..6b1a6b02c --- /dev/null +++ b/scripts/lib/jobs.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# jobs.sh — resolve the compile job count. Sourced by the configure/build/test +# trilogy; not executable on its own. +# +# resolve_jobs [FLAG_VALUE] [SANITIZED] +# FLAG_VALUE the -j/--jobs argument, empty when not given +# SANITIZED non-empty when the build instruments with ASan/TSan +# +# Precedence: -j, then MOQX_BUILD_JOBS, then the default. An explicit value is +# never clamped in either direction — distcc wants far more jobs than local +# cores, a RAM-starved host wants fewer. +# +# The default is the core count, derated by free RAM for sanitizer builds only: +# those TUs carry folly's coroutines through -O2 -g plus ASan/UBSan and peak +# >2 GB each, so the core count OOMs the compiler on a RAM-bound host. +# Uninstrumented builds are roughly 4x lighter and stay at the core count. + +# Callers all define die(); this is only reached if one forgets. +declare -F die >/dev/null || die() { echo "$*" >&2; exit 1; } + +_cores() { nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4; } + +# Free RAM in whole GB, or 0 when it cannot be read — an unknown budget must +# not silently derate the build. macOS reports total, not available: it has no +# cheap MemAvailable equivalent, and overcommitting there costs swap, not a kill. +_avail_gb() { + if [[ -r /proc/meminfo ]]; then + awk '/^MemAvailable:/ { print int($2 / 1048576); found = 1 } END { if (!found) print 0 }' /proc/meminfo + elif [[ "$(uname)" == Darwin ]]; then + local bytes + bytes=$(sysctl -n hw.memsize 2>/dev/null) && echo $(( bytes / 1073741824 )) || echo 0 + else + echo 0 + fi +} + +resolve_jobs() { + local jobs="${1:-${MOQX_BUILD_JOBS:-}}" sanitized="${2:-}" + if [[ -z "$jobs" ]]; then + jobs="$(_cores)" + if [[ -n "$sanitized" ]]; then + local cap=$(( $(_avail_gb) * 2 / 5 )) # one job per 2.5 GB + (( cap > 0 && cap < jobs )) && jobs="$cap" + fi + fi + [[ "$jobs" =~ ^[1-9][0-9]*$ ]] || die "invalid job count '$jobs' (expected a positive integer)" + echo "$jobs" +} diff --git a/scripts/lint.sh b/scripts/lint.sh deleted file mode 100755 index cf5022125..000000000 --- a/scripts/lint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BUILD_DIR=${1:-build} - -run-clang-tidy -p "${BUILD_DIR}" diff --git a/scripts/moqx-run.sh b/scripts/moqx-run.sh index 064f1036c..d7920b8bc 100755 --- a/scripts/moqx-run.sh +++ b/scripts/moqx-run.sh @@ -2,7 +2,7 @@ # Local moqx CLI launcher for bench/scaling runs. # # Resolves the ${MOQX_*}/${DOMAIN} placeholders in a config template -# (default scripts/config.bench.yaml) from CLI flags > .env > built-in +# (default scripts/perf/config.bench.yaml) from CLI flags > .env > built-in # defaults, then serves the resolved config. Reads a local .env if present. set -euo pipefail @@ -54,9 +54,9 @@ Listener (templated into the config; CLI > .env > default): Targeting: --subcmd CMD moqx subcommand (default: serve) - --config FILE config YAML template (default: scripts/config.bench.yaml) + --config FILE config YAML template (default: scripts/perf/config.bench.yaml) --env FILE alternate .env file (default: scripts/.env if present) - --bin FILE moqx binary path (default: /build/moqx) + --bin FILE moqx binary path (default: /build/default/moqx) Execution: -j, --jemalloc [PATH] LD_PRELOAD jemalloc for the relay (~10% speedup). @@ -207,8 +207,8 @@ if [[ -f "$ENV_FILE" ]]; then fi # ── Paths (CLI > env > defaults) ───────────────────────────────────────── -MOQX_BIN="${CLI_BIN:-${MOQX_BIN:-$PROJECT_ROOT/build/moqx}}" -CONFIG_TEMPLATE="${CLI_CONFIG:-${MOQX_CONFIG:-$SCRIPT_DIR/config.bench.yaml}}" +MOQX_BIN="${CLI_BIN:-${MOQX_BIN:-$PROJECT_ROOT/build/default/moqx}}" +CONFIG_TEMPLATE="${CLI_CONFIG:-${MOQX_CONFIG:-$SCRIPT_DIR/perf/config.bench.yaml}}" [[ -x "$MOQX_BIN" ]] || { echo "moqx binary not found: $MOQX_BIN" >&2; exit 1; } [[ -f "$CONFIG_TEMPLATE" ]] || { echo "config not found: $CONFIG_TEMPLATE" >&2; exit 1; } diff --git a/scripts/collect-libs.sh b/scripts/perf/collect-libs.sh old mode 100644 new mode 100755 similarity index 93% rename from scripts/collect-libs.sh rename to scripts/perf/collect-libs.sh index f421ab747..3b534e172 --- a/scripts/collect-libs.sh +++ b/scripts/perf/collect-libs.sh @@ -2,7 +2,7 @@ # collect-libs.sh — Extract all shared library dependencies from a binary # and optionally copy them to a destination directory. # -# Usage: scripts/collect-libs.sh [dest_dir] +# Usage: scripts/perf/collect-libs.sh [dest_dir] # binary_path Path to the binary to analyze # dest_dir (Optional) Directory to copy libs to diff --git a/scripts/config.bench.yaml b/scripts/perf/config.bench.yaml similarity index 100% rename from scripts/config.bench.yaml rename to scripts/perf/config.bench.yaml diff --git a/scripts/perf-compare.py b/scripts/perf/perf-compare.py similarity index 99% rename from scripts/perf-compare.py rename to scripts/perf/perf-compare.py index d34c3b1dd..7f9146696 100755 --- a/scripts/perf-compare.py +++ b/scripts/perf/perf-compare.py @@ -5,7 +5,7 @@ compares the current run, and outputs a markdown summary suitable for PR comments. Usage: - scripts/perf-compare.py --current results.json --data-dir data/ [--window 10] [--threshold 5] + scripts/perf/perf-compare.py --current results.json --data-dir data/ [--window 10] [--threshold 5] Exit code is always 0 (non-blocking). Regressions are flagged in the output only. """ diff --git a/scripts/perf-metrics.sh b/scripts/perf/perf-metrics.sh similarity index 99% rename from scripts/perf-metrics.sh rename to scripts/perf/perf-metrics.sh index 8ad9e451a..1fea91839 100755 --- a/scripts/perf-metrics.sh +++ b/scripts/perf/perf-metrics.sh @@ -8,7 +8,7 @@ # Bucket lines are skipped; histogram _sum/_count pairs are collapsed into a # single avg_ column. # -# Usage: scripts/perf-metrics.sh [admin_port] [log_file] +# Usage: scripts/perf/perf-metrics.sh [admin_port] [log_file] # admin_port default 19701 # log_file default /tmp/moqx_metrics_.log # diff --git a/scripts/perf-results-to-json.sh b/scripts/perf/perf-results-to-json.sh similarity index 99% rename from scripts/perf-results-to-json.sh rename to scripts/perf/perf-results-to-json.sh index 6d80a0767..9d2291e99 100755 --- a/scripts/perf-results-to-json.sh +++ b/scripts/perf/perf-results-to-json.sh @@ -2,7 +2,7 @@ # perf-results-to-json.sh — Parse moqperf_test_client output and metrics # into a structured JSON results file for trend tracking. # -# Usage: scripts/perf-results-to-json.sh [options] +# Usage: scripts/perf/perf-results-to-json.sh [options] # --client-output PATH Path to client stdout capture # --metrics-log PATH Path to metrics TSV log (optional) # --commit SHA Git commit SHA diff --git a/scripts/perf-test-ci.sh b/scripts/perf/perf-test-ci.sh similarity index 92% rename from scripts/perf-test-ci.sh rename to scripts/perf/perf-test-ci.sh index 78799bf1b..47cf4394c 100755 --- a/scripts/perf-test-ci.sh +++ b/scripts/perf/perf-test-ci.sh @@ -12,9 +12,9 @@ # PERF_RELAY_PORT — Relay QUIC port (default: 4433) # PERF_ADMIN_PORT — Relay admin port (default: 19701) # -# Usage: scripts/perf-test-ci.sh [options] -# --binary PATH Path to moqx binary (default: build/moqx) -# --moqbin PATH Path to moxygen bin dir (default: .scratch/moxygen-install/bin) +# Usage: scripts/perf/perf-test-ci.sh [options] +# --binary PATH Path to moqx binary (default: build/default/moqx) +# --moqbin PATH Path to moxygen bin dir (default: the moxygen install bin (auto-detected from the build)) # --output PATH Output JSON file (default: perf-results.json) # --subscriber-max N Max subscribers (default: 1000) # --ramp N Subscribers/sec (default: 100) @@ -31,16 +31,15 @@ # excluding teardown noise (default: 5) # # The relay config is rendered on the relay VM by scripts/moqx-run.sh from -# scripts/config.bench.yaml — the SAME path scripts/perf-test.sh uses — so the +# scripts/perf/config.bench.yaml — the SAME path scripts/perf/perf-test.sh uses — so the # two harnesses stay in lockstep and CI trend data can't silently drift. set -euo pipefail -REPO="$(cd "$(dirname "$0")/.." && pwd)" +REPO="$(cd "$(dirname "$0")/../.." && pwd)" # ── Defaults ─────────────────────────────────────────────────────────────────── -BINARY="${BINARY:-$REPO/build/moqx}" -MOQBIN="${MOQBIN:-$REPO/.scratch/moxygen-install/bin}" +BINARY="${BINARY:-$REPO/build/default/moqx}" OUTPUT="perf-results.json" SUBSCRIBER_MAX=1000 RAMP=100 @@ -79,6 +78,16 @@ while [[ $# -gt 0 ]]; do esac done +# moxygen sample-binary dir: --moqbin/env override, else the tool-paths file that +# sits beside the relay. After the arg loop, so it follows a --binary pointing at +# a different build rather than the default one. +if [[ -z "${MOQBIN:-}" && -f "$(dirname "$BINARY")/moqx-tools.env" ]]; then + source "$(dirname "$BINARY")/moqx-tools.env" +fi +# Always leave MOQBIN set (possibly empty) so the binary checks below report a +# clear not-found error instead of aborting under `set -u`. +MOQBIN="${MOQBIN:-}" + # ── Validation ───────────────────────────────────────────────────────────────── if [[ -z "$RELAY_HOST" ]]; then echo "ERROR: PERF_RELAY_HOST not set" >&2; exit 1 @@ -104,7 +113,7 @@ MOQTEST_SERVER="$MOQBIN/moqtest_server" MOQPERF_CLIENT="$MOQBIN/moqperf_test_client" RELAY_RUN_SCRIPT="$REPO/scripts/moqx-run.sh" -RELAY_CONFIG_TEMPLATE="$REPO/scripts/config.bench.yaml" +RELAY_CONFIG_TEMPLATE="$REPO/scripts/perf/config.bench.yaml" for f in "$BINARY" "$MOQTEST_SERVER" "$MOQPERF_CLIENT" "$RELAY_RUN_SCRIPT" "$RELAY_CONFIG_TEMPLATE"; do if [[ ! -f "$f" ]]; then @@ -116,9 +125,9 @@ done echo "Collecting shared library dependencies..." LOCAL_LIBDIR="/tmp/moqx-perf-libs-$$" mkdir -p "$LOCAL_LIBDIR" -bash "$REPO/scripts/collect-libs.sh" "$BINARY" "$LOCAL_LIBDIR" > /dev/null -bash "$REPO/scripts/collect-libs.sh" "$MOQTEST_SERVER" "$LOCAL_LIBDIR" > /dev/null -bash "$REPO/scripts/collect-libs.sh" "$MOQPERF_CLIENT" "$LOCAL_LIBDIR" > /dev/null +bash "$REPO/scripts/perf/collect-libs.sh" "$BINARY" "$LOCAL_LIBDIR" > /dev/null +bash "$REPO/scripts/perf/collect-libs.sh" "$MOQTEST_SERVER" "$LOCAL_LIBDIR" > /dev/null +bash "$REPO/scripts/perf/collect-libs.sh" "$MOQPERF_CLIENT" "$LOCAL_LIBDIR" > /dev/null echo "Libraries collected: $(ls $LOCAL_LIBDIR/*.so* 2>/dev/null | wc -l) files" # ── Git metadata ─────────────────────────────────────────────────────────────── @@ -155,7 +164,7 @@ echo "Deploying binaries..." rsync -az -e "ssh ${SSH_OPTS[*]}" "$BINARY" "${RELAY_HOST}:${REMOTE_DIR}/moqx" rsync -az -e "ssh ${SSH_OPTS[*]}" "$MOQTEST_SERVER" "${RELAY_HOST}:${REMOTE_DIR}/moqtest_server" rsync -az -e "ssh ${SSH_OPTS[*]}" "$MOQPERF_CLIENT" "${CLIENT_HOST}:${REMOTE_DIR}/moqperf_test_client" -rsync -az -e "ssh ${SSH_OPTS[*]}" "$REPO/scripts/perf-metrics.sh" "${RELAY_HOST}:${REMOTE_DIR}/perf-metrics.sh" +rsync -az -e "ssh ${SSH_OPTS[*]}" "$REPO/scripts/perf/perf-metrics.sh" "${RELAY_HOST}:${REMOTE_DIR}/perf-metrics.sh" rsync -az -e "ssh ${SSH_OPTS[*]}" "$RELAY_RUN_SCRIPT" "${RELAY_HOST}:${REMOTE_DIR}/moqx-run.sh" rsync -az -e "ssh ${SSH_OPTS[*]}" "$RELAY_CONFIG_TEMPLATE" "${RELAY_HOST}:${REMOTE_DIR}/config.bench.yaml" rsync -az -e "ssh ${SSH_OPTS[*]}" "$LOCAL_LIBDIR/" "${RELAY_HOST}:${REMOTE_DIR}/lib/" @@ -176,7 +185,7 @@ cleanup() { trap cleanup EXIT # ── Start relay (via moqx-run.sh + config.bench.yaml on the relay VM) ────────── -# Mirrors scripts/perf-test.sh's relay launch so both harnesses share identical +# Mirrors scripts/perf/perf-test.sh's relay launch so both harnesses share identical # tuning (thread count, flow control, UDP buffer, bbr2, GSO, recv batch). This # is what makes --io-threads actually take effect and stops CI trend drift. echo "Starting relay on $RELAY_HOST (io_threads=$IO_THREADS)..." @@ -340,7 +349,7 @@ NET_THROUGHPUT=$(metrics_col_avg "ext_Mbps" "%.1f") # ── Parse results and generate JSON ────────────────────────────────────────── echo "Generating results JSON..." -bash "$REPO/scripts/perf-results-to-json.sh" \ +bash "$REPO/scripts/perf/perf-results-to-json.sh" \ --client-output /tmp/perf-client-output.txt \ --metrics-log /tmp/perf-metrics.log \ --commit "$COMMIT_SHA" \ diff --git a/scripts/perf-test.sh b/scripts/perf/perf-test.sh similarity index 95% rename from scripts/perf-test.sh rename to scripts/perf/perf-test.sh index 7769e6283..e7697ed66 100755 --- a/scripts/perf-test.sh +++ b/scripts/perf/perf-test.sh @@ -5,10 +5,10 @@ # (subscriber ramp), then prints the client's output. Logs for all three # processes are always saved to /tmp/moqx-perf-/. # -# Usage: scripts/perf-test.sh [options] -# --relay PATH Path to moqx binary (default: build/moqx) +# Usage: scripts/perf/perf-test.sh [options] +# --relay PATH Path to moqx binary (default: build/default/moqx) # --moqbin PATH Path to moxygen bin dir -# (default: .scratch/moxygen-install/bin) +# (default: the moxygen install bin (auto-detected from the build)) # -s, --subscriber-max N Max total subscribers (default: 500) # --ramp N Subscribers added per second (default: 100) # -d, --duration N Test duration in seconds (default: 30) @@ -56,11 +56,10 @@ set -euo pipefail -REPO="$(cd "$(dirname "$0")/.." && pwd)" +REPO="$(cd "$(dirname "$0")/../.." && pwd)" # ── Defaults ─────────────────────────────────────────────────────────────────── -BINARY="${RELAY:-$REPO/build/moqx}" -MOQBIN="${MOQBIN:-$REPO/.scratch/moxygen-install/bin}" +BINARY="${RELAY:-$REPO/build/default/moqx}" SUBSCRIBER_MAX=500 RAMP=100 DURATION=30 @@ -120,9 +119,19 @@ while [[ $# -gt 0 ]]; do esac done +# moxygen sample-binary dir: --moqbin/env override, else the tool-paths file that +# sits beside the relay. After the arg loop, so it follows a --relay pointing at a +# different build rather than the default one. +if [[ -z "${MOQBIN:-}" && -f "$(dirname "$BINARY")/moqx-tools.env" ]]; then + source "$(dirname "$BINARY")/moqx-tools.env" +fi +# Always leave MOQBIN set (possibly empty) so the binary checks below report a +# clear not-found error instead of aborting under `set -u`. +MOQBIN="${MOQBIN:-}" + MOQTEST_SERVER="$MOQBIN/moqtest_server" MOQPERF_CLIENT="$MOQBIN/moqperf_test_client" -METRICS_SCRIPT="$REPO/scripts/perf-metrics.sh" +METRICS_SCRIPT="$REPO/scripts/perf/perf-metrics.sh" # jemalloc detection for the relay is delegated to moqx-run.sh (-j auto): it probes # the common multiarch + /lib64 paths and LD_PRELOADs the lib, warning (in the relay @@ -254,7 +263,7 @@ trap cleanup EXIT { echo "date: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "moqx_git: $(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo unknown)" - echo "moxygen_git: $(git -C "$REPO/deps/moxygen" rev-parse --short HEAD 2>/dev/null || echo unknown)" + echo "moxygen_git: $(cmake -DPIN=MOXYGEN_REV -P "$REPO/cmake/print-pin.cmake" 2>/dev/null | cut -c1-12 || echo unknown)" echo "relay_binary: $BINARY" echo "moqbin: $MOQBIN" echo "relay_url: $RELAY_URL" diff --git a/scripts/setup-deps-dev-artifact.sh b/scripts/setup-deps-dev-artifact.sh deleted file mode 100755 index b9bbdb0a9..000000000 --- a/scripts/setup-deps-dev-artifact.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# setup-deps-dev-artifact.sh — Populate .scratch with a dev-build moxygen -# artifact produced by the openmoq/moxygen omoq-dev-build workflow. -# -# Used as a fallback when the rolling snapshot release SHA doesn't match the -# moxygen submodule pin (typical case: developer pointed the submodule at a -# moxygen PR/feature branch that hasn't been merged-then-snapshotted yet). -# Looks up the most recent non-expired dev-build artifact whose name encodes -# the submodule's short SHA + the current platform; downloads it; extracts -# to .scratch/moxygen-install. -# -# Exits 0 on success (artifact found and extracted). Exits 1 if no matching -# artifact exists (or auth/network failure) — build.sh then falls through to -# the source-build mode. -# -# Usage: ./scripts/setup-deps-dev-artifact.sh -# -# Env: -# GITHUB_TOKEN / GH_TOKEN required (artifacts API needs auth) -# MOQX_MOXYGEN_REPO default: openmoq/moxygen -# MOQX_PLATFORM default: auto-detected via uname - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SCRATCH="${MOQX_SCRATCH_PATH:-${PROJECT_ROOT}/.scratch}" -MOXYGEN_REPO="${MOQX_MOXYGEN_REPO:-openmoq/moxygen}" -MOXYGEN_DIR="${PROJECT_ROOT}/deps/moxygen" -INSTALL_DIR="${SCRATCH}/moxygen-install" - -# ── Platform detection ──────────────────────────────────────────────────────── -# Shared with setup-deps-tarball.sh so both resolve identical platform names -# matching the omoq-dev-build workflow's `target` input (macos-15-arm64, -# ubuntu-22.04-amd64, bookworm-amd64, ...), including Ubuntu/Debian -# derivatives via ID_LIKE. See detect-platform.sh. - -# shellcheck source=scripts/detect-platform.sh -source "$SCRIPT_DIR/detect-platform.sh" - -PLATFORM="${MOQX_PLATFORM:-$(detect_platform)}" - -# ── Resolve target artifact name from submodule SHA + platform ─────────────── -SUBMODULE_SHA=$(git -C "$MOXYGEN_DIR" rev-parse HEAD) -SHA7="${SUBMODULE_SHA:0:7}" -ARTIFACT_NAME="moxygen-dev-${PLATFORM}-${SHA7}.tar.gz" - -# ── Auth: artifacts API requires a token even for public repos ─────────────── -TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-}}" -if [[ -z "$TOKEN" ]]; then - echo "==> Dev-build artifact lookup: no GITHUB_TOKEN/GH_TOKEN set, skipping" >&2 - exit 1 -fi - -# ── Query the artifacts API ─────────────────────────────────────────────────── -# `?name=` filters server-side; we just take the first non-expired hit. -echo "==> Searching for dev-build artifact: ${ARTIFACT_NAME}" -ARTIFACT_JSON=$(curl -fsSL -H "Authorization: Bearer ${TOKEN}" \ - "https://api.github.com/repos/${MOXYGEN_REPO}/actions/artifacts?name=${ARTIFACT_NAME}") || { - echo "Error: failed to query artifacts API for ${MOXYGEN_REPO}" >&2 - exit 1 -} - -ARTIFACT_URL=$(printf '%s' "$ARTIFACT_JSON" | python3 -c " -import json, sys -data = json.load(sys.stdin) -for a in data.get('artifacts', []): - if not a.get('expired', False): - print(a['archive_download_url']) - break -") - -if [[ -z "$ARTIFACT_URL" ]]; then - echo "==> No non-expired dev-build artifact found for ${SHA7} on ${PLATFORM}" - exit 1 -fi - -# ── Download + extract ──────────────────────────────────────────────────────── -echo "==> Downloading dev-build artifact..." -DOWNLOAD_DIR="${SCRATCH}/downloads" -mkdir -p "$DOWNLOAD_DIR" -ZIP="${DOWNLOAD_DIR}/${ARTIFACT_NAME}.zip" -rm -f "$ZIP" -curl -fsSL -H "Authorization: Bearer ${TOKEN}" "${ARTIFACT_URL}" -o "$ZIP" - -echo "==> Extracting to ${INSTALL_DIR}..." -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -unzip -q "$ZIP" -d "$TMP" -mkdir -p "$INSTALL_DIR" -tar -C "$INSTALL_DIR" -xzf "$TMP"/*.tar.gz - -# ── Sanity check ────────────────────────────────────────────────────────────── -# Either path is acceptable (CMake layouts have varied across moxygen versions). -if [[ ! -f "$INSTALL_DIR/lib/cmake/moxygen/moxygen-config.cmake" ]] && \ - [[ ! -f "$INSTALL_DIR/lib/cmake/folly/moxygen-config.cmake" ]]; then - echo "Error: artifact extracted but moxygen-config.cmake not found in install tree" >&2 - exit 1 -fi - -# Write the sentinel cmake_prefix_path.txt that build.sh checks before -# building. Mirrors setup-deps-tarball.sh / setup-deps-standalone.sh so the -# dev-artifact mode produces the same post-setup state as the other modes. -echo "$INSTALL_DIR" > "${SCRATCH}/cmake_prefix_path.txt" - -echo "==> Done: dev-build artifact extracted to ${INSTALL_DIR}" diff --git a/scripts/setup-deps-standalone.sh b/scripts/setup-deps-standalone.sh deleted file mode 100755 index 1f87946f7..000000000 --- a/scripts/setup-deps-standalone.sh +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env bash -# setup-deps-standalone.sh — Build moxygen + deps from source (standalone/FetchContent). -# -# Uses deps/moxygen/standalone/CMakeLists.txt which fetches Meta OSS deps -# (folly, fizz, wangle, mvfst, proxygen) via FetchContent and builds them -# as static libraries alongside moxygen. Installs everything to .scratch/ -# moxygen-install and writes cmake_prefix_path.txt for configure.sh. -# -# This is the "deep" build — slower first time but fully self-contained. -# Subsequent builds are incremental (cmake only rebuilds what changed). -# -# Usage: -# ./scripts/setup-deps-standalone.sh [--profile NAME] [-j N] -# -# Job count: -j/--jobs, else MOQX_BUILD_JOBS, else core count. -# -# System deps required (Ubuntu): -# deps/moxygen/standalone/install-system-deps.sh - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SCRATCH="${MOQX_SCRATCH_PATH:-${PROJECT_ROOT}/.scratch}" -MOXYGEN_DIR="${MOQX_MOXYGEN_DIR:-${PROJECT_ROOT}/deps/moxygen}" -STANDALONE_SRC="${MOXYGEN_DIR}/standalone" - -PROFILE="default" -JOBS="${MOQX_BUILD_JOBS:-}" -while (( $# > 0 )); do - case "$1" in - --profile) PROFILE="$2"; shift 2 ;; - -j|--jobs) JOBS="$2"; shift 2 ;; - -j*) JOBS="${1#-j}"; shift ;; - *) echo "Unknown option: $1" >&2; exit 1 ;; - esac -done - -if [[ "$PROFILE" == "default" ]]; then - BUILD_DIR="${SCRATCH}/standalone-build" - INSTALL_DIR="${SCRATCH}/moxygen-install" - PREFIX_PATH_FILE="${SCRATCH}/cmake_prefix_path.txt" -else - BUILD_DIR="${SCRATCH}/standalone-build-${PROFILE}" - INSTALL_DIR="${SCRATCH}/moxygen-install-${PROFILE}" - PREFIX_PATH_FILE="${SCRATCH}/cmake_prefix_path-${PROFILE}.txt" -fi - -if [[ -z "${MOQX_MOXYGEN_DIR:-}" ]] && [[ ! -e "$MOXYGEN_DIR/.git" ]]; then - echo "Error: deps/moxygen submodule not initialized." >&2 - echo " Run: git submodule update --init" >&2 - exit 1 -fi - -if [[ ! -f "${STANDALONE_SRC}/CMakeLists.txt" ]]; then - echo "Error: standalone/CMakeLists.txt not found in ${MOXYGEN_DIR}" >&2 - exit 1 -fi - -JOB_COUNT="${JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}" -if [[ ! "$JOB_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo "Error: invalid job count '$JOB_COUNT' (expected a positive integer)" >&2 - exit 1 -fi - -# Profile-specific cmake flags -CMAKE_BUILD_TYPE="RelWithDebInfo" -EXTRA_CMAKE_ARGS=() -CXX_FLAGS="" -if [[ "$PROFILE" == "san" ]]; then - CMAKE_BUILD_TYPE="Debug" - # ASAN only (no UBSAN): folly uses static_assert on syscall function addresses - # (recvmmsg, sendmmsg) which become non-constant under UBSAN's function - # interposition. ASAN alone is sufficient for memory safety in deps. - CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" - EXTRA_CMAKE_ARGS+=( - "-DCMAKE_C_FLAGS=${CXX_FLAGS}" - "-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address" - "-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address" - ) -elif [[ "$PROFILE" == "tsan" ]]; then - CMAKE_BUILD_TYPE="Debug" - # TSan must instrument the full dep chain: folly/mvfst atomics and EventBase - # internals are invisible to TSan without instrumentation, causing false positives - # on every lock operation. - CXX_FLAGS="-fsanitize=thread -fno-omit-frame-pointer" - EXTRA_CMAKE_ARGS+=( - "-DCMAKE_C_FLAGS=${CXX_FLAGS}" - "-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread" - "-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=thread" - ) -fi - -# moxygen pins fmt 10.2.1; Apple Clang 21+ (Xcode 26 / recent CLT) rejects its -# C++20 consteval format-string validation (fmtlib/fmt#4740). Define -# FMT_CONSTEVAL empty so fmt falls back to runtime checking for this dep build. -if [[ "$(uname -s)" == "Darwin" ]] && command -v c++ >/dev/null 2>&1; then - _apple_clang_ver="$(c++ --version 2>/dev/null \ - | sed -En 's/.*clang version ([0-9]+).*/\1/p' | head -1)" - if [[ -n "${_apple_clang_ver:-}" && "${_apple_clang_ver}" -ge 21 ]]; then - CXX_FLAGS+=" -DFMT_CONSTEVAL=" - fi -fi -if [[ -n "$CXX_FLAGS" ]]; then - # shellcheck disable=SC2086 - EXTRA_CMAKE_ARGS+=("-DCMAKE_CXX_FLAGS=${CXX_FLAGS}") -fi - -# Boost linking: prefer static when available (more portable artifacts); -# fall back to shared on distros that don't ship libboost_*.a (CentOS/RHEL). -# Override via env: BOOST_USE_STATIC_LIBS={auto,on,off} -# auto (default) — probe the compiler for the static archives moxygen needs -# on — force static (cmake configure fails loudly if missing) -# off — force shared -# -# The auto probe asks the compiler driver to resolve each required Boost -# component's static archive via its own library search paths (no hardcoded -# paths, no distro sniffing). -print-file-name echoes back an absolute path -# when the archive exists and the bare name otherwise. We require *every* -# component moxygen links (standalone/CMakeLists.txt) to be present, since a -# single missing .a breaks the static link at ninja time -- which is what -# `cmake --find-package -DMODE=EXIST` failed to catch (it only checks that -# Boost is locatable at all and ignores Boost_USE_STATIC_LIBS). -# -# We always pass the flag explicitly (ON or OFF), never omit it: a re-run of -# `setup` reuses the existing build dir, and omitting would leave a stale -# Boost_USE_STATIC_LIBS=ON in CMakeCache.txt in force. -case "${BOOST_USE_STATIC_LIBS:-auto}" in - on|ON|1|true) - boost_static=ON - ;; - off|OFF|0|false) - boost_static=OFF - ;; - auto|Auto|AUTO|"") - boost_static=ON - for comp in context filesystem program_options regex; do - loc=$("${CXX:-c++}" -print-file-name="libboost_${comp}.a" 2>/dev/null) - if [[ "$loc" != /* ]]; then - boost_static=OFF - break - fi - done - ;; - *) - echo "ERROR: BOOST_USE_STATIC_LIBS must be auto|on|off (got '${BOOST_USE_STATIC_LIBS}')" >&2 - exit 1 - ;; -esac -BOOST_STATIC_ARG=(-DBoost_USE_STATIC_LIBS="${boost_static}") -echo "==> Boost linking: ${boost_static} (static libs)" - -# Wipe any previous install BEFORE configuring, not just before installing. -# CMake implicitly searches CMAKE_INSTALL_PREFIX for packages, so a stale -# install tree (e.g. from a prior tarball setup) satisfies find_package() -# probes with files this build then deletes — moxygen's gtest probe "found" -# the tarball's GTestConfig.cmake here, skipped bundling GoogleTest, and the -# final install shipped no GTest config, breaking moqx's configure. -echo "==> Removing previous install at $INSTALL_DIR..." -rm -rf "$INSTALL_DIR" - -echo "==> Configuring standalone moxygen build (profile: ${PROFILE})..." -# BUILD_TESTS=ON: gates the GoogleTest FetchContent in moxygen's standalone -# CMake. Without it, moxygen-install ships no GTest config and moqx's -# find_package(GTest REQUIRED CONFIG) fails at configure. -cmake -S "$STANDALONE_SRC" -B "$BUILD_DIR" \ - -G Ninja \ - -DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE" \ - -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ - -DINSTALL_DEPS=ON \ - -DBUILD_TESTS=ON \ - -DBUILD_SAMPLES=ON \ - -DBUILD_SHARED_LIBS=OFF \ - "${BOOST_STATIC_ARG[@]}" \ - "${EXTRA_CMAKE_ARGS[@]+"${EXTRA_CMAKE_ARGS[@]}"}" - -echo "==> Building ($JOB_COUNT jobs)..." -cmake --build "$BUILD_DIR" -j"$JOB_COUNT" - -echo "==> Installing to $INSTALL_DIR..." -cmake --install "$BUILD_DIR" - -# ── Write cmake_prefix_path.txt ─────────────────────────────────────────────── - -mkdir -p "$SCRATCH" -echo "$INSTALL_DIR" > "$PREFIX_PATH_FILE" - -echo "from-source" > "${SCRATCH}/deps-mode" - -NLIBS=$(find "$INSTALL_DIR/lib" -name '*.a' 2>/dev/null | wc -l) -echo "==> Done: $NLIBS static libs in $INSTALL_DIR" diff --git a/scripts/setup-deps-tarball.sh b/scripts/setup-deps-tarball.sh deleted file mode 100755 index 636ea8dbb..000000000 --- a/scripts/setup-deps-tarball.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash -# setup-deps-tarball.sh — Populate .scratch with prebuilt moxygen release artifacts. -# -# Downloads the moxygen `snapshot-latest` GitHub release tarball matching -# the current platform. By default, verifies the snapshot commit matches -# the submodule SHA (the moxygen-sync workflow keeps these aligned); on -# mismatch, exits non-zero so the caller (build.sh setup) can fall back -# to setup-deps-standalone.sh (source build). -# -# Pass --use-latest (or set MOQX_TARBALL_USE_LATEST=1) to bypass the SHA -# check and use the snapshot regardless of submodule pin. This is useful -# for quick development against the current moxygen tip when you don't -# need exact submodule reproducibility. -# -# Anonymous downloads from the public moxygen repo — no authentication -# required, works for fork PRs. -# -# Usage: -# ./scripts/setup-deps-tarball.sh [--use-latest] -# -# Requires: curl, deps/moxygen submodule initialized. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SCRATCH="${MOQX_SCRATCH_PATH:-${PROJECT_ROOT}/.scratch}" -MOXYGEN_DIR="${PROJECT_ROOT}/deps/moxygen" -MOXYGEN_REPO="${MOQX_MOXYGEN_REPO:-openmoq/moxygen}" -RELEASE_TAG="${MOQX_MOXYGEN_RELEASE_TAG:-snapshot-latest}" -USE_LATEST="${MOQX_TARBALL_USE_LATEST:-}" - -while [[ $# -gt 0 ]]; do - case "$1" in - --use-latest) USE_LATEST=1; shift ;; - -h|--help) - sed -n '2,22p' "$0" | sed 's/^# \?//' - exit 0 - ;; - *) - echo "Error: unknown argument: $1" >&2 - echo "Usage: $0 [--use-latest]" >&2 - exit 2 - ;; - esac -done - -if [[ ! -e "$MOXYGEN_DIR/.git" ]]; then - echo "Error: deps/moxygen submodule not initialized." >&2 - echo " Run: git submodule update --init" >&2 - exit 1 -fi - -# ── Platform detection ──────────────────────────────────────────────────────── -# Shared with setup-deps-dev-artifact.sh; handles Ubuntu/Debian derivatives -# (Linux Mint, Pop!_OS, ...) via ID_LIKE. See detect-platform.sh. - -# shellcheck source=scripts/detect-platform.sh -source "$SCRIPT_DIR/detect-platform.sh" - -PLATFORM="${MOQX_PLATFORM:-$(detect_platform)}" -echo "==> Platform: $PLATFORM" - -# ── Verify snapshot SHA matches submodule ───────────────────────────────────── - -SUBMODULE_SHA=$(git -C "$MOXYGEN_DIR" rev-parse HEAD) -echo "==> Moxygen submodule SHA: ${SUBMODULE_SHA:0:7}" - -echo "==> Fetching ${RELEASE_TAG} release metadata..." -# Authenticate when GITHUB_TOKEN/GH_TOKEN is set (in CI). The api.github.com -# unauthenticated rate limit (60/hour/IP) is exhausted quickly on shared -# runners, especially macOS. Authenticated requests get 1000+/hour, which -# applies even to read-only fork-PR GITHUB_TOKENs. -RELEASE_API_URL="https://api.github.com/repos/${MOXYGEN_REPO}/releases/tags/${RELEASE_TAG}" -TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-}}" -if [[ -n "$TOKEN" ]]; then - RELEASE_JSON=$(curl -fsSL -H "Authorization: Bearer ${TOKEN}" "$RELEASE_API_URL") || { - echo "Error: failed to fetch release metadata for ${MOXYGEN_REPO}@${RELEASE_TAG}" >&2 - exit 1 - } -else - RELEASE_JSON=$(curl -fsSL "$RELEASE_API_URL") || { - echo "Error: failed to fetch release metadata for ${MOXYGEN_REPO}@${RELEASE_TAG}" >&2 - echo " (no GITHUB_TOKEN set; api.github.com unauthenticated rate limit may be exhausted)" >&2 - exit 1 - } -fi - -# Extract embedded commit SHA from release body. publish-artifacts.sh writes -# `**Commit:** \`\`` into the body — match the first 40-hex backtick group. -SNAPSHOT_SHA=$(printf '%s' "$RELEASE_JSON" | grep -oE '`[a-f0-9]{40}`' | head -1 | tr -d '`') - -if [[ -z "$SNAPSHOT_SHA" ]]; then - echo "Error: could not parse snapshot commit SHA from release body" >&2 - exit 1 -fi - -echo "==> Snapshot release SHA: ${SNAPSHOT_SHA:0:7}" - -if [[ "$SNAPSHOT_SHA" != "$SUBMODULE_SHA" ]]; then - if [[ -n "$USE_LATEST" ]]; then - echo "Warning: snapshot SHA does not match submodule pin." >&2 - echo " submodule: ${SUBMODULE_SHA}" >&2 - echo " snapshot: ${SNAPSHOT_SHA}" >&2 - echo " --use-latest set; proceeding with snapshot anyway." >&2 - else - echo "Error: ${RELEASE_TAG} does not match the moxygen submodule pin." >&2 - echo " submodule: ${SUBMODULE_SHA}" >&2 - echo " snapshot: ${SNAPSHOT_SHA}" >&2 - echo " The moxygen-sync workflow normally keeps these aligned." >&2 - echo " Options:" >&2 - echo " scripts/build.sh setup --from-source # build pinned SHA from source" >&2 - echo " scripts/build.sh setup --use-latest # use snapshot anyway" >&2 - echo " git submodule update --remote deps/moxygen && git add --force deps/moxygen" >&2 - exit 1 - fi -fi - -# ── Download tarball ────────────────────────────────────────────────────────── - -TARBALL="moxygen-${PLATFORM}.tar.gz" -DOWNLOAD_DIR="${SCRATCH}/downloads" -mkdir -p "$DOWNLOAD_DIR" - -DOWNLOAD_URL="https://github.com/${MOXYGEN_REPO}/releases/download/${RELEASE_TAG}/${TARBALL}" -echo "==> Downloading $TARBALL..." -rm -f "${DOWNLOAD_DIR}/${TARBALL}" -curl -fsSL "$DOWNLOAD_URL" -o "${DOWNLOAD_DIR}/${TARBALL}" || { - echo "Error: failed to download ${DOWNLOAD_URL}" >&2 - echo " (the platform tarball may not be in this snapshot)" >&2 - exit 1 -} - -# ── Extract ─────────────────────────────────────────────────────────────────── - -INSTALL_DIR="${SCRATCH}/moxygen-install" -echo "==> Extracting to $INSTALL_DIR..." -rm -rf "$INSTALL_DIR" -mkdir -p "$INSTALL_DIR" -tar xzf "${DOWNLOAD_DIR}/${TARBALL}" -C "$INSTALL_DIR" - - -# ── Write cmake_prefix_path.txt ─────────────────────────────────────────────── - -echo "$INSTALL_DIR" > "${SCRATCH}/cmake_prefix_path.txt" -echo "tarball" > "${SCRATCH}/deps-mode" - -NLIBS=$(find "$INSTALL_DIR/lib" -name '*.a' 2>/dev/null | wc -l) -echo "==> Done: $NLIBS static libs in $INSTALL_DIR" diff --git a/scripts/test.sh b/scripts/test.sh index c0ca0eb35..c7f30fd23 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,5 +1,36 @@ #!/usr/bin/env bash +# test.sh — run the moqx test suite. +# +# Usage: test.sh [PROFILE] [CTEST_ARGS...] +# PROFILE: default | san | tsan, or any preset from CMakeUserPresets.json +# test.sh # all tests (default profile) +# test.sh san # all tests, sanitizer build +# test.sh default -R cache # only tests matching 'cache' +# +# Env: MOQX_TEST_JOBS caps ctest parallelism (default: MOQX_BUILD_JOBS, else all +# cores). Lower it for sanitizer builds: instrumented binaries are slow enough +# that a full-parallel run starves the timing-sensitive integration tests. set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" -BUILD_DIR=${1:-build} -ctest --test-dir "${BUILD_DIR}" --output-on-failure --parallel +die() { echo "test.sh: $*" >&2; exit 1; } +. "$ROOT/scripts/lib/jobs.sh" + +case "${1:-}" in + -h|--help) awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "${BASH_SOURCE[0]}"; exit 0 ;; +esac + +profile="default" +if (($#)) && [[ "$1" != -* ]]; then profile="$1"; shift; fi +build_dir="build/$profile" +[[ -f "$build_dir/CMakeCache.txt" ]] \ + || die "no configured build dir '$build_dir' — build first (scripts/configure.sh $profile --moxygen …, scripts/build.sh $profile)" + +# resolve_jobs, so a typo'd count is rejected here rather than by ctest, and so +# MOQX_TEST_JOBS/MOQX_BUILD_JOBS mean the same thing they do in the sibling +# scripts. No RAM derate: running tests is not compiling them. +jobs="$(resolve_jobs "${MOQX_TEST_JOBS:-}")" +# --parallel: shell tests carry unique ports so they can run concurrently. +# Explicit job count — a bare --parallel would swallow the next argument. +ctest --test-dir "$build_dir" --output-on-failure --parallel "$jobs" "$@" diff --git a/src/MoqxCache.cpp b/src/MoqxCache.cpp index cd2ea0350..6c0e6cd8d 100644 --- a/src/MoqxCache.cpp +++ b/src/MoqxCache.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/src/MoqxCache.h b/src/MoqxCache.h index 24125126d..c715abebe 100644 --- a/src/MoqxCache.h +++ b/src/MoqxCache.h @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/src/MoqxRelay.cpp b/src/MoqxRelay.cpp index 0f76a05b2..c0f58480f 100644 --- a/src/MoqxRelay.cpp +++ b/src/MoqxRelay.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/src/MoqxRelay.h b/src/MoqxRelay.h index 74c176471..35408e2d6 100644 --- a/src/MoqxRelay.h +++ b/src/MoqxRelay.h @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/superbuild/CMakeLists.txt b/superbuild/CMakeLists.txt new file mode 100644 index 000000000..6ec95f5f0 --- /dev/null +++ b/superbuild/CMakeLists.txt @@ -0,0 +1,162 @@ +# From-source build of moxygen (ExternalProject around moxygen's standalone/ +# tree) — installs a CMake package prefix into /moxygen-install for +# the moqx build to consume via find_package. moqx is NOT built here. +# Usage + knobs: superbuild/README.md; how it fits: BUILD.md#how-dependencies-work. + +cmake_minimum_required(VERSION 3.23) +project(moqx-moxygen-superbuild NONE) + +include(ExternalProject) + +get_filename_component(_top "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) + +# Same fail-fast dependency check as the moqx build — this build compiles the +# folly stack, where a missing -dev package surfaces far more cryptically. +include(${_top}/cmake/CheckSystemDeps.cmake) + +# moqx is a separate CMake build, so the moxygen source needs one stable home +# both can read — the shared root the moqx build resolves the same way. +include(${_top}/cmake/DepsCache.cmake) + +include(${_top}/cmake/CPM.cmake) +include(${_top}/cmake/dependencies.cmake) +CPMAddPackage( + NAME moxygen + GITHUB_REPOSITORY ${MOXYGEN_REPOSITORY} + GIT_TAG ${MOXYGEN_REV} + DOWNLOAD_ONLY YES # CPM only places the source; the ExternalProject builds it +) + +# CPM gives every rev its own source directory, and CMake refuses a build dir +# whose source moved ("does not match the source used to generate cache"). Drop +# the inner build when the pin changes, so a bump reconfigures instead of dying. +set(_moxygen_build "${CMAKE_BINARY_DIR}/moxygen-build") +if(EXISTS "${_moxygen_build}/CMakeCache.txt" + AND NOT "${moxygen_SOURCE_DIR}" STREQUAL "${MOQX_MOXYGEN_SOURCE_DIR}") + message(STATUS "moxygen: source moved to ${moxygen_SOURCE_DIR} — discarding ${_moxygen_build}") + file(REMOVE_RECURSE "${_moxygen_build}") +endif() +set(MOQX_MOXYGEN_SOURCE_DIR "${moxygen_SOURCE_DIR}" CACHE INTERNAL + "moxygen source the ExternalProject below last configured against") + +find_program(CCACHE_PROGRAM ccache) +set(_launchers "") +if(CCACHE_PROGRAM) + set(_launchers + -DCMAKE_C_COMPILER_LAUNCHER=${CCACHE_PROGRAM} + -DCMAKE_CXX_COMPILER_LAUNCHER=${CCACHE_PROGRAM}) +endif() + +# Mirrors moqx's san/tsan presets. moxygen has no sanitizer option, so it takes +# the raw flags, joined into strings to cross the ExternalProject boundary. +include(${_top}/cmake/SanitizerFlags.cmake) +set(MOQX_MOXYGEN_PROFILE "default" CACHE STRING + "moxygen build profile: default | san | tsan") +set(_build_type RelWithDebInfo) +set(_san_flags "") # -fsanitize=... for moxygen's compile + link +if(MOQX_MOXYGEN_PROFILE STREQUAL "default") +elseif(MOQX_MOXYGEN_PROFILE STREQUAL "san") + set(_build_type Debug) + string(JOIN " " _san_flags ${MOQX_ASAN_DEPS_FLAGS}) +elseif(MOQX_MOXYGEN_PROFILE STREQUAL "tsan") + set(_build_type Debug) + string(JOIN " " _san_flags ${MOQX_TSAN_FLAGS}) +else() + message(FATAL_ERROR "MOQX_MOXYGEN_PROFILE must be default|san|tsan (got '${MOQX_MOXYGEN_PROFILE}')") +endif() +set(_moxygen_install "${CMAKE_BINARY_DIR}/moxygen-install") +message(STATUS "moxygen will install to ${_moxygen_install}") + +# moxygen compile/link flags: sanitizers, plus a macOS fmt workaround. Apple +# Clang 21+ rejects fmt 10.2.1's consteval format-string check (fmtlib/fmt#4740); +# applied on all macOS since project(NONE) leaves no compiler version to gate on. +set(_moxygen_cxx "${_san_flags}") +if(APPLE) + string(APPEND _moxygen_cxx " -DFMT_CONSTEVAL=") +endif() +set(_moxygen_flags "") +if(NOT _san_flags STREQUAL "") + list(APPEND _moxygen_flags "-DCMAKE_C_FLAGS=${_san_flags}" + "-DCMAKE_EXE_LINKER_FLAGS=${_san_flags}" "-DCMAKE_SHARED_LINKER_FLAGS=${_san_flags}") +endif() +string(STRIP "${_moxygen_cxx}" _moxygen_cxx) +if(NOT _moxygen_cxx STREQUAL "") + list(APPEND _moxygen_flags "-DCMAKE_CXX_FLAGS=${_moxygen_cxx}") +endif() + +# Editing a local moxygen checkout needs the build re-run on every `cmake --build` +# — ExternalProject stamps the build, so source edits alone don't invalidate it. +option(MOQX_MOXYGEN_BUILD_ALWAYS + "Rebuild moxygen on every build (for editing a local moxygen checkout)" OFF) + +# Static archives make the more portable install, but Fedora/RHEL package no +# static Boost, so under "auto" the compiler driver decides. Both spellings, -D +# and env, are validated rather than passed through: "auto" reaching +# Boost_USE_STATIC_LIBS below would read as truthy and force static anyway. +if(DEFINED BOOST_USE_STATIC_LIBS) + set(_boost_req "${BOOST_USE_STATIC_LIBS}") +else() + set(_boost_req "$ENV{BOOST_USE_STATIC_LIBS}") +endif() +string(TOLOWER "${_boost_req}" _boost_req) +if(_boost_req MATCHES "^(on|1|true|yes)$") + set(BOOST_USE_STATIC_LIBS ON) +elseif(_boost_req MATCHES "^(off|0|false|no)$") + set(BOOST_USE_STATIC_LIBS OFF) +elseif(_boost_req STREQUAL "" OR _boost_req STREQUAL "auto") + find_program(_moqx_cxx NAMES $ENV{CXX} c++ g++ clang++) + set(BOOST_USE_STATIC_LIBS ON) + if(_moqx_cxx) + # MOQX_BOOST_COMPONENTS comes from cmake/CheckSystemDeps.cmake, above. + foreach(_comp IN LISTS MOQX_BOOST_COMPONENTS) + execute_process(COMMAND "${_moqx_cxx}" -print-file-name=libboost_${_comp}.a + OUTPUT_VARIABLE _loc OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(NOT _loc MATCHES "^/") + set(BOOST_USE_STATIC_LIBS OFF) + break() + endif() + endforeach() + endif() +else() + message(FATAL_ERROR + "BOOST_USE_STATIC_LIBS must be auto|on|off (got '${_boost_req}')") +endif() +message(STATUS "moxygen superbuild: Boost static libs: ${BOOST_USE_STATIC_LIBS}") + +# INSTALL_DEPS ships folly and friends into the prefix. BUILD_TESTS gates the +# bundled GTest config that moqx's find_package(GTest) needs. BUILD_SAMPLES gives +# the integration tests the binaries they reach through MOXYGEN_BIN_DIR. +ExternalProject_Add(moxygen + SOURCE_DIR "${moxygen_SOURCE_DIR}" + SOURCE_SUBDIR standalone + # Named rather than left at ExternalProject's default, so the pin-change wipe + # above owns the path instead of reaching into moxygen-prefix/src/. + BINARY_DIR "${_moxygen_build}" + INSTALL_DIR "${_moxygen_install}" + BUILD_ALWAYS ${MOQX_MOXYGEN_BUILD_ALWAYS} + USES_TERMINAL_CONFIGURE ON + USES_TERMINAL_BUILD ON + USES_TERMINAL_INSTALL ON + # Replace the prefix, don't merge into it: a rev bump reuses this build dir, + # and anything the new moxygen no longer installs would linger for moqx's + # find_package and includes to pick up. + INSTALL_COMMAND ${CMAKE_COMMAND} -E rm -rf + COMMAND ${CMAKE_COMMAND} --install --config ${_build_type} + CMAKE_ARGS + -DCMAKE_BUILD_TYPE=${_build_type} + # INSTALL_DIR only fills the placeholder; the prefix must be + # passed explicitly or the standalone build installs into /usr/local. + -DCMAKE_INSTALL_PREFIX=${_moxygen_install} + -DINSTALL_DEPS=ON + -DBUILD_TESTS=ON + -DBUILD_SAMPLES=ON + -DBUILD_SHARED_LIBS=OFF + -DBoost_USE_STATIC_LIBS=${BOOST_USE_STATIC_LIBS} + # Never let moxygen's find_package probes satisfy themselves from a + # PREVIOUS install in this prefix (e.g. its GTest probe finding the old + # GTestConfig and skipping the bundled GoogleTest). + -DCMAKE_FIND_NO_INSTALL_PREFIX=ON + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + ${_moxygen_flags} + ${_launchers} +) diff --git a/superbuild/README.md b/superbuild/README.md new file mode 100644 index 000000000..e0816c37f --- /dev/null +++ b/superbuild/README.md @@ -0,0 +1,33 @@ +# moxygen superbuild + +Builds **moxygen from source** — the whole Meta stack (folly / fizz / wangle / +mvfst / proxygen) plus the [openmoq/picoquic](https://github.com/openmoq/picoquic) +fork — and installs it as a CMake package prefix for the moqx build to consume +via `find_package(moxygen CONFIG)`. It wraps +[moxygen's `standalone/` tree](https://github.com/openmoq/moxygen/tree/main/standalone) +as an `ExternalProject`, at the revision pinned in +[/cmake/dependencies.cmake](/cmake/dependencies.cmake). + +moqx itself is **not** built here — the prebuilt and from-source paths build +moqx identically; only the origin of the moxygen prefix differs. See +[/BUILD.md](/BUILD.md). + +## Usage + +Normally driven by [`scripts/configure.sh --moxygen from-source`](/scripts/configure.sh), +which builds this into `.scratch/moxygen-build[-]` and configures moqx +against the result. Raw equivalent: + +```bash +cmake -S superbuild -B .scratch/moxygen-build -G Ninja +cmake --build .scratch/moxygen-build # -> .scratch/moxygen-build/moxygen-install +``` + +| Knob | Effect | +|------|--------| +| `-DCPM_moxygen_SOURCE=/path` | build a local moxygen checkout | +| `-DMOQX_MOXYGEN_BUILD_ALWAYS=ON` | rebuild on every build (local-checkout iteration) | +| `-DMOQX_MOXYGEN_PROFILE=san\|tsan` | build an instrumented moxygen (matches moqx's sanitizer presets) | +| `-DBOOST_USE_STATIC_LIBS=auto\|on\|off` | override the static/shared Boost probe (`auto` = probe, the default) | + +Each knob also reads the same-named env var. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ff4461b0e..e73f4bdf5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,257 +1,154 @@ find_package(GTest REQUIRED CONFIG) include(GoogleTest) +# moqx_add_gtest( SRCS LIBS ) +# Per-test extras (compile definitions, include dirs) go on after the call. +# moqx_warnings comes with it: test code is first-party code. +function(moqx_add_gtest name) + cmake_parse_arguments(ARG "" "" "SRCS;LIBS" ${ARGN}) + add_executable(${name} ${ARG_SRCS}) + target_link_libraries(${name} PRIVATE ${ARG_LIBS} moqx_warnings) + gtest_discover_tests(${name}) +endfunction() + add_library(moqx_test_utils STATIC TestUtils.cpp ) target_include_directories(moqx_test_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) -target_link_libraries(moqx_test_utils PUBLIC - Folly::folly_io_iobuf - Folly::folly_random +target_link_libraries(moqx_test_utils + PUBLIC + Folly::folly_io_iobuf + Folly::folly_random + PRIVATE + moqx_warnings ) add_library(moqx_test_main STATIC TestMain.cpp ) -target_link_libraries(moqx_test_main PUBLIC - Folly::folly_init_init - GTest::gtest +target_link_libraries(moqx_test_main + PUBLIC + Folly::folly_init_init + GTest::gtest + PRIVATE + moqx_warnings ) add_library(moqx_test_fixture STATIC MoqxRelayTestFixture.cpp ) -target_link_libraries(moqx_test_fixture PUBLIC - moqx_core - moqx_test_utils - GTest::gmock - moxygen::moxygen_events_moq_folly_executor_impl +target_link_libraries(moqx_test_fixture + PUBLIC + moqx_core + moqx_test_utils + GTest::gmock + moxygen::moxygen_events_moq_folly_executor_impl + PRIVATE + moqx_warnings ) -add_executable(moqx_relay_test - MoqxRelayPublishTests.cpp - MoqxRelaySubscribeTests.cpp - MoqxRelaySubNsTests.cpp - MoqxRelayDataPlaneTests.cpp - MoqxRelayFetchTests.cpp - MoqxRelayTrackStatusTests.cpp - MoqxRelayNGRTests.cpp - MoqxRelayPeerTests.cpp - MoqxRelayTracksTests.cpp - MoqxRelayTestModes.cpp -) -target_link_libraries(moqx_relay_test PRIVATE - moqx_test_fixture - moqx_test_main +moqx_add_gtest(moqx_relay_test + SRCS + MoqxRelayPublishTests.cpp + MoqxRelaySubscribeTests.cpp + MoqxRelaySubNsTests.cpp + MoqxRelayDataPlaneTests.cpp + MoqxRelayFetchTests.cpp + MoqxRelayTrackStatusTests.cpp + MoqxRelayNGRTests.cpp + MoqxRelayPeerTests.cpp + MoqxRelayTracksTests.cpp + MoqxRelayTestModes.cpp + LIBS moqx_test_fixture moqx_test_main ) -gtest_discover_tests(moqx_relay_test) -add_executable(moqx_namespace_tree_test - NamespaceTreeTest.cpp +moqx_add_gtest(moqx_namespace_tree_test + SRCS NamespaceTreeTest.cpp + LIBS moqx_core moqx_test_main GTest::gmock ) -target_link_libraries(moqx_namespace_tree_test PRIVATE - moqx_core - moqx_test_main - GTest::gmock -) -gtest_discover_tests(moqx_namespace_tree_test) -add_executable(moqx_config_test - config/LoaderTest.cpp -) -target_link_libraries(moqx_config_test PRIVATE - moqx_config_loader - GTest::gtest_main - GTest::gmock +moqx_add_gtest(moqx_config_test + SRCS config/LoaderTest.cpp + LIBS moqx_config_loader GTest::gtest_main GTest::gmock ) target_compile_definitions(moqx_config_test PRIVATE CONFIG_EXAMPLE_PATH="${PROJECT_SOURCE_DIR}/config.example.yaml" ) -if(NOT GFLAGS_SHARED) - # Force gflags's DEFINE_FLAG-registered .o files into the binary, even - # though the test code doesn't reference gflags symbols directly. Cross- - # platform raw linker flags — works with cmake 3.22, unlike the - # $ genex which needs cmake 3.24+. - if(APPLE) - target_link_libraries(moqx_config_test PRIVATE - "-Wl,-force_load,$" - ) - else() - target_link_libraries(moqx_config_test PRIVATE - "-Wl,--whole-archive" - gflags_nothreads_static - "-Wl,--no-whole-archive" - ) - endif() -endif() -gtest_discover_tests(moqx_config_test) - -add_executable(moqx_config_resolver_test - config/ConfigResolverTest.cpp -) -target_link_libraries(moqx_config_resolver_test PRIVATE - moqx_config_loader - OpenSSL::Crypto - GTest::gtest_main - GTest::gmock -) -if(NOT GFLAGS_SHARED) - if(APPLE) - target_link_libraries(moqx_config_resolver_test PRIVATE - "-Wl,-force_load,$" - ) - else() - target_link_libraries(moqx_config_resolver_test PRIVATE - "-Wl,--whole-archive" - gflags_nothreads_static - "-Wl,--no-whole-archive" - ) - endif() -endif() -gtest_discover_tests(moqx_config_resolver_test) - -add_executable(moqx_config_serializer_test - config/ConfigSerializerTest.cpp -) -target_link_libraries(moqx_config_serializer_test PRIVATE - moqx_config_loader - GTest::gtest_main - GTest::gmock -) -gtest_discover_tests(moqx_config_serializer_test) - -add_executable(moqx_pkcs12_test - config/Pkcs12Test.cpp -) -target_link_libraries(moqx_pkcs12_test PRIVATE - moqx_config_loader - OpenSSL::Crypto - GTest::gtest_main - GTest::gmock -) -if(NOT GFLAGS_SHARED) - if(APPLE) - target_link_libraries(moqx_pkcs12_test PRIVATE - "-Wl,-force_load,$" - ) - else() - target_link_libraries(moqx_pkcs12_test PRIVATE - "-Wl,--whole-archive" - gflags_nothreads_static - "-Wl,--no-whole-archive" - ) - endif() -endif() -gtest_discover_tests(moqx_pkcs12_test) - -add_executable(moqx_bounded_histogram_test - stats/BoundedHistogramTest.cpp -) -target_include_directories(moqx_bounded_histogram_test PRIVATE - ${PROJECT_SOURCE_DIR}/src -) -target_link_libraries(moqx_bounded_histogram_test PRIVATE - GTest::gtest_main - GTest::gmock -) -gtest_discover_tests(moqx_bounded_histogram_test) -add_executable(moqx_logging_multi_flag_test - LoggingMultiFlagTest.cpp -) -target_include_directories(moqx_logging_multi_flag_test PRIVATE - ${PROJECT_SOURCE_DIR}/src -) -target_link_libraries(moqx_logging_multi_flag_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock +moqx_add_gtest(moqx_config_resolver_test + SRCS config/ConfigResolverTest.cpp + LIBS moqx_config_loader OpenSSL::Crypto GTest::gtest_main GTest::gmock ) -gtest_discover_tests(moqx_logging_multi_flag_test) -add_executable(moqx_pico_quic_stats_test - stats/PicoQuicStatsCollectorTest.cpp +moqx_add_gtest(moqx_config_serializer_test + SRCS config/ConfigSerializerTest.cpp + LIBS moqx_config_loader GTest::gtest_main GTest::gmock ) -target_include_directories(moqx_pico_quic_stats_test PRIVATE - ${PROJECT_SOURCE_DIR}/src -) -target_link_libraries(moqx_pico_quic_stats_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock + +moqx_add_gtest(moqx_pkcs12_test + SRCS config/Pkcs12Test.cpp + LIBS moqx_config_loader OpenSSL::Crypto GTest::gtest_main GTest::gmock ) -gtest_discover_tests(moqx_pico_quic_stats_test) -add_executable(moqx_moq_stats_collector_test - stats/MoQStatsCollectorTest.cpp +# Header-only, tested in isolation — needs the src/ headers without moqx_core. +moqx_add_gtest(moqx_bounded_histogram_test + SRCS stats/BoundedHistogramTest.cpp + LIBS GTest::gtest_main GTest::gmock ) -target_include_directories(moqx_moq_stats_collector_test PRIVATE +target_include_directories(moqx_bounded_histogram_test PRIVATE ${PROJECT_SOURCE_DIR}/src ) -target_link_libraries(moqx_moq_stats_collector_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock -) -gtest_discover_tests(moqx_moq_stats_collector_test) -add_executable(moqx_service_matcher_test - ServiceMatcherTest.cpp +moqx_add_gtest(moqx_logging_multi_flag_test + SRCS LoggingMultiFlagTest.cpp + LIBS moqx_core GTest::gtest_main GTest::gmock ) -target_link_libraries(moqx_service_matcher_test PRIVATE - moqx_core - moqx_config - GTest::gtest_main - GTest::gmock + +moqx_add_gtest(moqx_pico_quic_stats_test + SRCS stats/PicoQuicStatsCollectorTest.cpp + LIBS moqx_core GTest::gtest_main GTest::gmock ) -gtest_discover_tests(moqx_service_matcher_test) -add_executable(moqx_auth_test - AuthTest.cpp +moqx_add_gtest(moqx_moq_stats_collector_test + SRCS stats/MoQStatsCollectorTest.cpp + LIBS moqx_core GTest::gtest_main GTest::gmock ) -target_link_libraries(moqx_auth_test PRIVATE - moqx_core - moqx_issuer_lib - GTest::gtest_main + +moqx_add_gtest(moqx_service_matcher_test + SRCS ServiceMatcherTest.cpp + LIBS moqx_core moqx_config GTest::gtest_main GTest::gmock ) -gtest_discover_tests(moqx_auth_test) -add_executable(moqx_auth_token_issuer_test - AuthTokenIssuerTest.cpp +moqx_add_gtest(moqx_auth_test + SRCS AuthTest.cpp + LIBS moqx_core moqx_issuer_lib GTest::gtest_main ) -target_link_libraries(moqx_auth_token_issuer_test PRIVATE - moqx_core - moqx_issuer_lib - GTest::gtest_main + +moqx_add_gtest(moqx_auth_token_issuer_test + SRCS AuthTokenIssuerTest.cpp + LIBS moqx_core moqx_issuer_lib GTest::gtest_main ) -gtest_discover_tests(moqx_auth_token_issuer_test) -# CborReader unit tests (header-only decoder, tested in isolation) -add_executable(moqx_cbor_reader_test - CborReaderTest.cpp +# Header-only decoder, tested in isolation — src/ headers without moqx_core. +moqx_add_gtest(moqx_cbor_reader_test + SRCS CborReaderTest.cpp + LIBS GTest::gtest_main GTest::gmock ) target_include_directories(moqx_cbor_reader_test PRIVATE ${PROJECT_SOURCE_DIR}/src ) -target_link_libraries(moqx_cbor_reader_test PRIVATE - GTest::gtest_main - GTest::gmock -) -gtest_discover_tests(moqx_cbor_reader_test) -add_executable(moqx_relay_context_test - MoqxRelayContextTest.cpp -) -target_link_libraries(moqx_relay_context_test PRIVATE - moqx_core - moqx_test_utils - moqx_test_main - GTest::gmock - moxygen::moxygen_events_moq_folly_executor_impl +moqx_add_gtest(moqx_relay_context_test + SRCS MoqxRelayContextTest.cpp + LIBS + moqx_core + moqx_test_utils + moqx_test_main + GTest::gmock + moxygen::moxygen_events_moq_folly_executor_impl ) -gtest_discover_tests(moqx_relay_context_test) add_test( NAME relay_chain @@ -259,7 +156,7 @@ add_test( ) set_tests_properties(relay_chain PROPERTIES TIMEOUT 120 - ENVIRONMENT "MOQBIN=${PROJECT_SOURCE_DIR}/.scratch/moxygen-install/bin" + ENVIRONMENT "MOQBIN=${MOXYGEN_BIN_DIR}" ) add_test( @@ -269,7 +166,7 @@ add_test( set_tests_properties(qmux_relay PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 - ENVIRONMENT "MOQBIN=${PROJECT_SOURCE_DIR}/.scratch/moxygen-install/bin" + ENVIRONMENT "MOQBIN=${MOXYGEN_BIN_DIR}" ) add_test( @@ -296,73 +193,47 @@ add_test( NAME admin_cache_purge_concurrency_test COMMAND bash ${PROJECT_SOURCE_DIR}/test/test_admin_cache_purge_race.sh $ ) - -add_executable(moqx_upstream_provider_test - UpstreamProviderTest.cpp +set_tests_properties(admin_cache_purge_concurrency_test PROPERTIES + ENVIRONMENT "MOQBIN=${MOXYGEN_BIN_DIR}" ) -target_link_libraries(moqx_upstream_provider_test PRIVATE - moqx_core - moqx_test_utils - moqx_test_main - moxygen::moxygen_events_moq_folly_executor_impl - moxygen::moxygen_moqclient - GTest::gmock -) -gtest_discover_tests(moqx_upstream_provider_test) -# SubscriptionRegistry unit tests -add_executable(moqx_subscription_registry_test - SubscriptionRegistryTest.cpp -) -target_link_libraries(moqx_subscription_registry_test PRIVATE - moqx_core - moqx_test_main - GTest::gmock +moqx_add_gtest(moqx_upstream_provider_test + SRCS UpstreamProviderTest.cpp + LIBS + moqx_core + moqx_test_utils + moqx_test_main + moxygen::moxygen_events_moq_folly_executor_impl + moxygen::moxygen_moqclient + GTest::gmock ) -gtest_discover_tests(moqx_subscription_registry_test) -# TopNFilter unit tests -add_executable(moqx_topn_filter_test - TopNFilterTest.cpp +moqx_add_gtest(moqx_subscription_registry_test + SRCS SubscriptionRegistryTest.cpp + LIBS moqx_core moqx_test_main GTest::gmock ) -target_link_libraries(moqx_topn_filter_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock -) -gtest_discover_tests(moqx_topn_filter_test) -# PropertyRanking base unit tests (no self-exclusion) -add_executable(moqx_property_ranking_base_test - PropertyRankingBaseTest.cpp -) -target_link_libraries(moqx_property_ranking_base_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock +moqx_add_gtest(moqx_topn_filter_test + SRCS TopNFilterTest.cpp + LIBS moqx_core GTest::gtest_main GTest::gmock ) -gtest_discover_tests(moqx_property_ranking_base_test) -# PropertyRanking self-exclusion / waterline unit tests -add_executable(moqx_property_ranking_self_exclusion_test - PropertyRankingSelfExclusionTest.cpp +# PropertyRanking base (no self-exclusion) vs. self-exclusion/waterline split. +moqx_add_gtest(moqx_property_ranking_base_test + SRCS PropertyRankingBaseTest.cpp + LIBS moqx_core GTest::gtest_main GTest::gmock ) -target_link_libraries(moqx_property_ranking_self_exclusion_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock + +moqx_add_gtest(moqx_property_ranking_self_exclusion_test + SRCS PropertyRankingSelfExclusionTest.cpp + LIBS moqx_core GTest::gtest_main GTest::gmock ) -gtest_discover_tests(moqx_property_ranking_self_exclusion_test) # TRACK_FILTER integration tests (in-process relay, no live server) -add_executable(moqx_track_filter_test - MoqxTrackFilterTest.cpp -) -target_link_libraries(moqx_track_filter_test PRIVATE - moqx_test_fixture - moqx_test_main +moqx_add_gtest(moqx_track_filter_test + SRCS MoqxTrackFilterTest.cpp + LIBS moqx_test_fixture moqx_test_main ) -gtest_discover_tests(moqx_track_filter_test) # TrackFilterLoadTest: requires a live relay process. # Run manually: ./track_filter_load_test --relay_url=https://localhost:9668/moq-relay @@ -382,65 +253,46 @@ target_link_libraries(track_filter_load_test PRIVATE moxygen::moxygen_util_insecure_verifier_dangerous_do_not_use_in_production ) -# CrossExecFilter unit tests -add_executable(moqx_cross_exec_filter_test - CrossExecFilterTest.cpp -) -target_link_libraries(moqx_cross_exec_filter_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock - moxygen::moqtest_utils - Folly::folly_executors_manual_executor +moqx_add_gtest(moqx_cross_exec_filter_test + SRCS CrossExecFilterTest.cpp + LIBS + moqx_core + GTest::gtest_main + GTest::gmock + moxygen::moqtest_utils + Folly::folly_executors_manual_executor +) + +moqx_add_gtest(moqx_publisher_cross_exec_filter_test + SRCS PublisherCrossExecFilterTest.cpp + LIBS + moqx_core + GTest::gtest_main + GTest::gmock + moxygen::moqtest_utils + Folly::folly_executors_manual_executor + Folly::folly_executors_cpu_thread_pool_executor +) + +moqx_add_gtest(moqx_subscriber_cross_exec_filter_test + SRCS SubscriberCrossExecFilterTest.cpp + LIBS + moqx_core + GTest::gtest_main + GTest::gmock + moxygen::moqtest_utils + Folly::folly_executors_manual_executor + Folly::folly_executors_cpu_thread_pool_executor +) + +moqx_add_gtest(moqx_cache_test + SRCS MoqxCacheTest.cpp + LIBS + moqx_cache + moqx_test_utils + moqx_test_main + moxygen::moqtest_utils + GTest::gmock + Folly::folly_coro_gtest_helpers + Folly::folly_io_async_scoped_event_base_thread ) -gtest_discover_tests(moqx_cross_exec_filter_test) - -# PublisherCrossExecFilter unit tests -add_executable(moqx_publisher_cross_exec_filter_test - PublisherCrossExecFilterTest.cpp -) -target_link_libraries(moqx_publisher_cross_exec_filter_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock - moxygen::moqtest_utils - Folly::folly_executors_manual_executor - Folly::folly_executors_cpu_thread_pool_executor -) -gtest_discover_tests(moqx_publisher_cross_exec_filter_test) - -# SubscriberCrossExecFilter unit tests -add_executable(moqx_subscriber_cross_exec_filter_test - SubscriberCrossExecFilterTest.cpp -) -target_link_libraries(moqx_subscriber_cross_exec_filter_test PRIVATE - moqx_core - GTest::gtest_main - GTest::gmock - moxygen::moqtest_utils - Folly::folly_executors_manual_executor - Folly::folly_executors_cpu_thread_pool_executor -) -gtest_discover_tests(moqx_subscriber_cross_exec_filter_test) - -# --- MoqxCache tests --- - -add_executable(moqx_cache_test - MoqxCacheTest.cpp -) - -target_link_libraries(moqx_cache_test PRIVATE - moqx_cache - moqx_test_utils - moqx_test_main - moxygen::moqtest_utils - GTest::gmock - Folly::folly_coro_gtest_helpers - Folly::folly_io_async_scoped_event_base_thread -) - -target_compile_options(moqx_cache_test PRIVATE - -Wall -Wextra -Wpedantic -) - -gtest_discover_tests(moqx_cache_test) diff --git a/test/MoqxCacheTest.cpp b/test/MoqxCacheTest.cpp index d4fbc057b..c45ecd2f9 100644 --- a/test/MoqxCacheTest.cpp +++ b/test/MoqxCacheTest.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayDataPlaneTests.cpp b/test/MoqxRelayDataPlaneTests.cpp index ebe7d3e6a..0e1ef4267 100644 --- a/test/MoqxRelayDataPlaneTests.cpp +++ b/test/MoqxRelayDataPlaneTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayFetchTests.cpp b/test/MoqxRelayFetchTests.cpp index 6fb80a803..b2d8cb553 100644 --- a/test/MoqxRelayFetchTests.cpp +++ b/test/MoqxRelayFetchTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayNGRTests.cpp b/test/MoqxRelayNGRTests.cpp index 4603211a0..ba0a1fded 100644 --- a/test/MoqxRelayNGRTests.cpp +++ b/test/MoqxRelayNGRTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayPeerTests.cpp b/test/MoqxRelayPeerTests.cpp index cdcc593f8..00bd7f900 100644 --- a/test/MoqxRelayPeerTests.cpp +++ b/test/MoqxRelayPeerTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayPublishTests.cpp b/test/MoqxRelayPublishTests.cpp index 4231322c2..be2b8f5ee 100644 --- a/test/MoqxRelayPublishTests.cpp +++ b/test/MoqxRelayPublishTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelaySubNsTests.cpp b/test/MoqxRelaySubNsTests.cpp index f5672bf2b..6f2006c82 100644 --- a/test/MoqxRelaySubNsTests.cpp +++ b/test/MoqxRelaySubNsTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelaySubscribeTests.cpp b/test/MoqxRelaySubscribeTests.cpp index 521a8a551..3aa19ae7e 100644 --- a/test/MoqxRelaySubscribeTests.cpp +++ b/test/MoqxRelaySubscribeTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayTestFixture.cpp b/test/MoqxRelayTestFixture.cpp index af1b9cd12..5fc70fb09 100644 --- a/test/MoqxRelayTestFixture.cpp +++ b/test/MoqxRelayTestFixture.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayTestFixture.h b/test/MoqxRelayTestFixture.h index ad2196e86..6aa9e4c50 100644 --- a/test/MoqxRelayTestFixture.h +++ b/test/MoqxRelayTestFixture.h @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ @@ -58,7 +59,10 @@ inline const TrackNamespace kTestNamespace{{"test", "namespace"}}; inline const TrackNamespace kAllowedPrefix{{"test"}}; inline const FullTrackName kTestTrackName{kTestNamespace, "track1"}; -// TestMoQExecutor that can be driven for tests +// TestMoQExecutor that can be driven for tests. +// Compiles with a -Winaccessible-base warning: folly::Executor is reached both +// virtually (DrivableExecutor) and non-virtually (moxygen's MoQExecutor), so it +// is ambiguous here. Nothing upcasts to Executor; the fix belongs in moxygen. class TestMoQExecutor : public MoQFollyExecutorImpl, public folly::DrivableExecutor { public: explicit TestMoQExecutor(); diff --git a/test/MoqxRelayTrackStatusTests.cpp b/test/MoqxRelayTrackStatusTests.cpp index 5204b43a2..1b883b3b0 100644 --- a/test/MoqxRelayTrackStatusTests.cpp +++ b/test/MoqxRelayTrackStatusTests.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/MoqxRelayTracksTests.cpp b/test/MoqxRelayTracksTests.cpp index 2d54f6826..82ed6bada 100644 --- a/test/MoqxRelayTracksTests.cpp +++ b/test/MoqxRelayTracksTests.cpp @@ -1,13 +1,14 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE for the original license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ // Draft 18+: SUBSCRIBE_TRACKS relay tests. -// Ported from deps/moxygen/moxygen/relay/test/MoQRelayTest.cpp +// Ported from moxygen moxygen/relay/test/MoQRelayTest.cpp #include "MoqxRelayTestFixture.h" diff --git a/test/NamespaceTreeTest.cpp b/test/NamespaceTreeTest.cpp index 1841b1721..1fff61c35 100644 --- a/test/NamespaceTreeTest.cpp +++ b/test/NamespaceTreeTest.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * Originally from github.com/facebookexperimental/moxygen. - * See deps/moxygen/LICENSE file in the root directory for license terms. + * See the moxygen LICENSE for the original license terms: + * https://github.com/openmoq/moxygen/blob/main/LICENSE * * Copyright (c) OpenMOQ contributors. */ diff --git a/test/test_admin_cache_purge.sh b/test/test_admin_cache_purge.sh index accacbdc5..2be9ba593 100755 --- a/test/test_admin_cache_purge.sh +++ b/test/test_admin_cache_purge.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BINARY="${1:-$(dirname "$0")/../build/moqx}" +BINARY="${1:-$(dirname "$0")/../build/default/moqx}" # shellcheck source=test_ports.sh source "$(dirname "$0")/test_ports.sh" LISTEN_PORT=$TEST_CACHE_PURGE_LISTEN diff --git a/test/test_admin_cache_purge_race.sh b/test/test_admin_cache_purge_race.sh index b2a246b60..926f0b164 100644 --- a/test/test_admin_cache_purge_race.sh +++ b/test/test_admin_cache_purge_race.sh @@ -22,8 +22,10 @@ set -euo pipefail REPO="$(cd "$(dirname "$0")/.." && pwd)" -BINARY="${1:-$REPO/build/moqx}" -MOQBIN="${MOQBIN:-$REPO/.scratch/moxygen-install/bin}" +BINARY="${1:-$REPO/build/default/moqx}" +# shellcheck source=test_moqbin.sh +source "$REPO/test/test_moqbin.sh" +resolve_moqbin "$BINARY" # shellcheck source=test_ports.sh source "$REPO/test/test_ports.sh" # shellcheck source=test_versions.sh diff --git a/test/test_admin_config.sh b/test/test_admin_config.sh index ae501d233..7f1693a7d 100755 --- a/test/test_admin_config.sh +++ b/test/test_admin_config.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BINARY="${1:-$(dirname "$0")/../build/moqx}" +BINARY="${1:-$(dirname "$0")/../build/default/moqx}" # shellcheck source=test_ports.sh source "$(dirname "$0")/test_ports.sh" LISTEN_PORT=$TEST_ADMIN_CONFIG_LISTEN diff --git a/test/test_admin_info.sh b/test/test_admin_info.sh index d5b34ab24..03ddbbedc 100755 --- a/test/test_admin_info.sh +++ b/test/test_admin_info.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BINARY="${1:-$(dirname "$0")/../build/moqx}" +BINARY="${1:-$(dirname "$0")/../build/default/moqx}" # shellcheck source=test_ports.sh source "$(dirname "$0")/test_ports.sh" LISTEN_PORT=$TEST_ADMIN_INFO_LISTEN diff --git a/test/test_admin_metrics.sh b/test/test_admin_metrics.sh index 7b266b6a9..9230534d6 100755 --- a/test/test_admin_metrics.sh +++ b/test/test_admin_metrics.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BINARY="${1:-$(dirname "$0")/../build/moqx}" +BINARY="${1:-$(dirname "$0")/../build/default/moqx}" # shellcheck source=test_ports.sh source "$(dirname "$0")/test_ports.sh" LISTEN_PORT=$TEST_ADMIN_METRICS_LISTEN diff --git a/test/test_admin_tls.sh b/test/test_admin_tls.sh index 26151729f..2fa4bdfa8 100755 --- a/test/test_admin_tls.sh +++ b/test/test_admin_tls.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BINARY="${1:-$(dirname "$0")/../build/moqx}" +BINARY="${1:-$(dirname "$0")/../build/default/moqx}" TESTDIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=test_ports.sh source "$(dirname "$0")/test_ports.sh" diff --git a/test/test_conformance.sh b/test/test_conformance.sh index 394cc292e..df11e8133 100755 --- a/test/test_conformance.sh +++ b/test/test_conformance.sh @@ -11,14 +11,14 @@ # # Environment: # MOQBIN — path to moxygen install bin/ (for moqtest_client, moqtest_server) -# defaults to .scratch/moxygen-install/bin +# defaults to the moxygen install bin (auto-detected from the build) # # Examples: -# test_conformance.sh ./build/moqx -# test_conformance.sh ./build/moqx 16 -# test_conformance.sh ./build/moqx 14 Q # mvfst, draft-14, raw QUIC -# test_conformance.sh ./build/moqx 16 Q pico # picoquic, draft-16, raw QUIC -# test_conformance.sh ./build/moqx 14 pico # picoquic, draft-14, WT +# test_conformance.sh ./build/default/moqx +# test_conformance.sh ./build/default/moqx 16 +# test_conformance.sh ./build/default/moqx 14 Q # mvfst, draft-14, raw QUIC +# test_conformance.sh ./build/default/moqx 16 Q pico # picoquic, draft-16, raw QUIC +# test_conformance.sh ./build/default/moqx 14 pico # picoquic, draft-14, WT set -euo pipefail @@ -26,10 +26,22 @@ MOQX_BIN="${1:?Usage: $0 [versions] [Q] [stack]}" shift EXTRA_ARGS=("$@") -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -MOQBIN="${MOQBIN:-${PROJECT_ROOT}/.scratch/moxygen-install/bin}" -CONFORMANCE_SCRIPT="${PROJECT_ROOT}/deps/moxygen/moxygen/moqtest/conformance_test.sh" +# `|| true`: a bad path must reach the friendly binary-not-found check below, +# not abort here under errexit. +BUILD_DIR="$(cd "$(dirname "$MOQX_BIN")" 2>/dev/null && pwd || true)" + +# shellcheck source=test_moqbin.sh +source "$(dirname "${BASH_SOURCE[0]}")/test_moqbin.sh" +resolve_moqbin "$MOQX_BIN" + +# moxygen conformance script: MOXYGEN_SRC override, else the moxygen source this +# build resolved — the cache records it wherever it lives, including a local +# checkout. +CONFORMANCE_SCRIPT="${MOXYGEN_SRC:+${MOXYGEN_SRC}/moxygen/moqtest/conformance_test.sh}" +if [[ -z "$CONFORMANCE_SCRIPT" || ! -x "$CONFORMANCE_SCRIPT" ]]; then + MOXYGEN_SRC_DIR="$(sed -n 's|^CPM_PACKAGE_moxygen_SOURCE_DIR:INTERNAL=||p' "$BUILD_DIR/CMakeCache.txt" 2>/dev/null || true)" + CONFORMANCE_SCRIPT="${MOXYGEN_SRC_DIR:-${BUILD_DIR}/_deps/moxygen-src}/moxygen/moqtest/conformance_test.sh" +fi # Validate binaries exist for bin in "$MOQX_BIN" "$MOQBIN/moqtest_client" "$MOQBIN/moqtest_server"; do @@ -49,7 +61,7 @@ QUIC_STACK="mvfst" DOWNSTREAM_ARGS=() SERVER_VERSIONS_FLAG=() SERVER_TRANSPORT_FLAG=() -for arg in "${EXTRA_ARGS[@]}"; do +for arg in "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; do case "$arg" in mvfst|pico) QUIC_STACK="$arg" @@ -153,8 +165,8 @@ echo "==> Starting moqtest_server..." # (every client invocation then hangs to its 30s transaction timeout). "$MOQBIN/moqtest_server" \ --relay_url="https://${URL_HOST}:${RELAY_PORT}/moq-relay" \ - "${SERVER_VERSIONS_FLAG[@]}" \ - "${SERVER_TRANSPORT_FLAG[@]}" \ + "${SERVER_VERSIONS_FLAG[@]+"${SERVER_VERSIONS_FLAG[@]}"}" \ + "${SERVER_TRANSPORT_FLAG[@]+"${SERVER_TRANSPORT_FLAG[@]}"}" \ --logtostderr & SERVER_PID=$! sleep 2 @@ -178,7 +190,7 @@ export MOXYGEN_DIR="$MOXYGEN_SHIM" trap 'rm -rf "$MOXYGEN_SHIM" "$TMPDIR"; kill "$RELAY_PID" "$SERVER_PID" 2>/dev/null; wait "$RELAY_PID" "$SERVER_PID" 2>/dev/null' EXIT set +e -bash "$CONFORMANCE_SCRIPT" "$RELAY_URL" "${DOWNSTREAM_ARGS[@]}" +bash "$CONFORMANCE_SCRIPT" "$RELAY_URL" "${DOWNSTREAM_ARGS[@]+"${DOWNSTREAM_ARGS[@]}"}" EXIT_CODE=$? set -e diff --git a/test/test_moqbin.sh b/test/test_moqbin.sh new file mode 100644 index 000000000..57d446386 --- /dev/null +++ b/test/test_moqbin.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Resolve MOQBIN — the moxygen install's bin/ (moqtest_client, moqdateserver, …) +# — for a shell test. +# +# source "$(dirname "$0")/test_moqbin.sh" +# resolve_moqbin "$BINARY" # $BINARY = the moqx binary under test +# +# An MOQBIN already in the environment wins (ctest sets it per test); otherwise +# it comes from the tool-paths file the configure writes next to the binary. +# Always leaves MOQBIN set — possibly empty — so callers report a clear +# not-found error instead of aborting under `set -u`. +resolve_moqbin() { + local build_dir + build_dir="$(dirname "${1:-}")" + if [[ -z "${MOQBIN:-}" && -f "$build_dir/moqx-tools.env" ]]; then + # shellcheck disable=SC1091 # generated at configure time + source "$build_dir/moqx-tools.env" + fi + MOQBIN="${MOQBIN:-}" +} diff --git a/test/test_qmux_relay.sh b/test/test_qmux_relay.sh index a9cc8317e..407335dca 100755 --- a/test/test_qmux_relay.sh +++ b/test/test_qmux_relay.sh @@ -10,8 +10,8 @@ # QUIC/WebTransport. moqdateserver/moqtextclient have a real client-side # --qmux (makeRelayClientTransport TransportType::QMUX), so they're used here. # -# moqdateserver/moqtextclient must be at: -# .scratch/moxygen-install/bin/ (relative to repo root) +# moqdateserver/moqtextclient come from the moxygen install bin, resolved from +# the build (MOQBIN overrides). # # Usage: bash test/test_qmux_relay.sh [path/to/moqx] @@ -19,27 +19,27 @@ set -euo pipefail REPO="$(cd "$(dirname "$0")/.." && pwd)" # Explicit arg wins; otherwise pick the most recently built moqx, so a fresh -# build-san/ is preferred over a stale build/ that may predate proxygen_qmux. +# build/san is preferred over a stale build/default that may predate +# proxygen_qmux. BINARY="${1:-}" if [[ -z "$BINARY" ]]; then - BINARY="$(ls -t "$REPO"/build*/moqx 2>/dev/null | head -1 || true)" - BINARY="${BINARY:-$REPO/build/moqx}" + BINARY="$(ls -t "$REPO"/build/*/moqx 2>/dev/null | head -1 || true)" + BINARY="${BINARY:-$REPO/build/default/moqx}" fi -MOQBIN="${MOQBIN:-$REPO/.scratch/moxygen-install/bin}" +# shellcheck source=test_moqbin.sh +source "$REPO/test/test_moqbin.sh" +resolve_moqbin "$BINARY" # shellcheck source=test_ports.sh source "$REPO/test/test_ports.sh" # shellcheck source=test_versions.sh source "$REPO/test/test_versions.sh" -# Resolve a qmux-capable sample binary. Prefer MOQBIN's flat layout (the install -# at .scratch/moxygen-install/bin); if that's a pre-qmux release, fall back to a -# from-source build under .scratch/standalone-build*/moxygen/samples (e.g. the -# asan build), where samples live in per-tool subdirs. Empty if none found. -# $1 = binary name, $2 = samples subdir for the fallback layout. +# Resolve a qmux-capable sample binary ($1 = name). Prefer MOQBIN; if that is a +# pre-qmux release, fall back to a from-source install under .scratch. Empty if +# neither has it. resolve_qmux_bin() { - local name="$1" sub="$2" cand - for cand in "$MOQBIN/$name" \ - "$REPO"/.scratch/standalone-build*/moxygen/samples/"$sub/$name"; do + local name="$1" cand + for cand in "$MOQBIN/$name" "$REPO"/.scratch/moxygen-build*/moxygen-install/bin/"$name"; do if [[ -x "$cand" ]] && grep -q "qmux" <<<"$("$cand" --help 2>&1 || true)"; then echo "$cand" return @@ -47,8 +47,8 @@ resolve_qmux_bin() { done } -DATESERVER="$(resolve_qmux_bin moqdateserver date)" -TEXTCLIENT="$(resolve_qmux_bin moqtextclient text-client)" +DATESERVER="$(resolve_qmux_bin moqdateserver)" +TEXTCLIENT="$(resolve_qmux_bin moqtextclient)" RELAY_PORT=$TEST_QMUX_RELAY_LISTEN ADMIN_PORT=$TEST_QMUX_RELAY_ADMIN diff --git a/test/test_relay_chain.sh b/test/test_relay_chain.sh index 4ff4973ac..f1de3f5a4 100755 --- a/test/test_relay_chain.sh +++ b/test/test_relay_chain.sh @@ -7,17 +7,19 @@ # the chain within the timeout. # # Requires draft 16+ for relay peering (wildcard subscribeNamespace). -# moqdateserver and moqtextclient must be at: -# .scratch/moxygen-install/bin/ (relative to repo root) +# moqdateserver and moqtextclient come from the moxygen install bin, resolved +# from the build (MOQBIN overrides). # -# Usage: bash scripts/test_relay_chain.sh [path/to/moqx] [--save-logs [dir]] +# Usage: bash test/test_relay_chain.sh [path/to/moqx] [--save-logs [dir]] # --save-logs [dir] Save relay DBG4 logs; dir defaults to /tmp/relay_chain_logs set -euo pipefail REPO="$(cd "$(dirname "$0")/.." && pwd)" -BINARY="${1:-$REPO/build/moqx}" -MOQBIN="${MOQBIN:-$REPO/.scratch/moxygen-install/bin}" +BINARY="${1:-$REPO/build/default/moqx}" +# shellcheck source=test_moqbin.sh +source "$REPO/test/test_moqbin.sh" +resolve_moqbin "$BINARY" # shellcheck source=test_ports.sh source "$REPO/test/test_ports.sh" # shellcheck source=test_versions.sh diff --git a/tools/metrics-dashboard.html b/tools/metrics-dashboard.html index af2ed4ce1..9e1dccb94 100644 --- a/tools/metrics-dashboard.html +++ b/tools/metrics-dashboard.html @@ -4,7 +4,7 @@ A single static file (Chart.js from CDN, no build). Open it in a browser and press Start; it scrapes one or more Prometheus /metrics endpoints once per - second and live-updates the graphs. GUI companion to scripts/perf-metrics.sh. + second and live-updates the graphs. GUI companion to scripts/perf/perf-metrics.sh. Endpoints (header fields): - app endpoint : the relay's /metrics (moqx admin HTTP), e.g. http://host:19702/metrics