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

tqchen pushed a commit to branch jenkins-wheel-test-artifacts
in repository https://gitbox.apache.org/repos/asf/tvm.git

commit c748eef52ef59d611076f5d6621c4f9cfc566ebf
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Oct 14 21:43:15 2026 +0000

    [CI] Build and install wheel artifacts for Jenkins tests
    
    Build TVM and its pinned FFI dependency through pyproject, and install 
those artifacts for Python tests and documentation. Run C++ tests in the same 
native build before upload, retain the JVM runtime artifact and specialized 
runtime checks, and replace the legacy build wrapper with direct commands.
---
 ci/jenkins/data.py                              |   8 +-
 ci/jenkins/generated/arm_jenkinsfile.groovy     |  59 +++++----
 ci/jenkins/generated/cpu_jenkinsfile.groovy     |  60 +++++----
 ci/jenkins/generated/docker_jenkinsfile.groovy  |  54 ++++----
 ci/jenkins/generated/gpu_jenkinsfile.groovy     |  64 +++++----
 ci/jenkins/generated/wasm_jenkinsfile.groovy    |  58 ++++----
 ci/jenkins/templates/arm_jenkinsfile.groovy.j2  |   5 +-
 ci/jenkins/templates/cpu_jenkinsfile.groovy.j2  |   6 +-
 ci/jenkins/templates/gpu_jenkinsfile.groovy.j2  |  10 +-
 ci/jenkins/templates/utils/Build.groovy.j2      |  46 ++++---
 ci/jenkins/templates/utils/Test.groovy.j2       |   6 -
 ci/jenkins/templates/utils/base.groovy.j2       |   1 -
 ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 |   4 +-
 docs/conf.py                                    |   3 +-
 tests/python/ci/test_wheel_artifacts.py         | 167 ++++++++++++++++++++++++
 tests/scripts/ci.py                             |  53 +++++---
 tests/scripts/task_build.py                     | 106 ---------------
 tests/scripts/task_python_docs.sh               |  17 +--
 tests/scripts/task_python_install_wheels.sh     |  54 ++++++++
 tests/scripts/task_python_unittest.sh           |   7 +-
 tests/scripts/task_python_unittest_gpuonly.sh   |   7 +-
 21 files changed, 486 insertions(+), 309 deletions(-)

diff --git a/ci/jenkins/data.py b/ci/jenkins/data.py
index 0811451b53..ebf29fc91b 100644
--- a/ci/jenkins/data.py
+++ b/ci/jenkins/data.py
@@ -31,10 +31,10 @@ data-driven artifact lists.
 import sys
 
 files_to_stash = {
-    # Executables and build files needed to run c++ tests
-    "cpptest": ["build/cpptest", "build/build.ninja", 
"build/CMakeFiles/rules.ninja"],
-    # runtime files
-    "tvm_runtime": ["build/lib/libtvm_runtime.so", "build/config.cmake"],
+    # Exact TVM and pinned FFI wheels, installed by Python test/docs consumers.
+    "wheels": ["build/wheels"],
+    # Maven's native assembly still consumes these paths directly.
+    "tvm_runtime": ["build/lib/libtvm_runtime.so", "build/lib/libtvm_ffi.so"],
     # compiler files
     "tvm_lib": [
         "build/lib/libtvm_compiler.so",
diff --git a/ci/jenkins/generated/arm_jenkinsfile.groovy 
b/ci/jenkins/generated/arm_jenkinsfile.groovy
index 3442caded7..1670f37db7 100644
--- a/ci/jenkins/generated/arm_jenkinsfile.groovy
+++ b/ci/jenkins/generated/arm_jenkinsfile.groovy
@@ -60,7 +60,7 @@
 // 'python3 jenkins/generate.py'
 // Note: This timestamp is here to ensure that updates to the Jenkinsfile are
 // always rebased on main before merging:
-// Generated at 2026-07-05T01:05:57.719405
+// Generated at 2026-10-14T21:26:39.010162
 
 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
 // These are set at runtime from data in ci/jenkins/docker-images.yml, update
@@ -411,30 +411,40 @@ def python_unittest(image) {
   )
 }
 
-def make_cpp_tests(image, build_dir) {
-  sh (
-    script: """
-      set -eux
-      ${docker_run} ${image} python3 ./tests/scripts/task_build.py \
-        --sccache-bucket tvm-sccache-prod \
-        --sccache-region us-west-2 \
-        --cmake-target cpptest \
-        --build-dir ${build_dir}
-      """,
-    label: 'Make C++ tests',
-  )
+// Keep compiler caching and executor limits identical for wheels and the
+// separate runtime-only compile check. Build commands remain at their callers.
+def build_environment() {
+  return '''
+    set -eux
+    export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))
+    if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi
+    if command -v sccache >/dev/null; then
+      export CC=/opt/sccache/cc CXX=/opt/sccache/c++
+      if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
+        export SCCACHE_BUCKET=tvm-sccache-prod SCCACHE_REGION=us-west-2
+      fi
+      sccache --start-server || true
+      sccache --show-stats
+      trap "sccache --show-stats" EXIT
+    fi
+  '''
 }
 
-def cmake_build(image, path) {
-  sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --build-dir ${path}",
-    label: 'Run CMake build',
-  )
-}
-def cpp_unittest(image) {
+def wheel_build(image, cpp_tests = false) {
+  def commands = build_environment() + '''
+    rm -rf build/wheels
+    uv build --wheel --python python3 --out-dir build/wheels ./3rdparty/tvm-ffi
+    uv build --wheel --python python3 --out-dir build/wheels . \
+      -Ccmake.build-type=RelWithDebInfo -Cbuild.verbose="${VERBOSE:-true}"
+  '''.stripIndent().trim()
+  if (cpp_tests) {
+    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
+    // cpptest links the same native tree that scikit-build just packaged.
+    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
+  }
   sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
-    label: 'Run C++ tests',
+    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} bash -c 
'${commands}'",
+    label: 'Build wheel artifacts' + (cpp_tests ? ' and run C++ tests' : ''),
   )
 }
 
@@ -461,10 +471,9 @@ def run_build(node_type) {
           script: "${docker_run} ${ci_arm} 
./tests/scripts/task_config_build_arm.sh build",
           label: 'Create ARM CMake config',
         )
-        cmake_build(ci_arm, 'build')
-        make_cpp_tests(ci_arm, 'build')
+        wheel_build(ci_arm, true)
         sh(
-            script: "./${jenkins_scripts_root}/s3.py --action upload --bucket 
${s3_bucket} --prefix ${s3_prefix}/arm --bundle tvm_lib --bundle cpptest",
+            script: "./${jenkins_scripts_root}/s3.py --action upload --bucket 
${s3_bucket} --prefix ${s3_prefix}/arm --bundle wheels",
             label: 'Upload artifacts to S3',
           )
           })
diff --git a/ci/jenkins/generated/cpu_jenkinsfile.groovy 
b/ci/jenkins/generated/cpu_jenkinsfile.groovy
index 0d3e408235..56255dc5fb 100644
--- a/ci/jenkins/generated/cpu_jenkinsfile.groovy
+++ b/ci/jenkins/generated/cpu_jenkinsfile.groovy
@@ -60,7 +60,7 @@
 // 'python3 jenkins/generate.py'
 // Note: This timestamp is here to ensure that updates to the Jenkinsfile are
 // always rebased on main before merging:
-// Generated at 2026-07-05T01:05:57.741181
+// Generated at 2026-10-14T21:26:39.031061
 
 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
 // These are set at runtime from data in ci/jenkins/docker-images.yml, update
@@ -411,30 +411,40 @@ def python_unittest(image) {
   )
 }
 
-def make_cpp_tests(image, build_dir) {
-  sh (
-    script: """
-      set -eux
-      ${docker_run} ${image} python3 ./tests/scripts/task_build.py \
-        --sccache-bucket tvm-sccache-prod \
-        --sccache-region us-west-2 \
-        --cmake-target cpptest \
-        --build-dir ${build_dir}
-      """,
-    label: 'Make C++ tests',
-  )
+// Keep compiler caching and executor limits identical for wheels and the
+// separate runtime-only compile check. Build commands remain at their callers.
+def build_environment() {
+  return '''
+    set -eux
+    export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))
+    if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi
+    if command -v sccache >/dev/null; then
+      export CC=/opt/sccache/cc CXX=/opt/sccache/c++
+      if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
+        export SCCACHE_BUCKET=tvm-sccache-prod SCCACHE_REGION=us-west-2
+      fi
+      sccache --start-server || true
+      sccache --show-stats
+      trap "sccache --show-stats" EXIT
+    fi
+  '''
 }
 
-def cmake_build(image, path) {
-  sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --build-dir ${path}",
-    label: 'Run CMake build',
-  )
-}
-def cpp_unittest(image) {
+def wheel_build(image, cpp_tests = false) {
+  def commands = build_environment() + '''
+    rm -rf build/wheels
+    uv build --wheel --python python3 --out-dir build/wheels ./3rdparty/tvm-ffi
+    uv build --wheel --python python3 --out-dir build/wheels . \
+      -Ccmake.build-type=RelWithDebInfo -Cbuild.verbose="${VERBOSE:-true}"
+  '''.stripIndent().trim()
+  if (cpp_tests) {
+    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
+    // cpptest links the same native tree that scikit-build just packaged.
+    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
+  }
   sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
-    label: 'Run C++ tests',
+    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} bash -c 
'${commands}'",
+    label: 'Build wheel artifacts' + (cpp_tests ? ' and run C++ tests' : ''),
   )
 }
 
@@ -461,10 +471,9 @@ def run_build(node_type) {
           script: "${docker_run} ${ci_cpu} 
./tests/scripts/task_config_build_cpu.sh build",
           label: 'Create CPU CMake config',
         )
-        cmake_build(ci_cpu, 'build')
-        make_cpp_tests(ci_cpu, 'build')
+        wheel_build(ci_cpu, true)
         sh(
-            script: "./${jenkins_scripts_root}/s3.py --action upload --bucket 
${s3_bucket} --prefix ${s3_prefix}/cpu --bundle tvm_lib --bundle cpptest",
+            script: "./${jenkins_scripts_root}/s3.py --action upload --bucket 
${s3_bucket} --prefix ${s3_prefix}/cpu --bundle wheels",
             label: 'Upload artifacts to S3',
           )
           })
@@ -521,7 +530,6 @@ def run_unittest_CPU(node_type) {
                   label: 'Download artifacts from S3',
                 )
 
-              cpp_unittest(ci_cpu)
               python_unittest(ci_cpu)
           })
         }
diff --git a/ci/jenkins/generated/docker_jenkinsfile.groovy 
b/ci/jenkins/generated/docker_jenkinsfile.groovy
index 8ffe78942a..3a691900a6 100644
--- a/ci/jenkins/generated/docker_jenkinsfile.groovy
+++ b/ci/jenkins/generated/docker_jenkinsfile.groovy
@@ -60,7 +60,7 @@
 // 'python3 jenkins/generate.py'
 // Note: This timestamp is here to ensure that updates to the Jenkinsfile are
 // always rebased on main before merging:
-// Generated at 2026-07-05T01:05:57.759905
+// Generated at 2026-10-14T21:26:39.048782
 
 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
 // These are set at runtime from data in ci/jenkins/docker-images.yml, update
@@ -411,30 +411,40 @@ def python_unittest(image) {
   )
 }
 
-def make_cpp_tests(image, build_dir) {
-  sh (
-    script: """
-      set -eux
-      ${docker_run} ${image} python3 ./tests/scripts/task_build.py \
-        --sccache-bucket tvm-sccache-prod \
-        --sccache-region us-west-2 \
-        --cmake-target cpptest \
-        --build-dir ${build_dir}
-      """,
-    label: 'Make C++ tests',
-  )
+// Keep compiler caching and executor limits identical for wheels and the
+// separate runtime-only compile check. Build commands remain at their callers.
+def build_environment() {
+  return '''
+    set -eux
+    export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))
+    if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi
+    if command -v sccache >/dev/null; then
+      export CC=/opt/sccache/cc CXX=/opt/sccache/c++
+      if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
+        export SCCACHE_BUCKET=tvm-sccache-prod SCCACHE_REGION=us-west-2
+      fi
+      sccache --start-server || true
+      sccache --show-stats
+      trap "sccache --show-stats" EXIT
+    fi
+  '''
 }
 
-def cmake_build(image, path) {
-  sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --build-dir ${path}",
-    label: 'Run CMake build',
-  )
-}
-def cpp_unittest(image) {
+def wheel_build(image, cpp_tests = false) {
+  def commands = build_environment() + '''
+    rm -rf build/wheels
+    uv build --wheel --python python3 --out-dir build/wheels ./3rdparty/tvm-ffi
+    uv build --wheel --python python3 --out-dir build/wheels . \
+      -Ccmake.build-type=RelWithDebInfo -Cbuild.verbose="${VERBOSE:-true}"
+  '''.stripIndent().trim()
+  if (cpp_tests) {
+    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
+    // cpptest links the same native tree that scikit-build just packaged.
+    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
+  }
   sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
-    label: 'Run C++ tests',
+    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} bash -c 
'${commands}'",
+    label: 'Build wheel artifacts' + (cpp_tests ? ' and run C++ tests' : ''),
   )
 }
 
diff --git a/ci/jenkins/generated/gpu_jenkinsfile.groovy 
b/ci/jenkins/generated/gpu_jenkinsfile.groovy
index e7ce14c4ba..2421da1881 100644
--- a/ci/jenkins/generated/gpu_jenkinsfile.groovy
+++ b/ci/jenkins/generated/gpu_jenkinsfile.groovy
@@ -60,7 +60,7 @@
 // 'python3 jenkins/generate.py'
 // Note: This timestamp is here to ensure that updates to the Jenkinsfile are
 // always rebased on main before merging:
-// Generated at 2026-07-05T12:52:04.884879
+// Generated at 2026-10-14T21:26:39.068100
 
 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
 // These are set at runtime from data in ci/jenkins/docker-images.yml, update
@@ -411,30 +411,40 @@ def python_unittest(image) {
   )
 }
 
-def make_cpp_tests(image, build_dir) {
-  sh (
-    script: """
-      set -eux
-      ${docker_run} ${image} python3 ./tests/scripts/task_build.py \
-        --sccache-bucket tvm-sccache-prod \
-        --sccache-region us-west-2 \
-        --cmake-target cpptest \
-        --build-dir ${build_dir}
-      """,
-    label: 'Make C++ tests',
-  )
+// Keep compiler caching and executor limits identical for wheels and the
+// separate runtime-only compile check. Build commands remain at their callers.
+def build_environment() {
+  return '''
+    set -eux
+    export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))
+    if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi
+    if command -v sccache >/dev/null; then
+      export CC=/opt/sccache/cc CXX=/opt/sccache/c++
+      if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
+        export SCCACHE_BUCKET=tvm-sccache-prod SCCACHE_REGION=us-west-2
+      fi
+      sccache --start-server || true
+      sccache --show-stats
+      trap "sccache --show-stats" EXIT
+    fi
+  '''
 }
 
-def cmake_build(image, path) {
-  sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --build-dir ${path}",
-    label: 'Run CMake build',
-  )
-}
-def cpp_unittest(image) {
+def wheel_build(image, cpp_tests = false) {
+  def commands = build_environment() + '''
+    rm -rf build/wheels
+    uv build --wheel --python python3 --out-dir build/wheels ./3rdparty/tvm-ffi
+    uv build --wheel --python python3 --out-dir build/wheels . \
+      -Ccmake.build-type=RelWithDebInfo -Cbuild.verbose="${VERBOSE:-true}"
+  '''.stripIndent().trim()
+  if (cpp_tests) {
+    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
+    // cpptest links the same native tree that scikit-build just packaged.
+    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
+  }
   sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
-    label: 'Run C++ tests',
+    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} bash -c 
'${commands}'",
+    label: 'Build wheel artifacts' + (cpp_tests ? ' and run C++ tests' : ''),
   )
 }
 
@@ -458,9 +468,9 @@ def run_build(node_type) {
             'PLATFORM=gpu',
             ], {
             sh "${docker_run} --no-gpu ${ci_gpu} 
./tests/scripts/task_config_build_gpu.sh build"
-        cmake_build("${ci_gpu} --no-gpu", 'build')
+        wheel_build("${ci_gpu} --no-gpu")
         sh(
-            script: "./${jenkins_scripts_root}/s3.py --action upload --bucket 
${s3_bucket} --prefix ${s3_prefix}/gpu --bundle tvm_lib --bundle 
tvm_lib_gpu_extra",
+            script: "./${jenkins_scripts_root}/s3.py --action upload --bucket 
${s3_bucket} --prefix ${s3_prefix}/gpu --bundle wheels --bundle tvm_runtime",
             label: 'Upload artifacts to S3',
           )
 
@@ -468,7 +478,11 @@ def run_build(node_type) {
         // compiler test
         sh "rm -rf build"
         sh "${docker_run} --no-gpu ${ci_gpu} 
./tests/scripts/task_config_build_gpu_other.sh build"
-        sh "${docker_run} --env CI_NUM_EXECUTORS ${ci_gpu} --no-gpu 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --cmake-target tvm_runtime --build-dir build"
+        def runtime_commands = build_environment() + '''
+          cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo
+          cmake --build build --target tvm_runtime --verbose
+        '''
+        sh "${docker_run} --env CI_NUM_EXECUTORS ${ci_gpu} --no-gpu bash -c 
'${runtime_commands}'"
           })
         }
       }
diff --git a/ci/jenkins/generated/wasm_jenkinsfile.groovy 
b/ci/jenkins/generated/wasm_jenkinsfile.groovy
index 7421fa86ca..cd9861e7f6 100644
--- a/ci/jenkins/generated/wasm_jenkinsfile.groovy
+++ b/ci/jenkins/generated/wasm_jenkinsfile.groovy
@@ -60,7 +60,7 @@
 // 'python3 jenkins/generate.py'
 // Note: This timestamp is here to ensure that updates to the Jenkinsfile are
 // always rebased on main before merging:
-// Generated at 2026-07-05T01:05:57.802104
+// Generated at 2026-10-14T21:26:39.087178
 
 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
 // These are set at runtime from data in ci/jenkins/docker-images.yml, update
@@ -411,30 +411,40 @@ def python_unittest(image) {
   )
 }
 
-def make_cpp_tests(image, build_dir) {
-  sh (
-    script: """
-      set -eux
-      ${docker_run} ${image} python3 ./tests/scripts/task_build.py \
-        --sccache-bucket tvm-sccache-prod \
-        --sccache-region us-west-2 \
-        --cmake-target cpptest \
-        --build-dir ${build_dir}
-      """,
-    label: 'Make C++ tests',
-  )
+// Keep compiler caching and executor limits identical for wheels and the
+// separate runtime-only compile check. Build commands remain at their callers.
+def build_environment() {
+  return '''
+    set -eux
+    export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))
+    if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi
+    if command -v sccache >/dev/null; then
+      export CC=/opt/sccache/cc CXX=/opt/sccache/c++
+      if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
+        export SCCACHE_BUCKET=tvm-sccache-prod SCCACHE_REGION=us-west-2
+      fi
+      sccache --start-server || true
+      sccache --show-stats
+      trap "sccache --show-stats" EXIT
+    fi
+  '''
 }
 
-def cmake_build(image, path) {
-  sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --build-dir ${path}",
-    label: 'Run CMake build',
-  )
-}
-def cpp_unittest(image) {
+def wheel_build(image, cpp_tests = false) {
+  def commands = build_environment() + '''
+    rm -rf build/wheels
+    uv build --wheel --python python3 --out-dir build/wheels ./3rdparty/tvm-ffi
+    uv build --wheel --python python3 --out-dir build/wheels . \
+      -Ccmake.build-type=RelWithDebInfo -Cbuild.verbose="${VERBOSE:-true}"
+  '''.stripIndent().trim()
+  if (cpp_tests) {
+    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
+    // cpptest links the same native tree that scikit-build just packaged.
+    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
+  }
   sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
-    label: 'Run C++ tests',
+    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} bash -c 
'${commands}'",
+    label: 'Build wheel artifacts' + (cpp_tests ? ' and run C++ tests' : ''),
   )
 }
 
@@ -461,9 +471,7 @@ def run_build(node_type) {
           script: "${docker_run} ${ci_wasm} 
./tests/scripts/task_config_build_wasm.sh build",
           label: 'Create WASM CMake config',
         )
-        cmake_build(ci_wasm, 'build')
-        make_cpp_tests(ci_wasm, 'build')
-        cpp_unittest(ci_wasm)
+        wheel_build(ci_wasm, true)
         sh (
           script: "${docker_run} ${ci_wasm} ./tests/scripts/task_web_wasm.sh",
           label: 'Run WASM lint and tests',
diff --git a/ci/jenkins/templates/arm_jenkinsfile.groovy.j2 
b/ci/jenkins/templates/arm_jenkinsfile.groovy.j2
index 2bddbab4c7..bbc5a2e4d7 100644
--- a/ci/jenkins/templates/arm_jenkinsfile.groovy.j2
+++ b/ci/jenkins/templates/arm_jenkinsfile.groovy.j2
@@ -29,7 +29,6 @@
     script: "${docker_run} ${ci_arm} ./tests/scripts/task_config_build_arm.sh 
build",
     label: 'Create ARM CMake config',
   )
-  cmake_build(ci_arm, 'build')
-  make_cpp_tests(ci_arm, 'build')
-  {{ m.upload_artifacts(tag='arm', bundles=["tvm_lib", "cpptest"]) }}
+  wheel_build(ci_arm, true)
+  {{ m.upload_artifacts(tag='arm', bundles=["wheels"]) }}
 {% endcall %}
diff --git a/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2 
b/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2
index 62b7c3dfea..b7c7e9ded0 100644
--- a/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2
+++ b/ci/jenkins/templates/cpu_jenkinsfile.groovy.j2
@@ -29,9 +29,8 @@
     script: "${docker_run} ${ci_cpu} ./tests/scripts/task_config_build_cpu.sh 
build",
     label: 'Create CPU CMake config',
   )
-  cmake_build(ci_cpu, 'build')
-  make_cpp_tests(ci_cpu, 'build')
-  {{ m.upload_artifacts(tag='cpu', bundles=["tvm_lib", "cpptest"]) }}
+  wheel_build(ci_cpu, true)
+  {{ m.upload_artifacts(tag='cpu', bundles=["wheels"]) }}
 {% endcall %}
 
 {% set test_method_names = [] %}
@@ -45,7 +44,6 @@
   test_method_names=test_method_names,
 ) %}
   {{ m.download_artifacts(tag='cpu') }}
-  cpp_unittest(ci_cpu)
   python_unittest(ci_cpu)
 {% endcall %}
 
diff --git a/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2 
b/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2
index 4553fbefa2..d50e9278cb 100644
--- a/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2
+++ b/ci/jenkins/templates/gpu_jenkinsfile.groovy.j2
@@ -26,13 +26,17 @@
   platform="gpu",
 ) %}
   sh "${docker_run} --no-gpu ${ci_gpu} 
./tests/scripts/task_config_build_gpu.sh build"
-  cmake_build("${ci_gpu} --no-gpu", 'build')
-  {{ m.upload_artifacts(tag='gpu', bundles=["tvm_lib", "tvm_lib_gpu_extra"]) }}
+  wheel_build("${ci_gpu} --no-gpu")
+  {{ m.upload_artifacts(tag='gpu', bundles=["wheels", "tvm_runtime"]) }}
 
   // compiler test
   sh "rm -rf build"
   sh "${docker_run} --no-gpu ${ci_gpu} 
./tests/scripts/task_config_build_gpu_other.sh build"
-  sh "${docker_run} --env CI_NUM_EXECUTORS ${ci_gpu} --no-gpu 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --cmake-target tvm_runtime --build-dir build"
+  def runtime_commands = build_environment() + '''
+    cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo
+    cmake --build build --target tvm_runtime --verbose
+  '''
+  sh "${docker_run} --env CI_NUM_EXECUTORS ${ci_gpu} --no-gpu bash -c 
'${runtime_commands}'"
 {% endcall %}
 
 {% set test_method_names = [] %}
diff --git a/ci/jenkins/templates/utils/Build.groovy.j2 
b/ci/jenkins/templates/utils/Build.groovy.j2
index 42bd4a991f..fd9ad79ac5 100644
--- a/ci/jenkins/templates/utils/Build.groovy.j2
+++ b/ci/jenkins/templates/utils/Build.groovy.j2
@@ -5,23 +5,39 @@ def python_unittest(image) {
   )
 }
 
-def make_cpp_tests(image, build_dir) {
-  sh (
-    script: """
-      set -eux
-      ${docker_run} ${image} python3 ./tests/scripts/task_build.py \
-        --sccache-bucket tvm-sccache-prod \
-        --sccache-region us-west-2 \
-        --cmake-target cpptest \
-        --build-dir ${build_dir}
-      """,
-    label: 'Make C++ tests',
-  )
+// Keep compiler caching and executor limits identical for wheels and the
+// separate runtime-only compile check. Build commands remain at their callers.
+def build_environment() {
+  return '''
+    set -eux
+    export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))
+    if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi
+    if command -v sccache >/dev/null; then
+      export CC=/opt/sccache/cc CXX=/opt/sccache/c++
+      if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
+        export SCCACHE_BUCKET=tvm-sccache-prod SCCACHE_REGION=us-west-2
+      fi
+      sccache --start-server || true
+      sccache --show-stats
+      trap "sccache --show-stats" EXIT
+    fi
+  '''
 }
 
-def cmake_build(image, path) {
+def wheel_build(image, cpp_tests = false) {
+  def commands = build_environment() + '''
+    rm -rf build/wheels
+    uv build --wheel --python python3 --out-dir build/wheels ./3rdparty/tvm-ffi
+    uv build --wheel --python python3 --out-dir build/wheels . \
+      -Ccmake.build-type=RelWithDebInfo -Cbuild.verbose="${VERBOSE:-true}"
+  '''.stripIndent().trim()
+  if (cpp_tests) {
+    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
+    // cpptest links the same native tree that scikit-build just packaged.
+    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
+  }
   sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_build.py --sccache-bucket tvm-sccache-prod 
--sccache-region us-west-2 --build-dir ${path}",
-    label: 'Run CMake build',
+    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} bash -c 
'${commands}'",
+    label: 'Build wheel artifacts' + (cpp_tests ? ' and run C++ tests' : ''),
   )
 }
diff --git a/ci/jenkins/templates/utils/Test.groovy.j2 
b/ci/jenkins/templates/utils/Test.groovy.j2
deleted file mode 100644
index 48041983b9..0000000000
--- a/ci/jenkins/templates/utils/Test.groovy.j2
+++ /dev/null
@@ -1,6 +0,0 @@
-def cpp_unittest(image) {
-  sh (
-    script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
-    label: 'Run C++ tests',
-  )
-}
diff --git a/ci/jenkins/templates/utils/base.groovy.j2 
b/ci/jenkins/templates/utils/base.groovy.j2
index a8fda460b7..031ed2ce3a 100644
--- a/ci/jenkins/templates/utils/base.groovy.j2
+++ b/ci/jenkins/templates/utils/base.groovy.j2
@@ -100,7 +100,6 @@ jenkins_scripts_root = "ci/scripts/jenkins"
 // are purely to satisfy the JVM
 {% include "utils/Prepare.groovy.j2" %}
 {% include "utils/Build.groovy.j2" %}
-{% include "utils/Test.groovy.j2" %}
 
 cancel_previous_build()
 
diff --git a/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2 
b/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2
index 8e7d909bd1..80016d8361 100644
--- a/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2
+++ b/ci/jenkins/templates/wasm_jenkinsfile.groovy.j2
@@ -29,9 +29,7 @@
     script: "${docker_run} ${ci_wasm} 
./tests/scripts/task_config_build_wasm.sh build",
     label: 'Create WASM CMake config',
   )
-  cmake_build(ci_wasm, 'build')
-  make_cpp_tests(ci_wasm, 'build')
-  cpp_unittest(ci_wasm)
+  wheel_build(ci_wasm, true)
   sh (
     script: "${docker_run} ${ci_wasm} ./tests/scripts/task_web_wasm.sh",
     label: 'Run WASM lint and tests',
diff --git a/docs/conf.py b/docs/conf.py
index 599e03ca38..6f40efe457 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -50,7 +50,8 @@ if curr_path.name == "_staging":
 else:
     tvm_path = Path(os.pardir)
 
-sys.path.insert(0, str(tvm_path.resolve() / "python"))
+if os.environ.get("TVM_DOCS_USE_WHEEL") != "1":
+    sys.path.insert(0, str(tvm_path.resolve() / "python"))
 sys.path.insert(0, str(tvm_path.resolve() / "docs"))
 
 # -- General configuration ------------------------------------------------
diff --git a/tests/python/ci/test_wheel_artifacts.py 
b/tests/python/ci/test_wheel_artifacts.py
new file mode 100644
index 0000000000..9eaa8613bc
--- /dev/null
+++ b/tests/python/ci/test_wheel_artifacts.py
@@ -0,0 +1,167 @@
+# 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.
+"""Wheel artifact transport and local CI command contracts, without a TVM 
import."""
+
+import importlib.util
+import json
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[3]
+
+
+def load_local_ci():
+    spec = importlib.util.spec_from_file_location("local_ci", ROOT / 
"tests/scripts/ci.py")
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
[email protected]("platform", ["cpu", "gpu", "arm", "wasm"])
+def test_local_wheel_build(monkeypatch, platform):
+    ci = load_local_ci()
+    calls = []
+    monkeypatch.setattr(ci, "docker", lambda **kwargs: calls.append(kwargs))
+    command, _, _ = ci.generate_command(platform, {}, "wheel build")
+    command(cpp=True)
+    scripts = calls[0]["scripts"]
+    wheel = next(script for script in scripts if "-Cbuild-dir=" in script)
+    assert "-Ccmake.build-type=RelWithDebInfo" in wheel
+    assert "all;cpptest" in wheel
+    assert any("./3rdparty/tvm-ffi" in script for script in scripts)
+    assert any("task_python_install_wheels.sh" in script for script in 
scripts) == (
+        platform != "wasm"
+    )
+    calls.clear()
+    command(skip_build=True, tests=["tests/python/example.py"])
+    assert not any("uv build" in script for script in calls[0]["scripts"])
+
+
[email protected]("succeed", [True, False])
+def test_wheel_directory_upload_retries_each_transfer(tmp_path, succeed):
+    # Exercise the existing S3 CLI/retry boundary with a directory bundle.
+    for relative in [
+        "ci/scripts/jenkins/s3.py",
+        "ci/scripts/jenkins/cmd_utils.py",
+        "ci/scripts/jenkins/retry.sh",
+        "ci/jenkins/data.py",
+    ]:
+        target = tmp_path / relative
+        target.parent.mkdir(parents=True, exist_ok=True)
+        shutil.copy(ROOT / relative, target)
+    wheels = tmp_path / "build/wheels"
+    wheels.mkdir(parents=True)
+    (wheels / "apache_tvm.whl").write_text("tvm")
+    (wheels / "apache_tvm_ffi.whl").write_text("ffi")
+    binaries = tmp_path / "bin"
+    binaries.mkdir()
+    aws = binaries / "aws"
+    aws.write_text(
+        f"#!{sys.executable}\n"
+        "import json, pathlib, sys\n"
+        "path = pathlib.Path('attempts.json')\n"
+        "calls = json.loads(path.read_text()) if path.exists() else []\n"
+        "calls.append(sys.argv[1:])\n"
+        "path.write_text(json.dumps(calls))\n"
+        f"sys.exit(0 if {succeed!r} and len(calls) == 3 else 1)\n"
+    )
+    aws.chmod(0o755)
+    sleep = binaries / "sleep"
+    sleep.write_text('#!/bin/sh\necho "$1" >> delays.txt\n')
+    sleep.chmod(0o755)
+    result = subprocess.run(
+        [
+            sys.executable,
+            "ci/scripts/jenkins/s3.py",
+            "--action",
+            "upload",
+            "--bucket",
+            "test",
+            "--prefix",
+            "cpu",
+            "--bundle",
+            "wheels",
+        ],
+        cwd=tmp_path,
+        env={**os.environ, "PATH": f"{binaries}:{os.environ['PATH']}"},
+        capture_output=True,
+        text=True,
+    )
+    assert (result.returncode == 0) == succeed, result.stdout + result.stderr
+    attempts = json.loads((tmp_path / "attempts.json").read_text())
+    assert len(attempts) == 3
+    assert all(
+        call[-3:] == ["--recursive", "build/wheels", 
"s3://test/cpu/build/wheels"]
+        for call in attempts
+    )
+    delays = [int(line) for line in (tmp_path / 
"delays.txt").read_text().splitlines()]
+    assert len(delays) == 2 and all(30 <= delay <= 200 for delay in delays)
+
+
[email protected]("fail", [False, True])
+def test_local_interactive_keeps_cache_stats(tmp_path, monkeypatch, fail):
+    ci = load_local_ci()
+    monkeypatch.setattr(ci, "check_docker", lambda: None)
+    monkeypatch.setattr(ci, "cmd", lambda *args, **kwargs: None)
+    monkeypatch.setattr(ci, "SCRIPT_DIR", tmp_path / "scripts")
+    monkeypatch.setattr(ci, "REPO_ROOT", tmp_path)
+    monkeypatch.delenv("USE_SCCACHE", raising=False)
+    monkeypatch.setenv("DEBUG", "1")
+    binaries = tmp_path / "bin"
+    binaries.mkdir()
+    for name in ["sccache", "bash"]:
+        binary = binaries / name
+        binary.write_text(f'#!/bin/sh\necho "{name} $*" >> "$CALL_LOG"\n')
+        binary.chmod(0o755)
+    ci.docker("interactive", "ci_cpu", ["false" if fail else "true"], {}, True)
+    result = subprocess.run(
+        [shutil.which("bash"), str(ci.SCRIPT_DIR / "interactive.sh")],
+        env={
+            **os.environ,
+            "PATH": f"{binaries}:{os.environ['PATH']}",
+            "CALL_LOG": str(tmp_path / "calls"),
+        },
+        capture_output=True,
+        text=True,
+    )
+    assert (result.returncode != 0) == fail
+    calls = (tmp_path / "calls").read_text().splitlines()
+    assert calls == [
+        "sccache --start-server",
+        "sccache --show-stats",
+        "sccache --show-stats",
+        "bash ",
+    ]
+
+
[email protected]("wheel_docs", [False, True])
+def test_docs_import_path(tmp_path, monkeypatch, wheel_docs):
+    # Run the real Sphinx path bootstrap without requiring Sphinx extensions.
+    # A stale --target=python FFI package must not precede Jenkins' wheel 
install.
+    monkeypatch.chdir(ROOT / "docs")
+    source_python = str(ROOT / "python")
+    monkeypatch.setattr(sys, "path", [path for path in sys.path if path != 
source_python])
+    monkeypatch.setenv("TVM_DOCS_USE_WHEEL", "1" if wheel_docs else "0")
+    config = ROOT / "docs/conf.py"
+    bootstrap = config.read_text().split("# -- General configuration")[0]
+    exec(compile(bootstrap, str(config), "exec"), {"__file__": str(config)})
+    assert (source_python in sys.path) != wheel_docs
diff --git a/tests/scripts/ci.py b/tests/scripts/ci.py
index fa027c150a..f242f110b5 100755
--- a/tests/scripts/ci.py
+++ b/tests/scripts/ci.py
@@ -165,27 +165,24 @@ def docker(
     """
     check_docker()
 
-    # As sccache is added to these images these can be uncommented
-    sccache_images = {
-        "ci_gpu",
-        "ci_cpu",
-        # "ci_wasm",
-        "ci_cortexm",
-        "ci_arm",
-        "ci_riscv",
-        "ci_adreno",
-    }
-
-    if image in sccache_images and os.getenv("USE_SCCACHE", "1") == "1":
+    if os.getenv("USE_SCCACHE", "1") == "1":
         scripts = [
-            "sccache --start-server",
+            "if command -v sccache >/dev/null; then",
+            "  export CC=/opt/sccache/cc CXX=/opt/sccache/c++",
+            "  sccache --start-server || true",
+            "  sccache --show-stats",
+            '  trap "sccache --show-stats' + ("; interact" if interactive else 
"") + '" EXIT',
+            "fi",
         ] + scripts
-        # Set the C/C++ compiler so CMake picks them up in the build
-        env["CC"] = "/opt/sccache/cc"
-        env["CXX"] = "/opt/sccache/c++"
         env["SCCACHE_CACHE_SIZE"] = os.getenv("SCCACHE_CACHE_SIZE", "50G")
         env["SCCACHE_SERVER_PORT"] = os.getenv("SCCACHE_SERVER_PORT", "4226")
 
+    # Match Jenkins' per-executor native build budget, including wheel builds.
+    scripts = [
+        "export CMAKE_BUILD_PARALLEL_LEVEL=$(( $(getconf _NPROCESSORS_ONLN) / "
+        "${CI_NUM_EXECUTORS:-1} ))",
+        'if [ "$CMAKE_BUILD_PARALLEL_LEVEL" -lt 1 ]; then export 
CMAKE_BUILD_PARALLEL_LEVEL=1; fi',
+    ] + scripts
     env["PLATFORM"] = name
 
     docker_bash = REPO_ROOT / "docker" / "bash.sh"
@@ -292,7 +289,8 @@ def docs(
 
     scripts = extra_setup + [
         config_script,
-        f"./tests/scripts/task_build.py --build-dir {build_dir}",
+        f"cmake -S . -B {build_dir} -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo",
+        f"cmake --build {build_dir} --verbose",
     ]
 
     if skip_build:
@@ -402,9 +400,22 @@ def generate_command(
         else:
             scripts = [
                 f"./tests/scripts/task_config_build_{name}.sh {build_dir}",
-                f"./tests/scripts/task_build.py --build-dir {build_dir}",
+                f"rm -rf {build_dir}/wheels",
+                f"uv build --wheel --python python3 --out-dir 
{build_dir}/wheels ./3rdparty/tvm-ffi",
+                f"uv build --wheel --python python3 --out-dir 
{build_dir}/wheels . "
+                f"-Cbuild-dir={build_dir} -Ccmake.build-type=RelWithDebInfo "
+                f"-Cbuild.verbose={'true' if verbose else 'false'}"
+                + (
+                    " -Ccmake.define.USE_GTEST=ON 
'-Cbuild.targets=all;cpptest'"
+                    if kwargs.get("cpp")
+                    else ""
+                ),
             ]
 
+        # Direct --tests and interactive Python use the same installed 
artifacts.
+        if name != "wasm":
+            scripts.append("source 
./tests/scripts/task_python_install_wheels.sh")
+
         if post_build is not None:
             scripts += post_build
 
@@ -422,10 +433,10 @@ def generate_command(
                 scripts.extend(script.format(build_dir=build_dir) for script 
in extra_scripts)
 
         docker_env = {
-            # Need to specify the library path manually or else TVM can't
-            # determine which build directory to use (i.e. if there are
-            # multiple copies of libtvm_compiler.so / libtvm_runtime.so laying 
around)
+            # The specialized Wasm source tests retain a native build path;
+            # the wheel installer clears it for Python wheel consumers.
             "TVM_LIBRARY_PATH": str(REPO_ROOT / get_build_dir(name)),
+            "TVM_WHEEL_DIR": str(REPO_ROOT / build_dir / "wheels"),
             "VERBOSE": "true" if verbose else "false",
         }
 
diff --git a/tests/scripts/task_build.py b/tests/scripts/task_build.py
deleted file mode 100755
index c6c30c9bbd..0000000000
--- a/tests/scripts/task_build.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/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.
-# ruff: noqa: E402
-import argparse
-import logging
-import multiprocessing
-import os
-import shutil
-import sys
-from pathlib import Path
-
-# Hackery to enable importing of utils from ci/scripts/jenkins
-REPO_ROOT = Path(__file__).resolve().parent.parent.parent
-sys.path.append(str(REPO_ROOT / "ci" / "scripts" / "jenkins"))
-from cmd_utils import REPO_ROOT, Sh, init_log
-
-if __name__ == "__main__":
-    init_log()
-
-    parser = argparse.ArgumentParser(description="List pytest nodeids for a 
folder")
-    parser.add_argument("--sccache-bucket", required=False, help="sccache 
bucket name")
-    parser.add_argument("--sccache-region", required=False, help="sccache 
region")
-    parser.add_argument("--build-dir", default="build", help="build folder")
-    parser.add_argument("--cmake-target", help="optional build target")
-    parser.add_argument("--debug", required=False, action="store_true", 
help="build in debug mode")
-    args = parser.parse_args()
-    sccache_exe = shutil.which("sccache")
-
-    if args.cmake_target in ["standalone_crt", "crttest"]:
-        logging.info("Skipping standalone_crt build")
-        exit(0)
-
-    use_sccache = sccache_exe is not None
-    build_dir = Path(os.getcwd()) / args.build_dir
-    build_dir = build_dir.relative_to(REPO_ROOT)
-    env = {}
-
-    if use_sccache:
-        if args.sccache_bucket and "AWS_ACCESS_KEY_ID" in os.environ:
-            env["SCCACHE_BUCKET"] = args.sccache_bucket
-            env["SCCACHE_REGION"] = args.sccache_region if args.sccache_region 
else "us-west-2"
-            logging.info(f"Using sccache bucket: {args.sccache_bucket}")
-            logging.info(f"Using sccache region: {env['SCCACHE_REGION']}")
-        else:
-            logging.info("No sccache bucket set, using local cache")
-        if "SCCACHE_SERVER_PORT" in os.environ:
-            env["SCCACHE_SERVER_PORT"] = os.getenv("SCCACHE_SERVER_PORT")
-        env["CXX"] = "/opt/sccache/c++"
-        env["CC"] = "/opt/sccache/cc"
-
-    else:
-        if sccache_exe is None:
-            reason = "'sccache' executable not found"
-        else:
-            reason = "<unknown>"
-        logging.info(f"Not using sccache, reason: {reason}")
-
-    sh = Sh(env)
-
-    if use_sccache:
-        sh.run("sccache --start-server", check=False)
-        logging.info("===== sccache stats =====")
-        sh.run("sccache --show-stats")
-
-    executors = int(os.environ.get("CI_NUM_EXECUTORS", 1))
-    build_platform = os.environ.get("PLATFORM", None)
-
-    nproc = multiprocessing.cpu_count()
-
-    available_cpus = nproc // executors
-    num_cpus = max(available_cpus, 1)
-
-    if args.debug:
-        sh.run("cmake -GNinja -DCMAKE_BUILD_TYPE=Debug ..", cwd=build_dir)
-    else:
-        sh.run("cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..", 
cwd=build_dir)
-
-    target = ""
-    if args.cmake_target:
-        target = args.cmake_target
-
-    verbose = os.environ.get("VERBOSE", "true").lower() in {"1", "true", "yes"}
-    ninja_args = [target, f"-j{num_cpus}"]
-    if verbose:
-        ninja_args.append("-v")
-
-    sh.run("cmake --build . -- " + " ".join(ninja_args), cwd=build_dir)
-
-    if use_sccache:
-        logging.info("===== sccache stats =====")
-        sh.run("sccache --show-stats")
diff --git a/tests/scripts/task_python_docs.sh 
b/tests/scripts/task_python_docs.sh
index e45d439929..73d2662465 100755
--- a/tests/scripts/task_python_docs.sh
+++ b/tests/scripts/task_python_docs.sh
@@ -18,7 +18,14 @@
 
 set -euxo pipefail
 
-export PYTHONPATH="$(pwd)/python"
+# Local docs retain source builds; Jenkins imports the exact build-node wheels.
+if [ "${IS_LOCAL:-0}" != "1" ]; then
+    source "$(dirname "$0")/task_python_install_wheels.sh"
+    export TVM_DOCS_USE_WHEEL=1
+else
+    uv pip install -v --target=python ./3rdparty/tvm-ffi/
+    export PYTHONPATH="$(pwd)/python"
+fi
 
 # to avoid CI CPU thread throttling.
 export TVM_BIND_THREADS=0
@@ -47,9 +54,6 @@ sphinx_precheck() {
     clean_files
     echo "PreCheck sphinx doc generation WARNINGS.."
 
-    # setup tvm-ffi into python folder
-    uv pip install -v --target=python ./3rdparty/tvm-ffi/
-
     pushd docs
     make clean
     TVM_TUTORIAL_EXEC_PATTERN=none make html 2>&1 | tee /tmp/$$.log.txt
@@ -129,15 +133,12 @@ clean_files
 find . -type f -path "*.log" | xargs rm -f
 find . -type f -path "*.pyc" | xargs rm -f
 
-# setup tvm-ffi into python folder
-uv pip install -v --target=python ./3rdparty/tvm-ffi/
-
 # LLVMModule JIT execution, needed by the tutorials this build runs.
 uv pip install --no-deps apache-tvm-ffi-orcjit==0.1.2
 
 
 cd docs
-PYTHONPATH=$(pwd)/../python make htmldepoly SPHINXOPTS='-j auto' |& tee 
/tmp/$$.log.txt
+make htmldepoly SPHINXOPTS='-j auto' |& tee /tmp/$$.log.txt
 if grep -E "failed to execute|Segmentation fault" < /tmp/$$.log.txt; then
     echo "Some of sphinx-gallery item example failed to execute."
     exit 1
diff --git a/tests/scripts/task_python_install_wheels.sh 
b/tests/scripts/task_python_install_wheels.sh
new file mode 100644
index 0000000000..f54c57c9c5
--- /dev/null
+++ b/tests/scripts/task_python_install_wheels.sh
@@ -0,0 +1,54 @@
+#!/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.
+
+# Source this file so subsequent Python invocations use the wheel environment.
+# Docker defaults PYTHONPATH to the checkout; never let it shadow the 
artifacts.
+unset PYTHONPATH TVM_LIBRARY_PATH TVM_FFI_LIBRARY_PATH
+export PYTHONNOUSERSITE=1
+TVM_WHEEL_DIR="$(realpath "${TVM_WHEEL_DIR:-$(pwd)/build/wheels}")"
+# Preserve the image's active environment (often itself a venv), including when
+# local ci.py sources this setup again for a named test suite.
+export TVM_IMAGE_PYTHON="${TVM_IMAGE_PYTHON:-$(command -v python3)}"
+export 
TVM_IMAGE_SITE_PACKAGES="${TVM_IMAGE_SITE_PACKAGES:-$("$TVM_IMAGE_PYTHON" -I -c 
'import site; print("\n".join(site.getsitepackages()))')}"
+export VIRTUAL_ENV="${TVM_WHEEL_DIR%/}/../wheel-venv"
+uv venv --python "$TVM_IMAGE_PYTHON" --clear "$VIRTUAL_ENV"
+# Append the image dependencies after our wheels without executing its .pth 
files:
+# system-site-packages can install editable-import hooks ahead of the wheel.
+TVM_WHEEL_SITE="$("$VIRTUAL_ENV/bin/python3" -c 'import sysconfig; 
print(sysconfig.get_path("purelib"))')"
+printf '%s\n' "$TVM_IMAGE_SITE_PACKAGES" > 
"$TVM_WHEEL_SITE/image-dependencies.pth"
+export PATH="$VIRTUAL_ENV/bin:$PATH"
+uv pip install --python "$VIRTUAL_ENV/bin/python3" --no-deps --reinstall \
+    "$TVM_WHEEL_DIR"/apache_tvm-*.whl "$TVM_WHEEL_DIR"/apache_tvm_ffi-*.whl
+
+# Fail before pytest if a source/editable install or native override leaked in.
+python3 - <<'PYTHON'
+from pathlib import Path
+import sys
+import tvm
+import tvm_ffi
+
+prefix = Path(sys.prefix).resolve()
+for module in (tvm, tvm_ffi):
+    path = Path(module.__file__).resolve()
+    assert path.is_relative_to(prefix), path
+    print(f"Installed {module.__name__}: {path}")
+for name, library in {"tvm_ffi": tvm_ffi.LIB, **tvm.base._LOADED_LIBS}.items():
+    path = Path(library._name).resolve()
+    assert path.is_relative_to(prefix), (name, path)
+    print(f"Installed {name}: {path}")
+PYTHON
diff --git a/tests/scripts/task_python_unittest.sh 
b/tests/scripts/task_python_unittest.sh
index 511b3ec2a5..8472b142b8 100755
--- a/tests/scripts/task_python_unittest.sh
+++ b/tests/scripts/task_python_unittest.sh
@@ -18,15 +18,12 @@
 
 set -euxo pipefail
 
-export PYTHONPATH="$(pwd)/python"
+source "$(dirname "$0")/task_python_install_wheels.sh"
 export PYTEST_ADDOPTS="${CI_PYTEST_ADD_OPTIONS:-} ${PYTEST_ADDOPTS:-}"
 
-# setup tvm-ffi into python folder
-uv pip install -v --target=python ./3rdparty/tvm-ffi/
-
 # LLVMModule JIT execution. Installed here as well as in the CI image because 
a docker/
 # change does not reach the test containers in the same run (they use the 
pinned tag).
 # --no-deps keeps the submodule-built tvm-ffi above from being replaced by a 
PyPI build.
 uv pip install --no-deps apache-tvm-ffi-orcjit==0.1.2
 
-python3 -m pytest -vvs -n auto -m "${TVM_TEST_MARKER:-not gpu}" tests/python
+python3 -m pytest -vvs -n auto -m "${TVM_TEST_MARKER:-not gpu}" 
"${@:-tests/python}"
diff --git a/tests/scripts/task_python_unittest_gpuonly.sh 
b/tests/scripts/task_python_unittest_gpuonly.sh
index ac9e112b84..9e4b95ec3d 100755
--- a/tests/scripts/task_python_unittest_gpuonly.sh
+++ b/tests/scripts/task_python_unittest_gpuonly.sh
@@ -31,9 +31,4 @@ export TVM_TEST_TARGETS='cuda;metal;rocm;nvptx'
 # only minimal functionality of Vulkan runtime.
 export TVM_TEST_TARGETS='{"kind":"vulkan","from_device":0}'
 
-export PYTHONPATH="$(pwd)/python"
-export PYTEST_ADDOPTS="${CI_PYTEST_ADD_OPTIONS:-} ${PYTEST_ADDOPTS:-}"
-
-python3 -m pytest -vvs -n auto -m "${TVM_TEST_MARKER}" \
-    tests/python/backend/vulkan/codegen/test_vulkan.py \
-    tests/python/backend/vulkan/test_vulkan_module_export.py
+./tests/scripts/task_python_unittest.sh 
tests/python/codegen/test_target_codegen_vulkan.py

Reply via email to