This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 32c5a1a9 build(bindings): package complete legal metadata (#561)
32c5a1a9 is described below

commit 32c5a1a9d271b061581af946ee583f0d9a0b00aa
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Jul 22 22:47:47 2026 +0800

    build(bindings): package complete legal metadata (#561)
---
 .gitattributes                               |  11 +
 .github/workflows/ci.yml                     |  58 ++
 .github/workflows/release-go-binding.yml     |  92 ++-
 .github/workflows/release_python_binding.yml | 104 ++-
 .licenserc.yaml                              |   1 +
 about.hbs                                    |  80 +++
 .licenserc.yaml => about.toml                |  36 +-
 bindings/go/RELEASE.md                       |   8 +-
 bindings/python/THIRD-PARTY-LICENSES.html    |  33 +
 bindings/python/pyproject.toml               |  11 +-
 scripts/release_licenses.py                  | 916 +++++++++++++++++++++++++++
 scripts/tests/test_release_licenses.py       |  72 +++
 scripts/tests/test_verify_python_wheels.py   |  62 ++
 scripts/verify_go_release_artifacts.py       | 194 ++++++
 scripts/verify_python_wheels.py              | 654 +++++++++++++++++++
 third-party-licenses/README.md               |  34 +
 third-party-licenses/openssl-1.1.1.LICENSE   | 124 ++++
 17 files changed, 2432 insertions(+), 58 deletions(-)

diff --git a/.gitattributes b/.gitattributes
index a313521e..d179afe7 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,13 @@
 # Generated TSVs keep trailing empty columns.
 *.tsv text eol=lf linguist-generated=true whitespace=-blank-at-eol
+
+# Legal files are verified byte-for-byte.
+LICENSE text eol=lf
+NOTICE text eol=lf
+**/LICENSE text eol=lf
+**/NOTICE text eol=lf
+**/THIRD-PARTY-LICENSES*.html text eol=lf linguist-generated=true
+third-party-licenses/* text eol=lf
+
+# Generated wheel legal files must never enter the ASF source archive.
+bindings/python/licenses/** export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0c10b63f..3113ba80 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,6 +39,51 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v7
+        with:
+          fetch-depth: 0
+
+      - name: Detect release license inputs
+        id: release_license_inputs
+        shell: bash
+        env:
+          EVENT_NAME: ${{ github.event_name }}
+          BEFORE_SHA: ${{ github.event.before }}
+          BASE_REF: ${{ github.base_ref }}
+        run: |
+          set -euo pipefail
+          if [[ "$EVENT_NAME" == "pull_request" ]]; then
+            base="origin/$BASE_REF"
+          elif [[ -n "$BEFORE_SHA" && ! "$BEFORE_SHA" =~ ^0+$ ]] && \
+            git cat-file -e "$BEFORE_SHA^{commit}"; then
+            base="$BEFORE_SHA"
+          else
+            echo "changed=true" >> "$GITHUB_OUTPUT"
+            exit 0
+          fi
+
+          if git diff --quiet "$base" HEAD -- \
+            .gitattributes \
+            LICENSE \
+            NOTICE \
+            ':(glob)**/LICENSE' \
+            ':(glob)**/LICENSE.*' \
+            ':(glob)**/NOTICE' \
+            ':(glob)**/NOTICE.*' \
+            Cargo.lock \
+            about.hbs \
+            about.toml \
+            bindings/go/LICENSE \
+            bindings/go/NOTICE \
+            ':(glob)bindings/go/THIRD-PARTY-LICENSES.*.html' \
+            bindings/python/licenses \
+            bindings/python/THIRD-PARTY-LICENSES.html \
+            scripts/release_licenses.py \
+            third-party-licenses \
+            ':(glob)**/Cargo.toml'; then
+            echo "changed=false" >> "$GITHUB_OUTPUT"
+          else
+            echo "changed=true" >> "$GITHUB_OUTPUT"
+          fi
 
       - name: Validate .asf.yaml
         run: python3 scripts/validate_asf_yaml.py
@@ -46,11 +91,20 @@ jobs:
       - name: Check License Header
         uses: apache/skywalking-eyes/[email protected]
 
+      - name: Test release artifact verifiers
+        run: python3 -m unittest discover -s scripts/tests -p 'test_*.py'
+
       - name: Install cargo-deny
         uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 
# v2.84.0
         with:
           tool: [email protected]
 
+      - name: Install cargo-about
+        if: steps.release_license_inputs.outputs.changed == 'true'
+        uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 
# v2.84.0
+        with:
+          tool: [email protected]
+
       - name: Fetch locked dependencies
         run: cargo fetch --locked
 
@@ -60,6 +114,10 @@ jobs:
       - name: Verify dependency reports
         run: python3 scripts/dependencies.py verify
 
+      - name: Verify release license files
+        if: steps.release_license_inputs.outputs.changed == 'true'
+        run: python3 scripts/release_licenses.py --check
+
       - name: Format
         run: cargo fmt --all -- --check
 
diff --git a/.github/workflows/release-go-binding.yml 
b/.github/workflows/release-go-binding.yml
index efa7bf2e..9d61675d 100644
--- a/.github/workflows/release-go-binding.yml
+++ b/.github/workflows/release-go-binding.yml
@@ -36,7 +36,7 @@ on:
         type: string
 
 permissions:
-  contents: write
+  contents: read
 
 jobs:
   validate:
@@ -103,12 +103,20 @@ jobs:
         include:
           - runner: ubuntu-latest
             expected_file: libpaimon_c.linux.amd64.so.zst
+            rust_target: x86_64-unknown-linux-gnu
+            license_file: THIRD-PARTY-LICENSES.linux.amd64.html
           - runner: ubuntu-24.04-arm
             expected_file: libpaimon_c.linux.arm64.so.zst
+            rust_target: aarch64-unknown-linux-gnu
+            license_file: THIRD-PARTY-LICENSES.linux.arm64.html
           - runner: macos-15-intel
             expected_file: libpaimon_c.darwin.amd64.dylib.zst
+            rust_target: x86_64-apple-darwin
+            license_file: THIRD-PARTY-LICENSES.darwin.amd64.html
           - runner: macos-14
             expected_file: libpaimon_c.darwin.arm64.dylib.zst
+            rust_target: aarch64-apple-darwin
+            license_file: THIRD-PARTY-LICENSES.darwin.arm64.html
 
     steps:
       - uses: actions/checkout@v7
@@ -120,6 +128,10 @@ jobs:
           rustup update stable
           rustup default stable
 
+      - uses: actions/setup-python@v6
+        with:
+          python-version: "3.11"
+
       - name: Cache Rust build outputs
         uses: actions/cache@v6
         with:
@@ -131,6 +143,11 @@ jobs:
             target/
           key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ 
hashFiles('**/Cargo.lock') }}
 
+      - name: Install cargo-about
+        uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 
# v2.84.0
+        with:
+          tool: [email protected]
+
       - name: Install zstd on Linux
         if: runner.os == 'Linux'
         run: sudo apt-get update && sudo apt-get install -y zstd
@@ -143,17 +160,27 @@ jobs:
         working-directory: bindings/go
         run: make build
 
-      - name: Verify packaged library
-        working-directory: bindings/go
+      - name: Stage and verify packaged library
         shell: bash
         run: |
-          test -s '${{ matrix.expected_file }}'
+          mkdir -p release/go
+          cargo fetch --locked
+          python3 scripts/release_licenses.py \
+            --generate-go-report \
+            '${{ matrix.rust_target }}' \
+            release/go
+          cp 'bindings/go/${{ matrix.expected_file }}' release/go/
+          python3 scripts/verify_go_release_artifacts.py \
+            --artifacts-dir release/go \
+            --target '${{ matrix.rust_target }}'
 
       - name: Upload packaged library
         uses: actions/upload-artifact@v7
         with:
           name: ${{ matrix.expected_file }}
-          path: bindings/go/${{ matrix.expected_file }}
+          path: |
+            release/go/${{ matrix.expected_file }}
+            release/go/${{ matrix.license_file }}
           if-no-files-found: error
 
   publish:
@@ -161,30 +188,41 @@ jobs:
       - validate
       - build
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
     steps:
       - uses: actions/checkout@v7
         with:
           fetch-depth: 0
           ref: ${{ needs.validate.outputs.source_sha }}
 
+      - uses: actions/setup-python@v6
+        with:
+          python-version: "3.11"
+
+      - name: Install cargo-about
+        uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 
# v2.84.0
+        with:
+          tool: [email protected]
+
+      - name: Fetch locked release inputs
+        run: cargo fetch --locked
+
+      - name: Install artifact verification tools
+        run: sudo apt-get update && sudo apt-get install -y file zstd
+
       - name: Download packaged libraries
         uses: actions/download-artifact@v8
         with:
-          path: bindings/go
+          path: release/go
           merge-multiple: true
 
-      - name: Verify embedded assets
-        working-directory: bindings/go
+      - name: Generate and verify release legal files
         shell: bash
         run: |
-          for file in \
-            libpaimon_c.linux.amd64.so.zst \
-            libpaimon_c.linux.arm64.so.zst \
-            libpaimon_c.darwin.amd64.dylib.zst \
-            libpaimon_c.darwin.arm64.dylib.zst
-          do
-            test -s "$file"
-          done
+          python3 scripts/release_licenses.py \
+            --generate-go-release release/go
+          python3 scripts/verify_go_release_artifacts.py --artifacts-dir 
release/go
 
       - name: Create release tag commit
         env:
@@ -197,7 +235,17 @@ jobs:
           git config user.name 'github-actions[bot]'
           git config user.email 
'41898282+github-actions[bot]@users.noreply.github.com'
 
-          git add bindings/go/libpaimon_c.*.zst
+          cp release/go/libpaimon_c.*.zst bindings/go/
+          cp \
+            release/go/LICENSE \
+            release/go/NOTICE \
+            release/go/THIRD-PARTY-LICENSES.*.html \
+            bindings/go/
+          git add \
+            bindings/go/libpaimon_c.*.zst \
+            bindings/go/LICENSE \
+            bindings/go/NOTICE \
+            bindings/go/THIRD-PARTY-LICENSES.*.html
 
           if ! git diff --cached --quiet; then
             git commit -m "bindings/go: release ${VERSION} (from 
${SOURCE_REF}@${SOURCE_SHA})"
@@ -211,12 +259,18 @@ jobs:
           TAG: ${{ needs.validate.outputs.tag }}
           VERSION: ${{ needs.validate.outputs.version }}
           GH_TOKEN: ${{ github.token }}
-        working-directory: bindings/go
+        working-directory: release/go
         run: |
           gh release create "$TAG" \
             --title "Release Go binding $VERSION" \
             --generate-notes \
+            LICENSE \
+            NOTICE \
             libpaimon_c.linux.amd64.so.zst \
             libpaimon_c.linux.arm64.so.zst \
             libpaimon_c.darwin.amd64.dylib.zst \
-            libpaimon_c.darwin.arm64.dylib.zst
+            libpaimon_c.darwin.arm64.dylib.zst \
+            THIRD-PARTY-LICENSES.linux.amd64.html \
+            THIRD-PARTY-LICENSES.linux.arm64.html \
+            THIRD-PARTY-LICENSES.darwin.amd64.html \
+            THIRD-PARTY-LICENSES.darwin.arm64.html
diff --git a/.github/workflows/release_python_binding.yml 
b/.github/workflows/release_python_binding.yml
index c6b0b89b..b2b95823 100644
--- a/.github/workflows/release_python_binding.yml
+++ b/.github/workflows/release_python_binding.yml
@@ -53,41 +53,72 @@ jobs:
         with:
           ref: ${{ env.RELEASE_TAG }}
 
+      - uses: actions/setup-python@v6
+        with:
+          python-version: "3.11"
+
       - name: Set Python release version
+        shell: bash
         run: >
           python3 scripts/python_release_version.py
           --tag "${RELEASE_TAG}"
           --pyproject bindings/python/pyproject.toml
 
+      - name: Verify Cargo lockfile
+        run: cargo metadata --locked --no-deps --format-version 1 > /dev/null
+
       - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # 
v1.51.0
         with:
+          maturin-version: v1.14.1
           working-directory: bindings/python
           command: sdist
           args: -o dist
-          before-script-linux: |
-            sudo apt-get update && sudo apt-get install -y libssl-dev
+
+      - name: Verify sdist
+        run: python scripts/verify_python_wheels.py --allow-partial 
bindings/python/dist
 
       - name: Upload sdist
         uses: actions/upload-artifact@v7
         with:
           name: wheels-sdist
-          path: bindings/python/dist
+          path: bindings/python/dist/*.tar.gz
 
   wheels:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         include:
-          - { os: windows-latest }
-          - { os: macos-15-intel, target: "x86_64-apple-darwin" }
-          - { os: macos-latest, target: "aarch64-apple-darwin" }
-          - { os: ubuntu-latest, target: "x86_64" }
-          - { os: ubuntu-latest, target: "aarch64", manylinux: 
"manylinux_2_28" }
+          - os: windows-latest
+            target: "x86_64-pc-windows-msvc"
+            rust_target: "x86_64-pc-windows-msvc"
+          - os: macos-15-intel
+            target: "x86_64-apple-darwin"
+            rust_target: "x86_64-apple-darwin"
+          - os: macos-latest
+            target: "aarch64-apple-darwin"
+            rust_target: "aarch64-apple-darwin"
+          - os: ubuntu-latest
+            target: "x86_64-unknown-linux-gnu"
+            rust_target: "x86_64-unknown-linux-gnu"
+            manylinux: "manylinux2014"
+          - os: ubuntu-latest
+            target: "aarch64-unknown-linux-gnu"
+            rust_target: "aarch64-unknown-linux-gnu"
+            manylinux: "manylinux_2_28"
     steps:
       - uses: actions/checkout@v7
         with:
           ref: ${{ env.RELEASE_TAG }}
 
+      - uses: actions/setup-python@v6
+        with:
+          python-version: "3.11"
+
+      - name: Install cargo-about
+        uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 
# v2.84.0
+        with:
+          tool: [email protected]
+
       - name: Set Python release version
         shell: bash
         run: >
@@ -95,16 +126,22 @@ jobs:
           --tag "${RELEASE_TAG}"
           --pyproject bindings/python/pyproject.toml
 
+      - name: Generate wheel legal files
+        run: |
+          cargo fetch --locked
+          python3 scripts/release_licenses.py --stage-python ${{ 
matrix.rust_target }}
+
       - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # 
v1.51.0
         with:
+          maturin-version: v1.14.1
           working-directory: bindings/python
           target: ${{ matrix.target }}
           command: build
-          args: --release -o dist
+          args: --release --locked -o dist
           manylinux: ${{ matrix.manylinux || 'auto' }}
           before-script-linux: |
             # Install OpenSSL for x86_64 (manylinux2014 with OpenSSL 1.1)
-            if [ "${{ matrix.target }}" = "x86_64" ]; then
+            if [ "${{ matrix.rust_target }}" = "x86_64-unknown-linux-gnu" ]; 
then
               yum install -y openssl11-devel
               # Create symlinks so pkg-config finds openssl11 as openssl
               ln -sf /usr/lib64/pkgconfig/openssl11.pc 
/usr/lib64/pkgconfig/openssl.pc
@@ -113,15 +150,23 @@ jobs:
             fi
 
             # Install precompiled OpenSSL from Debian packages for aarch64
-            if [ "${{ matrix.target }}" = "aarch64" ]; then
+            if [ "${{ matrix.rust_target }}" = "aarch64-unknown-linux-gnu" ]; 
then
               set -e
               echo "Installing OpenSSL for aarch64 from Debian packages..."
 
               (cd /tmp
-              # Debian Bullseye arm64 packages (OpenSSL 1.1.1)
-              DEBIAN_MIRROR="http://ftp.debian.org/debian/pool/main/o/openssl";
-              curl -sLO "${DEBIAN_MIRROR}/libssl1.1_1.1.1w-0+deb11u1_arm64.deb"
-              curl -sLO 
"${DEBIAN_MIRROR}/libssl-dev_1.1.1w-0+deb11u1_arm64.deb"
+              
DEBIAN_MIRROR="https://snapshot.debian.org/archive/debian/20231003T205808Z/pool/main/o/openssl";
+              LIBSSL_DEB="libssl1.1_1.1.1w-0+deb11u1_arm64.deb"
+              
LIBSSL_SHA256="fe7a7d313c87e46e62e614a07137e4a476a79fc9e5aab7b23e8235211280fee3"
+              LIBSSL_DEV_DEB="libssl-dev_1.1.1w-0+deb11u1_arm64.deb"
+              
LIBSSL_DEV_SHA256="6223f761bd4b961aa0c7c1662a6e99ab31150a1a7e6529d440428b361a9c8c4a"
+
+              curl --fail --location --show-error --silent \
+                --output "${LIBSSL_DEB}" "${DEBIAN_MIRROR}/${LIBSSL_DEB}"
+              curl --fail --location --show-error --silent \
+                --output "${LIBSSL_DEV_DEB}" 
"${DEBIAN_MIRROR}/${LIBSSL_DEV_DEB}"
+              printf '%s  %s\n' "${LIBSSL_SHA256}" "${LIBSSL_DEB}" | sha256sum 
-c -
+              printf '%s  %s\n' "${LIBSSL_DEV_SHA256}" "${LIBSSL_DEV_DEB}" | 
sha256sum -c -
 
               # Extract .deb packages (ar format)
               for deb in *.deb; do
@@ -156,11 +201,14 @@ jobs:
               export CFLAGS_aarch64_unknown_linux_gnu="-D__ARM_ARCH=8"
             fi
 
+      - name: Verify wheel
+        run: python scripts/verify_python_wheels.py --allow-partial 
bindings/python/dist
+
       - name: Upload wheels
         uses: actions/upload-artifact@v7
         with:
-          name: wheels-${{ matrix.os }}-${{ matrix.target || 'native' }}
-          path: bindings/python/dist
+          name: wheels-${{ matrix.os }}-${{ matrix.rust_target }}
+          path: bindings/python/dist/*.whl
 
   release:
     name: Publish to PyPI
@@ -170,12 +218,34 @@ jobs:
     needs: [sdist, wheels]
     if: startsWith(inputs.tag || github.ref_name, 'v')
     steps:
+      - uses: actions/checkout@v7
+        with:
+          ref: ${{ env.RELEASE_TAG }}
+
+      - uses: actions/setup-python@v6
+        with:
+          python-version: "3.11"
+
+      - name: Install cargo-about
+        uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 
# v2.84.0
+        with:
+          tool: [email protected]
+
+      - name: Generate expected wheel legal files
+        run: |
+          cargo fetch --locked
+          python3 scripts/release_licenses.py \
+            --generate-python-reports bindings/python/licenses
+
       - uses: actions/download-artifact@v8
         with:
           pattern: wheels-*
           merge-multiple: true
           path: bindings/python/dist
 
+      - name: Verify release artifacts
+        run: python scripts/verify_python_wheels.py bindings/python/dist
+
       - name: Publish to TestPyPI
         if: contains(env.RELEASE_TAG, '-')
         uses: 
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
diff --git a/.licenserc.yaml b/.licenserc.yaml
index 44db53da..c3cac61b 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -28,6 +28,7 @@ header:
     - ".github/PULL_REQUEST_TEMPLATE.md"
     - "crates/paimon/tests/**/*.json"
     - "crates/paimon/testdata/**"
+    - "third-party-licenses/openssl-1.1.1.LICENSE"
     - "**/go.sum"
     - "**/DEPENDENCIES.*.tsv"
     - ".devcontainer/devcontainer.json"
diff --git a/about.hbs b/about.hbs
new file mode 100644
index 00000000..3ecdc9cb
--- /dev/null
+++ b/about.hbs
@@ -0,0 +1,80 @@
+{{!
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+}}
+<!doctype html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <title>Apache Paimon Rust third-party licenses</title>
+    <style>
+        body {
+            font-family: sans-serif;
+            max-width: 900px;
+            margin: 0 auto;
+            padding: 1rem;
+        }
+        .licenses-list {
+            list-style-type: none;
+            margin: 0;
+            padding: 0;
+        }
+        .license-used-by {
+            margin-top: -10px;
+        }
+        .license-text {
+            white-space: pre-wrap;
+        }
+    </style>
+</head>
+
+<body>
+    <main>
+        <h1>Bundled Rust Components and Licenses</h1>
+        <p>
+            This document lists components distributed in an Apache Paimon
+            Rust binary artifact, together with their versions, copyright
+            notices, and complete license texts.
+        </p>
+
+        <h2>Overview of licenses</h2>
+        <ul>
+            {{#each overview}}
+            <li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
+            {{/each}}
+        </ul>
+
+        <h2>Rust crate license texts</h2>
+        <ul class="licenses-list">
+            {{#each licenses}}
+            <li class="license">
+                <h3 id="{{id}}">{{name}}</h3>
+                <h4>Used by</h4>
+                <ul class="license-used-by">
+                    {{#each used_by}}
+                    <li><a href="{{#if 
crate.repository}}{{crate.repository}}{{else}}https://crates.io/crates/{{crate.name}}{{/if}}";>{{crate.name}}
 {{crate.version}}</a></li>
+                    {{/each}}
+                </ul>
+                <pre class="license-text">{{text}}</pre>
+            </li>
+            {{/each}}
+        </ul>
+    </main>
+</body>
+
+</html>
diff --git a/.licenserc.yaml b/about.toml
similarity index 63%
copy from .licenserc.yaml
copy to about.toml
index 44db53da..ed5d0c06 100644
--- a/.licenserc.yaml
+++ b/about.toml
@@ -15,22 +15,22 @@
 # specific language governing permissions and limitations
 # under the License.
 
-header:
-  license:
-    spdx-id: Apache-2.0
-    copyright-owner: Apache Software Foundation
+accepted = [
+    "0BSD",
+    "Apache-2.0",
+    "Apache-2.0 WITH LLVM-exception",
+    "MIT",
+    "BSD-2-Clause",
+    "BSD-3-Clause",
+    "CC0-1.0",
+    "CDLA-Permissive-2.0",
+    "ISC",
+    "MPL-2.0",
+    "Unicode-3.0",
+    "Zlib",
+    "bzip2-1.0.6",
+]
 
-  paths-ignore:
-    - "LICENSE"
-    - "NOTICE"
-    - "Cargo.lock"
-    - ".gitattributes"
-    - ".github/PULL_REQUEST_TEMPLATE.md"
-    - "crates/paimon/tests/**/*.json"
-    - "crates/paimon/testdata/**"
-    - "**/go.sum"
-    - "**/DEPENDENCIES.*.tsv"
-    - ".devcontainer/devcontainer.json"
-    - "bindings/python/python/pypaimon_rust/py.typed"
-
-  comment: on-failure
+# Report linked runtime components only.
+ignore-build-dependencies = true
+ignore-dev-dependencies = true
diff --git a/bindings/go/RELEASE.md b/bindings/go/RELEASE.md
index 9fd547b9..2c1c19f6 100644
--- a/bindings/go/RELEASE.md
+++ b/bindings/go/RELEASE.md
@@ -76,6 +76,8 @@ submodule tag to contain the embedded artifacts required by 
Go module consumers.
   - `libpaimon_c.linux.arm64.so.zst`
   - `libpaimon_c.darwin.amd64.dylib.zst`
   - `libpaimon_c.darwin.arm64.dylib.zst`
+- Confirm `python3 scripts/release_licenses.py --check` can reproduce all 
binary
+  legal files from the locked dependency graph.
 - Confirm CI is green on `main` before publishing.
 
 ## Publish
@@ -91,16 +93,20 @@ submodule tag to contain the embedded artifacts required by 
Go module consumers.
   - `linux/arm64`
   - `darwin/amd64`
   - `darwin/arm64`
+- Confirm each build job generates and verifies the target-specific legal 
report.
 - Confirm the workflow creates the annotated tag `bindings/go/v0.1.0`.
 - Confirm the workflow publishes a GitHub release for that tag.
 
 ## After Release
 
-- Confirm the release contains all four compressed shared-library assets.
+- Confirm the release contains LICENSE, NOTICE, four compressed shared 
libraries,
+  and four matching third-party license reports.
 - Confirm the tag resolves to a commit that contains:
   - `bindings/go/go.mod`
   - `bindings/go/RELEASE.md`
   - the four `libpaimon_c.*.zst` files
+  - `bindings/go/LICENSE` and `bindings/go/NOTICE`
+  - the four `bindings/go/THIRD-PARTY-LICENSES.*.html` files
 - Record which Rust release tag or baseline commit this Go binding release was
   built from.
 - Verify module resolution from a clean environment:
diff --git a/bindings/python/THIRD-PARTY-LICENSES.html 
b/bindings/python/THIRD-PARTY-LICENSES.html
new file mode 100644
index 00000000..29afced7
--- /dev/null
+++ b/bindings/python/THIRD-PARTY-LICENSES.html
@@ -0,0 +1,33 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>Apache Paimon Rust Python source distribution licenses</title>
+</head>
+<body>
+    <h1>Third-party licenses for the Python source distribution</h1>
+    <p>This source distribution contains no compiled native library.</p>
+    <p>
+        Published wheels replace this file with a target-specific report
+        generated from Cargo.lock by <code>scripts/release_licenses.py</code>.
+    </p>
+</body>
+</html>
diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
index b2d94e63..131404e9 100644
--- a/bindings/python/pyproject.toml
+++ b/bindings/python/pyproject.toml
@@ -24,7 +24,8 @@ name = "pypaimon-rust"
 readme = "project-description.md"
 requires-python = ">=3.10,<4"
 dynamic = ["version"]
-license = { file = "LICENSE" }
+license = "Apache-2.0"
+license-files = ["LICENSE", "NOTICE", "THIRD-PARTY-LICENSES.html"]
 classifiers = [
     "Development Status :: 3 - Alpha",
     "Intended Audience :: Developers",
@@ -49,10 +50,14 @@ video = [
 module-name = "pypaimon_rust.pypaimon_rust"
 python-source = "python"
 include = [
-    { path = "LICENSE", format = ["sdist", "wheel"] },
-    { path = "NOTICE", format = ["sdist", "wheel"] },
+    { path = "LICENSE", format = "wheel" },
+    { path = "NOTICE", format = "wheel" },
+    { path = "THIRD-PARTY-LICENSES.html", format = "wheel" },
     { path = "python/pypaimon_rust/py.typed", format = ["sdist", "wheel"] },
 ]
+exclude = [
+    { path = "licenses/**", format = "sdist" },
+]
 
 [dependency-groups]
 dev = [
diff --git a/scripts/release_licenses.py b/scripts/release_licenses.py
new file mode 100644
index 00000000..8c6e6323
--- /dev/null
+++ b/scripts/release_licenses.py
@@ -0,0 +1,916 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Generate artifact-exact legal metadata for native release artifacts."""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import re
+import subprocess
+import sys
+import tempfile
+from dataclasses import dataclass
+from pathlib import Path
+
+
+CARGO_ABOUT_VERSION = "0.9.1"
+PYTHON_TARGETS = (
+    "x86_64-unknown-linux-gnu",
+    "aarch64-unknown-linux-gnu",
+    "x86_64-apple-darwin",
+    "aarch64-apple-darwin",
+    "x86_64-pc-windows-msvc",
+)
+GO_REPORTS = {
+    "x86_64-unknown-linux-gnu": "THIRD-PARTY-LICENSES.linux.amd64.html",
+    "aarch64-unknown-linux-gnu": "THIRD-PARTY-LICENSES.linux.arm64.html",
+    "x86_64-apple-darwin": "THIRD-PARTY-LICENSES.darwin.amd64.html",
+    "aarch64-apple-darwin": "THIRD-PARTY-LICENSES.darwin.arm64.html",
+}
+
+
+@dataclass(frozen=True)
+class Report:
+    component: str
+    manifest: str
+    target: str
+    output: str
+
+
+@dataclass(frozen=True)
+class LicenseCorrection:
+    crates: tuple[str, ...]
+    license_crate: str
+    license_path: str
+    license_name: str
+    anchor: str
+
+
+@dataclass(frozen=True)
+class BundledComponent:
+    crate: str
+    license_path: str
+    component: str
+    component_url: str
+    license_name: str
+    anchor: str
+    components: tuple[str, ...] = ()
+    targets: tuple[str, ...] = ()
+    license_from_repository: bool = False
+    required: bool = False
+    relationship: str = "bundled by"
+    crate_version: str = ""
+    required_features: tuple[str, ...] = ()
+
+
+ALLOC_CORRECTIONS = (
+    LicenseCorrection(
+        crates=("alloc-stdlib",),
+        license_crate="alloc-no-stdlib",
+        license_path="LICENSE",
+        license_name="BSD 3-Clause License (rust-alloc-no-stdlib)",
+        anchor="bsd-rust-alloc-no-stdlib",
+    ),
+    LicenseCorrection(
+        crates=("aws-lc-sys",),
+        license_crate="aws-lc-sys",
+        license_path="LICENSE",
+        license_name="AWS-LC License",
+        anchor="aws-lc-license",
+    ),
+)
+
+COMMON_MIT_CORRECTIONS = (
+    LicenseCorrection(
+        crates=("async-stream", "async-stream-impl"),
+        license_crate="async-stream",
+        license_path="LICENSE",
+        license_name="MIT License (async-stream workspace)",
+        anchor="mit-async-stream-workspace",
+    ),
+    LicenseCorrection(
+        crates=("brotli-decompressor",),
+        license_crate="brotli-decompressor",
+        license_path="LICENSE",
+        license_name="MIT License (brotli-decompressor)",
+        anchor="mit-brotli-decompressor",
+    ),
+    LicenseCorrection(
+        crates=("libm",),
+        license_crate="libm",
+        license_path="LICENSE.txt",
+        license_name="MIT License (libm)",
+        anchor="mit-libm",
+    ),
+)
+
+PYTHON_MIT_CORRECTIONS = (
+    LicenseCorrection(
+        crates=(
+            "ownedbytes",
+            "tantivy-bitpacker",
+            "tantivy-columnar",
+            "tantivy-common",
+            "tantivy-query-grammar",
+            "tantivy-sstable",
+            "tantivy-stacker",
+            "tantivy-tokenizer-api",
+        ),
+        license_crate="tantivy",
+        license_path="LICENSE",
+        license_name="MIT License (Tantivy workspace)",
+        anchor="mit-tantivy-workspace",
+    ),
+)
+
+BUNDLED_COMPONENTS = (
+    BundledComponent(
+        crate="zstd-sys",
+        license_path="zstd/LICENSE",
+        component="vendored Zstandard C sources",
+        component_url="https://github.com/facebook/zstd";,
+        license_name="BSD 3-Clause License",
+        anchor="bundled-zstandard-bsd-3-clause",
+    ),
+    BundledComponent(
+        crate="rust-stemmers",
+        license_path="algorithms/LICENSE",
+        component="generated Snowball stemming algorithms",
+        component_url="https://snowballstem.org/";,
+        license_name="BSD 3-Clause License",
+        anchor="bundled-snowball-bsd-3-clause",
+    ),
+    BundledComponent(
+        crate="regex-syntax",
+        license_path="src/unicode_tables/LICENSE-UNICODE",
+        component="generated Unicode character database tables",
+        component_url="https://www.unicode.org/";,
+        license_name="Unicode Data Files and Software License",
+        anchor="bundled-regex-syntax-unicode",
+    ),
+    BundledComponent(
+        crate="liblzma-sys",
+        license_path="xz/COPYING.0BSD",
+        component="XZ Utils 5.8.3 liblzma C sources",
+        component_url="https://github.com/tukaani-project/xz/tree/v5.8.3";,
+        license_name="BSD Zero Clause License",
+        anchor="bundled-xz-utils-0bsd",
+        components=("python",),
+        required=True,
+        relationship="statically linked through",
+        crate_version="0.4.7",
+        required_features=("static",),
+    ),
+    BundledComponent(
+        crate="openssl-sys",
+        license_path="third-party-licenses/openssl-1.1.1.LICENSE",
+        component="OpenSSL 1.1.1k FIPS libssl and libcrypto shared libraries",
+        component_url="https://github.com/openssl/openssl/tree/OpenSSL_1_1_1k";,
+        license_name="OpenSSL 1.1.1 and Original SSLeay Licenses",
+        anchor="bundled-openssl-1.1.1",
+        components=("python",),
+        targets=("x86_64-unknown-linux-gnu",),
+        license_from_repository=True,
+        required=True,
+        relationship="linked through",
+    ),
+    BundledComponent(
+        crate="openssl-sys",
+        license_path="third-party-licenses/openssl-1.1.1.LICENSE",
+        component="OpenSSL 1.1.1w libssl and libcrypto shared libraries",
+        component_url="https://github.com/openssl/openssl/tree/OpenSSL_1_1_1w";,
+        license_name="OpenSSL 1.1.1 and Original SSLeay Licenses",
+        anchor="bundled-openssl-1.1.1",
+        components=("python",),
+        targets=("aarch64-unknown-linux-gnu",),
+        license_from_repository=True,
+        required=True,
+        relationship="linked through",
+    ),
+)
+
+ALLOC_PLACEHOLDER = "Copyright (c) &lt;year&gt; &lt;owner&gt;."
+MIT_PLACEHOLDER = "Copyright (c) &lt;year&gt; &lt;copyright holders&gt;"
+LICENSE_PLACEHOLDERS = (
+    "&lt;year&gt;",
+    "&lt;owner&gt;",
+    "&lt;copyright holders&gt;",
+    "<year>",
+    "<owner>",
+    "<copyright holders>",
+)
+ASF_DEVELOPED_NOTICE = re.compile(
+    r"This product includes software developed at\n"
+    r"The Apache Software Foundation \(https?://www\.apache\.org/\)\."
+)
+
+
+def repository_root() -> Path:
+    return Path(
+        subprocess.check_output(
+            ["git", "rev-parse", "--show-toplevel"], text=True
+        ).strip()
+    )
+
+
+def report_specs() -> list[Report]:
+    reports = [
+        Report(
+            component="python",
+            manifest="bindings/python/Cargo.toml",
+            target=target,
+            
output=(f"bindings/python/licenses/{target}/THIRD-PARTY-LICENSES.html"),
+        )
+        for target in PYTHON_TARGETS
+    ]
+    reports.extend(
+        Report(
+            component="go",
+            manifest="bindings/c/Cargo.toml",
+            target=target,
+            output=f"bindings/go/{filename}",
+        )
+        for target, filename in GO_REPORTS.items()
+    )
+    return reports
+
+
+def verify_cargo_about(root: Path) -> None:
+    output = subprocess.check_output(
+        ["cargo", "about", "--version"], cwd=root, text=True
+    ).strip()
+    actual = output.rsplit(" ", 1)[-1]
+    if actual != CARGO_ABOUT_VERSION:
+        raise RuntimeError(
+            f"cargo-about {CARGO_ABOUT_VERSION} is required, found {output!r}"
+        )
+
+
+def cargo_metadata(root: Path, report: Report) -> dict:
+    output = subprocess.check_output(
+        [
+            "cargo",
+            "metadata",
+            "--locked",
+            "--format-version",
+            "1",
+            "--manifest-path",
+            report.manifest,
+            "--filter-platform",
+            report.target,
+        ],
+        cwd=root,
+        text=True,
+    )
+    metadata = json.loads(output)
+    tree = subprocess.check_output(
+        [
+            "cargo",
+            "tree",
+            "--locked",
+            "--manifest-path",
+            report.manifest,
+            "--target",
+            report.target,
+            "--edges",
+            "normal",
+            "--prefix",
+            "none",
+            "--format",
+            "{p}",
+        ],
+        cwd=root,
+        text=True,
+    )
+    packages = set()
+    for line in tree.splitlines():
+        match = re.match(r"^([A-Za-z0-9_-]+) v([^ ]+)", line)
+        if match is None:
+            raise RuntimeError(f"unexpected cargo tree package line: {line!r}")
+        packages.add(match.groups())
+    metadata["normal_packages"] = packages
+    return metadata
+
+
+def generate_base_report(root: Path, report: Report, output: Path) -> str:
+    subprocess.run(
+        [
+            "cargo",
+            "about",
+            "generate",
+            "--frozen",
+            "--fail",
+            "--config",
+            str(root / "about.toml"),
+            "--manifest-path",
+            report.manifest,
+            "--target",
+            report.target,
+            "--output-file",
+            str(output),
+            str(root / "about.hbs"),
+        ],
+        cwd=root,
+        check=True,
+    )
+    return output.read_text(encoding="utf-8")
+
+
+def resolved_packages(metadata: dict) -> list[dict]:
+    normal_packages = metadata["normal_packages"]
+    return [
+        package
+        for package in metadata["packages"]
+        if (package["name"], package["version"]) in normal_packages
+    ]
+
+
+def package_by_name(
+    metadata: dict, crate_name: str, required: bool = True
+) -> dict | None:
+    matches = [
+        package
+        for package in resolved_packages(metadata)
+        if package["name"] == crate_name
+    ]
+    if not matches and not required:
+        return None
+    if len(matches) != 1:
+        versions = [package["version"] for package in matches]
+        raise RuntimeError(
+            f"expected exactly one resolved {crate_name} package, found 
{versions}"
+        )
+    return matches[0]
+
+
+def package_features(metadata: dict, package: dict) -> set[str]:
+    matches = [
+        node["features"]
+        for node in metadata["resolve"]["nodes"]
+        if node["id"] == package["id"]
+    ]
+    if len(matches) != 1:
+        raise RuntimeError(f"could not resolve features for {package['name']}")
+    return set(matches[0])
+
+
+def correction_html(metadata: dict, correction: LicenseCorrection) -> str:
+    used_by = []
+    for crate_name in correction.crates:
+        package = package_by_name(metadata, crate_name)
+        repository = package.get("repository") or (
+            f"https://crates.io/crates/{crate_name}";
+        )
+        used_by.append(
+            f'                    <li><a href="{html.escape(repository, 
quote=True)}">'
+            f"{html.escape(crate_name)} 
{html.escape(package['version'])}</a></li>"
+        )
+
+    license_package = package_by_name(metadata, correction.license_crate)
+    license_file = (
+        Path(license_package["manifest_path"]).parent / correction.license_path
+    )
+    if not license_file.is_file():
+        raise RuntimeError(f"corrected license file is missing: 
{license_file}")
+    license_text = license_file.read_text(encoding="utf-8")
+
+    return "\n".join(
+        [
+            '            <li class="license corrected-license">',
+            f'                <h3 id="{html.escape(correction.anchor)}">'
+            f"{html.escape(correction.license_name)}</h3>",
+            "                <h4>Used by</h4>",
+            '                <ul class="license-used-by">',
+            *used_by,
+            "                </ul>",
+            f'                <pre 
class="license-text">{html.escape(license_text)}</pre>',
+            "            </li>",
+        ]
+    )
+
+
+def replace_placeholder_entry(
+    report_text: str,
+    metadata: dict,
+    marker: str,
+    corrections: tuple[LicenseCorrection, ...],
+) -> str:
+    if report_text.count(marker) != 1:
+        raise RuntimeError(
+            f"expected exactly one license placeholder {marker!r}, "
+            f"found {report_text.count(marker)}"
+        )
+    marker_index = report_text.index(marker)
+    entry_start = report_text.rfind('            <li class="license">', 0, 
marker_index)
+    entry_end = report_text.find("            </li>", marker_index)
+    if entry_start == -1 or entry_end == -1:
+        raise RuntimeError(f"could not locate license entry for {marker!r}")
+    entry_end += len("            </li>")
+    placeholder_entry = report_text[entry_start:entry_end]
+
+    actual_crates = set(
+        re.findall(
+            r'<li><a href="[^"]+">([A-Za-z0-9_-]+) [^<]+</a></li>',
+            placeholder_entry,
+        )
+    )
+    expected_crates = {
+        crate_name for correction in corrections for crate_name in 
correction.crates
+    }
+    if actual_crates != expected_crates:
+        raise RuntimeError(
+            f"placeholder dependency set changed for {marker!r}: expected "
+            f"{sorted(expected_crates)}, found {sorted(actual_crates)}"
+        )
+
+    replacement = "\n".join(
+        correction_html(metadata, correction) for correction in corrections
+    )
+    return report_text[:entry_start] + replacement + report_text[entry_end:]
+
+
+def bundled_component_html(
+    root: Path, report: Report, report_text: str, metadata: dict
+) -> str:
+    items = []
+    for component in BUNDLED_COMPONENTS:
+        if component.components and report.component not in 
component.components:
+            continue
+        if component.targets and report.target not in component.targets:
+            continue
+        package = package_by_name(metadata, component.crate, required=False)
+        if package is None:
+            if component.required:
+                raise RuntimeError(
+                    f"required bundled component crate is missing: 
{component.crate}"
+                )
+            continue
+        if component.crate_version and package["version"] != 
component.crate_version:
+            raise RuntimeError(
+                f"expected {component.crate} {component.crate_version}, "
+                f"found {package['version']}"
+            )
+        missing_features = set(component.required_features) - package_features(
+            metadata, package
+        )
+        if missing_features:
+            raise RuntimeError(
+                f"{component.crate} is missing required features: "
+                f"{sorted(missing_features)}"
+            )
+        marker = f">{component.crate} {package['version']}</a>"
+        if marker not in report_text:
+            if component.required:
+                raise RuntimeError(
+                    f"required bundled component is absent from report: 
{component.crate}"
+                )
+            # Cargo metadata may include workspace-unified features.
+            continue
+        if component.license_from_repository:
+            license_file = root / component.license_path
+        else:
+            license_file = (
+                Path(package["manifest_path"]).parent / component.license_path
+            )
+        if not license_file.is_file():
+            raise RuntimeError(f"bundled license file is missing: 
{license_file}")
+        license_text = license_file.read_text(encoding="utf-8")
+        repository = package.get("repository") or (
+            f"https://crates.io/crates/{component.crate}";
+        )
+        items.append(
+            "\n".join(
+                [
+                    '            <li class="license bundled-subcomponent">',
+                    f'                <h3 
id="{html.escape(component.anchor)}">'
+                    f"{html.escape(component.license_name)}</h3>",
+                    "                <h4>Bundled component</h4>",
+                    '                <ul class="license-used-by">',
+                    "                    <li>",
+                    f'                        <a 
href="{html.escape(component.component_url, quote=True)}">'
+                    f"{html.escape(component.component)}</a>, "
+                    f"{html.escape(component.relationship)}",
+                    f'                        <a 
href="{html.escape(repository, quote=True)}">'
+                    f"{html.escape(component.crate)} 
{html.escape(package['version'])}</a>",
+                    "                    </li>",
+                    "                </ul>",
+                    f'                <pre 
class="license-text">{html.escape(license_text)}</pre>',
+                    "            </li>",
+                ]
+            )
+        )
+
+    if not items:
+        return ""
+    return "\n".join(
+        [
+            "",
+            "        <h2>Licenses for bundled native and source 
components</h2>",
+            "        <p>",
+            "            These components ship with the native artifact but 
have",
+            "            license files outside their Rust package metadata.",
+            "        </p>",
+            '        <ul class="licenses-list bundled-subcomponents">',
+            *items,
+            "        </ul>",
+        ]
+    )
+
+
+def complete_report(
+    root: Path, base_report: str, report: Report, metadata: dict
+) -> str:
+    result = replace_placeholder_entry(
+        base_report, metadata, ALLOC_PLACEHOLDER, ALLOC_CORRECTIONS
+    )
+    mit_corrections = COMMON_MIT_CORRECTIONS
+    if report.component == "python":
+        mit_corrections += PYTHON_MIT_CORRECTIONS
+    result = replace_placeholder_entry(
+        result, metadata, MIT_PLACEHOLDER, mit_corrections
+    )
+
+    for placeholder in LICENSE_PLACEHOLDERS:
+        if placeholder in result:
+            raise RuntimeError(
+                f"generated report still contains license placeholder 
{placeholder!r}"
+            )
+
+    description = (
+        "\n        <p><strong>Artifact:</strong> "
+        f"<code>{html.escape(report.component)}</code></p>"
+        "\n        <p><strong>Rust target:</strong> "
+        f"<code>{html.escape(report.target)}</code></p>"
+        "\n        <p><strong>Root manifest:</strong> "
+        f"<code>{html.escape(report.manifest)}</code></p>"
+    )
+    first_paragraph_end = result.find("</p>")
+    if first_paragraph_end == -1:
+        raise RuntimeError("about.hbs output has no introductory paragraph")
+    first_paragraph_end += len("</p>")
+    result = result[:first_paragraph_end] + description + 
result[first_paragraph_end:]
+
+    closing_main = result.rfind("    </main>")
+    if closing_main == -1:
+        raise RuntimeError("about.hbs output has no closing main element")
+    result = (
+        result[:closing_main]
+        + bundled_component_html(root, report, result, metadata)
+        + "\n"
+        + result[closing_main:]
+    )
+    return "\n".join(line.rstrip() for line in result.rstrip().splitlines()) + 
"\n"
+
+
+def binary_license(apache_license: str, heading: str, reports: list[str]) -> 
str:
+    appendix = [
+        "",
+        "=" * 79,
+        "BUNDLED THIRD-PARTY COMPONENTS",
+        "=" * 79,
+        "",
+        heading,
+        "The component inventory, copyright notices, and complete license 
texts",
+        "are provided in:",
+        "",
+    ]
+    appendix.extend(f"    {report}" for report in reports)
+    return apache_license.rstrip() + "\n" + "\n".join(appendix) + "\n"
+
+
+def notice_paragraphs(text: str) -> list[str]:
+    normalized = "\n".join(line.rstrip() for line in text.strip().splitlines())
+    return [part.strip() for part in re.split(r"\n\s*\n", normalized) if 
part.strip()]
+
+
+def binary_notice(root: Path, metadata_by_target: list[dict]) -> str:
+    paragraphs = notice_paragraphs((root / 
"NOTICE").read_text(encoding="utf-8"))
+    seen_paragraphs = set(paragraphs)
+    seen_packages = set()
+    for metadata in metadata_by_target:
+        for package in resolved_packages(metadata):
+            package_key = (
+                package["name"],
+                package["version"],
+                package["manifest_path"],
+            )
+            if package_key in seen_packages:
+                continue
+            seen_packages.add(package_key)
+            package_dir = Path(package["manifest_path"]).parent
+            notice_files = sorted(
+                path
+                for path in package_dir.iterdir()
+                if path.is_file()
+                and path.name.lower() in {"notice", "notice.txt", "notice.md"}
+            )
+            for notice_file in notice_files:
+                notice_text = notice_file.read_text(encoding="utf-8")
+                for paragraph in notice_paragraphs(notice_text):
+                    if ASF_DEVELOPED_NOTICE.fullmatch(paragraph):
+                        continue
+                    if paragraph in seen_paragraphs:
+                        continue
+                    seen_paragraphs.add(paragraph)
+                    paragraphs.append(paragraph)
+
+    return "\n\n".join(paragraphs) + "\n"
+
+
+def verify_python_source_legal_files(root: Path) -> None:
+    for name in ("LICENSE", "NOTICE"):
+        source = root / name
+        copy = root / "bindings/python" / name
+        if source.read_bytes() != copy.read_bytes():
+            raise RuntimeError(f"restore bindings/python/{name} before 
generation")
+    source_report = root / "bindings/python/THIRD-PARTY-LICENSES.html"
+    report_text = source_report.read_text(encoding="utf-8")
+    for marker in (
+        "Python source distribution licenses",
+        "contains no compiled native library",
+    ):
+        if marker not in report_text:
+            raise RuntimeError(f"Python source license report is missing 
{marker!r}")
+
+
+def generate_reports(
+    root: Path, reports: list[Report]
+) -> tuple[dict[Report, str], dict[Report, dict]]:
+    verify_cargo_about(root)
+    result = {}
+    metadata_by_report = {}
+    with tempfile.TemporaryDirectory(prefix="paimon-rust-license-reports-") as 
temp:
+        temp_root = Path(temp)
+        for index, report in enumerate(reports):
+            print(f"generating {report.component} license report for 
{report.target}")
+            base = generate_base_report(
+                root, report, temp_root / f"report-{index}.html"
+            )
+            metadata = cargo_metadata(root, report)
+            metadata_by_report[report] = metadata
+            result[report] = complete_report(root, base, report, metadata)
+    return result, metadata_by_report
+
+
+def python_binary_files(
+    root: Path, target: str, report: str, metadata: dict
+) -> dict[str, str]:
+    apache_license = (root / "LICENSE").read_text(encoding="utf-8")
+    return {
+        "LICENSE": binary_license(
+            apache_license,
+            f"This binary wheel bundles the Rust native library for {target}.",
+            ["THIRD-PARTY-LICENSES.html"],
+        ),
+        "NOTICE": binary_notice(root, [metadata]),
+        "THIRD-PARTY-LICENSES.html": report,
+    }
+
+
+def python_target_files(root: Path, target: str, output_dir: Path) -> 
dict[Path, str]:
+    if target not in PYTHON_TARGETS:
+        raise ValueError(f"unsupported Python release target: {target}")
+    verify_python_source_legal_files(root)
+    report = next(
+        item
+        for item in report_specs()
+        if item.component == "python" and item.target == target
+    )
+    reports, metadata = generate_reports(root, [report])
+    return {
+        output_dir / name: content
+        for name, content in python_binary_files(
+            root, target, reports[report], metadata[report]
+        ).items()
+    }
+
+
+def python_verification_files(root: Path, output_dir: Path) -> dict[Path, str]:
+    verify_python_source_legal_files(root)
+    reports = [report for report in report_specs() if report.component == 
"python"]
+    generated, metadata = generate_reports(root, reports)
+    result = {}
+    for report in reports:
+        target_dir = output_dir / report.target
+        result.update(
+            {
+                target_dir / name: content
+                for name, content in python_binary_files(
+                    root,
+                    report.target,
+                    generated[report],
+                    metadata[report],
+                ).items()
+            }
+        )
+    return result
+
+
+def go_target_files(root: Path, target: str, output_dir: Path) -> dict[Path, 
str]:
+    if target not in GO_REPORTS:
+        raise ValueError(f"unsupported Go release target: {target}")
+    report = next(
+        item
+        for item in report_specs()
+        if item.component == "go" and item.target == target
+    )
+    generated, _ = generate_reports(root, [report])
+    return {output_dir / GO_REPORTS[target]: generated[report]}
+
+
+def go_release_files(root: Path, output_dir: Path) -> dict[Path, str]:
+    reports = [report for report in report_specs() if report.component == "go"]
+    generated, metadata = generate_reports(root, reports)
+    apache_license = (root / "LICENSE").read_text(encoding="utf-8")
+    result = {
+        output_dir / GO_REPORTS[report.target]: generated[report] for report 
in reports
+    }
+    result[output_dir / "LICENSE"] = binary_license(
+        apache_license,
+        "This Go module bundles Rust native libraries for four release 
targets.",
+        list(GO_REPORTS.values()),
+    )
+    result[output_dir / "NOTICE"] = binary_notice(
+        root, [metadata[report] for report in reports]
+    )
+    return result
+
+
+def generated_files(root: Path) -> dict[Path, str]:
+    verify_python_source_legal_files(root)
+    reports = report_specs()
+    generated, metadata = generate_reports(root, reports)
+    result = {root / report.output: generated[report] for report in reports}
+
+    apache_license = (root / "LICENSE").read_text(encoding="utf-8")
+    python_reports = [report for report in reports if report.component == 
"python"]
+    for report in python_reports:
+        license_dir = root / "bindings/python/licenses" / report.target
+        files = python_binary_files(
+            root, report.target, generated[report], metadata[report]
+        )
+        for name in ("LICENSE", "NOTICE"):
+            result[license_dir / name] = files[name]
+
+    go_reports = list(GO_REPORTS.values())
+    result[root / "bindings/go/LICENSE"] = binary_license(
+        apache_license,
+        "This Go module bundles Rust native libraries for four release 
targets.",
+        go_reports,
+    )
+    result[root / "bindings/go/NOTICE"] = binary_notice(
+        root,
+        [
+            metadata[report]
+            for report in reports
+            if report.component == "go"
+        ],
+    )
+    return result
+
+
+def check_generated_files(files: dict[Path, str], root: Path) -> None:
+    relative_paths = [str(path.relative_to(root)) for path in files]
+    tracked = subprocess.check_output(
+        ["git", "ls-files", "--", *relative_paths], cwd=root, text=True
+    ).splitlines()
+    if tracked:
+        raise RuntimeError(
+            "generated binary legal files must not be tracked: "
+            + ", ".join(tracked)
+        )
+
+    python_paths = [
+        relative
+        for relative in relative_paths
+        if relative.startswith("bindings/python/licenses/")
+    ]
+    for relative in python_paths:
+        attribute = subprocess.check_output(
+            ["git", "check-attr", "export-ignore", "--", relative],
+            cwd=root,
+            text=True,
+        ).strip()
+        if not attribute.endswith(": set"):
+            raise RuntimeError(f"generated binary legal file is not 
export-ignored: {relative}")
+
+    print(f"validated {len(files)} generated binary legal files")
+
+
+def write_files(files: dict[Path, str], root: Path) -> None:
+    for path, content in files.items():
+        path.parent.mkdir(parents=True, exist_ok=True)
+        path.write_bytes(content.encode("utf-8"))
+        display_path = path.relative_to(root) if path.is_relative_to(root) 
else path
+        print(f"generated {display_path}")
+
+
+def verify_existing_go_reports(files: dict[Path, str]) -> None:
+    report_names = set(GO_REPORTS.values())
+    for path, expected in files.items():
+        if path.name not in report_names:
+            continue
+        if not path.is_file():
+            raise RuntimeError(f"missing generated Go report: {path}")
+        if path.read_bytes() != expected.encode("utf-8"):
+            raise RuntimeError(f"generated Go report differs from release 
input: {path}")
+
+
+def stage_python(root: Path, target: str) -> None:
+    write_files(python_target_files(root, target, root / "bindings/python"), 
root)
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description=__doc__)
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument(
+        "--check",
+        action="store_true",
+        help="validate reproducible binary legal output without writing it",
+    )
+    group.add_argument(
+        "--stage-python",
+        metavar="TARGET",
+        help="stage one target's legal files for a maturin wheel build",
+    )
+    group.add_argument(
+        "--generate-python-reports",
+        metavar="DIRECTORY",
+        help="generate all target legal files for final wheel verification",
+    )
+    group.add_argument(
+        "--generate-go-report",
+        nargs=2,
+        metavar=("TARGET", "DIRECTORY"),
+        help="generate one target report for a Go build job",
+    )
+    group.add_argument(
+        "--generate-go-release",
+        metavar="DIRECTORY",
+        help="verify Go target reports and generate aggregate legal files",
+    )
+    args = parser.parse_args()
+    root = repository_root()
+
+    try:
+        if args.stage_python:
+            stage_python(root, args.stage_python)
+            return 0
+        if args.generate_python_reports:
+            files = python_verification_files(
+                root, Path(args.generate_python_reports).resolve()
+            )
+            write_files(files, root)
+            return 0
+        if args.generate_go_report:
+            target, directory = args.generate_go_report
+            files = go_target_files(root, target, Path(directory).resolve())
+            write_files(files, root)
+            return 0
+        if args.generate_go_release:
+            files = go_release_files(root, 
Path(args.generate_go_release).resolve())
+            verify_existing_go_reports(files)
+            write_files(
+                {
+                    path: content
+                    for path, content in files.items()
+                    if path.name in {"LICENSE", "NOTICE"}
+                },
+                root,
+            )
+            return 0
+        files = generated_files(root)
+    except (OSError, RuntimeError, ValueError, subprocess.CalledProcessError) 
as error:
+        print(f"release license generation failed: {error}", file=sys.stderr)
+        return 1
+
+    if args.check:
+        try:
+            check_generated_files(files, root)
+        except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
+            print(f"release license generation failed: {error}", 
file=sys.stderr)
+            return 1
+        return 0
+    write_files(files, root)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/tests/test_release_licenses.py 
b/scripts/tests/test_release_licenses.py
new file mode 100644
index 00000000..1742030e
--- /dev/null
+++ b/scripts/tests/test_release_licenses.py
@@ -0,0 +1,72 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from scripts.release_licenses import binary_notice, resolved_packages
+
+
+class ResolvedPackagesTest(unittest.TestCase):
+    def test_uses_normal_cargo_tree_packages(self) -> None:
+        metadata = {
+            "normal_packages": {("root", "1.0.0"), ("required", "2.0.0")},
+            "packages": [
+                {"name": "root", "version": "1.0.0"},
+                {"name": "required", "version": "2.0.0"},
+                {"name": "optional", "version": "3.0.0"},
+            ],
+        }
+        self.assertEqual(
+            [package["name"] for package in resolved_packages(metadata)],
+            ["root", "required"],
+        )
+
+    def test_notice_uses_only_normal_dependencies(self) -> None:
+        with tempfile.TemporaryDirectory() as temp:
+            root = Path(temp)
+            (root / "NOTICE").write_text("Root notice\n", encoding="utf-8")
+            packages = []
+            for name, notice in (
+                ("required", "Required dependency notice\n"),
+                ("optional", "Optional dependency notice\n"),
+            ):
+                package_dir = root / name
+                package_dir.mkdir()
+                manifest = package_dir / "Cargo.toml"
+                manifest.write_text("[package]\n", encoding="utf-8")
+                (package_dir / "NOTICE").write_text(notice, encoding="utf-8")
+                packages.append(
+                    {
+                        "name": name,
+                        "version": "1.0.0",
+                        "manifest_path": str(manifest),
+                    }
+                )
+
+            metadata = {
+                "normal_packages": {("required", "1.0.0")},
+                "packages": packages,
+            }
+            notice = binary_notice(root, [metadata])
+            self.assertIn("Required dependency notice", notice)
+            self.assertNotIn("Optional dependency notice", notice)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/scripts/tests/test_verify_python_wheels.py 
b/scripts/tests/test_verify_python_wheels.py
new file mode 100644
index 00000000..3b47cbe7
--- /dev/null
+++ b/scripts/tests/test_verify_python_wheels.py
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import tarfile
+import unittest
+
+from scripts.verify_python_wheels import (
+    VerificationError,
+    index_tar_members,
+    normalized_relative_path,
+)
+
+
+class ArchivePathTest(unittest.TestCase):
+    def test_rejects_non_canonical_paths(self) -> None:
+        for value in ("root/./LICENSE", "root//LICENSE", "C:/LICENSE"):
+            with self.subTest(value=value):
+                with self.assertRaises(VerificationError):
+                    normalized_relative_path(value, "archive member")
+
+    def test_rejects_tar_links(self) -> None:
+        member = tarfile.TarInfo("root/LICENSE")
+        member.type = tarfile.SYMTYPE
+        member.linkname = "NOTICE"
+        with self.assertRaises(VerificationError):
+            index_tar_members([member], "sdist")
+
+    def test_rejects_duplicate_tar_members(self) -> None:
+        first = tarfile.TarInfo("root/LICENSE")
+        second = tarfile.TarInfo("root/LICENSE")
+        with self.assertRaises(VerificationError):
+            index_tar_members([first, second], "sdist")
+
+    def test_rejects_portable_tar_collisions(self) -> None:
+        upper = tarfile.TarInfo("root/LICENSE")
+        lower = tarfile.TarInfo("root/license")
+        with self.assertRaises(VerificationError):
+            index_tar_members([upper, lower], "sdist")
+
+    def test_indexes_regular_tar_members(self) -> None:
+        license_member = tarfile.TarInfo("root/LICENSE")
+        notice_member = tarfile.TarInfo("root/NOTICE")
+        indexed = index_tar_members([license_member, notice_member], "sdist")
+        self.assertEqual(set(indexed), {"root/LICENSE", "root/NOTICE"})
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/scripts/verify_go_release_artifacts.py 
b/scripts/verify_go_release_artifacts.py
new file mode 100644
index 00000000..fc8c8e13
--- /dev/null
+++ b/scripts/verify_go_release_artifacts.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Verify Go native release artifacts and their target-specific licenses."""
+
+from __future__ import annotations
+
+import argparse
+import subprocess
+import sys
+import tempfile
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass(frozen=True)
+class Artifact:
+    library: str
+    report: str
+    format_markers: tuple[str, ...]
+    architecture_markers: tuple[str, ...]
+
+
+ARTIFACTS = {
+    "x86_64-unknown-linux-gnu": Artifact(
+        library="libpaimon_c.linux.amd64.so.zst",
+        report="THIRD-PARTY-LICENSES.linux.amd64.html",
+        format_markers=("elf 64-bit", "shared object"),
+        architecture_markers=("x86-64", "x86_64"),
+    ),
+    "aarch64-unknown-linux-gnu": Artifact(
+        library="libpaimon_c.linux.arm64.so.zst",
+        report="THIRD-PARTY-LICENSES.linux.arm64.html",
+        format_markers=("elf 64-bit", "shared object"),
+        architecture_markers=("aarch64", "arm64"),
+    ),
+    "x86_64-apple-darwin": Artifact(
+        library="libpaimon_c.darwin.amd64.dylib.zst",
+        report="THIRD-PARTY-LICENSES.darwin.amd64.html",
+        format_markers=("mach-o 64-bit", "dynamically linked shared library"),
+        architecture_markers=("x86_64", "x86-64"),
+    ),
+    "aarch64-apple-darwin": Artifact(
+        library="libpaimon_c.darwin.arm64.dylib.zst",
+        report="THIRD-PARTY-LICENSES.darwin.arm64.html",
+        format_markers=("mach-o 64-bit", "dynamically linked shared library"),
+        architecture_markers=("arm64", "aarch64"),
+    ),
+}
+
+LICENSE_PLACEHOLDERS = (
+    "&lt;year&gt;",
+    "&lt;owner&gt;",
+    "&lt;copyright holders&gt;",
+    "<year>",
+    "<owner>",
+    "<copyright holders>",
+)
+AVRO_NOTICE_MARKERS = (
+    "Apache Avro",
+    "Flavien Raynaud",
+    "donated to the Apache Avro project in 2020.",
+)
+
+
+def repository_root() -> Path:
+    return Path(__file__).resolve().parent.parent
+
+
+def require_file(path: Path) -> None:
+    if not path.is_file() or path.stat().st_size == 0:
+        raise ValueError(f"missing or empty release file: {path}")
+
+
+def verify_avro_notice(content: str, description: str) -> None:
+    for marker in AVRO_NOTICE_MARKERS:
+        if marker not in content:
+            raise ValueError(f"{description} is missing {marker!r}")
+
+
+def verify_report(report: Path, target: str) -> None:
+    require_file(report)
+    report_text = report.read_text(encoding="utf-8")
+    required_markers = (
+        "<strong>Artifact:</strong> <code>go</code>",
+        f"<strong>Rust target:</strong> <code>{target}</code>",
+        "<strong>Root manifest:</strong> <code>bindings/c/Cargo.toml</code>",
+    )
+    for marker in required_markers:
+        if marker not in report_text:
+            raise ValueError(f"{report} is missing {marker!r}")
+    for placeholder in LICENSE_PLACEHOLDERS:
+        if placeholder in report_text:
+            raise ValueError(f"{report} contains placeholder {placeholder!r}")
+
+
+def verify_library(library: Path, artifact: Artifact) -> None:
+    require_file(library)
+    with tempfile.TemporaryDirectory(prefix="paimon-go-release-") as temp_dir:
+        unpacked = Path(temp_dir) / library.name.removesuffix(".zst")
+        with unpacked.open("wb") as destination:
+            result = subprocess.run(
+                ["zstd", "-d", "-q", "-c", str(library)],
+                stdout=destination,
+                stderr=subprocess.PIPE,
+                text=True,
+                check=False,
+            )
+        if result.returncode != 0:
+            raise ValueError(f"cannot decompress {library}: 
{result.stderr.strip()}")
+        require_file(unpacked)
+        description = subprocess.check_output(
+            ["file", "-b", str(unpacked)], text=True
+        ).strip()
+
+    normalized = description.lower()
+    if any(marker not in normalized for marker in artifact.format_markers):
+        raise ValueError(f"unexpected binary format for {library}: 
{description}")
+    if not any(marker in normalized for marker in 
artifact.architecture_markers):
+        raise ValueError(f"unexpected binary architecture for {library}: 
{description}")
+
+
+def verify_target(root: Path, artifacts_dir: Path, target: str) -> None:
+    artifact = ARTIFACTS[target]
+    library = artifacts_dir / artifact.library
+    report = artifacts_dir / artifact.report
+    verify_library(library, artifact)
+    verify_report(report, target)
+    print(f"verified {artifact.library}: {target}")
+
+
+def verify_release_legal_files(root: Path, artifacts_dir: Path) -> None:
+    license_path = artifacts_dir / "LICENSE"
+    require_file(license_path)
+    license_content = license_path.read_text(encoding="utf-8")
+    apache_license = (root / "LICENSE").read_text(encoding="utf-8").rstrip()
+    if not license_content.startswith(apache_license + "\n"):
+        raise ValueError("binary LICENSE does not begin with the Apache 
License")
+    for artifact in ARTIFACTS.values():
+        if artifact.report not in license_content:
+            raise ValueError(f"binary LICENSE is missing {artifact.report}")
+
+    notice_path = artifacts_dir / "NOTICE"
+    require_file(notice_path)
+    notice = notice_path.read_text(encoding="utf-8")
+    verify_avro_notice(notice, "NOTICE")
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "--artifacts-dir",
+        required=True,
+        type=Path,
+        help="directory containing downloaded or locally built release assets",
+    )
+    parser.add_argument(
+        "--target",
+        choices=tuple(ARTIFACTS),
+        help="verify one matrix target; omit to verify the complete release 
set",
+    )
+    args = parser.parse_args()
+
+    root = repository_root()
+    artifacts_dir = args.artifacts_dir.resolve()
+    try:
+        if args.target:
+            verify_target(root, artifacts_dir, args.target)
+        else:
+            verify_release_legal_files(root, artifacts_dir)
+            for target in ARTIFACTS:
+                verify_target(root, artifacts_dir, target)
+    except (OSError, UnicodeError, ValueError, subprocess.SubprocessError) as 
error:
+        print(f"Go release artifact verification failed: {error}", 
file=sys.stderr)
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/verify_python_wheels.py b/scripts/verify_python_wheels.py
new file mode 100644
index 00000000..a01242f3
--- /dev/null
+++ b/scripts/verify_python_wheels.py
@@ -0,0 +1,654 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Verify Python release archives and their legal metadata."""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import csv
+import hashlib
+import io
+import re
+import stat
+import sys
+import tarfile
+import tomllib
+import unicodedata
+import zipfile
+from email import policy
+from email.message import Message
+from email.parser import BytesParser
+from pathlib import Path, PurePosixPath
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PYTHON_DIR = ROOT / "bindings/python"
+LEGAL_FILES = ("LICENSE", "NOTICE", "THIRD-PARTY-LICENSES.html")
+TARGETS = {
+    "x86_64-unknown-linux-gnu",
+    "aarch64-unknown-linux-gnu",
+    "x86_64-apple-darwin",
+    "aarch64-apple-darwin",
+    "x86_64-pc-windows-msvc",
+}
+PLACEHOLDERS = (
+    b"&lt;year&gt;",
+    b"&lt;owner&gt;",
+    b"&lt;copyright holders&gt;",
+    b"<year>",
+    b"<owner>",
+    b"<copyright holders>",
+)
+AVRO_NOTICE_MARKERS = (
+    b"Apache Avro",
+    b"Flavien Raynaud",
+    b"donated to the Apache Avro project in 2020.",
+)
+XZ_LICENSE_ANCHOR = b'id="bundled-xz-utils-0bsd"'
+
+
+class VerificationError(RuntimeError):
+    """A release archive failed verification."""
+
+
+def require(condition: bool, message: str) -> None:
+    if not condition:
+        raise VerificationError(message)
+
+
+def normalized_relative_path(
+    value: str, description: str, allow_trailing_slash: bool = False
+) -> PurePosixPath:
+    require(bool(value), f"empty {description}")
+    require("\\" not in value, f"invalid {description}: {value!r}")
+    require(
+        re.match(r"^[A-Za-z]:", value) is None,
+        f"Windows drive {description}: {value!r}",
+    )
+    candidate = value[:-1] if allow_trailing_slash and value.endswith("/") 
else value
+    path = PurePosixPath(candidate)
+    require(not path.is_absolute(), f"absolute {description}: {value!r}")
+    require(".." not in path.parts, f"unsafe {description}: {value!r}")
+    canonical = path.as_posix()
+    require(
+        value == canonical
+        or (allow_trailing_slash and value == f"{canonical}/"),
+        f"non-canonical {description}: {value!r}",
+    )
+    return path
+
+
+def portable_path_key(path: PurePosixPath) -> str:
+    return unicodedata.normalize("NFC", path.as_posix()).casefold()
+
+
+def index_tar_members(
+    members: list[tarfile.TarInfo], description: str
+) -> dict[str, tarfile.TarInfo]:
+    result = {}
+    portable_names = set()
+    for member in members:
+        require(
+            member.isfile() or member.isdir(),
+            f"{description} has a link or special member: {member.name}",
+        )
+        normalized = normalized_relative_path(
+            member.name,
+            f"{description} member",
+            allow_trailing_slash=member.isdir(),
+        )
+        name = normalized.as_posix()
+        require(
+            name not in result,
+            f"{description} has duplicate normalized member: {name}",
+        )
+        portable_name = portable_path_key(normalized)
+        require(
+            portable_name not in portable_names,
+            f"{description} has a portable path collision: {name}",
+        )
+        result[name] = member
+        portable_names.add(portable_name)
+    return result
+
+
+def verify_record(
+    archive: zipfile.ZipFile,
+    names: list[str],
+    record_member: str,
+    description: str,
+) -> None:
+    try:
+        content = archive.read(record_member).decode("utf-8")
+        rows = list(csv.reader(io.StringIO(content, newline=""), strict=True))
+    except (UnicodeDecodeError, csv.Error) as error:
+        raise VerificationError(f"invalid RECORD in {description}: {error}") 
from error
+
+    entries = {}
+    for row in rows:
+        require(len(row) == 3, f"invalid RECORD row in {description}: {row}")
+        name, digest, size = row
+        normalized_relative_path(name, "RECORD path")
+        require(name not in entries, f"duplicate RECORD path in {description}: 
{name}")
+        entries[name] = (digest, size)
+
+    require(
+        set(entries) == set(names),
+        f"RECORD members differ in {description}",
+    )
+    record_dir = PurePosixPath(record_member).parent
+    unsigned = {
+        record_member,
+        str(record_dir / "RECORD.jws"),
+        str(record_dir / "RECORD.p7s"),
+    }
+    for name in names:
+        digest, size = entries[name]
+        if name in unsigned:
+            require(not digest and not size, f"signed RECORD row in 
{description}: {name}")
+            continue
+        data = archive.read(name)
+        encoded = 
base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=")
+        require(
+            digest == f"sha256={encoded.decode()}",
+            f"RECORD hash differs in {description}: {name}",
+        )
+        require(
+            size.isdecimal() and int(size) == len(data),
+            f"RECORD size differs in {description}: {name}",
+        )
+
+
+def verify_legal_bytes(content: bytes, description: str) -> None:
+    require(b"\r" not in content, f"{description} contains CRLF line endings")
+    lowered = content.lower()
+    for placeholder in PLACEHOLDERS:
+        require(
+            placeholder not in lowered,
+            f"{description} contains license placeholder {placeholder!r}",
+        )
+
+
+def verify_avro_notice(content: bytes, description: str) -> None:
+    for marker in AVRO_NOTICE_MARKERS:
+        require(marker in content, f"{description} is missing {marker!r}")
+
+
+def verify_license_report(content: bytes, target: str, description: str) -> 
None:
+    markers = (
+        b"<strong>Artifact:</strong> <code>python</code>",
+        f"<strong>Rust target:</strong> <code>{target}</code>".encode(),
+        b"<strong>Root manifest:</strong> 
<code>bindings/python/Cargo.toml</code>",
+        XZ_LICENSE_ANCHOR,
+    )
+    for marker in markers:
+        require(marker in content, f"{description} is missing {marker!r}")
+
+
+def parse_metadata(content: bytes, description: str) -> Message:
+    try:
+        metadata = BytesParser(policy=policy.default).parsebytes(content)
+    except Exception as error:
+        raise VerificationError(f"invalid {description}: {error}") from error
+    require(
+        metadata.get("Metadata-Version") == "2.4",
+        f"{description} must use Metadata-Version 2.4",
+    )
+    require(
+        metadata.get("Name") == "pypaimon-rust",
+        f"{description} has an invalid package name",
+    )
+    require(bool(metadata.get("Version")), f"{description} has no version")
+    require(
+        metadata.get("License-Expression") == "Apache-2.0",
+        f"{description} has an invalid License-Expression",
+    )
+    license_files = metadata.get_all("License-File", [])
+    require(
+        sorted(license_files) == sorted(LEGAL_FILES),
+        f"{description} has invalid License-File entries: {license_files}",
+    )
+    return metadata
+
+
+def workspace_version() -> str:
+    with (ROOT / "Cargo.toml").open("rb") as source:
+        return str(tomllib.load(source)["workspace"]["package"]["version"])
+
+
+def release_pyproject(source: str, version: str, base_version: str) -> str:
+    if version == base_version:
+        return source
+    dynamic_version = 'dynamic = ["version"]\n'
+    fixed_version = f'version = "{version}"\n'
+    dynamic_count = source.count(dynamic_version)
+    fixed_count = source.count(fixed_version)
+    if dynamic_count == 1 and fixed_count == 0:
+        return source.replace(dynamic_version, fixed_version, 1)
+    require(
+        dynamic_count == 0 and fixed_count == 1,
+        "unexpected Python release version setting",
+    )
+    return source
+
+
+def expected_sdist_pyproject(version: str) -> bytes:
+    source = (PYTHON_DIR / "pyproject.toml").read_text(encoding="utf-8")
+    source = release_pyproject(source, version, workspace_version())
+    python_source = 'python-source = "python"\n'
+    dependency_groups = "\n[dependency-groups]\n"
+    require(source.count(python_source) == 1, "unexpected python-source 
setting")
+    require(
+        source.count(dependency_groups) == 1,
+        "unexpected dependency-groups section",
+    )
+    source = source.replace(python_source, "", 1)
+    rewrite = (
+        'manifest-path = "bindings/python/Cargo.toml"\n'
+        + python_source
+        + dependency_groups
+    )
+    return source.replace(dependency_groups, rewrite, 1).encode()
+
+
+def target_from_wheel_name(path: Path) -> str:
+    parts = path.name.removesuffix(".whl").rsplit("-", 3)
+    require(len(parts) == 4, f"invalid wheel filename: {path.name}")
+    platform_tags = parts[-1].split(".")
+    targets = set()
+    for platform_tag in platform_tags:
+        if platform_tag == "win_amd64":
+            targets.add("x86_64-pc-windows-msvc")
+        elif platform_tag.startswith("macosx_") and 
platform_tag.endswith("_x86_64"):
+            targets.add("x86_64-apple-darwin")
+        elif platform_tag.startswith("macosx_") and 
platform_tag.endswith("_arm64"):
+            targets.add("aarch64-apple-darwin")
+        elif re.fullmatch(
+            r"(?:manylinux(?:\d{4}|_\d+_\d+)|linux)_x86_64", platform_tag
+        ):
+            targets.add("x86_64-unknown-linux-gnu")
+        elif re.fullmatch(
+            r"(?:manylinux(?:\d{4}|_\d+_\d+)|linux)_aarch64", platform_tag
+        ):
+            targets.add("aarch64-unknown-linux-gnu")
+        else:
+            raise VerificationError(
+                f"unsupported wheel platform tag {platform_tag!r}: {path.name}"
+            )
+    require(len(targets) == 1, f"wheel has mixed platform targets: 
{path.name}")
+    return targets.pop()
+
+
+def verify_native_header(content: bytes, target: str, description: str) -> 
None:
+    if target.endswith("linux-gnu"):
+        require(content[:4] == b"\x7fELF", f"{description} is not ELF")
+        require(len(content) >= 20 and content[4] == 2, f"{description} is not 
ELF64")
+        byte_order = "little" if content[5] == 1 else "big"
+        machine = int.from_bytes(content[18:20], byte_order)
+        expected = 62 if target.startswith("x86_64") else 183
+        require(machine == expected, f"{description} has ELF machine 
{machine}")
+        return
+
+    if target.endswith("windows-msvc"):
+        require(content[:2] == b"MZ" and len(content) >= 64, f"{description} 
is not PE")
+        pe_offset = int.from_bytes(content[60:64], "little")
+        require(
+            len(content) >= pe_offset + 6, f"{description} has a truncated PE 
header"
+        )
+        require(
+            content[pe_offset : pe_offset + 4] == b"PE\0\0", f"{description} 
is not PE"
+        )
+        machine = int.from_bytes(content[pe_offset + 4 : pe_offset + 6], 
"little")
+        require(machine == 0x8664, f"{description} has PE machine 
{machine:#x}")
+        return
+
+    magic = content[:4]
+    if magic == b"\xcf\xfa\xed\xfe":
+        byte_order = "little"
+    elif magic == b"\xfe\xed\xfa\xcf":
+        byte_order = "big"
+    else:
+        raise VerificationError(f"{description} is not 64-bit Mach-O")
+    require(len(content) >= 8, f"{description} has a truncated Mach-O header")
+    cpu_type = int.from_bytes(content[4:8], byte_order)
+    expected = 0x01000007 if target.startswith("x86_64") else 0x0100000C
+    require(cpu_type == expected, f"{description} has Mach-O CPU type 
{cpu_type:#x}")
+
+
+def is_native_library(name: str) -> bool:
+    filename = PurePosixPath(name).name.lower()
+    return filename.endswith((".so", ".pyd", ".dylib", ".dll")) or ".so." in 
filename
+
+
+def verify_wheel(path: Path) -> tuple[str, str]:
+    target = target_from_wheel_name(path)
+    expected_dir = PYTHON_DIR / "licenses" / target
+    if not expected_dir.is_dir():
+        expected_dir = PYTHON_DIR
+    filename_prefix = "pypaimon_rust-"
+    filename_head = path.name.removesuffix(".whl").rsplit("-", 3)[0]
+    require(
+        filename_head.startswith(filename_prefix),
+        f"invalid wheel distribution name: {path.name}",
+    )
+    filename_version = filename_head.removeprefix(filename_prefix)
+
+    try:
+        archive = zipfile.ZipFile(path)
+    except (OSError, zipfile.BadZipFile) as error:
+        raise VerificationError(f"cannot open wheel {path}: {error}") from 
error
+
+    with archive:
+        infos = [info for info in archive.infolist() if not info.is_dir()]
+        names = [info.filename for info in infos]
+        portable_names = set()
+        for info in infos:
+            mode = info.external_attr >> 16
+            file_type = stat.S_IFMT(mode)
+            require(
+                file_type in {0, stat.S_IFREG},
+                f"wheel has a link or special member: {info.filename}",
+            )
+            normalized = normalized_relative_path(info.filename, "wheel 
member")
+            key = portable_path_key(normalized)
+            require(
+                key not in portable_names,
+                f"wheel has a portable path collision: 
{path.name}:{info.filename}",
+            )
+            portable_names.add(key)
+        bad_member = archive.testzip()
+        require(bad_member is None, f"wheel has a corrupt member: 
{bad_member}")
+
+        metadata_members = [
+            name for name in names if name.endswith(".dist-info/METADATA")
+        ]
+        require(
+            len(metadata_members) == 1, f"wheel must contain one METADATA: 
{path.name}"
+        )
+        metadata_member = metadata_members[0]
+        metadata = parse_metadata(archive.read(metadata_member), 
metadata_member)
+        version = str(metadata["Version"])
+        require(
+            version == filename_version,
+            f"wheel filename and metadata versions differ: {path.name}",
+        )
+        dist_info = PurePosixPath(metadata_member).parent
+
+        wheel_members = [name for name in names if name == 
f"{dist_info}/WHEEL"]
+        require(len(wheel_members) == 1, f"wheel metadata is incomplete: 
{path.name}")
+        wheel_metadata = BytesParser(policy=policy.default).parsebytes(
+            archive.read(wheel_members[0])
+        )
+        require(
+            wheel_metadata.get("Root-Is-Purelib", "").lower() == "false",
+            f"wheel is incorrectly marked pure: {path.name}",
+        )
+        record_member = f"{dist_info}/RECORD"
+        require(record_member in names, f"wheel has no RECORD: {path.name}")
+        verify_record(archive, names, record_member, path.name)
+
+        license_report = None
+        for license_file in metadata.get_all("License-File", []):
+            relative = normalized_relative_path(license_file, "License-File 
entry")
+            members = (
+                str(relative),
+                str(dist_info / "licenses" / relative),
+            )
+            matches = [
+                name
+                for name in names
+                if PurePosixPath(name).name == relative.name
+                and relative.name in LEGAL_FILES
+            ]
+            require(
+                sorted(matches) == sorted(members),
+                f"wheel has invalid {relative.name} members: {matches}",
+            )
+            expected_path = expected_dir / relative
+            require(
+                expected_path.is_file(),
+                f"missing committed legal file: {expected_path}",
+            )
+            expected = expected_path.read_bytes()
+            for member in members:
+                actual = archive.read(member)
+                require(
+                    actual == expected,
+                    f"wheel {member} differs for {target}",
+                )
+                verify_legal_bytes(actual, f"{path.name}:{member}")
+                if relative.name == "NOTICE":
+                    verify_avro_notice(actual, f"{path.name}:{member}")
+                if relative.name == "THIRD-PARTY-LICENSES.html":
+                    license_report = actual
+                    verify_license_report(actual, target, 
f"{path.name}:{member}")
+
+        native_members = [
+            name
+            for name in names
+            if len(PurePosixPath(name).parts) == 2
+            and PurePosixPath(name).parts[0] == "pypaimon_rust"
+            and PurePosixPath(name).name.startswith("pypaimon_rust.")
+            and PurePosixPath(name).suffix in {".so", ".pyd"}
+        ]
+        require(
+            len(native_members) == 1,
+            f"wheel must contain one native module: {path.name}",
+        )
+        native_member = native_members[0]
+        expected_suffix = ".pyd" if target.endswith("windows-msvc") else ".so"
+        require(
+            PurePosixPath(native_member).suffix == expected_suffix,
+            f"wheel has an invalid native module suffix: {native_member}",
+        )
+        with archive.open(native_member) as native_file:
+            verify_native_header(
+                native_file.read(65536), target, f"{path.name}:{native_member}"
+            )
+
+        extra_native_members = [
+            name for name in names if name != native_member and 
is_native_library(name)
+        ]
+        if target.endswith("linux-gnu"):
+            openssl_version = (
+                b"OpenSSL 1.1.1k  FIPS"
+                if target.startswith("x86_64")
+                else b"OpenSSL 1.1.1w"
+            )
+            openssl_libraries = (
+                (r"pypaimon_rust\.libs/libcrypto-[^/]+\.so\.1\.1", 
openssl_version),
+                (r"pypaimon_rust\.libs/libssl-[^/]+\.so\.1\.1", None),
+            )
+            for pattern, version_marker in openssl_libraries:
+                matches = [
+                    name for name in extra_native_members if 
re.fullmatch(pattern, name)
+                ]
+                require(
+                    len(matches) == 1,
+                    f"wheel must contain one {pattern}: 
{extra_native_members}",
+                )
+                with archive.open(matches[0]) as native_file:
+                    content = native_file.read()
+                    verify_native_header(
+                        content,
+                        target,
+                        f"{path.name}:{matches[0]}",
+                    )
+                    if version_marker is not None:
+                        require(
+                            version_marker in content,
+                            f"wheel libcrypto is missing {version_marker!r}",
+                        )
+            require(
+                len(extra_native_members) == len(openssl_libraries),
+                f"wheel has unexpected native libraries: 
{extra_native_members}",
+            )
+            require(
+                license_report is not None
+                and b'id="bundled-openssl-1.1.1"' in license_report,
+                "Linux wheel license report is missing the OpenSSL anchor",
+            )
+        else:
+            require(
+                not extra_native_members,
+                f"wheel has unexpected native libraries: 
{extra_native_members}",
+            )
+
+    return target, version
+
+
+def verify_sdist(path: Path) -> str:
+    try:
+        archive = tarfile.open(path, mode="r:gz")
+    except (OSError, tarfile.TarError) as error:
+        raise VerificationError(f"cannot open sdist {path}: {error}") from 
error
+
+    with archive:
+        members_by_name = index_tar_members(
+            archive.getmembers(), f"sdist {path.name}"
+        )
+        names = list(members_by_name)
+        roots = {PurePosixPath(name).parts[0] for name in names}
+        require(len(roots) == 1, f"sdist must have one root directory: 
{path.name}")
+        root = roots.pop()
+
+        def read_member(name: str) -> bytes:
+            try:
+                member = members_by_name[name]
+            except KeyError as error:
+                raise VerificationError(
+                    f"sdist is missing {name}: {path.name}"
+                ) from error
+            require(member.isfile(), f"sdist member is not a file: {name}")
+            extracted = archive.extractfile(member)
+            require(extracted is not None, f"cannot read sdist member: {name}")
+            return extracted.read()
+
+        metadata = parse_metadata(read_member(f"{root}/PKG-INFO"), 
f"{root}/PKG-INFO")
+        version = str(metadata["Version"])
+        require(root == f"pypaimon_rust-{version}", f"invalid sdist root: 
{root}")
+        require(
+            path.name == f"pypaimon_rust-{version}.tar.gz",
+            f"invalid sdist filename: {path.name}",
+        )
+        for license_file in metadata.get_all("License-File", []):
+            relative = normalized_relative_path(license_file, "License-File 
entry")
+            member = f"{root}/{relative}"
+            actual = read_member(member)
+            expected_path = PYTHON_DIR / relative
+            require(
+                expected_path.is_file(), f"missing source legal file: 
{expected_path}"
+            )
+            require(
+                actual == expected_path.read_bytes(),
+                f"sdist {relative} differs from source",
+            )
+            verify_legal_bytes(actual, f"{path.name}:{member}")
+
+        require(
+            read_member(f"{root}/Cargo.lock") == (ROOT / 
"Cargo.lock").read_bytes(),
+            "sdist Cargo.lock differs from source",
+        )
+        require(
+            read_member(f"{root}/pyproject.toml") == 
expected_sdist_pyproject(version),
+            "sdist pyproject.toml has an unexpected rewrite",
+        )
+        target_reports = [
+            name for name in names if "/bindings/python/licenses/" in name
+        ]
+        require(
+            not target_reports,
+            f"sdist contains target-specific license reports: 
{target_reports}",
+        )
+        native_members = [name for name in names if is_native_library(name)]
+        require(
+            not native_members, f"sdist contains native libraries: 
{native_members}"
+        )
+    return version
+
+
+def collect_artifacts(inputs: list[str]) -> list[Path]:
+    candidates = [Path(value) for value in inputs] or [PYTHON_DIR / "dist"]
+    artifacts = []
+    for candidate in candidates:
+        if candidate.is_dir():
+            artifacts.extend(sorted(candidate.iterdir()))
+        else:
+            artifacts.append(candidate)
+    artifacts = [
+        path
+        for path in artifacts
+        if path.name.endswith(".whl") or path.name.endswith(".tar.gz")
+    ]
+    unique = list(dict.fromkeys(path.resolve() for path in artifacts))
+    require(bool(unique), "no wheel or sdist artifacts found")
+    for path in unique:
+        require(path.is_file(), f"artifact is not a file: {path}")
+    return unique
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("artifacts", nargs="*", help="artifact files or 
directories")
+    parser.add_argument(
+        "--allow-partial",
+        action="store_true",
+        help="verify supplied artifacts without requiring the full release 
set",
+    )
+    args = parser.parse_args()
+
+    try:
+        artifacts = collect_artifacts(args.artifacts)
+        targets = {}
+        sdist_count = 0
+        versions = set()
+        for artifact in artifacts:
+            if artifact.name.endswith(".whl"):
+                target, version = verify_wheel(artifact)
+                require(target not in targets, f"duplicate wheel target: 
{target}")
+                targets[target] = artifact
+                versions.add(version)
+                print(f"verified {artifact.name} ({target})")
+            else:
+                versions.add(verify_sdist(artifact))
+                sdist_count += 1
+                print(f"verified {artifact.name} (sdist)")
+
+        require(len(versions) == 1, f"artifact versions differ: 
{sorted(versions)}")
+        require(sdist_count <= 1, f"expected at most one sdist, found 
{sdist_count}")
+        if not args.allow_partial:
+            missing = sorted(TARGETS - targets.keys())
+            require(not missing, f"missing wheel targets: {missing}")
+            require(sdist_count == 1, f"expected one sdist, found 
{sdist_count}")
+        print(f"verified {len(artifacts)} Python release artifacts")
+        return 0
+    except (
+        KeyError,
+        OSError,
+        VerificationError,
+        tarfile.TarError,
+        tomllib.TOMLDecodeError,
+        zipfile.BadZipFile,
+    ) as error:
+        print(f"Python release verification failed: {error}", file=sys.stderr)
+        return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/third-party-licenses/README.md b/third-party-licenses/README.md
new file mode 100644
index 00000000..642d169a
--- /dev/null
+++ b/third-party-licenses/README.md
@@ -0,0 +1,34 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Bundled third-party licenses
+
+`openssl-1.1.1.LICENSE` covers OpenSSL 1.1.1k FIPS on x86_64 Linux and
+OpenSSL 1.1.1w on aarch64 Linux. Both versions use the same license text.
+
+The XZ Utils 5.8.3 license is read from the locked `liblzma-sys` crate at
+`xz/COPYING.0BSD`.
+
+Target-specific Python and Go reports are generated in their binary build jobs.
+They are intentionally not committed or included in the ASF source archive.
+
+Sources:
+
+- <https://github.com/openssl/openssl/blob/OpenSSL_1_1_1k/LICENSE>
+- <https://github.com/openssl/openssl/blob/OpenSSL_1_1_1w/LICENSE>
diff --git a/third-party-licenses/openssl-1.1.1.LICENSE 
b/third-party-licenses/openssl-1.1.1.LICENSE
new file mode 100644
index 00000000..5b5ccdc9
--- /dev/null
+++ b/third-party-licenses/openssl-1.1.1.LICENSE
@@ -0,0 +1,124 @@
+
+  LICENSE ISSUES
+  ==============
+
+  The OpenSSL toolkit stays under a double license, i.e. both the conditions of
+  the OpenSSL License and the original SSLeay license apply to the toolkit.
+  See below for the actual license texts.
+
+  OpenSSL License
+  ---------------
+
+/* ====================================================================
+ * Copyright (c) 1998-2019 The OpenSSL Project.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this
+ *    software must display the following acknowledgment:
+ *    "This product includes software developed by the OpenSSL Project
+ *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+ *
+ * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+ *    endorse or promote products derived from this software without
+ *    prior written permission. For written permission, please contact
+ *    [email protected].
+ *
+ * 5. Products derived from this software may not be called "OpenSSL"
+ *    nor may "OpenSSL" appear in their names without prior written
+ *    permission of the OpenSSL Project.
+ *
+ * 6. Redistributions of any form whatsoever must retain the following
+ *    acknowledgment:
+ *    "This product includes software developed by the OpenSSL Project
+ *    for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+ * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE OpenSSL PROJECT OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This product includes cryptographic software written by Eric Young
+ * ([email protected]).  This product includes software written by Tim
+ * Hudson ([email protected]).
+ *
+ */
+
+ Original SSLeay License
+ -----------------------
+
+/* Copyright (C) 1995-1998 Eric Young ([email protected])
+ * All rights reserved.
+ *
+ * This package is an SSL implementation written
+ * by Eric Young ([email protected]).
+ * The implementation was written so as to conform with Netscapes SSL.
+ *
+ * This library is free for commercial and non-commercial use as long as
+ * the following conditions are aheared to.  The following conditions
+ * apply to all code found in this distribution, be it the RC4, RSA,
+ * lhash, DES, etc., code; not just the SSL code.  The SSL documentation
+ * included with this distribution is covered by the same copyright terms
+ * except that the holder is Tim Hudson ([email protected]).
+ *
+ * Copyright remains Eric Young's, and as such any Copyright notices in
+ * the code are not to be removed.
+ * If this package is used in a product, Eric Young should be given attribution
+ * as the author of the parts of the library used.
+ * This can be in the form of a textual message at program startup or
+ * in documentation (online or textual) provided with the package.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ *    must display the following acknowledgement:
+ *    "This product includes cryptographic software written by
+ *     Eric Young ([email protected])"
+ *    The word 'cryptographic' can be left out if the rouines from the library
+ *    being used are not cryptographic related :-).
+ * 4. If you include any Windows specific code (or a derivative thereof) from
+ *    the apps directory (application code) you must include an 
acknowledgement:
+ *    "This product includes software written by Tim Hudson 
([email protected])"
+ *
+ * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * The licence and distribution terms for any publically available version or
+ * derivative of this code cannot be changed.  i.e. this code cannot simply be
+ * copied and put under another distribution licence
+ * [including the GNU Public Licence.]
+ */

Reply via email to