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

JingsongLi pushed a commit to branch release-0.3
in repository https://gitbox.apache.org/repos/asf/paimon-vector-index.git


The following commit(s) were added to refs/heads/release-0.3 by this push:
     new 9736e35  fix: harden release binary artifacts
9736e35 is described below

commit 9736e35f2c2d7c8e4a1bdb84508e756cd542f3e4
Author: JingsongLi <[email protected]>
AuthorDate: Sun Jul 26 20:01:48 2026 +0800

    fix: harden release binary artifacts
---
 .github/workflows/ci.yml                           |  36 +++-
 .github/workflows/release-java.yml                 |  62 ++++++
 .github/workflows/release-python-publish.yml       |   5 +
 .github/workflows/release-python.yml               |   9 +
 LICENSE-binary                                     | 160 ++++++++++++++
 LICENSE-binary-ffi                                 | 107 ++++++++++
 core/STORAGE_FORMAT.md                             |   7 +-
 docs/api.html                                      |   1 +
 docs/creating-a-release.html                       |   2 +-
 docs/development.html                              |   3 +-
 docs/releases.html                                 |   1 +
 docs/verifying-a-release-candidate.html            |  10 +-
 java/pom.xml                                       |  14 ++
 .../paimon/index/vector/NativeLibraryLoader.java   | 140 ++++++++++++
 .../paimon/index/vector/VectorIndexNative.java     |   4 +
 .../index/vector/VectorIndexJavaApiTest.java       |  24 +++
 .../vector/VectorIndexNativeHandleSafetyTest.java  |   6 +-
 .../vector/VectorIndexNativeLoaderSmokeTest.java   |  49 +++++
 .../vector/VectorIndexNativePanicBoundaryTest.java |   6 +-
 .../vector/VectorIndexNativeValidationTest.java    |   6 +-
 python/pyproject.toml                              |   8 +-
 python/setup.py                                    |  56 ++++-
 tools/README.md                                    |  17 +-
 tools/check_license_headers.py                     |   2 +
 tools/create_source_archive.sh                     |  44 ++++
 tools/create_source_release.sh                     |  10 +-
 tools/deploy_java_staging.sh                       |  56 ++++-
 tools/verify_binary_artifact.py                    | 237 +++++++++++++++++++++
 28 files changed, 1036 insertions(+), 46 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 064a8d9..e8b61d8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,7 +46,10 @@ jobs:
           python-version: '3.9'
 
       - name: Check benchmark tools on Python 3.9
-        run: python3 -m py_compile tools/convert_ann_benchmarks.py
+        run: |
+          python3 -m py_compile \
+            tools/convert_ann_benchmarks.py \
+            tools/verify_binary_artifact.py
 
       - name: Validate .asf.yaml
         run: pip install pyyaml -q && python3 tools/validate_asf_yaml.py
@@ -54,6 +57,19 @@ jobs:
       - name: Check ASF license headers
         run: python3 tools/check_license_headers.py
 
+      - name: Check binary legal inventory
+        run: python3 tools/verify_binary_artifact.py --source
+
+      - name: Check reproducible source archive
+        shell: bash
+        run: |
+          set -euo pipefail
+          first="$(mktemp)"
+          second="$(mktemp)"
+          tools/create_source_archive.sh 0.0.0 "$first"
+          tools/create_source_archive.sh 0.0.0 "$second"
+          cmp "$first" "$second"
+
       - name: Install cargo-deny
         uses: taiki-e/install-action@v2
         with:
@@ -192,6 +208,21 @@ jobs:
         env:
           RAYON_NUM_THREADS: 4
 
+      - name: Package and test the bundled JNI loader
+        shell: bash
+        run: |
+          set -euo pipefail
+          mkdir -p java/src/main/resources/native/linux/x86_64
+          cp target/release/libpaimon_vindex_jni.so \
+            java/src/main/resources/native/linux/x86_64/
+          mvn -f java/pom.xml package -DskipTests
+          jar_file="java/target/paimon-vector-index-java-$(sed -n 
's#.*<version>\([^<]*\)</version>.*#\1#p' java/pom.xml | sed -n '2p').jar"
+          python3 tools/verify_binary_artifact.py \
+            --jar "$jar_file" \
+            --jar-native native/linux/x86_64/libpaimon_vindex_jni.so
+          java -cp "$jar_file:java/target/test-classes" \
+            org.apache.paimon.index.vector.VectorIndexNativeLoaderSmokeTest
+
   python-build:
     runs-on: ubuntu-latest
     steps:
@@ -258,6 +289,9 @@ jobs:
           python -m pip install --upgrade pip build
           python -m build --wheel
 
+      - name: Verify wheel legal and native contents
+        run: python tools/verify_binary_artifact.py --wheel "python/dist/*.whl"
+
       - name: Test wheel
         if: runner.os != 'Windows'
         working-directory: python
diff --git a/.github/workflows/release-java.yml 
b/.github/workflows/release-java.yml
index ecf7f10..d05dae0 100644
--- a/.github/workflows/release-java.yml
+++ b/.github/workflows/release-java.yml
@@ -167,8 +167,70 @@ jobs:
         working-directory: java
         run: mvn clean verify -Prelease -Dgpg.skip=true -DskipTests
 
+      - name: Verify multi-platform Java package
+        shell: bash
+        run: |
+          set -euo pipefail
+          jar_file="java/target/paimon-vector-index-java-$(sed -n 
's#.*<version>\([^<]*\)</version>.*#\1#p' java/pom.xml | sed -n '2p').jar"
+          sources_jar="${jar_file%.jar}-sources.jar"
+          python3 tools/verify_binary_artifact.py --jar "$jar_file"
+          if grep -Eq '^(native/|META-INF/LICENSE-binary$)' <<<"$(jar tf 
"$sources_jar")"; then
+            echo "Sources JAR contains binary-only resources" >&2
+            exit 1
+          fi
+          java -cp "$jar_file:java/target/test-classes" \
+            org.apache.paimon.index.vector.VectorIndexNativeLoaderSmokeTest
+
       - name: Upload Java package
         uses: actions/upload-artifact@v5
         with:
           name: java-package
           path: java/target/*.jar
+
+  smoke-java-package:
+    if: github.repository == 'apache/paimon-vector-index' && 
startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '-rc')
+    needs: [package-java]
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - os: ubuntu-latest
+            classpath_separator: ':'
+          - os: ubuntu-24.04-arm
+            classpath_separator: ':'
+          - os: macos-latest
+            classpath_separator: ':'
+          - os: windows-latest
+            classpath_separator: ';'
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Set up JDK ${{ env.JDK_VERSION }}
+        uses: actions/setup-java@v4
+        with:
+          java-version: ${{ env.JDK_VERSION }}
+          distribution: 'temurin'
+
+      - name: Download final Java package
+        uses: actions/download-artifact@v5
+        with:
+          name: java-package
+          path: java-package
+
+      - name: Load the bundled JNI library from the final JAR
+        shell: bash
+        run: |
+          set -euo pipefail
+          jar_file="$(
+            find java-package -type f -name '*.jar' \
+              ! -name '*-sources.jar' \
+              ! -name '*-javadoc.jar' \
+              -print -quit
+          )"
+          test -n "$jar_file"
+          mkdir -p smoke-classes
+          javac -cp "$jar_file" -d smoke-classes \
+            
java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeLoaderSmokeTest.java
+          java -cp "$jar_file${{ matrix.classpath_separator }}smoke-classes" \
+            org.apache.paimon.index.vector.VectorIndexNativeLoaderSmokeTest
diff --git a/.github/workflows/release-python-publish.yml 
b/.github/workflows/release-python-publish.yml
index 44b1cc8..ef8ad94 100644
--- a/.github/workflows/release-python-publish.yml
+++ b/.github/workflows/release-python-publish.yml
@@ -37,12 +37,17 @@ jobs:
     if: github.repository == 'apache/paimon-vector-index' && 
startsWith(github.ref, 'refs/tags/')
     runs-on: ubuntu-latest
     steps:
+      - uses: actions/checkout@v6
+
       - uses: actions/download-artifact@v5
         with:
           pattern: wheels-*
           merge-multiple: true
           path: dist
 
+      - name: Verify wheel legal and native contents
+        run: python3 tools/verify_binary_artifact.py --wheel "dist/*.whl"
+
       - name: Verify wheel versions
         env:
           TAG_NAME: ${{ github.ref_name }}
diff --git a/.github/workflows/release-python.yml 
b/.github/workflows/release-python.yml
index 610de65..0ce8ff6 100644
--- a/.github/workflows/release-python.yml
+++ b/.github/workflows/release-python.yml
@@ -75,6 +75,9 @@ jobs:
             cargo build --release -p paimon-vindex-ffi &&
             cp target/release/libpaimon_vindex_ffi.so {package}/paimon_vindex/
 
+      - name: Verify wheel legal and native contents
+        run: python3 tools/verify_binary_artifact.py --wheel "wheelhouse/*.whl"
+
       - name: Upload wheels
         uses: actions/upload-artifact@v5
         with:
@@ -133,6 +136,9 @@ jobs:
           rm python/dist/*.whl
           mv python/dist/repaired/*.whl python/dist/
 
+      - name: Verify wheel legal and native contents
+        run: python3 tools/verify_binary_artifact.py --wheel 
"python/dist/*.whl"
+
       - name: Upload wheel
         uses: actions/upload-artifact@v5
         with:
@@ -176,6 +182,9 @@ jobs:
         working-directory: python
         run: python -m build --wheel
 
+      - name: Verify wheel legal and native contents
+        run: python tools/verify_binary_artifact.py --wheel "python/dist/*.whl"
+
       - name: Upload wheel
         uses: actions/upload-artifact@v5
         with:
diff --git a/LICENSE-binary b/LICENSE-binary
new file mode 100644
index 0000000..f9bb069
--- /dev/null
+++ b/LICENSE-binary
@@ -0,0 +1,160 @@
+Apache Paimon Vector Index binary distributions
+================================================
+
+The Java binary distribution includes compiled Rust dependencies.
+
+Except for the components listed below, Apache Paimon Vector Index selects the
+Apache License, Version 2.0 option for dependencies that offer it. The full
+Apache License, Version 2.0 text is in LICENSE.
+
+byteorder 1.5.0 and memchr 2.8.1
+--------------------------------
+
+These components are distributed under the MIT license option.
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Andrew Gallant
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+bytes 1.11.1
+------------
+
+Copyright (c) 2018 Carl Lerche
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+combine 4.6.7
+-------------
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Markus Westerlind
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+crunchy 0.2.4
+-------------
+
+The MIT License (MIT)
+
+Copyright 2017-2023 Eira Fransham.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+unicode-ident 1.0.24
+--------------------
+
+The Rust code is distributed under the Apache License, Version 2.0 option. Its
+Unicode data is additionally covered by the following Unicode License V3.
+
+UNICODE LICENSE V3
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 1991-2023 Unicode, Inc.
+
+NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
+SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
+TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
+DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of data files and any associated documentation (the "Data Files") or
+software and any associated documentation (the "Software") to deal in the
+Data Files or Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, and/or sell
+copies of the Data Files or Software, and to permit persons to whom the
+Data Files or Software are furnished to do so, provided that either (a)
+this copyright and permission notice appear with all copies of the Data
+Files or Software, or (b) this copyright and permission notice appear in
+associated Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+THIRD PARTY RIGHTS.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
+BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
+FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall
+not be used in advertising or otherwise to promote the sale, use or other
+dealings in these Data Files or Software without prior written
+authorization of the copyright holder.
diff --git a/LICENSE-binary-ffi b/LICENSE-binary-ffi
new file mode 100644
index 0000000..71de11a
--- /dev/null
+++ b/LICENSE-binary-ffi
@@ -0,0 +1,107 @@
+Apache Paimon Vector Index Python binary distributions
+=======================================================
+
+The Python wheels include the compiled paimon-vindex-ffi Rust dependency
+closure.
+
+Except for the components listed below, Apache Paimon Vector Index selects the
+Apache License, Version 2.0 option for dependencies that offer it. The full
+Apache License, Version 2.0 text is in LICENSE.
+
+byteorder 1.5.0
+---------------
+
+This component is distributed under the MIT license option.
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Andrew Gallant
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+crunchy 0.2.4
+-------------
+
+The MIT License (MIT)
+
+Copyright 2017-2023 Eira Fransham.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+unicode-ident 1.0.24
+--------------------
+
+The Rust code is distributed under the Apache License, Version 2.0 option. Its
+Unicode data is additionally covered by the following Unicode License V3.
+
+UNICODE LICENSE V3
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 1991-2023 Unicode, Inc.
+
+NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
+SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
+TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
+DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of data files and any associated documentation (the "Data Files") or
+software and any associated documentation (the "Software") to deal in the
+Data Files or Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, and/or sell
+copies of the Data Files or Software, and to permit persons to whom the
+Data Files or Software are furnished to do so, provided that either (a)
+this copyright and permission notice appear with all copies of the Data
+Files or Software, or (b) this copyright and permission notice appear in
+associated Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+THIRD PARTY RIGHTS.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
+BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
+FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall
+not be used in advertising or otherwise to promote the sale, use or other
+dealings in these Data Files or Software without prior written
+authorization of the copyright holder.
diff --git a/core/STORAGE_FORMAT.md b/core/STORAGE_FORMAT.md
index 9616d59..27ec901 100644
--- a/core/STORAGE_FORMAT.md
+++ b/core/STORAGE_FORMAT.md
@@ -20,8 +20,11 @@
 # Vector Index Storage Format
 
 This document describes the v1 on-disk formats written by the
-`paimon-vindex-core` crate. Version 1 is the first release format. Pre-release
-layouts are not part of the compatibility contract.
+`paimon-vindex-core` crate starting with version 0.3.0. The 0.3.0 release
+intentionally resets the pre-1.0 storage contract: it does not read the
+experimental IVF-HNSW-FLAT (`IHFL`) or IVF-HNSW-SQ (`IHSQ`) layouts published
+by 0.2.x. Rebuild those indexes when upgrading, and do not rely on 0.2.x
+readers as a rollback path for files written by 0.3.0.
 
 ## Compatibility Policy
 
diff --git a/docs/api.html b/docs/api.html
index 1940e72..0fdde75 100644
--- a/docs/api.html
+++ b/docs/api.html
@@ -201,6 +201,7 @@ auto result = reader.search(
         <section class="article-section" id="java">
           <h2>Java / JNI</h2>
           <p>The package is <code>org.apache.paimon.index.vector</code>. 
String options map directly to Paimon table and index properties; Rust parses 
and validates them when a Trainer is created.</p>
+          <p>The Maven JAR contains the supported Linux x86-64, Linux aarch64, 
macOS arm64, and Windows x86-64 JNI libraries. The first native API call 
selects the current platform, extracts that library to a private temporary 
file, and loads it automatically. To use a separately built library instead, 
start the JVM with 
<code>-Dpaimon.vindex.native.path=/absolute/path/to/the/library</code>.</p>
           <div class="code-block"><span class="code-label">Java · build and 
search</span><pre><code>Map&lt;String, String&gt; options = new 
HashMap&lt;&gt;();
 options.put("index.type", "ivf_sq");
 options.put("metric", "l2");
diff --git a/docs/creating-a-release.html b/docs/creating-a-release.html
index a16f397..ce41ef1 100644
--- a/docs/creating-a-release.html
+++ b/docs/creating-a-release.html
@@ -137,7 +137,7 @@ cp 
tools/release/apache-paimon-vector-index-${RELEASE_VERSION}-src.* \
 svn add "paimon-dist-dev/paimon-vector-index-${RELEASE_VERSION}-rc${RC_NUM}"
 svn commit -m "Add Paimon Vector Index ${RELEASE_VERSION} RC${RC_NUM}" \
   paimon-dist-dev</code></pre></div>
-          <p>The script archives committed Git objects, excludes 
development-only files, writes a reproducible gzip stream, creates an armored 
detached signature, and records a SHA-512 checksum.</p>
+          <p>The script archives the current commit with its stable commit 
timestamp, excludes development-only files, writes a reproducible gzip stream, 
creates an armored detached signature, and records a SHA-512 checksum. 
Re-running it for the same commit and release version must produce the same 
<code>.tgz</code> bytes.</p>
 
           <h3>Stage the Java convenience artifact</h3>
           <p>Find the numeric run ID of the successful RC-tag 
<code>Release</code> workflow, then validate the exact CI native artifacts 
before any remote deploy.</p>
diff --git a/docs/development.html b/docs/development.html
index 6383e8d..6993a82 100644
--- a/docs/development.html
+++ b/docs/development.html
@@ -154,7 +154,7 @@ LD_LIBRARY_PATH=target/release 
cpp/build/test_vindex_cpp</code></pre></div>
 
         <section class="article-section" id="java">
           <h2>Java / JNI tests</h2>
-          <p>The Maven test phase compiles the Java sources and runs 
<code>VectorIndexJavaApiTest</code>. This test covers result and metadata 
objects, closed-handle behavior, and API compilation without loading the native 
library:</p>
+          <p>The Maven test phase compiles the Java sources and runs 
<code>VectorIndexJavaApiTest</code>. This test covers result and metadata 
objects, native resource mapping, closed-handle behavior, and API compilation 
without loading the native library:</p>
           <div class="code-block"><span 
class="code-label">Shell</span><pre><code>mvn -f java/pom.xml 
test</code></pre></div>
           <p>Native validation, panic-boundary, and handle-safety tests are 
standalone <code>main</code> programs. The following Linux commands mirror the 
additional JNI checks run by CI:</p>
           <div class="code-block"><span class="code-label">Shell · Linux JNI 
checks</span><pre><code>cargo build -p paimon-vindex-jni --release
@@ -169,6 +169,7 @@ java -cp java/target/test-classes:java/target/classes \
   org.apache.paimon.index.vector.VectorIndexNativeHandleSafetyTest \
   "$(pwd)/target/release/libpaimon_vindex_jni.so"</code></pre></div>
           <div class="callout"><strong>Platform note</strong>The 
shared-library filename above is for Linux. Use the native library filename 
produced in <code>target/release</code> on macOS or Windows.</div>
+          <p>Release CI additionally builds the multi-platform Maven JAR and 
runs <code>VectorIndexNativeLoaderSmokeTest</code> from the final 
<code>java-package</code> artifact with no native path argument on Linux 
x86-64, Linux aarch64, macOS arm64, and Windows x86-64. This ensures consumers 
can use every bundled JNI library directly.</p>
         </section>
 
         <section class="article-section" id="python">
diff --git a/docs/releases.html b/docs/releases.html
index 1e34440..aed9f95 100644
--- a/docs/releases.html
+++ b/docs/releases.html
@@ -49,6 +49,7 @@
         <section class="article-section" id="upcoming">
           <h2>Upcoming: 0.3.0</h2>
           <p>The repository is currently developing the 0.3.0 line. Until an 
ASF vote passes and the signed source archive appears under Apache downloads, 
code and packages from this line are development artifacts rather than an 
Apache release.</p>
+          <div class="callout warning"><strong>Stored-index upgrade 
note</strong>Version 0.3.0 intentionally does not read the experimental 
IVF-HNSW-FLAT (<code>IHFL</code>) and IVF-HNSW-SQ (<code>IHSQ</code>) files 
written by 0.2.x. Rebuild those indexes with a 0.3.0-supported index type 
during upgrade. Index files written by 0.3.0 are likewise not a safe rollback 
boundary for 0.2.x; retain source vectors or a 0.2-compatible index copy until 
the upgrade is accepted.</div>
         </section>
 
         <section class="article-section" id="release-020">
diff --git a/docs/verifying-a-release-candidate.html 
b/docs/verifying-a-release-candidate.html
index 72473d8..a3648f1 100644
--- a/docs/verifying-a-release-candidate.html
+++ b/docs/verifying-a-release-candidate.html
@@ -155,8 +155,12 @@ paimon-vindex-core = {
 # native/linux/x86_64/libpaimon_vindex_jni.so
 # native/linux/aarch64/libpaimon_vindex_jni.so
 # native/macos/aarch64/libpaimon_vindex_jni.dylib
-# native/windows/x86_64/paimon_vindex_jni.dll</code></pre></div>
-          <p>Verify the main JAR plus source and Javadoc JARs, POM, 
signatures, and checksums. Confirm that the source JAR does not embed native 
libraries and that the POM version has no <code>-SNAPSHOT</code> suffix.</p>
+# native/windows/x86_64/paimon_vindex_jni.dll
+# META-INF/LICENSE
+# META-INF/NOTICE
+# META-INF/LICENSE-binary
+# org/apache/paimon/index/vector/NativeLibraryLoader.class</code></pre></div>
+          <p>Run the smoke program with the staged main JAR first on the 
classpath and without passing or preloading a native library. Verify that it 
creates a native Trainer successfully. Also verify the main JAR plus source and 
Javadoc JARs, POM, signatures, and checksums. Confirm that the source JAR does 
not embed native libraries and that the POM version has no 
<code>-SNAPSHOT</code> suffix.</p>
 
           <h3>Python TestPyPI</h3>
           <div class="code-block"><span class="code-label">Shell · isolated 
environment</span><pre><code>python3 -m venv rc-wheel
@@ -166,7 +170,7 @@ python -m pip install \
   --extra-index-url https://pypi.org/simple/ \
   "paimon-vindex==${RELEASE_VERSION}rc${RC_NUM}"
 python -c "import paimon_vindex; print('OK')"</code></pre></div>
-          <p>Check that TestPyPI provides Linux x86-64, Linux aarch64, macOS 
arm64, and Windows x86-64 wheels, and test the wheel matching your platform 
against a small build/search/read round trip.</p>
+          <p>Check that TestPyPI provides Linux x86-64, Linux aarch64, macOS 
arm64, and Windows x86-64 wheels. Every wheel must contain 
<code>LICENSE</code>, <code>NOTICE</code>, and <code>LICENSE-binary</code> 
alongside the package and in the wheel metadata license directory. Test the 
wheel matching your platform against a small build/search/read round trip.</p>
         </section>
 
         <section class="article-section" id="vote">
diff --git a/java/pom.xml b/java/pom.xml
index 03d8371..3a9e340 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -70,6 +70,18 @@
     </properties>
 
     <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+            <resource>
+                <directory>${project.basedir}/..</directory>
+                <targetPath>META-INF</targetPath>
+                <includes>
+                    <include>LICENSE-binary</include>
+                </includes>
+            </resource>
+        </resources>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
@@ -139,6 +151,8 @@
                                  code (and META-INF metadata), not platform 
binaries. -->
                             <excludes>
                                 <exclude>native/**</exclude>
+                                <exclude>LICENSE-binary</exclude>
+                                <exclude>**/LICENSE-binary</exclude>
                             </excludes>
                         </configuration>
                         <executions>
diff --git 
a/java/src/main/java/org/apache/paimon/index/vector/NativeLibraryLoader.java 
b/java/src/main/java/org/apache/paimon/index/vector/NativeLibraryLoader.java
new file mode 100644
index 0000000..51cfb95
--- /dev/null
+++ b/java/src/main/java/org/apache/paimon/index/vector/NativeLibraryLoader.java
@@ -0,0 +1,140 @@
+// 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.
+
+package org.apache.paimon.index.vector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Locale;
+
+/** Loads the JNI library from an explicit path or the platform resource 
bundled in the JAR. */
+final class NativeLibraryLoader {
+
+    static final String NATIVE_PATH_PROPERTY = "paimon.vindex.native.path";
+
+    private static boolean loaded;
+
+    private NativeLibraryLoader() {}
+
+    static synchronized void load() {
+        if (loaded) {
+            return;
+        }
+
+        String explicitPath = System.getProperty(NATIVE_PATH_PROPERTY);
+        if (explicitPath != null && !explicitPath.trim().isEmpty()) {
+            
System.load(Paths.get(explicitPath).toAbsolutePath().normalize().toString());
+            loaded = true;
+            return;
+        }
+
+        String resourcePath =
+                resourcePath(
+                        System.getProperty("os.name"), 
System.getProperty("os.arch"));
+        String fileName = resourcePath.substring(resourcePath.lastIndexOf('/') 
+ 1);
+        int extension = fileName.lastIndexOf('.');
+        String suffix = extension >= 0 ? fileName.substring(extension) : null;
+
+        try (InputStream input = 
NativeLibraryLoader.class.getResourceAsStream(resourcePath)) {
+            if (input == null) {
+                throw loadError(
+                        "Bundled JNI library is missing: "
+                                + resourcePath
+                                + ". Set -D"
+                                + NATIVE_PATH_PROPERTY
+                                + "=/absolute/path/to/the/library to use an 
external build.",
+                        null);
+            }
+
+            Path extracted = Files.createTempFile("paimon-vindex-jni-", 
suffix);
+            extracted.toFile().deleteOnExit();
+            Files.copy(input, extracted, StandardCopyOption.REPLACE_EXISTING);
+            System.load(extracted.toAbsolutePath().toString());
+            loaded = true;
+        } catch (IOException e) {
+            throw loadError("Failed to extract bundled JNI library " + 
resourcePath, e);
+        }
+    }
+
+    static String resourcePath(String osName, String osArch) {
+        String os = normalizeOs(osName);
+        String arch = normalizeArch(osArch);
+        String fileName;
+
+        if ("linux".equals(os)) {
+            fileName = "libpaimon_vindex_jni.so";
+        } else if ("macos".equals(os)) {
+            fileName = "libpaimon_vindex_jni.dylib";
+        } else if ("windows".equals(os)) {
+            fileName = "paimon_vindex_jni.dll";
+        } else {
+            throw loadError("Unsupported operating system: " + osName, null);
+        }
+
+        if (("macos".equals(os) && !"aarch64".equals(arch))
+                || ("windows".equals(os) && !"x86_64".equals(arch))) {
+            throw loadError(
+                    "No bundled JNI library for "
+                            + osName
+                            + " / "
+                            + osArch
+                            + ". Set -D"
+                            + NATIVE_PATH_PROPERTY
+                            + "=/absolute/path/to/the/library to use an 
external build.",
+                    null);
+        }
+
+        return "/native/" + os + "/" + arch + "/" + fileName;
+    }
+
+    private static String normalizeOs(String osName) {
+        String value = osName == null ? "" : osName.toLowerCase(Locale.ROOT);
+        if (value.contains("linux")) {
+            return "linux";
+        }
+        if (value.contains("mac") || value.contains("darwin")) {
+            return "macos";
+        }
+        if (value.contains("windows")) {
+            return "windows";
+        }
+        return value;
+    }
+
+    private static String normalizeArch(String osArch) {
+        String value = osArch == null ? "" : osArch.toLowerCase(Locale.ROOT);
+        if ("amd64".equals(value) || "x86_64".equals(value) || 
"x64".equals(value)) {
+            return "x86_64";
+        }
+        if ("aarch64".equals(value) || "arm64".equals(value)) {
+            return "aarch64";
+        }
+        throw loadError("Unsupported CPU architecture: " + osArch, null);
+    }
+
+    private static UnsatisfiedLinkError loadError(String message, Throwable 
cause) {
+        UnsatisfiedLinkError error = new UnsatisfiedLinkError(message);
+        if (cause != null) {
+            error.initCause(cause);
+        }
+        return error;
+    }
+}
diff --git 
a/java/src/main/java/org/apache/paimon/index/vector/VectorIndexNative.java 
b/java/src/main/java/org/apache/paimon/index/vector/VectorIndexNative.java
index cbbd0a9..7d48be3 100644
--- a/java/src/main/java/org/apache/paimon/index/vector/VectorIndexNative.java
+++ b/java/src/main/java/org/apache/paimon/index/vector/VectorIndexNative.java
@@ -19,6 +19,10 @@ package org.apache.paimon.index.vector;
 
 final class VectorIndexNative {
 
+    static {
+        NativeLibraryLoader.load();
+    }
+
     private VectorIndexNative() {}
 
     static native long createTrainer(String[] optionKeys, String[] 
optionValues);
diff --git 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexJavaApiTest.java 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexJavaApiTest.java
index 3e86508..39f6ad9 100644
--- 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexJavaApiTest.java
+++ 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexJavaApiTest.java
@@ -24,6 +24,7 @@ import java.util.Map;
 public class VectorIndexJavaApiTest {
 
     public static void main(String[] args) {
+        testNativeLibraryResourcePaths();
         testSingleResultCopiesArrays();
         testBatchResultCopiesArraysAndSlicesRows();
         testMetadata();
@@ -36,6 +37,29 @@ public class VectorIndexJavaApiTest {
         testReaderAndWriterApiCompile();
     }
 
+    private static void testNativeLibraryResourcePaths() {
+        assertEquals(
+                "/native/linux/x86_64/libpaimon_vindex_jni.so",
+                NativeLibraryLoader.resourcePath("Linux", "amd64"));
+        assertEquals(
+                "/native/linux/aarch64/libpaimon_vindex_jni.so",
+                NativeLibraryLoader.resourcePath("Linux", "arm64"));
+        assertEquals(
+                "/native/macos/aarch64/libpaimon_vindex_jni.dylib",
+                NativeLibraryLoader.resourcePath("Mac OS X", "aarch64"));
+        assertEquals(
+                "/native/windows/x86_64/paimon_vindex_jni.dll",
+                NativeLibraryLoader.resourcePath("Windows 11", "x86_64"));
+        assertThrows(
+                UnsatisfiedLinkError.class,
+                new ThrowingRunnable() {
+                    @Override
+                    public void run() {
+                        NativeLibraryLoader.resourcePath("Mac OS X", "x86_64");
+                    }
+                });
+    }
+
     private static void testSearchParametersRemainAlgorithmSpecific() {
         VectorSearchParams params = new VectorSearchParams(10, 
4).withLSearch(200);
 
diff --git 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeHandleSafetyTest.java
 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeHandleSafetyTest.java
index ac348b9..8f8d19d 100644
--- 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeHandleSafetyTest.java
+++ 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeHandleSafetyTest.java
@@ -26,11 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 public class VectorIndexNativeHandleSafetyTest {
 
     public static void main(String[] args) {
-        if (args.length != 1) {
-            throw new IllegalArgumentException("native library path is 
required");
-        }
-
-        System.load(args[0]);
+        VectorIndexNativeLoaderSmokeTest.configureExternalLibrary(args);
 
         testWriterRejectsReentrantCloseDuringNativeCall();
         testReaderRejectsReentrantCloseDuringNativeCall();
diff --git 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeLoaderSmokeTest.java
 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeLoaderSmokeTest.java
new file mode 100644
index 0000000..450468a
--- /dev/null
+++ 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeLoaderSmokeTest.java
@@ -0,0 +1,49 @@
+// 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.
+
+package org.apache.paimon.index.vector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Smoke test used with the packaged multi-platform JAR as the first 
classpath entry. */
+public class VectorIndexNativeLoaderSmokeTest {
+
+    public static void main(String[] args) {
+        configureExternalLibrary(args);
+
+        Map<String, String> options = new HashMap<>();
+        options.put("index.type", "ivf_flat");
+        options.put("dimension", "2");
+        options.put("nlist", "1");
+        options.put("metric", "l2");
+
+        try (VectorIndexTrainer ignored = VectorIndexTrainer.create(options)) {
+            // Creating the native trainer proves that the JNI methods are 
linked.
+        }
+    }
+
+    static void configureExternalLibrary(String[] args) {
+        if (args.length > 1) {
+            throw new IllegalArgumentException(
+                    "expected zero arguments or one native library path");
+        }
+        if (args.length == 1) {
+            System.setProperty(NativeLibraryLoader.NATIVE_PATH_PROPERTY, 
args[0]);
+        }
+    }
+}
diff --git 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativePanicBoundaryTest.java
 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativePanicBoundaryTest.java
index ec0273f..2d0e9b8 100644
--- 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativePanicBoundaryTest.java
+++ 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativePanicBoundaryTest.java
@@ -24,11 +24,7 @@ import java.util.Map;
 public class VectorIndexNativePanicBoundaryTest {
 
     public static void main(String[] args) {
-        if (args.length != 1) {
-            throw new IllegalArgumentException("native library path is 
required");
-        }
-
-        System.load(args[0]);
+        VectorIndexNativeLoaderSmokeTest.configureExternalLibrary(args);
 
         testVoidEntrypointErrorBecomesRuntimeException();
         testObjectEntrypointPanicBecomesRuntimeException();
diff --git 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeValidationTest.java
 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeValidationTest.java
index f872df0..06cbff9 100644
--- 
a/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeValidationTest.java
+++ 
b/java/src/test/java/org/apache/paimon/index/vector/VectorIndexNativeValidationTest.java
@@ -29,11 +29,7 @@ public class VectorIndexNativeValidationTest {
     private static final int ROUNDTRIP_VECTOR_COUNT = ROUNDTRIP_NLIST * 
ROUNDTRIP_PER_LIST;
 
     public static void main(String[] args) {
-        if (args.length != 1) {
-            throw new IllegalArgumentException("native library path is 
required");
-        }
-
-        System.load(args[0]);
+        VectorIndexNativeLoaderSmokeTest.configureExternalLibrary(args);
 
         testWriterValidationComesFromCore();
         testWriterRejectsNonFiniteValues();
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 41a8135..4113a03 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -16,14 +16,15 @@
 # under the License.
 
 [build-system]
-requires = ["setuptools>=64", "wheel"]
+requires = ["setuptools>=77", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "paimon-vindex"
 version = "0.3.0"
 description = "Python bindings for Apache Paimon Vector Index"
-license = {text = "Apache-2.0"}
+license = "Apache-2.0"
+license-files = ["LICENSE", "NOTICE", "LICENSE-binary"]
 requires-python = ">=3.9"
 dependencies = ["numpy"]
 
@@ -40,6 +41,9 @@ include = ["paimon_vindex*"]
 
 [tool.setuptools.package-data]
 paimon_vindex = [
+    "LICENSE",
+    "NOTICE",
+    "LICENSE-binary",
     "libpaimon_vindex_ffi.dylib",
     "libpaimon_vindex_ffi.so",
     "paimon_vindex_ffi.dll",
diff --git a/python/setup.py b/python/setup.py
index 85a706f..81940a5 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""Build helper: copies the pre-built native FFI library into the package."""
+"""Build helper: stages the native library and binary-distribution legal 
files."""
 
 import os
 import platform
@@ -25,6 +25,13 @@ from setuptools import Distribution, setup
 from setuptools.command.build_py import build_py
 from wheel.bdist_wheel import bdist_wheel
 
+LEGAL_FILES = ("LICENSE", "NOTICE", "LICENSE-binary")
+LEGAL_SOURCE_FILES = {
+    "LICENSE": "LICENSE",
+    "NOTICE": "NOTICE",
+    "LICENSE-binary": "LICENSE-binary-ffi",
+}
+
 
 def _lib_name():
     system = platform.system()
@@ -55,6 +62,31 @@ def _find_native_lib():
     return None
 
 
+def _stage_legal_files(destination_dir):
+    here = os.path.dirname(os.path.abspath(__file__))
+    staged = []
+
+    for name in LEGAL_FILES:
+        source = os.path.join(here, "..", LEGAL_SOURCE_FILES[name])
+        destination = os.path.join(destination_dir, name)
+        if not os.path.isfile(source):
+            raise RuntimeError(f"required binary legal file is missing: 
{source}")
+        if os.path.exists(destination):
+            with open(source, "rb") as source_file:
+                source_bytes = source_file.read()
+            with open(destination, "rb") as destination_file:
+                destination_bytes = destination_file.read()
+            if source_bytes != destination_bytes:
+                raise RuntimeError(
+                    f"staged binary legal file does not match repository copy: 
{destination}"
+                )
+            continue
+        shutil.copy2(source, destination)
+        staged.append(destination)
+
+    return staged
+
+
 class BuildPyWithNativeLib(build_py):
     def run(self):
         src = _find_native_lib()
@@ -65,7 +97,14 @@ class BuildPyWithNativeLib(build_py):
                 _lib_name(),
             )
             shutil.copy2(src, dst)
-        super().run()
+        staged_legal_files = _stage_legal_files(
+            os.path.join(os.path.dirname(os.path.abspath(__file__)), 
"paimon_vindex")
+        )
+        try:
+            super().run()
+        finally:
+            for path in staged_legal_files:
+                os.remove(path)
 
 
 class PlatformWheel(bdist_wheel):
@@ -87,7 +126,14 @@ class BinaryDistribution(Distribution):
         return True
 
 
-setup(
-    cmdclass={"build_py": BuildPyWithNativeLib, "bdist_wheel": PlatformWheel},
-    distclass=BinaryDistribution,
+staged_distribution_legal_files = _stage_legal_files(
+    os.path.dirname(os.path.abspath(__file__))
 )
+try:
+    setup(
+        cmdclass={"build_py": BuildPyWithNativeLib, "bdist_wheel": 
PlatformWheel},
+        distclass=BinaryDistribution,
+    )
+finally:
+    for path in staged_distribution_legal_files:
+        os.remove(path)
diff --git a/tools/README.md b/tools/README.md
index 95154e2..17cac1c 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -62,13 +62,17 @@ GitHub Actions does **not** sign or deploy the Java staging 
artifacts. The
 release workflow only:
 
 1. builds the four JNI native libraries;
-2. verifies the Java release profile with GPG disabled; and
-3. uploads the native libraries and verified Java jars as workflow artifacts.
+2. packages the multi-platform JAR and smoke-tests the final JAR without an
+   external native path on all four supported platform/architecture pairs; and
+3. uploads the native libraries plus the verified `java-package` JARs as
+   workflow artifacts.
 
 The committer then runs this script locally. The script checks that the release
 workflow run succeeded for the current RC tag, downloads the native libraries,
-verifies their platform formats, places them into the Java resource tree, and
-runs Maven locally.
+and `java-package`, verifies their platform formats and legal files, places the
+native libraries into the Java resource tree, and runs Maven locally. Both the
+CI-generated JAR and the locally staged JAR must pass the bundled-native loader
+smoke test before deployment.
 
 ### Required local setup
 
@@ -184,7 +188,7 @@ Always run a dry-run first with the real RC workflow 
artifacts:
 ```
 
 Dry-run mode validates the GitHub Actions run id, downloads the native
-libraries, and runs:
+libraries and `java-package`, and runs:
 
 ```bash
 mvn clean verify -Prelease -Dgpg.skip=true -DskipTests
@@ -199,6 +203,9 @@ It does not sign and does not deploy to Nexus. It verifies:
   commit matches the RC tag;
 - all four native libraries are present;
 - native library file formats match their target platforms;
+- both the CI-generated and locally packaged JARs contain binary legal files,
+  the four native libraries, and `NativeLibraryLoader`;
+- both JARs load the current platform library without an external native path;
 - the Java jar, sources jar, and javadoc jar are produced;
 - the Java jar contains all four native library entries.
 
diff --git a/tools/check_license_headers.py b/tools/check_license_headers.py
index c959926..b22fdc4 100755
--- a/tools/check_license_headers.py
+++ b/tools/check_license_headers.py
@@ -36,6 +36,8 @@ EXEMPT_FILES = {
     "ffi/DEPENDENCIES.rust.tsv",
     "jni/DEPENDENCIES.rust.tsv",
     "LICENSE",
+    "LICENSE-binary",
+    "LICENSE-binary-ffi",
     "NOTICE",
     # Golden test data; adding comments changes the fixture format.
     "core/tests/fixtures/ivf_flat_v1.hex",
diff --git a/tools/create_source_archive.sh b/tools/create_source_archive.sh
new file mode 100755
index 0000000..bb0f3a2
--- /dev/null
+++ b/tools/create_source_archive.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+#
+# 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.
+#
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+if [[ $# -ne 2 || -z "$1" || -z "$2" ]]; then
+  echo "Usage: create_source_archive.sh RELEASE_VERSION OUTPUT_FILE" >&2
+  exit 1
+fi
+
+RELEASE_VERSION=$1
+OUTPUT_FILE=$2
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+REPO_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
+
+# Archive the commit, rather than its tree object, so Git uses the stable
+# commit timestamp instead of the current time for tar headers.
+git -C "$REPO_DIR" archive \
+  --format=tar \
+  --prefix="paimon-vector-index-${RELEASE_VERSION}/" \
+  HEAD . \
+  ':(exclude).gitignore' ':(exclude).gitattributes' \
+  ':(exclude).asf.yaml' ':(exclude).github' \
+  ':(exclude)deploysettings.xml' ':(exclude)target' \
+  ':(exclude).idea' ':(exclude)*.iml' ':(exclude).DS_Store' \
+  | gzip -n > "$OUTPUT_FILE"
diff --git a/tools/create_source_release.sh b/tools/create_source_release.sh
index cc04991..b437db4 100755
--- a/tools/create_source_release.sh
+++ b/tools/create_source_release.sh
@@ -64,13 +64,9 @@ cd ..
 echo "Creating source package"
 
 ARCHIVE="apache-paimon-vector-index-${RELEASE_VERSION}-src.tgz"
-# Archive from Git objects so filesystem metadata such as macOS xattrs is not 
included.
-git archive --format=tar --prefix="paimon-vector-index-${RELEASE_VERSION}/" 
'HEAD^{tree}' . \
-  ':(exclude).gitignore' ':(exclude).gitattributes' \
-  ':(exclude).asf.yaml' ':(exclude).github' \
-  ':(exclude)deploysettings.xml' ':(exclude)target' \
-  ':(exclude).idea' ':(exclude)*.iml' ':(exclude).DS_Store' \
-  | gzip -n > "tools/release/${ARCHIVE}"
+"${CURR_DIR}/create_source_archive.sh" \
+  "${RELEASE_VERSION}" \
+  "${CURR_DIR}/release/${ARCHIVE}"
 
 cd tools/release
 
diff --git a/tools/deploy_java_staging.sh b/tools/deploy_java_staging.sh
index 201106a..0795a94 100755
--- a/tools/deploy_java_staging.sh
+++ b/tools/deploy_java_staging.sh
@@ -225,7 +225,7 @@ else
 fi
 
 check_java_package_inputs_clean() {
-  local paths=(java DEPENDENCIES.rust.tsv)
+  local paths=(java DEPENDENCIES.rust.tsv LICENSE-binary 
tools/verify_binary_artifact.py)
   local untracked
 
   if ! git -C "$REPO_DIR" diff --quiet -- "${paths[@]}" ||
@@ -309,7 +309,8 @@ for artifact in \
   native-linux-x86_64 \
   native-linux-aarch64 \
   native-macos-aarch64 \
-  native-windows-x86_64
+  native-windows-x86_64 \
+  java-package
 do
   gh run download "$RUN_ID" \
     --repo "$REPO" \
@@ -491,16 +492,34 @@ validate_maven_artifacts() {
   local 
jar_file="$REPO_DIR/java/target/paimon-vector-index-java-${RELEASE_VERSION}.jar"
   local 
sources_jar="$REPO_DIR/java/target/paimon-vector-index-java-${RELEASE_VERSION}-sources.jar"
   local 
javadoc_jar="$REPO_DIR/java/target/paimon-vector-index-java-${RELEASE_VERSION}-javadoc.jar"
+  local 
ci_jar="$NATIVE_DIR/java-package/paimon-vector-index-java-${RELEASE_VERSION}.jar"
+  local 
ci_sources_jar="$NATIVE_DIR/java-package/paimon-vector-index-java-${RELEASE_VERSION}-sources.jar"
+  local 
ci_javadoc_jar="$NATIVE_DIR/java-package/paimon-vector-index-java-${RELEASE_VERSION}-javadoc.jar"
   local artifact
   local native_entry
-
-  for artifact in "$jar_file" "$sources_jar" "$javadoc_jar"; do
+  local legal_entry
+
+  for artifact in \
+    "$jar_file" \
+    "$sources_jar" \
+    "$javadoc_jar" \
+    "$ci_jar" \
+    "$ci_sources_jar" \
+    "$ci_javadoc_jar"
+  do
     if [[ ! -f "$artifact" ]]; then
       echo "Expected Maven artifact is missing: $artifact" >&2
       exit 1
     fi
   done
 
+  for artifact in "$sources_jar" "$ci_sources_jar"; do
+    if grep -Eq '^(native/|META-INF/LICENSE-binary$)' <<<"$(jar tf 
"$artifact")"; then
+      echo "Sources jar contains binary-only resources: $artifact" >&2
+      exit 1
+    fi
+  done
+
   for native_entry in \
     native/linux/x86_64/libpaimon_vindex_jni.so \
     native/linux/aarch64/libpaimon_vindex_jni.so \
@@ -512,6 +531,35 @@ validate_maven_artifacts() {
       exit 1
     fi
   done
+
+  for legal_entry in \
+    META-INF/LICENSE \
+    META-INF/NOTICE \
+    META-INF/LICENSE-binary
+  do
+    if ! jar tf "$jar_file" | grep -qx "$legal_entry"; then
+      echo "Packaged jar is missing legal entry: $legal_entry" >&2
+      exit 1
+    fi
+  done
+
+  if ! jar tf "$jar_file" |
+      grep -qx 'org/apache/paimon/index/vector/NativeLibraryLoader.class'; then
+    echo "Packaged jar is missing NativeLibraryLoader.class" >&2
+    exit 1
+  fi
+
+  python3 "$REPO_DIR/tools/verify_binary_artifact.py" --jar "$jar_file" 
"$ci_jar"
+
+  local test_classes="$REPO_DIR/java/target/test-classes"
+  if [[ ! -f 
"$test_classes/org/apache/paimon/index/vector/VectorIndexNativeLoaderSmokeTest.class"
 ]]; then
+    echo "Packaged JAR loader smoke test class is missing from test output." 
>&2
+    exit 1
+  fi
+  java -cp "$jar_file:$test_classes" \
+    org.apache.paimon.index.vector.VectorIndexNativeLoaderSmokeTest
+  java -cp "$ci_jar:$test_classes" \
+    org.apache.paimon.index.vector.VectorIndexNativeLoaderSmokeTest
 }
 
 if [[ "$DRY_RUN" == "true" ]]; then
diff --git a/tools/verify_binary_artifact.py b/tools/verify_binary_artifact.py
new file mode 100644
index 0000000..2310abb
--- /dev/null
+++ b/tools/verify_binary_artifact.py
@@ -0,0 +1,237 @@
+#!/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 legal files and native resources in binary convenience artifacts."""
+
+import argparse
+import glob
+import json
+import subprocess
+import sys
+import zipfile
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parent.parent
+LEGAL_FILES = ("LICENSE", "NOTICE", "LICENSE-binary")
+WHEEL_LEGAL_SOURCES = {
+    "LICENSE": "LICENSE",
+    "NOTICE": "NOTICE",
+    "LICENSE-binary": "LICENSE-binary-ffi",
+}
+JAR_LEGAL_SOURCES = {
+    "LICENSE-binary": "LICENSE-binary",
+}
+JAR_NATIVE_FILES = (
+    "native/linux/x86_64/libpaimon_vindex_jni.so",
+    "native/linux/aarch64/libpaimon_vindex_jni.so",
+    "native/macos/aarch64/libpaimon_vindex_jni.dylib",
+    "native/windows/x86_64/paimon_vindex_jni.dll",
+)
+
+
+def resolve_artifacts(patterns):
+    artifacts = []
+    for pattern in patterns:
+        matches = sorted(glob.glob(pattern))
+        if not matches:
+            raise ValueError(f"artifact pattern matched no files: {pattern}")
+        artifacts.extend(Path(match) for match in matches)
+    return artifacts
+
+
+def require_entries(archive, artifact, entries):
+    names = set(archive.namelist())
+    missing = [entry for entry in entries if entry not in names]
+    if missing:
+        raise ValueError(f"{artifact} is missing entries: {', 
'.join(missing)}")
+
+
+def require_canonical_content(archive, artifact, archive_prefix, sources):
+    for name, source in sources.items():
+        entry = f"{archive_prefix}/{name}"
+        expected = (ROOT / source).read_bytes()
+        actual = archive.read(entry)
+        if actual != expected:
+            raise ValueError(f"{artifact}!/{entry} does not match repository 
{source}")
+
+
+def verify_wheel(artifact):
+    with zipfile.ZipFile(artifact) as archive:
+        names = archive.namelist()
+        dist_info_metadata = [
+            name for name in names if name.endswith(".dist-info/METADATA")
+        ]
+        if len(dist_info_metadata) != 1:
+            raise ValueError(
+                f"{artifact} must contain exactly one .dist-info/METADATA 
entry"
+            )
+        dist_info = dist_info_metadata[0][: -len("METADATA")]
+        legal_entries = [f"{dist_info}licenses/{name}" for name in LEGAL_FILES]
+        package_legal_entries = [f"paimon_vindex/{name}" for name in 
LEGAL_FILES]
+        require_entries(archive, artifact, legal_entries + 
package_legal_entries)
+        require_canonical_content(
+            archive, artifact, "paimon_vindex", WHEEL_LEGAL_SOURCES
+        )
+        require_canonical_content(
+            archive,
+            artifact,
+            f"{dist_info}licenses".rstrip("/"),
+            WHEEL_LEGAL_SOURCES,
+        )
+
+        native_names = [
+            name
+            for name in names
+            if name.startswith("paimon_vindex/")
+            and name.endswith((".so", ".dylib", ".dll"))
+        ]
+        if len(native_names) != 1:
+            raise ValueError(
+                f"{artifact} must contain exactly one platform native library, 
found "
+                f"{len(native_names)}"
+            )
+
+    print(f"Verified Python wheel: {artifact}")
+
+
+def verify_jar(artifact, native_files):
+    with zipfile.ZipFile(artifact) as archive:
+        required = (
+            "META-INF/LICENSE",
+            "META-INF/NOTICE",
+            "META-INF/LICENSE-binary",
+            "org/apache/paimon/index/vector/NativeLibraryLoader.class",
+        ) + tuple(native_files)
+        require_entries(archive, artifact, required)
+        require_canonical_content(
+            archive,
+            artifact,
+            "META-INF",
+            JAR_LEGAL_SOURCES,
+        )
+
+    print(f"Verified Java JAR: {artifact}")
+
+
+def verify_source_legal_inventory():
+    metadata = json.loads(
+        subprocess.check_output(
+            ["cargo", "metadata", "--format-version", "1", "--locked"],
+            cwd=ROOT,
+            text=True,
+        )
+    )
+    packages = {package["id"]: package for package in metadata["packages"]}
+    nodes = {node["id"]: node for node in metadata["resolve"]["nodes"]}
+    for root_name, license_name in (
+        ("paimon-vindex-ffi", "LICENSE-binary-ffi"),
+        ("paimon-vindex-jni", "LICENSE-binary"),
+    ):
+        root_id = next(
+            package["id"]
+            for package in metadata["packages"]
+            if package["source"] is None and package["name"] == root_name
+        )
+        resolved = set()
+        pending = [root_id]
+        while pending:
+            package_id = pending.pop()
+            if package_id in resolved:
+                continue
+            resolved.add(package_id)
+            for dependency in nodes[package_id]["deps"]:
+                if any(
+                    kind["kind"] in (None, "normal")
+                    for kind in dependency["dep_kinds"]
+                ):
+                    pending.append(dependency["pkg"])
+
+        binary_license = (ROOT / license_name).read_text(encoding="utf-8")
+        missing = []
+        for package_id in resolved:
+            package = packages[package_id]
+            if package["source"] is None:
+                continue
+            expression = package.get("license") or ""
+            needs_additional_text = (
+                "Apache-2.0" not in expression or " AND " in expression
+            )
+            marker = f"{package['name']} {package['version']}"
+            if needs_additional_text and marker not in binary_license:
+                missing.append(f"{marker} ({expression})")
+
+        if missing:
+            raise ValueError(
+                f"{license_name} does not account for {root_name} runtime 
dependencies: "
+                + ", ".join(sorted(missing))
+            )
+
+    for name in (
+        "LICENSE",
+        "NOTICE",
+        "LICENSE-binary",
+        "LICENSE-binary-ffi",
+        "ffi/DEPENDENCIES.rust.tsv",
+        "jni/DEPENDENCIES.rust.tsv",
+    ):
+        if not (ROOT / name).is_file():
+            raise ValueError(f"repository legal file is missing: {name}")
+
+    print("Verified binary legal inventory against native Rust dependencies")
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--wheel", nargs="+", default=[], help="Wheel path or 
glob")
+    parser.add_argument("--jar", nargs="+", default=[], help="JAR path or 
glob")
+    parser.add_argument(
+        "--jar-native",
+        action="append",
+        choices=JAR_NATIVE_FILES,
+        help=(
+            "Native entry required in a JAR. Repeat for a partial-platform CI 
build; "
+            "the default requires every release platform."
+        ),
+    )
+    parser.add_argument(
+        "--source",
+        action="store_true",
+        help="Verify LICENSE-binary against native Rust runtime dependencies",
+    )
+    args = parser.parse_args()
+
+    if not args.wheel and not args.jar and not args.source:
+        parser.error("at least one --source, --wheel, or --jar is required")
+
+    try:
+        if args.source:
+            verify_source_legal_inventory()
+        for artifact in resolve_artifacts(args.wheel):
+            verify_wheel(artifact)
+        for artifact in resolve_artifacts(args.jar):
+            verify_jar(artifact, args.jar_native or JAR_NATIVE_FILES)
+    except (OSError, ValueError, zipfile.BadZipFile, KeyError) as error:
+        print(f"Binary artifact verification failed: {error}", file=sys.stderr)
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

Reply via email to