diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 865bd14..13ee7b6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -170,9 +170,59 @@ jobs: path: dist/*-${{ matrix.target }}.tar.gz retention-days: 14 if-no-files-found: error + bundle-check: + needs: [policy, hygiene, source, msrv, alpine] + if: needs.policy.outputs.scope == 'full' + runs-on: ubuntu-24.04 + timeout-minutes: 25 + env: + MIX_ENV: docs + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.20.4' + otp-version: '29.1.1' + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.14.10' + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: nif-* + merge-multiple: true + path: dist + - name: Exercise complete release assembly without publication + run: | + python scripts/release.py validate + make deps docs inventory + version=$(python scripts/release.py validate) + tar -czf "dist/docs-$version.tar.gz" -C doc . + python scripts/package.py assemble --all + python scripts/release.py verify + previous=$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || true) + if test -n "$previous"; then + python scripts/release.py notes --from "$previous" --output dist/RELEASE_NOTES.md + else + python scripts/release.py notes --output dist/RELEASE_NOTES.md + fi + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-bundle + path: | + dist/ortex-* + dist/docs-* + dist/source.cdx.json + dist/artifacts.json + dist/SHA256SUMS + dist/RELEASE_NOTES.md + retention-days: 14 + if-no-files-found: error validated: if: always() - needs: [policy, hygiene, source, msrv, alpine] + needs: [policy, hygiene, source, msrv, alpine, bundle-check] runs-on: ubuntu-24.04 timeout-minutes: 5 steps: @@ -185,7 +235,7 @@ jobs: jobs = json.loads(os.environ['RESULTS']) scope = jobs['policy']['outputs'].get('scope') for name, job in jobs.items(): - allowed = {'success', 'skipped'} if name == 'alpine' and scope == 'pr' else {'success'} + allowed = {'success', 'skipped'} if name in {'alpine', 'bundle-check'} and scope == 'pr' else {'success'} if job['result'] not in allowed: raise SystemExit(f"{name}: {job['result']}") PY diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..08f7641 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: Publish approved Hex release +on: + release: + types: [published] +permissions: + contents: read +concurrency: + group: publish-${{ github.event.release.tag_name }} + cancel-in-progress: false +jobs: + publish: + if: github.repository == 'RiverFinancial/ortex' && vars.ORTEX_HEX_PUBLISH == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: release + permissions: + contents: read + attestations: read # Verify the previously attested artifacts before publishing. + id-token: write # Exchange the protected job identity for a package-scoped Hex token. + env: + MIX_ENV: docs + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 0 + persist-credentials: false + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: '1.20.4' + otp-version: '29.1.1' + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.14.10' + - name: Verify the approved release bundle + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + python scripts/release.py validate --tag "$RELEASE_TAG" + gh release download "$RELEASE_TAG" --repo RiverFinancial/ortex --dir dist + for artifact in dist/*; do + gh attestation verify "$artifact" --repo RiverFinancial/ortex + done + python scripts/release.py verify + make deps + - name: Publish package and documentation with Hex workload identity + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + ORTEX_HEX_PUBLISH: ${{ vars.ORTEX_HEX_PUBLISH }} + run: python scripts/release.py publish --tag "$RELEASE_TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..45b7078 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Stage release +on: + push: + tags: ['v*'] +permissions: + contents: read +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false +jobs: + validate: + # actionlint 1.7.12 does not yet parse GitHub's $/ reusable-workflow syntax. + uses: ./.github/workflows/checks.yml # zizmor: ignore[self-repository] + with: + scope: full + audit: + uses: ./.github/workflows/audit.yml # zizmor: ignore[self-repository] + verify-release: + needs: [validate, audit] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.14.10' + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-bundle + path: dist + - name: Verify the tag and complete tested bundle + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python scripts/release.py validate --tag "$RELEASE_TAG" + python scripts/release.py verify + stage: + if: github.repository == 'RiverFinancial/ortex' && vars.ORTEX_STAGE_RELEASES == 'true' + needs: verify-release + runs-on: ubuntu-24.04 + timeout-minutes: 10 + environment: release + permissions: + contents: write # Create only the fork's draft release and upload its bundle. + id-token: write # Authenticate build provenance attestation. + attestations: write # Record provenance for the completed release bundle. + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-bundle + path: dist + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/* + - name: Stage the draft release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + gh release create "$RELEASE_TAG" --repo RiverFinancial/ortex --draft --verify-tag --notes-file dist/RELEASE_NOTES.md + gh release upload "$RELEASE_TAG" dist/* --repo RiverFinancial/ortex diff --git a/Makefile b/Makefile index d8dc4c9..5aba60e 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ export ORT_DYLIB_PATH ?= $(shell $(PYTHON) scripts/install_runtime.py path 2>/de .PHONY: test.system test.package test.models test.tooling coverage bench fixtures package .PHONY: test.alpine .PHONY: setup.audit audit setup.rust +.PHONY: inventory release.notes help: ## Show development commands @awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_.-]+:.*## / {printf " %-22s %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -88,3 +89,9 @@ setup.rust: ## Install the pinned Rust compiler, formatter, and Clippy audit: ## Report Rust advisories and retired Hex dependencies $(PYTHON) scripts/audit.py + +inventory: ## Write the source dependency inventory (consumer-supplied ORT excluded) + MIX_ENV=prod $(PYTHON) scripts/inventory.py + +release.notes: ## Render conventional-commit notes and assembled artifact hashes + $(PYTHON) scripts/release.py notes $(ARGS) diff --git a/README.md b/README.md index 53c54fc..75e1ed0 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,66 @@ # Ortex -`Ortex` is a wrapper around [ONNX Runtime](https://onnxruntime.ai/) (implemented as -bindings to [`ort`](https://github.com/pykeio/ort)). Ortex leverages -[`Nx.Serving`](https://hexdocs.pm/nx/Nx.Serving.html) to easily deploy ONNX models -that run concurrently and distributed in a cluster. Ortex also provides a storage-only -tensor implementation for ease of use. +Ortex wraps [ONNX Runtime](https://onnxruntime.ai/) for Elixir through Rust and +supports numeric tensor transfer and inference with Nx.Serving. -ONNX models are a standard machine learning model format that can be exported from most ML -libraries like PyTorch and TensorFlow. Ortex allows for easy loading and fast inference of -ONNX models using different backends available to ONNX Runtime such as CUDA, TensorRT, Core -ML, and ARM Compute Library. +This is the RiverFinancial fork. Pin a reviewed commit from +https://github.com/RiverFinancial/ortex when consuming unreleased work; the +foundation stack does not publish a new Hex release. -## Examples +## Development -TL;DR: +Review `mise.toml`, run `mise trust` and `mise install`, then: -```elixir -iex> model = Ortex.load("./models/resnet50.onnx") -#Ortex.Model< - inputs: [{"input", "Float32", [nil, 3, 224, 224]}] - outputs: [{"output", "Float32", [nil, 1000]}]> -iex> {output} = Ortex.run(model, Nx.broadcast(0.0, {1, 3, 224, 224})) -iex> output |> Nx.backend_transfer() |> Nx.argmax -#Nx.Tensor< - s64 - 499 -> +```sh +mise exec -- make setup +mise exec -- make doctor +mise exec -- make help ``` -Inspecting a model shows the expected inputs, outputs, data types, and shapes. Axes with -`nil` represent a dynamic size. - -To see more real world examples see the `examples` folder. +Supported BEAM pairs are Elixir 1.19 / OTP 28 and Elixir 1.20 / OTP 29. Rust 1.92 +is the minimum. The source NIF currently retains ort rc.8 / Rustler 0.29; their +migration is separate feature work. Nx versions from 0.6.2 up to 1.0 are allowed +(excluding 1.0), with 0.13.1 locked for normal development. -### Serving - -`Ortex` also implements `Nx.Serving` behaviour. To use it in your application's -supervision tree consult the `Nx.Serving` docs. +CPU dynamic loading is the development default. ONNX Runtime is installed +separately by `make setup`. Make reads its recorded path; set ORT_DYLIB_PATH to +use another library. When consuming this source package, configure the mode +explicitly in your application's config: ```elixir -iex> serving = Nx.Serving.new(Ortex.Serving, model) -iex> batch = Nx.Batch.stack([{Nx.broadcast(0.0, {3, 224, 224})}]) -iex> {result} = Nx.Serving.run(serving, batch) -iex> result |> Nx.backend_transfer() |> Nx.argmax(axis: 1) -#Nx.Tensor< - s64[1] - [499] -> +import Config +config :ortex, Ortex.Native, features: ["ort/load-dynamic"] ``` -## Installation +The consumer supplies ORT_DYLIB_PATH. Execution providers require a matching +runtime and explicit Cargo features; they are not inferred from the host OS. -`Ortex` can be installed by adding `ortex` to your list of dependencies in `mix.exs`: +## Checks and examples -```elixir -def deps do - [ - {:ortex, "~> 0.1.10"} - ] -end +```sh +make check +make test.system +make test.package +make test.models +make coverage +make docs +make test.alpine +make bench ARGS=5000 ``` -You will need [Rust](https://www.rust-lang.org/tools/install) for compilation to succeed. +Library tests and doctests use normal ExUnit discovery under test/. System, +package, and model suites are documented in [testing](docs/testing.md). XGBoost, +Iris, and MNIST have prepared reference inputs and outputs; for example, with +ORT_DYLIB_PATH set: + +```sh +MIX_ENV=test mix run examples/run.exs xgboost +``` + +Numeric outputs use Ortex.Backend for storage. Transfer to a computation backend +before arithmetic. See [examples](examples/README.md) and +[Nx.Serving](https://hexdocs.pm/nx/Nx.Serving.html) for usage. + +[Development](docs/development.md), [CI policy](docs/ci.md), and +[release procedures](docs/releasing.md) describe the supported environment, +validation matrices, artifacts, and publishing gates. diff --git a/ci/containers/alpine.Dockerfile b/ci/containers/alpine.Dockerfile index fe0ef07..16cc30e 100644 --- a/ci/containers/alpine.Dockerfile +++ b/ci/containers/alpine.Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2@sha256:ad851f40ce103dcb4ad56f23877d99473ef5c013e09b9e57921ffe877ac6d6a9 +FROM hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2@sha256:ad851f40ce103dcb4ad56f23877d99473ef5c013e09b9e57921ffe877ac6d6a9 AS build RUN apk add --no-cache build-base git bash python3 rust=1.96.1-r0 cargo=1.96.1-r0 ca-certificates && \ apk add --no-cache \ --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \ @@ -11,4 +11,19 @@ COPY mix.exs mix.lock ./ RUN mix local.hex --force && mix local.rebar --force && mix deps.get --check-locked COPY . . RUN mix compile --warnings-as-errors -CMD ["sh", "-ec", "mix test && python3 scripts/fixtures.py && mix test test/models --include model && python3 scripts/scenario.py && python3 scripts/package.py test --mode source && python3 scripts/package.py test --mode prebuilt && if test -n \"${TARGET:-}\"; then python3 scripts/package.py nif --target \"$TARGET\"; fi"] +RUN mix test && python3 scripts/fixtures.py && mix test test/models --include model && \ + python3 scripts/scenario.py && python3 scripts/package.py test --mode source + +FROM hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2@sha256:ad851f40ce103dcb4ad56f23877d99473ef5c013e09b9e57921ffe877ac6d6a9 +RUN apk add --no-cache git bash python3 ca-certificates && \ + apk add --no-cache \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \ + --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing \ + onnxruntime=1.24.4-r1 +WORKDIR /app +ENV MIX_ENV=test ORT_DYLIB_PATH=/usr/lib/libonnxruntime.so.1.24.4 +COPY --from=build /app /app +COPY --from=build /root/.mix /root/.mix +RUN ! command -v cargo && ! command -v rustc +CMD ["sh", "-ec", "python3 scripts/package.py test --mode prebuilt && if test -n \"${TARGET:-}\"; then python3 scripts/package.py nif --target \"$TARGET\"; fi"] diff --git a/docs/ci.md b/docs/ci.md index b197929..c48c825 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,7 +1,7 @@ # CI policy Checks runs all hygiene checks and the library, model, system, and package suites -on three PR combinations. Main and the nightly use seven native combinations +on three PR combinations. Main and the nightly use eight native combinations (pairwise BEAM/Nx/runtime boundaries and five host platforms), plus both Alpine architectures. This is an explicit selection, not a Cartesian product. The `ci:full` PR label opts into the complete matrix before merge. @@ -46,3 +46,8 @@ Alpine edge repositories, whose transitive package selection is mutable. These images establish compatibility, not bit-for-bit reproducibility. glibc libraries are tested separately from musl. Published native artifacts include only the NIF; the consumer supplies ONNX Runtime. + +Full sweeps also assemble and verify the complete seven-target bundle, docs, +source inventory, and release notes. The staging workflow consumes that tested +bundle. Alpine prebuilt installation runs in a separate final image with neither +Cargo nor rustc installed. diff --git a/docs/development.md b/docs/development.md index db101f6..5d2781d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,6 +1,7 @@ # Development -Use `mise install`, then `mise exec -- make setup` and `mise exec -- make doctor`. +Review mise.toml and run `mise trust`, then `mise install`, +`mise exec -- make setup`, and `mise exec -- make doctor`. Rustup reads the pinned rust-toolchain.toml. GNU Make 3.81 or newer is sufficient; Windows developers can use Git Bash with Make available on PATH. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..1c8910f --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,48 @@ +# Releasing the fork + +Use atomic conventional commits: `feat`, `fix`, `perf`, `docs`, `build`, `ci`, +`test`, `refactor`, `style`, and `chore`. An optional scope describes the affected +area. `!` or a `BREAKING CHANGE:` footer marks a breaking change. Release notes +bucket these commits, retain other subjects under Other, and include artifact +SHA-256 values. These conventions do not require rewriting existing history. + +Before the first fork release, decide its Hex package identity and ownership, +choose a new version, and update Mix, Cargo.toml, Cargo.lock, and the changelog. +The current version is inherited from main; this stack does not claim that a new +0.1.10 release can be published over the existing public package. + +`make package` builds the source Hex archive. `scripts/package.py nif --target +TARGET` archives the already tested library in priv/native. `make inventory` +writes a CycloneDX source dependency inventory, including build-time Rust crates. +It is not an inventory of the precise code retained by the linker in each NIF. +ONNX Runtime is consumer supplied and is explicitly outside the package inventory. + +`scripts/package.py assemble --all` requires all seven target archives, checks +their member names and binary architectures, builds the Hex package, and writes +artifacts.json and SHA256SUMS. Partial local assembly omits --all and is explicitly +marked incomplete. A release must never use partial assembly. Native archive +metadata is normalized; no claim of reproducible native compilation is made. + +Configure required reviewers and tag restrictions on the release environment, +then explicitly enable ORTEX_STAGE_RELEASES in repository variables. Push a +matching vVERSION tag whose commit is already on the fork's main branch. +The staging workflow runs full compatibility and dependency audits, builds docs +and the inventory, assembles the complete bundle, generates notes, and creates a +draft GitHub release after protected-environment approval. Provenance covers the +bundle. Review it before publishing the GitHub release. No publication occurs as +part of developing this stack. Dependency advisories must be addressed before +the release audit can pass. + +Hex publication is disabled unless ORTEX_HEX_PUBLISH is explicitly set to true +in repository variables. Before enabling it, configure ownership, a Hex workload +identity restricted to this repository and publish.yml, and required reviewers +and tag restrictions on the GitHub release environment. Public package workload +identities cannot bootstrap a package's first release. The current publisher's +package scope is hexpm/ortex; change that scope and Mix package metadata together +if River chooses a distinct package identity. Do not configure an upstream +package's credentials or ownership as part of foundation work. + +Publishing the reviewed GitHub release triggers the protected Hex job. It checks +tag ancestry and matching versions, verifies every downloaded artifact's +attestation and checksums, obtains a short-lived package-scoped token through +OIDC, and publishes the package and docs. No long-lived Hex key is stored. diff --git a/docs/testing.md b/docs/testing.md index 455ed27..ed9e840 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -17,7 +17,9 @@ that configuration; the foundation tests main's existing behavior. temporary directory, and runs the separate test/package project against it. It checks source compilation and an explicitly configured prebuilt library with Cargo/rustc replaced by failing stubs. The runtime path comes from the caller's -ORT_DYLIB_PATH. This is a package installation check, not the feature stack's +ORT_DYLIB_PATH. The package check also rejects a missing runtime in a separate VM; ort rc.8's +legacy behavior is a caught NIF panic, not the migrated descriptive loader error. +This is a package installation check, not the feature stack's automatic RustlerPrecompiled download path. That feature can reuse this harness. `make test.models` checks XGBoost, Iris, and MNIST. Prepared artifacts and reference @@ -26,7 +28,7 @@ from the original estimators; MNIST uses Python ORT as a binding reference, with synthetic pixels. None of these checks claim to validate ORT's operators or model accuracy. Iris uses tensor probabilities on main; ZipMap needs the separate container-output feature. Install fixtures/requirements.txt in a separate Python -3.12 environment before `make fixtures`. Generation validates exports with ONNX +3.12 environment before `make fixtures PYTHON=/path/to/exporter/bin/python`. Generation validates exports with ONNX and Python ORT; ordinary tests need neither Python exporters nor network access. `make test.tooling` checks the harnesses themselves, including partial staging, diff --git a/mix.exs b/mix.exs index b86b807..3c0797b 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,13 @@ defmodule Ortex.MixProject do homepage_url: "https://github.com/RiverFinancial/ortex", docs: [ main: "readme", - extras: ["README.md"] + extras: [ + "README.md", + "docs/development.md", + "docs/testing.md", + "docs/ci.md", + "docs/releasing.md" + ] ], package: package() ] @@ -38,7 +44,7 @@ defmodule Ortex.MixProject do defp deps do [ {:rustler, "~> 0.29.1"}, - {:nx, "~> 0.6"}, + {:nx, ">= 0.6.2 and < 1.0.0"}, {:credo, "~> 1.7.19", only: :test, runtime: false}, {:ex_doc, "~> 0.40.4", only: :docs, runtime: false} ] @@ -46,7 +52,8 @@ defmodule Ortex.MixProject do defp package do [ - files: ~w(lib .formatter.exs mix.exs README* LICENSE* native/ortex/src/ config/config.exs + files: + ~w(lib .formatter.exs mix.exs README* LICENSE* docs native/ortex/src/ config/config.exs native/ortex/Cargo.lock native/ortex/Cargo.toml native/ortex/.cargo/config.toml), licenses: ["MIT"], links: %{"GitHub" => "https://github.com/RiverFinancial/ortex"}, diff --git a/native/ortex/Cargo.lock b/native/ortex/Cargo.lock index 705e174..caaa7f5 100644 --- a/native/ortex/Cargo.lock +++ b/native/ortex/Cargo.lock @@ -17,15 +17,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -38,17 +29,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "half" version = "2.3.1" @@ -71,12 +51,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "libc" -version = "0.2.153" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - [[package]] name = "libloading" version = "0.8.9" @@ -87,24 +61,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata", -] - [[package]] name = "matrixmultiply" version = "0.3.2" @@ -135,16 +91,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-complex" version = "0.4.3" @@ -203,23 +149,14 @@ dependencies = [ [[package]] name = "ortex" -version = "0.1.0" +version = "0.1.10" dependencies = [ "half", "ndarray", - "num-traits", "ort", "rustler", - "rustls", - "tracing-subscriber", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "pin-project-lite" version = "0.2.9" @@ -282,36 +219,12 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax", -] - [[package]] name = "regex-syntax" version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys", -] - [[package]] name = "rustler" version = "0.29.1" @@ -345,64 +258,6 @@ dependencies = [ "unreachable", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" - -[[package]] -name = "rustls-webpki" -version = "0.102.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "smallvec" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "2.0.16" @@ -414,16 +269,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tracing" version = "0.1.37" @@ -442,36 +287,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -489,126 +304,14 @@ dependencies = [ "void", ] -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/native/ortex/Cargo.toml b/native/ortex/Cargo.toml index 10f6db3..383541b 100644 --- a/native/ortex/Cargo.toml +++ b/native/ortex/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "ortex" -version = "0.1.0" +version = "0.1.10" +license = "MIT" +repository = "https://github.com/RiverFinancial/ortex" authors = [] edition = "2018" rust-version = "1.92" @@ -12,12 +14,9 @@ crate-type = ["cdylib"] [dependencies] rustler = "0.29.0" -ort = { version = "2.0.0-rc.8", default-features = false, features = ["ndarray", "half"] } +ort = { version = "=2.0.0-rc.8", default-features = false, features = ["ndarray", "half"] } ndarray = "0.16.1" half = "2.2.1" -tracing-subscriber = { version = "0.3", features = [ "env-filter", "fmt" ] } -num-traits = "0.2.15" -rustls = "0.22.4" [features] default = ["ort/load-dynamic"] diff --git a/native/ortex/src/model.rs b/native/ortex/src/model.rs index ccc8108..e204e3a 100644 --- a/native/ortex/src/model.rs +++ b/native/ortex/src/model.rs @@ -40,9 +40,6 @@ pub fn init( eps: Vec, opt: i32, ) -> Result { - // TODO: send tracing logs to erlang/elixir _somehow_ - // tracing_subscriber::fmt::init(); - let session = Session::builder()? .with_optimization_level(map_opt_level(opt))? .with_execution_providers(eps)? diff --git a/scripts/audit.py b/scripts/audit.py index 36e9b82..4bac8fb 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -1,5 +1,6 @@ """Report Rust advisories and retired Hex dependencies without hiding failures.""" +import json import os import subprocess import sys @@ -41,6 +42,20 @@ def main(): except (OSError, subprocess.TimeoutExpired) as error: status, output = 1, str(error) (destination / f"{name}.txt").write_text(output) + if name == "cargo-audit": + try: + data = json.JSONDecoder().raw_decode(output)[0] + (destination / "cargo-audit.json").write_text(json.dumps(data, indent=2) + "\n") + issues = data.get("vulnerabilities", {}).get("list", []) + warnings = sum(len(items) for items in data.get("warnings", {}).values()) + output = f"{len(issues)} vulnerabilities; {warnings} informational warnings.\n" + if issues: + output += "\n".join( + f"{item['advisory']['id']}: {item['package']['name']} {item['package']['version']} — {item['advisory']['title']}" + for item in issues + ) + except (ValueError, KeyError): + pass sections.append( f"## {name}: {'failed' if status else 'passed'}\n\n```text\n{output.rstrip()}\n```\n" ) diff --git a/scripts/inventory.exs b/scripts/inventory.exs new file mode 100644 index 0000000..d72e511 --- /dev/null +++ b/scripts/inventory.exs @@ -0,0 +1,15 @@ +components = + Mix.Dep.cached() + |> Enum.map(fn dep -> + {:hex, name, version, _, _, _, _, _} = dep.opts[:lock] + + %{ + type: "library", + name: to_string(name), + version: version, + purl: "pkg:hex/#{name}@#{version}" + } + end) + +File.mkdir_p!("dist") +File.write!("dist/hex-components.json", :json.encode(components)) diff --git a/scripts/inventory.py b/scripts/inventory.py new file mode 100644 index 0000000..1795066 --- /dev/null +++ b/scripts/inventory.py @@ -0,0 +1,66 @@ +"""Write a source dependency inventory in CycloneDX format; ORT is external.""" + +import json +import subprocess +from pathlib import Path + +from package import version + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + subprocess.run(["mix", "run", "--no-start", "scripts/inventory.exs"], cwd=ROOT, check=True) + result = subprocess.check_output( + [ + "cargo", + "metadata", + "--locked", + "--format-version", + "1", + "--features", + "ort/load-dynamic", + ], + cwd=ROOT / "native/ortex", + text=True, + ) + metadata = json.loads(result) + components = json.loads((ROOT / "dist/hex-components.json").read_text()) + for package in metadata["packages"]: + if package["name"] == "ortex": + continue + component = { + "type": "library", + "name": package["name"], + "version": package["version"], + "purl": f"pkg:cargo/{package['name']}@{package['version']}", + } + if package.get("license"): + component["licenses"] = [{"expression": package["license"].replace("/", " OR ")}] + components.append(component) + for component in components: + component["bom-ref"] = component["purl"] + bom = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": {"type": "library", "name": "ortex", "version": version()}, + "properties": [ + { + "name": "ortex:scope", + "value": "Source dependency inventory, including build-time Rust crates. Not a per-binary linkage inventory.", + }, + { + "name": "ortex:external-runtime", + "value": "ONNX Runtime is supplied by the consumer and is not bundled.", + }, + ], + }, + "components": sorted(components, key=lambda item: item["purl"]), + } + (ROOT / "dist/source.cdx.json").write_text(json.dumps(bom, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/package.py b/scripts/package.py index a9ef151..1c806d6 100644 --- a/scripts/package.py +++ b/scripts/package.py @@ -2,10 +2,13 @@ import argparse import gzip +import hashlib import io +import json import os import re import shutil +import struct import subprocess import sys import tarfile @@ -40,7 +43,7 @@ def unpack(archive, destination): def nif_archive(target): - policy = __import__("json").loads((ROOT / "ci/compatibility.json").read_text()) + policy = json.loads((ROOT / "ci/compatibility.json").read_text()) targets = {row["target"] for key in ["pr", "platforms", "alpine"] for row in policy[key]} if target not in targets: raise ValueError("target is not in the artifact contract") @@ -59,6 +62,72 @@ def nif_archive(target): return destination +def inspect_binary(data, target): + if "linux" in target: + if data[:4] != b"\x7fELF" or data[4:6] != b"\x02\x01": + raise ValueError("expected a little-endian ELF64 library") + machine = struct.unpack_from(" + unless error.original == :nif_panicked, do: reraise(error, __STACKTRACE__) +end + +IO.puts("Missing runtime was rejected in a separate VM") diff --git a/test/tooling/release_test.py b/test/tooling/release_test.py new file mode 100644 index 0000000..26e3b52 --- /dev/null +++ b/test/tooling/release_test.py @@ -0,0 +1,107 @@ +"""Test release classification and fail-closed artifact checks.""" + +import hashlib +import io +import json +import struct +import tarfile +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from harness_test import load + + +class ReleaseTest(unittest.TestCase): + def test_conventional_commits_keep_breaking_and_unknown_subjects(self): + release = load("release") + self.assertEqual( + release.group_commit("feat(io): add names", ""), ("Features", "io: add names") + ) + self.assertEqual( + release.group_commit("fix!: change shape", ""), ("Breaking changes", "change shape") + ) + self.assertEqual( + release.group_commit("chore: update runtime", "BREAKING CHANGE: new floor"), + ("Breaking changes", "update runtime"), + ) + self.assertEqual( + release.group_commit("An inherited subject", ""), ("Other", "An inherited subject") + ) + + def test_binary_architecture_must_match_target(self): + package = load("package") + header = bytearray(64) + header[:6] = b"\x7fELF\x02\x01" + struct.pack_into("