diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..62ad303a --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,71 @@ +name: benchmark + +on: + push: + branches: [main] + paths-ignore: + - .github/** + - docs/** + - CONTRIBUTING.md + - CODE_OF_CONDUCT.md + - README.md + - mkdocs.yml + - examples/** + - .vscode/** + - scripts/** + pull_request: + types: [opened, reopened, labeled, unlabeled] + workflow_dispatch: + +jobs: + run_benchmarks: + runs-on: ubuntu-latest + if: > + contains(github.event.pull_request.labels.*.name, 'benchmark') || + github.event_name == 'workflow_dispatch' + + steps: + - uses: actions/checkout@v4 + + # - name: Cache cargo + # uses: actions/cache@v4 + # with: + # path: | + # ~/.cargo/registry + # ~/.cargo/git + # target + # key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # - name: Cargo clean if target too large + # run: make clean + + - name: Cache uv + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }} + + # - name: Cache cargo binaries + # uses: actions/cache@v4 + # with: + # path: ~/.cargo/bin + # key: ${{ runner.os }}-cargo-bins + + - name: Project setup + uses: ./.github/actions/project-setup + + - name: Install test dependencies + run: make setup + + - name: Install codspeed + run: cargo binstall cargo-codspeed --no-confirm --force + + - name: Build the benchmark target(s) + run: cargo codspeed build --all-features + + - name: Run the benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: cargo codspeed run + token: ${{ secrets.CODSPEED_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90024327..b48b81cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,55 +27,6 @@ on: workflow_dispatch: jobs: - run_benchmarks: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - # - name: Cache cargo - # uses: actions/cache@v4 - # with: - # path: | - # ~/.cargo/registry - # ~/.cargo/git - # target - # key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - # - name: Cargo clean if target too large - # run: make clean - - - name: Cache uv - uses: actions/cache@v4 - with: - path: ~/.cache/uv - key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }} - - # - name: Cache cargo binaries - # uses: actions/cache@v4 - # with: - # path: ~/.cargo/bin - # key: ${{ runner.os }}-cargo-bins - - - name: Project setup - uses: ./.github/actions/project-setup - - - name: Install test dependencies - run: make setup - - - name: Install codspeed - run: cargo binstall cargo-codspeed --no-confirm --force - - - name: Build the benchmark target(s) - run: cargo codspeed build --all-features - - - name: Run the benchmarks - uses: CodSpeedHQ/action@v4 - with: - mode: simulation - run: cargo codspeed run - token: ${{ secrets.CODSPEED_TOKEN }} - run_unit_tests: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4cd02ee0..e2720a59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,13 +3,15 @@ name: release on: pull_request: types: [closed] - branches: - - main + branches: [main] + workflow_dispatch: inputs: - version_override: - description: "Manually specify the release version (e.g. 1.2.3)" - required: false + dry_run: + description: Dry run + required: true + default: true + type: boolean permissions: contents: write @@ -17,228 +19,166 @@ permissions: id-token: write jobs: - create-tag: + compute-dry-run: + runs-on: ubuntu-latest + # only run if workflow dispatch or pull request merged *with* tag 'version-bump' if: > github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && - github.event.pull_request.merged == true && - (contains(github.event.pull_request.labels.*.name, 'beta') || - contains(github.event.pull_request.labels.*.name, 'rc') || - contains(github.event.pull_request.labels.*.name, 'release') || - contains(github.event.pull_request.labels.*.name, 'patch') || - contains(github.event.pull_request.labels.*.name, 'minor') || - contains(github.event.pull_request.labels.*.name, 'major'))) - runs-on: ubuntu-latest - + ( + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'version-bump') + ) outputs: - version: ${{ steps.versioning.outputs.VERSION }} - prerelease: ${{ steps.versioning.outputs.PRERELEASE }} + is_dry_run: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }} steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Set up Git - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - - name: Determine version - id: versioning + - name: Compute effective dry-run run: | - # 1) If manual workflow_dispatch override given, use that - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && \ - [[ -n "${{ github.event.inputs.version_override }}" ]]; then - VERSION="${{ github.event.inputs.version_override }}" - PRE="false" - echo "Using manual override: $VERSION" - + if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ + && [ "${{ github.event.inputs.dry_run }}" = "true" ]; then + echo "Running in DRY-RUN mode" else - # 2) Extract version from Cargo metadata - VERSION=$( - cargo metadata --no-deps --format-version=1 \ - | jq -r '.packages[] - | select(.name == "encoderfile") - | .version' - ) - - [ -n "$VERSION" ] || { echo "encoderfile not found"; exit 1; } - - # 3) Determine prerelease type from PR labels - if [[ "${{ contains(github.event.pull_request.labels.*.name, 'beta') }}" == "true" ]] || \ - [[ "${{ contains(github.event.pull_request.labels.*.name, 'rc') }}" == "true" ]]; then - PRE="true" - else - PRE="false" - fi + echo "Running in LIVE mode" fi - echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT" - echo "PRERELEASE=${PRE}" >> "$GITHUB_OUTPUT" - - - name: Create tag if missing - run: | - VERSION="${{ steps.versioning.outputs.VERSION }}" + create-tag: + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true) + runs-on: ubuntu-latest + needs: ["compute-dry-run"] - if git rev-parse "v${VERSION}" >/dev/null 2>&1; then - echo "Tag v${VERSION} already exists." - else - git tag -a "v${VERSION}" -m "Version ${VERSION}" - git push origin "v${VERSION}" - fi + outputs: + version: ${{ steps.version.outputs.version }} + prerelease: ${{ steps.version.outputs.is_prerelease == 'true' }} - publish-encoderfile: - needs: create-tag - runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + + - name: Get current version + id: version + uses: mozilla-ai/cargo-goose/actions/current-version@v1 + with: + force-single-version: true - - name: Project setup - uses: ./.github/actions/project-setup - - - name: Cargo publish (dry-run) - run: cargo publish -p encoderfile --dry-run - - - name: Cargo publish - run: cargo publish -p encoderfile - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - - upload-install-script: - needs: create-tag - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Upload artifact + - name: Create git tag + if: needs.compute-dry-run.outputs.is_dry_run != 'true' + run: | + VERSION=${{ steps.version.outputs.version }} + git tag -a "v$VERSION" -m "Version $VERSION" || true + git push origin "v$VERSION" || true - uses: actions/upload-artifact@v4 - with: - name: install - path: install.sh + - name: Dry-run tag + if: needs.compute-dry-run.outputs.is_dry_run == 'true' + run: echo "[DRY RUN] would create tag v${{ steps.version.outputs.version }}" - build-encoderfile-runtime: - needs: create-tag - runs-on: ${{ matrix.platform.runner }} + build-macos: + needs: [create-tag, compute-dry-run] + runs-on: macos-14 strategy: matrix: - platform: - - runner: macos-14 - target: x86_64-apple-darwin - - runner: macos-14 - target: aarch64-apple-darwin - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-gnu - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-gnu + target: + - x86_64-apple-darwin + - aarch64-apple-darwin + steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/project-setup - - name: Project setup - uses: ./.github/actions/project-setup - - - name: Install target - run: rustup target add ${{ matrix.platform.target }} + - run: rustup target add ${{ matrix.target }} - name: Build - run: cargo build --bin encoderfile-runtime --release --target ${{ matrix.platform.target }} - - - name: Create release tarball run: | - VERSION="${{ needs.create-tag.outputs.version }}" - TARGET="${{ matrix.platform.target }}" + cargo build --release --target ${{ matrix.target }} mkdir -p dist + cp target/${{ matrix.target }}/release/encoderfile dist/ + cp target/${{ matrix.target }}/release/encoderfile-runtime dist/ - cp target/${TARGET}/release/encoderfile-runtime dist/encoderfile-runtime - cp README.md dist/README.md - cp LICENSE dist/LICENSE - test -f THIRDPARTY.md && cp THIRDPARTY.md dist/THIRDPARTY.md - - TAR="encoderfile-runtime-${TARGET}.tar.gz" - tar -czf "$TAR" -C dist . - echo "Created $TAR" - - - name: Upload artifact - uses: actions/upload-artifact@v4 + - name: Package + run: | + mkdir pkg + cp dist/* README.md LICENSE pkg/ + test -f THIRDPARTY.md && cp THIRDPARTY.md pkg/ + tar -czf encoderfile-${{ matrix.target }}.tar.gz -C pkg . + tar -czf encoderfile-runtime-${{ matrix.target }}.tar.gz -C pkg . + + - uses: actions/upload-artifact@v4 + if: needs.compute-dry-run.outputs.is_dry_run != 'true' with: - name: encoderfile-runtime-${{ matrix.platform.target }} - path: encoderfile-runtime-${{ matrix.platform.target }}.tar.gz + name: macos-${{ matrix.target }} + path: "*.tar.gz" + + build-linux: + needs: [create-tag, compute-dry-run] + runs-on: ${{ matrix.runner }} - build-encoderfile: - needs: create-tag - runs-on: ${{ matrix.platform.runner }} strategy: + fail-fast: false matrix: - platform: - - runner: macos-14 - target: x86_64-apple-darwin - - runner: macos-14 - target: aarch64-apple-darwin - - runner: ubuntu-24.04 + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-latest target: x86_64-unknown-linux-gnu - - runner: ubuntu-24.04-arm + - arch: arm64 + platform: linux/arm64 + runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu + steps: - uses: actions/checkout@v4 - - name: Project setup - uses: ./.github/actions/project-setup - - - name: Install target - run: rustup target add ${{ matrix.platform.target }} - - - name: Build - run: cargo build --bin encoderfile --release --target ${{ matrix.platform.target }} - - - name: Create release tarball + - name: Build (bookworm, ${{ matrix.arch }}) run: | - VERSION="${{ needs.create-tag.outputs.version }}" - TARGET="${{ matrix.platform.target }}" + docker build \ + -f Dockerfile \ + --target build \ + --load \ + -t encoderfile-build:${{ matrix.arch }} \ + . + + CID=$(docker create encoderfile-build:${{ matrix.arch }}) mkdir -p dist + docker cp "$CID:/app/target/release/encoderfile" dist/ + docker cp "$CID:/app/target/release/encoderfile-runtime" dist/ + docker rm "$CID" - cp target/${TARGET}/release/encoderfile dist/encoderfile - cp README.md dist/README.md - cp LICENSE dist/LICENSE - test -f THIRDPARTY.md && cp THIRDPARTY.md dist/THIRDPARTY.md + - name: Package + run: | + mkdir pkg + cp dist/* README.md LICENSE pkg/ + test -f THIRDPARTY.md && cp THIRDPARTY.md pkg/ - TAR="encoderfile-${TARGET}.tar.gz" - tar -czf "$TAR" -C dist . - echo "Created $TAR" + tar -czf encoderfile-${{ matrix.target }}.tar.gz -C pkg . + tar -czf encoderfile-runtime-${{ matrix.target }}.tar.gz -C pkg . - - name: Upload artifact - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v4 + if: needs.compute-dry-run.outputs.is_dry_run != 'true' with: - name: encoderfile-${{ matrix.platform.target }} - path: encoderfile-${{ matrix.platform.target }}.tar.gz + name: linux-${{ matrix.arch }} + path: "*.tar.gz" - create-release: - needs: [create-tag, build-encoderfile, publish-encoderfile, upload-install-script, build-encoderfile-runtime] + release: + needs: [create-tag, build-macos, build-linux, compute-dry-run] runs-on: ubuntu-latest + steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 with: path: dist - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + - uses: softprops/action-gh-release@v2 + if: needs.compute-dry-run.outputs.is_dry_run != 'true' with: tag_name: v${{ needs.create-tag.outputs.version }} name: v${{ needs.create-tag.outputs.version }} prerelease: ${{ needs.create-tag.outputs.prerelease }} - files: | - dist/**/*.tar.gz - dist/install/install.sh + files: dist/**/*.tar.gz generate_release_notes: true docker-build: - needs: [create-release, create-tag] + needs: [release, create-tag, compute-dry-run] runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -254,8 +194,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Log in to GHCR - uses: docker/login-action@v3 + - uses: docker/login-action@v3 + if: needs.compute-dry-run.outputs.is_dry_run != 'true' with: registry: ghcr.io username: ${{ github.actor }} @@ -269,40 +209,57 @@ jobs: . - name: Push image + if: needs.compute-dry-run.outputs.is_dry_run != 'true' run: | - docker push ghcr.io/${{ github.repository_owner }}/encoderfile:${{ matrix.arch }}-${{ needs.create-tag.outputs.version }} + docker push \ + ghcr.io/${{ github.repository_owner }}/encoderfile:${{ matrix.arch }}-${{ needs.create-tag.outputs.version }} docker-manifest: - needs: [docker-build, create-tag] + needs: [docker-build, create-tag, compute-dry-run] + runs-on: ubuntu-latest steps: - - name: Set up Buildx - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v3 - - name: Log in to GHCR - uses: docker/login-action@v3 + - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - - name: Create multi-arch manifest + - name: Bundle dockerfiles + if: needs.compute-dry-run.outputs.is_dry_run != 'true' run: | VERSION=${{ needs.create-tag.outputs.version }} PRE=${{ needs.create-tag.outputs.prerelease }} if [[ "$PRE" == "true" ]]; then - echo "Prerelease detected — skipping latest tag" docker buildx imagetools create \ -t ghcr.io/${{ github.repository_owner }}/encoderfile:${VERSION} \ ghcr.io/${{ github.repository_owner }}/encoderfile:amd64-${VERSION} \ ghcr.io/${{ github.repository_owner }}/encoderfile:arm64-${VERSION} else - echo "Stable release — tagging latest" docker buildx imagetools create \ -t ghcr.io/${{ github.repository_owner }}/encoderfile:${VERSION} \ -t ghcr.io/${{ github.repository_owner }}/encoderfile:latest \ ghcr.io/${{ github.repository_owner }}/encoderfile:amd64-${VERSION} \ ghcr.io/${{ github.repository_owner }}/encoderfile:arm64-${VERSION} fi + + publish-encoderfile: + needs: [release, compute-dry-run] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/project-setup + + - name: Cargo publish (dry-run) + run: cargo publish -p encoderfile --dry-run + + - name: Cargo publish + if: needs.compute-dry-run.outputs.is_dry_run != 'true' + run: cargo publish -p encoderfile + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index d3dc7f4e..7f9cef1b 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -67,26 +67,15 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Setup project - uses: ./.github/actions/project-setup - - - name: Install cargo-goose - run: cargo install cargo-goose - + - name: Get current version id: current_version - run: | - VERSION=$(cargo metadata --no-deps --format-version=1 \ - | jq -r '.packages[] - | select(.name == "encoderfile") - | .version') - - [ -n "$VERSION" ] || { echo "encoderfile not found"; exit 1; } - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Bump version + uses: mozilla-ai/cargo-goose/actions/current-version@v1 + with: + force-single-version: true + + - name: Compute cargo-goose args + id: goose_args run: | ACTION="${{ github.event.inputs.action }}" LEVEL="${{ github.event.inputs.level }}" @@ -94,33 +83,38 @@ jobs: case "$ACTION" in bump-version) - cargo goose bump version "$LEVEL" + ARGS="bump version $LEVEL" ;; start-prerelease) - cargo goose bump version "$LEVEL" "$STAGE" + ARGS="bump version $LEVEL $STAGE" ;; bump-prerelease) - cargo goose bump prerelease + ARGS="bump prerelease" ;; transition-prerelease) - cargo goose bump prerelease "$STAGE" + ARGS="bump prerelease $STAGE" ;; finalize) - cargo goose bump release + ARGS="bump release" + ;; + *) + echo "Invalid action" + exit 1 ;; esac - - name: Get new version - id: new_version - run: | - VERSION=$(cargo metadata --no-deps --format-version=1 \ - | jq -r '.packages[] - | select(.name == "encoderfile") - | .version') + echo "args=$ARGS" >> "$GITHUB_OUTPUT" - [ -n "$VERSION" ] || { echo "encoderfile not found"; exit 1; } + - name: Bump version + uses: mozilla-ai/cargo-goose/actions/cargo-goose@v1 + with: + args: ${{ steps.goose_args.outputs.args }} - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Get new version + id: new_version + uses: mozilla-ai/cargo-goose/actions/current-version@v1 + with: + force-single-version: true - name: Abort if version did not change run: | @@ -133,20 +127,20 @@ jobs: - name: Update Cargo.lock run: cargo generate-lockfile - - name: Generate PR labels + - name: Generate labels id: labels run: | ACTION="${{ github.event.inputs.action }}" LEVEL="${{ github.event.inputs.level }}" - STAGE="${{ github.event.inputs.stage }}" - - LABELS="auto-generated\nversion-bump\n$ACTION" + IS_PRE="${{ steps.new_version.outputs.is_prerelease }}" + PRE="${{ steps.new_version.outputs.pre }}" - [ -n "$LEVEL" ] && LABELS="$LABELS\n$LEVEL" - [ -n "$STAGE" ] && LABELS="$LABELS\n$STAGE\nprerelease" + LABELS="version-bump\nauto-generated\n$ACTION" - if [ "$ACTION" = "finalize" ]; then - LABELS="$LABELS\nrelease" + if [ "$IS_PRE" = "true" ]; then + LABELS="$LABELS\nprerelease\n$PRE" + else + [ -n "$LEVEL" ] && LABELS="$LABELS\n$LEVEL" fi echo "labels<> "$GITHUB_OUTPUT" diff --git a/Cargo.lock b/Cargo.lock index 63e48617..a323db9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,9 +148,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", "zeroize", @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" dependencies = [ "cc", "cmake", @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "jobserver", @@ -350,9 +350,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "cmake" @@ -413,9 +413,9 @@ dependencies = [ [[package]] name = "codspeed" -version = "4.2.1" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0d98d97fd75ca4489a1a0997820a6521531085e7c8a98941bd0e1264d567dd" +checksum = "38c2eb3388ebe26b5a0ab6bf4969d9c4840143d7f6df07caa3cc851b0606cef6" dependencies = [ "anyhow", "cc", @@ -431,9 +431,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat" -version = "4.2.1" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4179ec5518e79efcd02ed50aa483ff807902e43c85146e87fff58b9cffc06078" +checksum = "b2de65b7489a59709724d489070c6d05b7744039e4bf751d0a2006b90bb5593d" dependencies = [ "clap", "codspeed", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat-macros" -version = "4.2.1" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15eaee97aa5bceb32cc683fe25cd6373b7fc48baee5c12471996b58b6ddf0d7c" +checksum = "56ca01ce4fd22b8dcc6c770dcd6b74343642e842482b94e8920d14e10c57638d" dependencies = [ "divan-macros", "itertools 0.14.0", @@ -458,9 +458,9 @@ dependencies = [ [[package]] name = "codspeed-divan-compat-walltime" -version = "4.2.1" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c38671153aa73be075d6019cab5ab1e6b31d36644067c1ac4cef73bf9723ce33" +checksum = "720ab9d0714718afe5f5832be6e5f5eb5ce97836e24ca7bf7042eea4308b9fb8" dependencies = [ "cfg-if", "clap", @@ -815,7 +815,7 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoderfile" -version = "0.4.0-beta.6" +version = "0.4.0-rc.1" dependencies = [ "anyhow", "axum", @@ -851,7 +851,7 @@ dependencies = [ "tar", "tempfile", "test-log", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokenizers", "tokio", "tonic", @@ -872,7 +872,7 @@ dependencies = [ [[package]] name = "encoderfile-runtime" -version = "0.4.0-beta.6" +version = "0.4.0-rc.1" dependencies = [ "anyhow", "clap", @@ -964,21 +964,20 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -1587,9 +1586,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1662,9 +1661,9 @@ dependencies = [ [[package]] name = "luajit-src" -version = "210.6.5+7152e15" +version = "210.6.6+707c12b" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e64ac463f01a02ee793423f9b351369cf244c5ee8bb9e2729a75b2eb404181" +checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295" dependencies = [ "cc", "which", @@ -1989,9 +1988,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -2015,7 +2014,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -2045,7 +2044,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest 0.12.28", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tonic", "tracing", @@ -2093,7 +2092,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.2", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2297,9 +2296,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2404,7 +2403,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2426,7 +2425,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2448,9 +2447,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2479,7 +2478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -2499,7 +2498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -2513,9 +2512,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2583,7 +2582,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2759,7 +2758,7 @@ dependencies = [ "serde", "serde_json", "sse-stream", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -2821,7 +2820,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -2838,9 +2837,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -2875,9 +2874,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -3135,9 +3134,9 @@ checksum = "51d44cfb396c3caf6fbfd0ab422af02631b69ddd96d2eff0b0f0724f9024051b" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3332,11 +3331,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -3352,9 +3351,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -3423,7 +3422,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -3620,9 +3619,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3976,18 +3975,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -3998,11 +3997,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4011,9 +4011,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4021,9 +4021,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -4034,9 +4034,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -4056,9 +4056,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -4452,9 +4452,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -4583,6 +4583,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Dockerfile b/Dockerfile index e81b260b..01348728 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,8 +36,7 @@ COPY encoderfile ./encoderfile COPY encoderfile-runtime ./encoderfile-runtime # Build release binary. -RUN cargo build --bin encoderfile --release - +RUN cargo build --release # ---- Final stage ------------------------------------------------------------ FROM debian:bookworm-slim AS final diff --git a/encoderfile-runtime/Cargo.toml b/encoderfile-runtime/Cargo.toml index d50208fa..7009b193 100644 --- a/encoderfile-runtime/Cargo.toml +++ b/encoderfile-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "encoderfile-runtime" -version = "0.4.0-beta.6" +version = "0.4.0-rc.1" edition = "2024" publish = false @@ -8,7 +8,7 @@ publish = false [dependencies.encoderfile] path = "../encoderfile" -version = "0.4.0-beta.6" +version = "0.4.0-rc.1" features = ["transport"] default-features = false diff --git a/encoderfile-runtime/src/main.rs b/encoderfile-runtime/src/main.rs index d569c5ec..85e3971c 100644 --- a/encoderfile-runtime/src/main.rs +++ b/encoderfile-runtime/src/main.rs @@ -2,19 +2,17 @@ use parking_lot::Mutex; use std::{ fs::File, io::{BufReader, Read, Seek}, - sync::Arc, }; use anyhow::Result; use clap::Parser; use encoderfile::{ - AppState, common::{ ModelType, model_type::{Embedding, SentenceEmbedding, SequenceClassification, TokenClassification}, }, format::codec::EncoderfileCodec, - runtime::EncoderfileLoader, + runtime::{EncoderfileLoader, EncoderfileState}, transport::cli::Cli, }; @@ -33,17 +31,19 @@ async fn main() -> Result<()> { macro_rules! run_cli { ($model_type:ident, $cli:expr, $config:expr, $session:expr, $tokenizer:expr, $model_config:expr) => {{ - let state = AppState::<$model_type>::new($config, $session, $tokenizer, $model_config); + let state = + EncoderfileState::<$model_type>::new($config, $session, $tokenizer, $model_config) + .into(); $cli.command.execute(state).await }}; } async fn entrypoint<'a, R: Read + Seek>(loader: &mut EncoderfileLoader<'a, R>) -> Result<()> { let cli = Cli::parse(); - let session = Arc::new(Mutex::new(loader.session()?)); - let model_config = Arc::new(loader.model_config()?); - let tokenizer = Arc::new(loader.tokenizer()?); - let config = Arc::new(loader.encoderfile_config()?); + let session = Mutex::new(loader.session()?); + let model_config = loader.model_config()?; + let tokenizer = loader.tokenizer()?; + let config = loader.encoderfile_config()?; match loader.model_type() { ModelType::Embedding => run_cli!(Embedding, cli, config, session, tokenizer, model_config), diff --git a/encoderfile/Cargo.toml b/encoderfile/Cargo.toml index ca860034..447f9c2e 100644 --- a/encoderfile/Cargo.toml +++ b/encoderfile/Cargo.toml @@ -47,9 +47,14 @@ required-features = ["dev-utils"] name = "test_services" required-features = ["dev-utils"] +[[test]] +name = "test_build_encoderfile" +path = "tests/integration/test_build.rs" +required-features = ["cli", "dev-utils"] + [package] name = "encoderfile" -version = "0.4.0-beta.6" +version = "0.4.0-rc.1" edition = "2024" license = "Apache-2.0" readme = "../README.md" diff --git a/encoderfile/benches/postprocessing.rs b/encoderfile/benches/postprocessing.rs index 93a4cc8a..dbb66816 100644 --- a/encoderfile/benches/postprocessing.rs +++ b/encoderfile/benches/postprocessing.rs @@ -16,7 +16,7 @@ fn main() { #[divan::bench(args = [(8, 16, 384), (16, 128, 768), (64, 512, 1024)])] fn embedding_postprocess(b: Bencher, dim: (usize, usize, usize)) { - let tokenizer = embedding_state().tokenizer; + let tokenizer = &embedding_state().tokenizer; let (batch, tokens, hidden) = dim; // Random embeddings @@ -27,7 +27,7 @@ fn embedding_postprocess(b: Bencher, dim: (usize, usize, usize)) { let outputs = Array::from_shape_vec((batch, tokens, hidden), data).unwrap(); // Dummy encodings - let encodings = generate_dummy_encodings(&tokenizer, batch, tokens); + let encodings = generate_dummy_encodings(tokenizer, batch, tokens); b.bench(|| embedding::postprocess(outputs.clone(), encodings.clone())); } @@ -35,7 +35,7 @@ fn embedding_postprocess(b: Bencher, dim: (usize, usize, usize)) { #[divan::bench(args = [8, 16, 64])] fn sequence_classification_postprocess(b: Bencher, batch: usize) { let state = sequence_classification_state(); - let config = state.model_config; + let config = &state.model_config; let n_labels = config.id2label.clone().unwrap().len(); let mut rng = rand::rng(); @@ -45,16 +45,16 @@ fn sequence_classification_postprocess(b: Bencher, batch: usize) { let outputs = Array::from_shape_vec((batch, n_labels), data).unwrap(); - b.bench(|| sequence_classification::postprocess(outputs.clone(), &config)); + b.bench(|| sequence_classification::postprocess(outputs.clone(), config)); } #[divan::bench(args = [(8, 16), (16, 128), (64, 512)])] fn token_classification_postprocess(b: Bencher, dim: (usize, usize)) { let state = token_classification_state(); - let config = state.model_config; + let config = &state.model_config; let n_labels = config.id2label.clone().unwrap().len(); - let tokenizer = embedding_state().tokenizer; + let tokenizer = &embedding_state().tokenizer; let (batch, tokens) = dim; // Random embeddings @@ -65,9 +65,9 @@ fn token_classification_postprocess(b: Bencher, dim: (usize, usize)) { let outputs = Array::from_shape_vec((batch, tokens, n_labels), data).unwrap(); // Dummy encodings - let encodings = generate_dummy_encodings(&tokenizer, batch, tokens); + let encodings = generate_dummy_encodings(tokenizer, batch, tokens); - b.bench(|| token_classification::postprocess(outputs.clone(), encodings.clone(), &config)); + b.bench(|| token_classification::postprocess(outputs.clone(), encodings.clone(), config)); } fn generate_dummy_encodings( diff --git a/encoderfile/src/build_cli/cli/build.rs b/encoderfile/src/build_cli/cli/build.rs index 1f91d629..189aea51 100644 --- a/encoderfile/src/build_cli/cli/build.rs +++ b/encoderfile/src/build_cli/cli/build.rs @@ -11,7 +11,7 @@ use crate::{ }, generated::manifest::Backend, }; -use anyhow::Result; +use anyhow::{Context, Result}; use std::{ borrow::Cow, fs::File, @@ -130,12 +130,22 @@ impl BuildArgs { // initialize final binary terminal::info("Writing encoderfile..."); let output_path = config.encoderfile.output_path(); - let out = File::create(output_path.clone())?; + let out = File::create(output_path.clone()).context(format!( + "Failed to create final encoderfile at {:?}", + output_path.as_path() + ))?; + let mut out = BufWriter::new(out); - let mut base = File::open(base_path)?; + let mut base = File::open(base_path.as_path()).context(format!( + "Failed to open base binary at {:?}", + base_path.as_path() + ))?; // copy base binary to out - std::io::copy(&mut base, &mut out)?; + std::io::copy(&mut base, &mut out).context(format!( + "Failed to copy base binary to {:?}", + output_path.as_path() + ))?; // get metadata start position let payload_start = out.stream_position()?; @@ -163,3 +173,18 @@ impl BuildArgs { Ok(()) } } + +#[cfg(feature = "dev-utils")] +pub fn test_build_args( + config: impl Into, + base_binary_path: impl Into, +) -> BuildArgs { + BuildArgs { + config: config.into(), + output_path: None, + base_binary_path: Some(base_binary_path.into()), + platform: None, + version: None, + no_download: true, + } +} diff --git a/encoderfile/src/build_cli/cli/mod.rs b/encoderfile/src/build_cli/cli/mod.rs index 61312be9..1752e78c 100644 --- a/encoderfile/src/build_cli/cli/mod.rs +++ b/encoderfile/src/build_cli/cli/mod.rs @@ -6,6 +6,9 @@ use clap_derive::{Args, Parser, Subcommand}; mod build; mod runtime; +#[cfg(feature = "dev-utils")] +pub use build::test_build_args; + #[derive(Debug, Parser)] pub struct Cli { #[command(subcommand)] @@ -15,7 +18,7 @@ pub struct Cli { pub global_args: GlobalArguments, } -#[derive(Debug, Clone, Args)] +#[derive(Debug, Default, Clone, Args)] pub struct GlobalArguments { #[arg( long = "cache-dir", diff --git a/encoderfile/src/dev_utils/mod.rs b/encoderfile/src/dev_utils/mod.rs index c8754008..5c88fa18 100644 --- a/encoderfile/src/dev_utils/mod.rs +++ b/encoderfile/src/dev_utils/mod.rs @@ -3,30 +3,30 @@ use crate::{ Config, ModelConfig, TokenizerConfig, model_type::{self, ModelTypeSpec}, }, - runtime::AppState, + runtime::{AppState, EncoderfileState}, }; use ort::session::Session; use parking_lot::Mutex; use std::str::FromStr; -use std::{fs::File, io::BufReader, sync::Arc}; +use std::{fs::File, io::BufReader}; const EMBEDDING_DIR: &str = "../models/embedding"; const SEQUENCE_CLASSIFICATION_DIR: &str = "../models/sequence_classification"; const TOKEN_CLASSIFICATION_DIR: &str = "../models/token_classification"; pub fn get_state(dir: &str) -> AppState { - let config = Arc::new(Config { + let config = Config { name: "my-model".to_string(), version: "0.0.1".to_string(), model_type: T::enum_val(), transform: None, - }); + }; - let model_config = Arc::new(get_model_config(dir)); - let tokenizer = Arc::new(get_tokenizer(dir)); - let session = Arc::new(get_model(dir)); + let model_config = get_model_config(dir); + let tokenizer = get_tokenizer(dir); + let session = get_model(dir); - AppState::new(config, session, tokenizer, model_config) + EncoderfileState::new(config, session, tokenizer, model_config).into() } pub fn embedding_state() -> AppState { diff --git a/encoderfile/src/runtime/mod.rs b/encoderfile/src/runtime/mod.rs index 5ae6b81e..d77ae5fa 100644 --- a/encoderfile/src/runtime/mod.rs +++ b/encoderfile/src/runtime/mod.rs @@ -6,7 +6,7 @@ mod state; mod tokenizer; pub use loader::EncoderfileLoader; -pub use state::{AppState, InferenceState}; +pub use state::{AppState, EncoderfileState}; pub use tokenizer::TokenizerService; pub type Model<'a> = MutexGuard<'a, Session>; diff --git a/encoderfile/src/runtime/state.rs b/encoderfile/src/runtime/state.rs index af448f30..6b084c95 100644 --- a/encoderfile/src/runtime/state.rs +++ b/encoderfile/src/runtime/state.rs @@ -1,52 +1,32 @@ use std::{marker::PhantomData, sync::Arc}; use ort::session::Session; -use parking_lot::{Mutex, RawMutex, lock_api::MutexGuard}; +use parking_lot::Mutex; use crate::{ common::{Config, ModelConfig, ModelType, model_type::ModelTypeSpec}, runtime::TokenizerService, }; -pub trait InferenceState { - fn config(&self) -> &Arc; - fn session(&self) -> MutexGuard<'_, RawMutex, Session>; - fn tokenizer(&self) -> &Arc; - fn model_config(&self) -> &Arc; -} - -impl InferenceState for AppState { - fn config(&self) -> &Arc { - &self.config - } - fn session(&self) -> MutexGuard<'_, RawMutex, Session> { - self.session.lock() - } - fn tokenizer(&self) -> &Arc { - &self.tokenizer - } - fn model_config(&self) -> &Arc { - &self.model_config - } -} +pub type AppState = Arc>; -#[derive(Debug, Clone)] -pub struct AppState { - pub config: Arc, - pub session: Arc>, - pub tokenizer: Arc, - pub model_config: Arc, +#[derive(Debug)] +pub struct EncoderfileState { + pub config: Config, + pub session: Mutex, + pub tokenizer: TokenizerService, + pub model_config: ModelConfig, _marker: PhantomData, } -impl AppState { +impl EncoderfileState { pub fn new( - config: Arc, - session: Arc>, - tokenizer: Arc, - model_config: Arc, - ) -> AppState { - AppState { + config: Config, + session: Mutex, + tokenizer: TokenizerService, + model_config: ModelConfig, + ) -> EncoderfileState { + EncoderfileState { config, session, tokenizer, diff --git a/encoderfile/src/services/embedding.rs b/encoderfile/src/services/embedding.rs index bf885aef..d03de781 100644 --- a/encoderfile/src/services/embedding.rs +++ b/encoderfile/src/services/embedding.rs @@ -2,7 +2,7 @@ use crate::{ common::{EmbeddingRequest, EmbeddingResponse, model_type}, error::ApiError, inference, - runtime::{AppState, InferenceState}, + runtime::AppState, transforms::EmbeddingTransform, }; @@ -15,13 +15,11 @@ impl Inference for AppState { fn inference(&self, request: impl Into) -> Result { let request = request.into(); - let session = self.session(); - let encodings = self.tokenizer.encode_text(request.inputs)?; let transform = EmbeddingTransform::new(self.transform_str())?; - let results = inference::embedding::embedding(session, &transform, encodings)?; + let results = inference::embedding::embedding(self.session.lock(), &transform, encodings)?; Ok(EmbeddingResponse { results, diff --git a/encoderfile/src/services/sentence_embedding.rs b/encoderfile/src/services/sentence_embedding.rs index 03912216..f04223ad 100644 --- a/encoderfile/src/services/sentence_embedding.rs +++ b/encoderfile/src/services/sentence_embedding.rs @@ -2,7 +2,7 @@ use crate::{ common::{SentenceEmbeddingRequest, SentenceEmbeddingResponse, model_type}, error::ApiError, inference, - runtime::{AppState, InferenceState}, + runtime::AppState, transforms::SentenceEmbeddingTransform, }; @@ -15,14 +15,15 @@ impl Inference for AppState { fn inference(&self, request: impl Into) -> Result { let request = request.into(); - let session = self.session(); - let encodings = self.tokenizer.encode_text(request.inputs)?; let transform = SentenceEmbeddingTransform::new(self.transform_str())?; - let results = - inference::sentence_embedding::sentence_embedding(session, &transform, encodings)?; + let results = inference::sentence_embedding::sentence_embedding( + self.session.lock(), + &transform, + encodings, + )?; Ok(SentenceEmbeddingResponse { results, diff --git a/encoderfile/src/services/sequence_classification.rs b/encoderfile/src/services/sequence_classification.rs index 4a2bdf05..0550a3de 100644 --- a/encoderfile/src/services/sequence_classification.rs +++ b/encoderfile/src/services/sequence_classification.rs @@ -2,7 +2,7 @@ use crate::{ common::{SequenceClassificationRequest, SequenceClassificationResponse, model_type}, error::ApiError, inference, - runtime::{AppState, InferenceState}, + runtime::AppState, transforms::SequenceClassificationTransform, }; @@ -15,14 +15,12 @@ impl Inference for AppState { fn inference(&self, request: impl Into) -> Result { let request = request.into(); - let session = self.session(); - let encodings = self.tokenizer.encode_text(request.inputs)?; let transform = SequenceClassificationTransform::new(self.transform_str())?; let results = inference::sequence_classification::sequence_classification( - session, + self.session.lock(), &transform, &self.model_config, encodings, diff --git a/encoderfile/src/transport/grpc/mod.rs b/encoderfile/src/transport/grpc/mod.rs index 0445d09e..0aaaf854 100644 --- a/encoderfile/src/transport/grpc/mod.rs +++ b/encoderfile/src/transport/grpc/mod.rs @@ -11,7 +11,7 @@ pub trait GrpcRouter: ModelTypeSpec where Self: Sized, { - fn router(state: AppState) -> axum::Router; + fn grpc_router(state: AppState) -> axum::Router; } pub struct GrpcService { @@ -35,7 +35,7 @@ macro_rules! generate_grpc_server { $server_type:ident ) => { impl GrpcRouter for model_type::$model_type { - fn router(state: AppState) -> axum::Router { + fn grpc_router(state: AppState) -> axum::Router { tonic::service::Routes::builder() .routes() .add_service($generated_mod::$server_mod::$server_type::new( diff --git a/encoderfile/src/transport/server.rs b/encoderfile/src/transport/server.rs index 7d3e5a3d..c55fdafa 100644 --- a/encoderfile/src/transport/server.rs +++ b/encoderfile/src/transport/server.rs @@ -5,8 +5,9 @@ use crate::{ transport::{grpc::GrpcRouter, http::HttpRouter}, }; use anyhow::Result; +use axum::extract::connect_info::IntoMakeServiceWithConnectInfo; use axum_server::tls_rustls::RustlsConfig; -use std::path::Path; +use std::{net::SocketAddr, path::Path}; use tower_http::trace::DefaultOnResponse; pub async fn run_grpc( @@ -16,58 +17,23 @@ pub async fn run_grpc( maybe_key_file: Option, state: AppState, ) -> Result<()> { - let addr = format!("{}:{}", &hostname, &port); - - let model_type = T::enum_val(); - - let router = T::router(state) - .layer( - tower_http::trace::TraceLayer::new_for_grpc() - .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)), - ) - .into_make_service_with_connect_info::(); - - tracing::debug!( - "TLS configuration: cert file: {:?}, cert key: {:?}", + serve_with_optional_tls( + hostname, + port, maybe_cert_file, - maybe_key_file - ); - - tracing::info!("Running {:?} gRPC server on {}", model_type, &addr); - - match (maybe_cert_file, maybe_key_file) { - (Some(cert_file), Some(key_file)) => { - // ref: https://github.com/tokio-rs/axum/blob/main/examples/tls-rustls/src/main.rs#L45 - // configure certificate and private key used by https - let config = - RustlsConfig::from_pem_file(Path::new(&cert_file), Path::new(&key_file)).await?; - - let socket_addr = addr.parse()?; - - axum_server::bind_rustls(socket_addr, config) - .serve(router) - .await? - } - (None, Some(_)) => { - return Err(crate::error::ApiError::ConfigError( - "Both cert and key file need to be set. Only the key file has been set.", - ))?; - } - (Some(_), None) => { - return Err(crate::error::ApiError::ConfigError( - "Both cert and key file need to be set. Only the cert file has been set.", - ))?; - } - (None, None) => { - let listener = tokio::net::TcpListener::bind(&addr) - .await - .expect("Invalid address: {addr}"); - - axum::serve(listener, router).await?; - } - } - - Ok(()) + maybe_key_file, + "gRPC", + state, + |state| { + T::grpc_router(state) + .layer( + tower_http::trace::TraceLayer::new_for_grpc() + .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)), + ) + .into_make_service_with_connect_info::() + }, + ) + .await } pub async fn run_http( @@ -80,51 +46,23 @@ pub async fn run_http( where AppState: Inference, { - let addr = format!("{}:{}", &hostname, &port); - - let model_type = T::enum_val(); - - let router = T::http_router(state) - .layer( - tower_http::trace::TraceLayer::new_for_http() - .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)), - ) - .into_make_service_with_connect_info::(); - - tracing::info!( - "TLS configuration: cert file: {:?}, cert key: {:?}", + serve_with_optional_tls( + hostname, + port, maybe_cert_file, - maybe_key_file - ); - - match (maybe_cert_file, maybe_key_file) { - (Some(cert_file), Some(key_file)) => { - tracing::info!("Running {:?} HTTPS server on {}", model_type, &addr); - - // ref: https://github.com/tokio-rs/axum/blob/main/examples/tls-rustls/src/main.rs#L45 - // configure certificate and private key used by https - let config = RustlsConfig::from_pem_file(Path::new(&cert_file), Path::new(&key_file)) - .await - .unwrap(); - - let socket_addr = addr.parse()?; - - axum_server::bind_rustls(socket_addr, config) - .serve(router) - .await? - } - _ => { - tracing::info!("Running {:?} HTTP server on {}", model_type, &addr); - - let listener = tokio::net::TcpListener::bind(&addr) - .await - .expect("Invalid address: {addr}"); - - axum::serve(listener, router).await?; - } - } - - Ok(()) + maybe_key_file, + "HTTP", + state, + |state| { + T::http_router(state) + .layer( + tower_http::trace::TraceLayer::new_for_http() + .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)), + ) + .into_make_service_with_connect_info::() + }, + ) + .await } pub async fn run_mcp( @@ -134,51 +72,76 @@ pub async fn run_mcp( maybe_key_file: Option, state: AppState, ) -> Result<()> { - let addr = format!("{}:{}", &hostname, &port); + serve_with_optional_tls( + hostname, + port, + maybe_cert_file, + maybe_key_file, + "MCP", + state, + |state| { + T::mcp_router(state) + .layer( + tower_http::trace::TraceLayer::new_for_http() + // TODO check if otel is enabled + // .make_span_with(crate::middleware::format_span) + .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)), + ) + .into_make_service_with_connect_info::() + }, + ) + .await +} - // FIXME add otel around here - let model_type = T::enum_val(); +async fn serve_with_optional_tls( + hostname: String, + port: String, + maybe_cert_file: Option, + maybe_key_file: Option, + server_type_str: &str, + state: AppState, + into_service_fn: impl Fn(AppState) -> IntoMakeServiceWithConnectInfo, +) -> Result<()> { + let addr = format!("{}:{}", &hostname, &port); - let router = T::mcp_router(state) - .layer( - tower_http::trace::TraceLayer::new_for_http() - // TODO check if otel is enabled - // .make_span_with(crate::middleware::format_span) - .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)), - ) - .into_make_service_with_connect_info::(); + let router = into_service_fn(state); - tracing::info!( - "TLS configuration: cert file: {:?}, cert key: {:?}", - maybe_cert_file, - maybe_key_file - ); + let model_type = T::enum_val(); match (maybe_cert_file, maybe_key_file) { - (Some(cert_file), Some(key_file)) => { - tracing::info!("Running {:?} MPC-on-TLS server on {}", model_type, &addr); - - // ref: https://github.com/tokio-rs/axum/blob/main/examples/tls-rustls/src/main.rs#L45 - // configure certificate and private key used by https - let config = RustlsConfig::from_pem_file(Path::new(&cert_file), Path::new(&key_file)) - .await - .unwrap(); - + (Some(cert), Some(key)) => { + tracing::debug!( + "TLS configuration: cert file: {:?}, cert key: {:?}", + cert.as_str(), + key.as_str() + ); + + tracing::info!( + "Running {:?} {:?} server with TLS on {}", + model_type, + server_type_str, + &addr + ); + + let config = RustlsConfig::from_pem_file(Path::new(&cert), Path::new(&key)).await?; let socket_addr = addr.parse()?; - axum_server::bind_rustls(socket_addr, config) .serve(router) - .await? + .await?; } - _ => { - tracing::info!("Running {:?} MCP server on {}", model_type, &addr); - - let listener = tokio::net::TcpListener::bind(&addr) - .await - .expect("Invalid address: {addr}"); - + (None, None) => { + tracing::info!( + "Running {:?} {:?} server on {}", + model_type, + server_type_str, + &addr + ); + let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, router).await?; } + _ => { + anyhow::bail!("Both cert and key file must be set when TLS is enabled"); + } } Ok(()) diff --git a/encoderfile/tests/integration/test_build.rs b/encoderfile/tests/integration/test_build.rs new file mode 100644 index 00000000..f6a9577d --- /dev/null +++ b/encoderfile/tests/integration/test_build.rs @@ -0,0 +1,176 @@ +use anyhow::{Context, Result, bail}; + +use encoderfile::build_cli::cli::GlobalArguments; +use std::{ + fs, + path::Path, + process::{Child, Command}, + thread::sleep, + time::{Duration, Instant}, +}; +use tempfile::tempdir; + +const BINARY_NAME: &str = "test.encoderfile"; + +fn config(model_path: &Path, output_path: &Path) -> String { + format!( + r##" +encoderfile: + name: test-model + path: {:?} + model_type: token_classification + output_path: {:?} + transform: | + --- Applies a softmax across token classification logits. + --- Each token classification is normalized independently. + --- + --- Args: + --- arr (Tensor): A tensor of shape [batch_size, n_tokens, n_labels]. + --- The softmax is applied along the third axis (n_labels). + --- + --- Returns: + --- Tensor: The input tensor with softmax-normalized embeddings. + ---@param arr Tensor + ---@return Tensor + function Postprocess(arr) + return arr:softmax(3) + end + "##, + model_path, output_path + ) +} + +const MODEL_ASSETS_PATH: &str = "../models/token_classification"; + +#[test] +fn test_build_encoderfile() -> Result<()> { + let dir = tempdir()?; + let path = dir + .path() + .canonicalize() + .expect("Failed to canonicalize temp path"); + + let tmp_model_path = path.join("models").join("token_classification"); + + let ef_config_path = path.join("encoderfile.yml"); + let encoderfile_path = path.join(BINARY_NAME); + + // copy model assets to temp dir + copy_dir_all(MODEL_ASSETS_PATH, tmp_model_path.as_path()) + .expect("Failed to copy model assets to temp directory"); + + if !tmp_model_path.join("model.onnx").exists() { + bail!( + "Path {:?} does not exist", + tmp_model_path.join("model.onnx") + ); + } + + // compile base binary and copy to temp dir + let _ = Command::new("cargo") + .args(["build", "-p", "encoderfile-runtime"]) + .status() + .expect("Failed to build encoderfile-runtime"); + + let base_binary_path = fs::canonicalize("../target/debug/encoderfile-runtime") + .expect("Failed to canonicalize base binary path"); + + // write encoderfile config + let config = config(tmp_model_path.as_path(), encoderfile_path.as_path()); + + fs::write(ef_config_path.as_path(), config.as_bytes()) + .expect("Failed to write encoderfile config"); + + let build_args = + encoderfile::build_cli::cli::test_build_args(ef_config_path.as_path(), base_binary_path); + + // build encoderfile + let global_args = GlobalArguments::default(); + + build_args + .run(&global_args) + .context("Failed to build encoderfile")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(encoderfile_path.as_path()) + .expect("Failed to get path for built encoderfile") + .permissions(); + + perms.set_mode(0o755); + fs::set_permissions(encoderfile_path.as_path(), perms).expect("Failed to set permissions"); + } + + // serve encoderfile + let mut child = spawn_encoderfile( + encoderfile_path + .to_str() + .expect("Failed to create encoderfile binary path"), + )?; + + wait_for_http("http://localhost:8080/health", Duration::from_secs(10))?; + + child.kill()?; + child.wait().ok(); + + Ok(()) +} + +fn wait_for_http(url: &str, timeout: Duration) -> Result<()> { + let client = reqwest::blocking::Client::new(); + let start = Instant::now(); + + loop { + if start.elapsed() > timeout { + anyhow::bail!("server did not become ready in time"); + } + + if let Ok(resp) = client.get(url).send() + && resp.status().is_success() + { + return Ok(()); + } + + sleep(Duration::from_millis(200)); + } +} + +fn spawn_encoderfile(path: &str) -> Result { + Command::new(path) + .arg("serve") + .arg("--disable-grpc") + .arg("--http-port") + .arg("8080") + .spawn() + .context("failed to spawn encoderfile process") +} + +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> anyhow::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + + fs::create_dir_all(dst).context(format!("Failed to create directory {:?}", &dst))?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let dest_path = dst.join(entry.file_name()); + + if ty.is_dir() { + copy_dir_all(entry.path(), dest_path.as_path()).context(format!( + "Failed to copy {:?} to {:?}", + entry.path(), + dest_path.as_path() + ))?; + } else { + fs::copy(entry.path(), dest_path.as_path()).context(format!( + "Failed to copy {:?} to {:?}", + entry.path(), + dest_path.as_path() + ))?; + } + } + + Ok(()) +}