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 43204d1820a6cbcd6113ce12929f50b12ac5d733
Merge: 0c852cf5e5 d5089384b5
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Oct 15 02:54:02 2026 +0000

    Merge native Jenkins cleanup and artifact interfaces into wheel pipelines

 .gitignore                                         |    4 -
 CMakeLists.txt                                     |   59 +-
 apps/cpp_rpc/CMakeLists.txt                        |   12 -
 ci/README.md                                       |    3 +-
 ci/jenkins/README.md                               |   79 +-
 ci/jenkins/{data.py => artifacts.py}               |   14 +-
 ci/jenkins/generated/arm_jenkinsfile.groovy        |    4 +-
 ci/jenkins/generated/common.groovy                 |  284 ++--
 ci/jenkins/generated/cpu_jenkinsfile.groovy        |    4 +-
 ci/jenkins/generated/gpu_jenkinsfile.groovy        |    8 +-
 ci/jenkins/generated/wasm_jenkinsfile.groovy       |    2 +
 ci/jenkins/scripts/s3.py                           |   83 +-
 ci/scripts/github/github_tvmbot.py                 |  799 -----------
 ci/scripts/jenkins/cmd_utils.py                    |  107 --
 ci/scripts/jenkins/determine_docker_images.py      |  153 ---
 ci/scripts/jenkins/git_change_docker.sh            |   55 -
 ci/scripts/jenkins/git_change_docs.sh              |   39 -
 ci/scripts/jenkins/git_skip_ci.py                  |   61 -
 ci/scripts/jenkins/git_skip_ci_globs.py            |   76 --
 ci/scripts/jenkins/git_utils.py                    |  212 ---
 ci/scripts/jenkins/http_utils.py                   |   34 -
 ci/scripts/jenkins/retry.sh                        |   39 -
 ci/scripts/jenkins/s3.py                           |  193 ---
 ci/scripts/jenkins/should_rebuild_docker.py        |  152 ---
 ci/scripts/jenkins/should_run_slow_tests.py        |   85 --
 ci/scripts/package/README.md                       |   31 -
 ci/scripts/package/build-environment.yaml          |   42 -
 .../package/manylinux_build_libtvm_runtime_cuda.sh |   64 -
 .../package/windows_build_libtvm_runtime_cuda.bat  |   99 --
 cmake/config.cmake                                 |   17 +-
 cmake/modules/Hexagon.cmake                        |  345 -----
 cmake/modules/HexagonSDK.cmake                     |  205 ---
 cmake/modules/LLVM.cmake                           |    4 +
 cmake/modules/RuntimeExtra.cmake                   |   12 +-
 docker/with_the_same_user                          |    4 -
 docs/reference/api/python/s_tir/tensor_intrin.rst  |    6 -
 docs/reference/api/python/script/script.rst        |    2 +-
 docs/tirx/api/cuda.rst                             |    4 +
 docs/tirx/api/execution.rst                        |   37 +-
 docs/tirx/api/ptx.rst                              |    4 +-
 docs/tirx/api/script.rst                           |   57 +
 docs/tirx/api/tile_dispatch.rst                    |   24 +-
 docs/tirx/api/tirx.rst                             |    2 +-
 docs/tirx/arch/lowering_pipeline.rst               |    2 +-
 docs/tirx/arch/tile_dispatch.rst                   |    8 +-
 include/tvm/s_tir/meta_schedule/mutator.h          |    2 -
 include/tvm/s_tir/meta_schedule/postproc.h         |    2 -
 include/tvm/s_tir/meta_schedule/schedule_rule.h    |   30 -
 include/tvm/script/ir_builder/base.h               |   14 +-
 include/tvm/tirx/layout.h                          |   10 +-
 include/tvm/tirx/script/ir_builder/frame.h         |    2 +-
 include/tvm/tirx/script/ir_builder/ir.h            |    4 +-
 include/tvm/tirx/stmt.h                            |    2 +-
 include/tvm/tirx/tile/context.h                    |   18 +-
 include/tvm/tirx/tile/exec_scope.h                 |   57 -
 include/tvm/tirx/tile/thread_scope.h               |   82 ++
 include/tvm/tirx/transform.h                       |    6 -
 python/tvm/backend/cuda/__init__.py                |    7 +-
 python/tvm/backend/cuda/op.py                      |    2 +-
 python/tvm/backend/cuda/script.py                  |    4 +-
 .../{trn/exec_scope.py => cuda/thread_scope.py}    |   18 +-
 python/tvm/backend/hexagon/__init__.py             |   16 -
 python/tvm/backend/trn/__init__.py                 |   11 +-
 .../tvm/backend/trn/thread_scope.py                |   12 +-
 python/tvm/backend/trn/tile/binary/default.py      |    2 +-
 python/tvm/backend/trn/tile/copy/default.py        |    2 +-
 python/tvm/backend/trn/tile/gemm/default.py        |    2 +-
 python/tvm/backend/trn/tile/reduction/utils.py     |    2 +-
 python/tvm/backend/trn/tile/select/default.py      |    2 +-
 python/tvm/backend/trn/tile/unary/default.py       |    2 +-
 .../tvm/backend/trn/tile/unary/with_bias_scale.py  |    2 +-
 .../backend/trn/transform/private_tensor_alloc.py  |    6 +-
 .../frontend/torch/base_fx_graph_translator.py     |   22 +-
 python/tvm/s_tir/meta_schedule/mutator/mutator.py  |    4 +-
 .../tvm/s_tir/meta_schedule/postproc/postproc.py   |    5 +-
 .../s_tir/meta_schedule/schedule_rule/__init__.py  |    1 -
 .../schedule_rule/multi_level_tiling.py            |   58 +-
 .../meta_schedule/schedule_rule/schedule_rule.py   |    5 +-
 .../space_generator/space_generator.py             |   12 +-
 .../meta_schedule/testing/space_generation.py      |    4 +-
 python/tvm/s_tir/pipeline.py                       |    1 -
 python/tvm/s_tir/tensor_intrin/__init__.py         |    1 -
 python/tvm/s_tir/tensor_intrin/cuda.py             | 1193 -----------------
 python/tvm/s_tir/tensor_intrin/rocm.py             |  107 +-
 python/tvm/s_tir/testing/tir.py                    |  119 +-
 python/tvm/script/ir_builder/base.py               |   15 +
 python/tvm/script/ir_builder/tracing.py            |  163 +++
 python/tvm/support/libinfo.py                      |    1 -
 python/tvm/testing/env.py                          |   10 +-
 python/tvm/testing/utils.py                        |    2 +-
 python/tvm/tirx/__init__.py                        |    2 +-
 python/tvm/tirx/compilation_pipeline.py            |    1 -
 python/tvm/tirx/exec_scope.py                      |   45 -
 python/tvm/tirx/layout.py                          |    8 +-
 python/tvm/tirx/script/ir_builder/ir.py            |    4 +-
 python/tvm/tirx/thread_scope.py                    |   64 +
 python/tvm/tirx/tile/dispatch.py                   |   10 +-
 python/tvm/tirx/tile/instruction.py                |   35 +-
 python/tvm/tirx/transform/transform.py             |   11 -
 src/backend/cuda/codegen/codegen_cuda.cc           |  131 +-
 src/backend/cuda/codegen/ptx.cc                    |  494 +------
 src/backend/cuda/codegen/ptx.h                     |   76 --
 src/backend/cuda/codegen/target_kind.cc            |   12 +
 src/backend/cuda/layout/axis.cc                    |   87 +-
 src/backend/cuda/op/legacy.cc                      |   61 +
 src/backend/cuda/op/target_builtin.cc              |   25 -
 src/backend/cuda/tile/exec_analysis.cc             |   10 +-
 src/backend/cuda/tile/exec_analysis.h              |    4 +-
 src/backend/cuda/tile/exec_context.h               |    2 +-
 .../cuda/tile/{exec_scope.cc => thread_scope.cc}   |    2 +-
 .../cuda/tile/{exec_scope.h => thread_scope.h}     |    6 +-
 .../hexagon/codegen/hexagon_fallback_module.cc     |   23 +-
 .../hexagon/codegen/hexagon_fallback_module.h      |    4 +-
 .../hexagon/codegen/llvm/codegen_hexagon.cc        |   11 +-
 src/backend/hexagon/{ => codegen}/memory_scope.h   |    6 +-
 src/backend/hexagon/codegen/target_kind.cc         |    2 +-
 src/backend/hexagon/runtime/README.md              |   74 --
 src/backend/hexagon/runtime/hexagon_buffer.cc      |  301 -----
 src/backend/hexagon/runtime/hexagon_buffer.h       |  204 ---
 .../hexagon/runtime/hexagon_buffer_manager.h       |   93 --
 src/backend/hexagon/runtime/hexagon_common.cc      |  101 --
 src/backend/hexagon/runtime/hexagon_common.h       |   52 -
 src/backend/hexagon/runtime/hexagon_device_api.cc  |  349 -----
 src/backend/hexagon/runtime/hexagon_device_api.h   |  270 ----
 src/backend/hexagon/runtime/hexagon_htp.cc         |   81 --
 src/backend/hexagon/runtime/hexagon_htp.h          |   62 -
 src/backend/hexagon/runtime/hexagon_hvx.cc         |   61 -
 src/backend/hexagon/runtime/hexagon_hvx.h          |   68 -
 src/backend/hexagon/runtime/hexagon_module.cc      |  156 ---
 .../hexagon/runtime/hexagon_power_manager.cc       |  108 --
 .../hexagon/runtime/hexagon_power_manager.h        |   63 -
 .../hexagon/runtime/hexagon_thread_manager.cc      |  377 ------
 .../hexagon/runtime/hexagon_thread_manager.h       |  250 ----
 src/backend/hexagon/runtime/hexagon_user_dma.cc    |  140 --
 src/backend/hexagon/runtime/hexagon_user_dma.h     |  110 --
 .../hexagon/runtime/hexagon_user_dma_descriptors.h |  319 -----
 .../runtime/hexagon_user_dma_instructions.h        |   79 --
 .../hexagon/runtime/hexagon_user_dma_registers.h   |  278 ----
 src/backend/hexagon/runtime/hexagon_vtcm_pool.cc   |  173 ---
 src/backend/hexagon/runtime/hexagon_vtcm_pool.h    |  118 --
 src/backend/hexagon/runtime/ops/conv2d.h           |  332 -----
 src/backend/hexagon/runtime/ops/conv2d_fp16_hvx.cc |  499 -------
 .../hexagon/runtime/ops/conv2d_quant_hvx.cc        |  330 -----
 src/backend/hexagon/runtime/ops/conv_utils.cc      |  221 ----
 src/backend/hexagon/runtime/qhl/qhl_wrapper.cc     |   93 --
 src/backend/hexagon/runtime/ring_buffer.h          |  191 ---
 src/backend/hexagon/runtime/rpc/android/session.cc |  136 --
 .../hexagon/runtime/rpc/hexagon/rpc_server.cc      |  355 -----
 src/backend/hexagon/runtime/rpc/hexagon_rpc.idl    |   29 -
 .../runtime/rpc/simulator/hexagon_sim_proto.h      |   75 --
 .../hexagon/runtime/rpc/simulator/rpc_server.cc    |  358 -----
 .../hexagon/runtime/rpc/simulator/session.cc       | 1395 --------------------
 src/backend/opencl/codegen/codegen_opencl.cc       |    2 +
 src/backend/trn/codegen/target_kind.cc             |    7 +
 src/runtime/threading_backend.cc                   |  111 +-
 src/runtime/vm/hexagon/builtin.cc                  |   89 --
 src/s_tir/meta_schedule/mutator/mutator.cc         |    3 -
 src/s_tir/meta_schedule/postproc/postproc.cc       |   15 -
 .../multi_level_tiling_tensor_core.cc              |  984 --------------
 .../meta_schedule/schedule_rule/schedule_rule.cc   |   43 -
 .../space_generator/space_generator.cc             |   22 -
 src/s_tir/transform/inject_permuted_layout.cc      |  116 +-
 src/s_tir/transform/inject_software_pipeline.cc    |   40 -
 src/s_tir/transform/transform_mma_tensor_layout.cc |   23 +-
 src/script/ir_builder/base.cc                      |   12 +-
 src/tirx/ir/layout/axis_registry.cc                |    8 +-
 src/tirx/ir/layout/tile_canonicalize.cc            |    4 +-
 src/tirx/ir/layout/tile_core.cc                    |   45 +-
 src/tirx/script/ir_builder/frame.cc                |    2 +-
 src/tirx/script/ir_builder/ir.cc                   |    2 +-
 src/tirx/script/printer/block.cc                   |   15 +-
 src/tirx/script/printer/expr.cc                    |    4 +-
 src/tirx/tile/context.cc                           |   16 +-
 src/tirx/tile/exec_scope.cc                        |   68 -
 src/tirx/tile/lower.cc                             |   12 +-
 src/tirx/tile/op.cc                                |   12 +-
 src/tirx/tile/thread_scope.cc                      |   80 ++
 src/tirx/transform/lower_warp_memory.cc            |  814 ------------
 tests/cpp/tirx/thread_scope_test.cc                |  108 ++
 .../python/backend/hexagon/codegen/test_hexagon.py |   29 +-
 tests/python/ci/jenkins_loader_test.groovy         |   84 ++
 tests/python/ci/test_ci.py                         |   57 +-
 tests/python/ci/test_jenkins_helpers.py            |  189 ++-
 .../python/ci/test_jenkins_loader.py               |   27 +-
 tests/python/ci/test_wheel_artifacts.py            |    2 +-
 tests/python/relax/test_frontend_from_fx.py        |   36 +
 .../test_meta_schedule_mma_tensorize.py            |  343 -----
 ...est_meta_schedule_postproc_rewrite_tensorize.py |    2 +-
 ...ule_space_cuda_async_multiple_initialization.py |    2 +-
 .../test_meta_schedule_space_generator.py          |   21 +
 .../test_meta_schedule_trace_apply.py              |    3 +-
 .../s_tir/schedule/test_tir_schedule_analysis.py   |   31 +-
 ..._tir_schedule_tensorize_ldmatrix_mma_numeric.py |  464 -------
 .../test_tir_schedule_tensorize_mfma_numeric.py    |  108 +-
 .../test_s_tir_transform_inject_permuted_layout.py |  360 +----
 ...est_s_tir_transform_inject_software_pipeline.py |  169 ---
 .../test_s_tir_transform_mma_tensor_layout.py      |   23 +
 tests/python/script/test_builder_void_emission.py  |    7 +-
 tests/python/testing/test_env.py                   |   30 +-
 tests/python/tirx/base/test_tirx_ptx_ldmatrix.py   |  101 --
 tests/python/tirx/base/test_tirx_ptx_mma.py        | 1264 ------------------
 tests/python/tirx/codegen/test_codegen_cuda.py     |   54 +
 .../tirx/script/test_tirx_script_basic_usage.py    |   18 +-
 tests/python/tirx/script/test_tirx_script_gpu.py   |    2 +-
 .../python/tirx/script/test_tirx_script_printer.py |    3 +-
 .../python/tirx/script/test_tirx_script_tracing.py |  312 +++++
 tests/python/tirx/test_exec_scope.py               |   43 -
 tests/python/tirx/test_layout.py                   |   24 +-
 tests/python/tirx/test_op.py                       |    2 +-
 tests/python/tirx/test_thread_scope.py             |  143 ++
 tests/python/tirx/test_tile_namespace.py           |   24 +-
 tests/python/tirx/test_tirx_axis_enum.py           |    4 +-
 .../python/tirx/tile/cuda/test_add_sub_mul_div.py  |   48 +-
 tests/python/tirx/tile/cuda/test_cp_async_bulk.py  |    6 +-
 .../tirx/tile/cuda/test_cp_async_bulk_tensor.py    |    4 +-
 tests/python/tirx/tile/cuda/test_mov.py            |   18 +-
 tests/python/tirx/tile/test_dispatcher.py          |   27 +-
 tests/python/tirx/tile/trn/test_binary_trn.py      |    6 +-
 tests/python/tirx/tile/trn/test_compose_op_trn.py  |    6 +-
 tests/python/tirx/tile/trn/test_copy_trn.py        |    6 +-
 tests/python/tirx/tile/trn/test_gemm_trn.py        |    6 +-
 tests/python/tirx/tile/trn/test_reduction_trn.py   |    6 +-
 tests/python/tirx/tile/trn/test_select_trn.py      |    6 +-
 tests/python/tirx/tile/trn/test_unary_trn.py       |    6 +-
 tests/scripts/task_show_node_info.sh               |   44 -
 225 files changed, 2589 insertions(+), 18847 deletions(-)

diff --cc ci/jenkins/README.md
index f4a6a97e18,e33f38da4c..cbe33aeb33
--- a/ci/jenkins/README.md
+++ b/ci/jenkins/README.md
@@@ -92,50 -111,3 +111,54 @@@ No job path or controller reconfigurati
  The community's infrastructure configuration and documentation live in
  [tlc-pack/ci](https://github.com/tlc-pack/ci). Coordinate external callers 
when
  changing any retained script interface.
 +
 +## Wheel build and test artifacts
 +
 +Normal builds use `uv build --wheel --no-build-isolation --offline` with the
 +existing platform CMake configuration. `uv build` builds only TVM; it does not
 +resolve runtime dependencies. CPU, ARM and Wasm host builds request 
`all;cpptest`
 +and run C++ tests in that same native tree before uploading anything. Python
 +consumers install the exact TVM wheel with `--no-deps --offline`, clear 
checkout
 +path overrides, and verify the package and native-library paths. FFI comes 
from
- the checked-out submodule. GPU Java tests also receive their existing native 
runtime libraries;
- the separate runtime-only compile check and Emscripten runtime build remain.
++the checked-out submodule. GPU Java tests also receive their existing native
++runtime libraries; the separate runtime-only compile check and Emscripten 
runtime
++build remain.
 +
 +Immediately before building, the same container and Python install the 
declared
 +`pyproject.toml` build requirements (scikit-build-core, setuptools-scm and
 +mlc-z3-static) and their build dependencies. A
 +failed bootstrap stops the build. Build requirements install into a writable 
local
 +directory, which is added to `PYTHONPATH` only for the build command. Both 
commands
 +select the original image Python, preserving its existing dependencies without
 +modifying the image environment or exposing the build overlay to test 
consumers.
 +`uv build` then runs offline without isolation.
 +
 +The selected images must already provide compatible runtime and test 
dependencies,
- apart from core FFI and its ORCJIT addon. Consumers install the checkout's 
pinned FFI source into
- the writable test environment with `--no-deps`, using its declared isolated 
build
++apart from core FFI and its ORCJIT addon. Consumers install the checkout's 
pinned
++FFI source into the writable test environment with `--no-deps`, using its 
declared
++isolated build
 +requirements, plus the explicit `apache-tvm-ffi-orcjit==0.1.2 --no-deps` 
prerequisite
 +used by legacy Python tests. They then install the transferred TVM wheel with
 +`--no-deps --offline`.
 +The checkout remains available for tests and resources through `TVM_HOME`, 
including
- Wasm assets under `web/dist`, without adding its Python package ahead of the 
wheel. Provenance checks verify both TVM/native libraries and
- source-built FFI load from the test environment; other missing runtime 
dependencies
++Wasm assets under `web/dist`, without adding its Python package ahead of the
++wheel. Provenance checks verify both TVM/native libraries and source-built FFI
++load from the test environment; other missing runtime dependencies
 +remain errors rather than permission to fetch replacements.
 +
 +`CMAKE_BUILD_PARALLEL_LEVEL` is passed through Docker as an environment 
variable.
 +An explicit value wins; otherwise builds use at least one CPU and divide the
 +node CPU count by `CI_NUM_EXECUTORS`. Both the wheel backend and specialized
 +native commands use that bound without an additional parallel flag.
 +
- ### Old pipeline compatibility
++### Current-main pipeline compatibility
 +
 +Jenkins may select the old trusted-main Jenkinsfile while checking out a newer
 +PR revision. Retain `tests/scripts/task_build.py`, the original raw-artifact
 +bundles, and the source-mode test interfaces until trusted main has migrated.
- The native pipelines and local wheel commands explicitly pass
- `TVM_TEST_USE_WHEEL=1`; only that mode installs wheel artifacts without 
fetching runtime
- dependencies. Calls without the flag retain the old source setup and 
dependency
- installation behavior. A missing wheel in wheel mode is an error, not a 
fallback
++The wheel entrypoints opt in after preparation, leaving the shared helper's
++default unchanged for older entrypoints. They and local wheel commands 
explicitly
++pass `TVM_TEST_USE_WHEEL=1`; that mode installs transferred TVM wheels with 
the
++explicit FFI setup above. Calls without the flag retain the old source setup 
and
++dependency installation behavior. A missing wheel in wheel mode is an error, 
not a fallback
 +to source mode.
diff --cc ci/jenkins/artifacts.py
index 8b92cc808d,d777ba7fb7..b222d0dd4d
--- a/ci/jenkins/artifacts.py
+++ b/ci/jenkins/artifacts.py
@@@ -30,11 -33,7 +33,10 @@@ artifact prefix
  
  import sys
  
- files_to_stash = {
+ ARTIFACTS = {
 +    # New pipelines install the exact TVM wheel and retain only JVM native 
files.
 +    "wheels": ["build/wheels"],
 +    "jvm_runtime": ["build/lib/libtvm_runtime.so", "build/lib/libtvm_ffi.so"],
-     # Keep the original bundles while trusted main still runs the old 
pipeline.
      # Executables and build files needed to run c++ tests
      "cpptest": ["build/cpptest", "build/build.ninja", 
"build/CMakeFiles/rules.ninja"],
      # runtime files
diff --cc ci/jenkins/generated/arm_jenkinsfile.groovy
index 562faf6c93,2ac6165a7e..b376794a3c
--- a/ci/jenkins/generated/arm_jenkinsfile.groovy
+++ b/ci/jenkins/generated/arm_jenkinsfile.groovy
@@@ -19,11 -19,12 +19,13 @@@
  ci = evaluate(readTrusted('ci/jenkins/generated/common.groovy'))
  ci.start()
  
++docker_run += ' --env TVM_TEST_USE_WHEEL=1'
++
  ci.build('ARM-GRAVITON3', 'BUILD: arm', 'tvm/build-arm', images.ci_arm, 
'arm') {
    sh(
      script: "${docker_run} ${images.ci_arm} 
./tests/scripts/task_config_build_arm.sh build",
      label: 'Create ARM CMake config',
    )
 -  ci.cmake_build(images.ci_arm, 'build')
 -  ci.make_cpp_tests(images.ci_arm, 'build')
 -  ci.upload('arm', '--artifacts tvm_lib --artifacts cpptest')
 +  ci.wheel_build(images.ci_arm, true)
-   ci.upload('arm', '--bundle wheels')
++  ci.upload('arm', '--artifacts wheels')
  }
diff --cc ci/jenkins/generated/common.groovy
index 68b7be3f92,1682ca1147..954e89c71e
--- a/ci/jenkins/generated/common.groovy
+++ b/ci/jenkins/generated/common.groovy
@@@ -283,64 -253,118 +253,160 @@@ def prepare(node_type) 
      }
    }
  }
- def python_unittest(image) {
-   sh (
-     script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh",
-     label: 'Run Python unit tests',
+ 
+ def retry_latest(ex, body) {
+   echo 'Exception during SPOT run ' + ex.toString()
+   if (!is_last_build()) {
+     echo 'Exit since it is not last build'
+     throw ex
+   }
+   echo 'Retry on-demand given it is last build'
+   currentBuild.result = 'SUCCESS'
+   // Do not catch failure of the on-demand retry.
+   body()
+ }
+ 
+ def run_on_node(label, name, folder, image, variables, body) {
+   if (skip_ci) {
+     Utils.markStageSkippedForConditional(name)
+     return
+   }
+   echo 'Begin running on node_type ' + label
+   node(label) {
+     ws(per_exec_ws(folder)) {
+       init_git()
+       docker_init(image)
+       timeout(time: max_time, unit: 'MINUTES') {
+         withEnv(variables) { body() }
+       }
+     }
+   }
+   echo 'End running on node_type ' + label
+ }
+ 
+ // Public entry points.
+ 
+ def docker_init(image) {
+   // Clear out all Docker images that aren't going to be used
+   sh(
+     script: """
+     set -eux
+     docker image ls --all
+     IMAGES=\$(docker image ls --all --format '{{.Repository}}:{{.Tag}}  
{{.ID}}')
+ 
+     echo -e "Found images:\\n\$IMAGES"
+     echo "\$IMAGES" | { grep -vE '${image}' || test \$? = 1; } | { xargs 
docker rmi || test \$? = 123; }
+ 
+     docker image ls --all
+     """,
+     label: 'Clean old Docker images',
    )
+ 
+   if (image.contains("amazonaws.com")) {
+     // If this string is in the image name it's from ECR and needs to be 
pulled
+     // with the right credentials
+     ecr_pull(image)
+   } else {
+     sh(
+       script: """
+       set -eux
+       . ${jenkins_scripts_root}/retry.sh
+       retry 5 docker pull ${image}
+       """,
+       label: 'Pull docker image',
+     )
+   }
+ }
+ 
+ // Preparation retries Exceptions unconditionally; Errors and cancellation of 
the
+ // previous build are deliberately outside this fallback.
+ def start() {
+   cancel_previous_build()
+   try {
+     prepare('CPU-SMALL-SPOT')
+   } catch (Exception ex) {
+     prepare('CPU-SMALL')
+   }
  }
  
 +// Share only the compiler/cache environment; build commands stay at their 
callers.
 +def build_environment() {
 +  return '''
 +    set -eux
 +    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
 +  '''
 +}
 +
 +// Preserve explicit caller limits, otherwise divide this node among its 
executors.
 +def build_docker() {
 +  return '''set -eu
 +    parallel=${CMAKE_BUILD_PARALLEL_LEVEL:-$(( $(getconf _NPROCESSORS_ONLN) / 
${CI_NUM_EXECUTORS:-1} ))}
 +    if [ "$parallel" -lt 1 ]; then parallel=1; fi
 +  ''' + "${docker_run} --env CMAKE_BUILD_PARALLEL_LEVEL=\"\$parallel\" "
 +}
 +
 +def wheel_build(image, cpp_tests = false) {
 +  // Overlay build requirements without modifying the image Python 
environment.
 +  // The build itself stays offline; runtime dependencies belong to the image.
 +  def commands = build_environment() + '''
 +    rm -rf build/wheels build/wheel-build-deps
 +    uv pip install --python python3 --target build/wheel-build-deps 
"scikit-build-core>=0.11" "setuptools-scm>=8" "mlc-z3-static>=4.16.0"
 +    PYTHONPATH="$PWD/build/wheel-build-deps${PYTHONPATH:+:$PYTHONPATH}" \
 +    uv build --wheel --no-build-isolation --offline --python python3 
--out-dir build/wheels . \
 +      -Cbuild-dir=build -Ccmake.build-type=RelWithDebInfo 
-Cbuild.verbose="${VERBOSE:-true}"
 +  '''.stripIndent().trim()
 +  if (cpp_tests) {
 +    commands += " -Ccmake.define.USE_GTEST=ON -Cbuild.targets=all\\;cpptest"
 +    commands += "\n./tests/scripts/task_cpp_unittest.sh build"
 +  }
 +  sh(script: build_docker() + "${image} bash -c '${commands}'",
 +     label: 'Build TVM wheel' + (cpp_tests ? ' and run C++ tests' : ''))
 +}
 +
- // Preparation retries Exceptions unconditionally; Errors and cancellation of 
the
- // previous build are deliberately outside this fallback.
- def start() {
-   cancel_previous_build()
-   try {
-     prepare('CPU-SMALL-SPOT')
-   } catch (Exception ex) {
-     prepare('CPU-SMALL')
-   }
+ // Legacy compatibility functions. Keep names, signatures and behavior until
+ // existing pipeline callers have migrated after merge.
+ 
+ def python_unittest(image) {
+   sh (
+     script: "${docker_run} ${image} ./tests/scripts/task_python_unittest.sh",
+     label: 'Run Python unit tests',
+   )
+ }
+ 
+ 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',
+   )
+ }
+ 
+ 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) {
+   sh (
+     script: "${docker_run} --env CI_NUM_EXECUTORS ${image} 
./tests/scripts/task_cpp_unittest.sh",
+     label: 'Run C++ tests',
+   )
  }
  
  // A failed build command is final. Infrastructure failures retry only while
diff --cc ci/jenkins/generated/cpu_jenkinsfile.groovy
index b064c89455,881cd2cedc..72ed6e4e6f
--- a/ci/jenkins/generated/cpu_jenkinsfile.groovy
+++ b/ci/jenkins/generated/cpu_jenkinsfile.groovy
@@@ -19,13 -19,14 +19,15 @@@
  ci = evaluate(readTrusted('ci/jenkins/generated/common.groovy'))
  ci.start()
  
++docker_run += ' --env TVM_TEST_USE_WHEEL=1'
++
  ci.build('CPU', 'BUILD: CPU', 'tvm/build-cpu', images.ci_cpu, 'cpu') {
    sh(
      script: "${docker_run} ${images.ci_cpu} 
./tests/scripts/task_config_build_cpu.sh build",
      label: 'Create CPU CMake config',
    )
 -  ci.cmake_build(images.ci_cpu, 'build')
 -  ci.make_cpp_tests(images.ci_cpu, 'build')
 -  ci.upload('cpu', '--artifacts tvm_lib --artifacts cpptest')
 +  ci.wheel_build(images.ci_cpu, true)
-   ci.upload('cpu', '--bundle wheels')
++  ci.upload('cpu', '--artifacts wheels')
  }
  
  stage('Test') {
diff --cc ci/jenkins/generated/gpu_jenkinsfile.groovy
index df4992f7d2,98c04b332e..b5afbe0b42
--- a/ci/jenkins/generated/gpu_jenkinsfile.groovy
+++ b/ci/jenkins/generated/gpu_jenkinsfile.groovy
@@@ -19,10 -19,10 +19,12 @@@
  ci = evaluate(readTrusted('ci/jenkins/generated/common.groovy'))
  ci.start()
  
++docker_run += ' --env TVM_TEST_USE_WHEEL=1'
++
  ci.build('CPU', 'BUILD: GPU', 'tvm/build-gpu', images.ci_gpu, 'gpu') {
    sh "${docker_run} --no-gpu ${images.ci_gpu} 
./tests/scripts/task_config_build_gpu.sh build"
 -  ci.cmake_build("${images.ci_gpu} --no-gpu", 'build')
 -  ci.upload('gpu', '--artifacts tvm_lib --artifacts tvm_lib_gpu_extra')
 +  ci.wheel_build("--no-gpu ${images.ci_gpu}")
-   ci.upload('gpu', '--bundle wheels --bundle jvm_runtime')
++  ci.upload('gpu', '--artifacts wheels --artifacts jvm_runtime')
  
    // Check the compiler-only GPU configuration after publishing the test 
build.
    sh 'rm -rf build'
diff --cc ci/jenkins/generated/wasm_jenkinsfile.groovy
index 289ac8dba8,ddf932529d..f514772dda
--- a/ci/jenkins/generated/wasm_jenkinsfile.groovy
+++ b/ci/jenkins/generated/wasm_jenkinsfile.groovy
@@@ -19,6 -19,6 +19,8 @@@
  ci = evaluate(readTrusted('ci/jenkins/generated/common.groovy'))
  ci.start()
  
++docker_run += ' --env TVM_TEST_USE_WHEEL=1'
++
  ci.build('CPU', 'BUILD: WASM', 'tvm/build-wasm', images.ci_wasm, 'wasm') {
    sh(
      script: "${docker_run} ${images.ci_wasm} 
./tests/scripts/task_config_build_wasm.sh build",
diff --cc tests/python/ci/test_jenkins_helpers.py
index cfc0c69ced,d6653f8198..39428aad66
--- a/tests/python/ci/test_jenkins_helpers.py
+++ b/tests/python/ci/test_jenkins_helpers.py
@@@ -137,3 -135,178 +135,184 @@@ with pathlib.Path(os.environ["COMMAND_L
      assert build[0][4].startswith("-j") and int(build[0][4][2:]) >= 1
      assert len(build[0]) == 5
      assert build[1:] == configure[1:]
+ 
+ 
+ @pytest.fixture
+ def artifact_cli(tmp_path):
+     script_root = tmp_path / "ci/jenkins/scripts"
+     script_root.mkdir(parents=True)
+     for name in ("s3.py", "retry.sh"):
+         shutil.copy(JENKINS_SCRIPT_ROOT / name, script_root / name)
+     registry = {
+         "wheels": ["build/dist/*.whl", "build/dist/a.whl", 
"./build/dist/a.whl"],
+         "extra": ["build/exact.so", "build/dir", "build/missing.so", 
"build/no-match/*.so"],
+     }
+     (script_root.parent / "artifacts.py").write_text("ARTIFACTS = " + 
repr(registry))
+     for name in (
+         "build/dist/b space.whl",
+         "build/dist/a.whl",
+         "build/exact.so",
+         "build/dir/nested",
+     ):
+         path = tmp_path / name
+         path.parent.mkdir(parents=True, exist_ok=True)
+         path.write_text("artifact")
+     binary_dir = tmp_path / "bin"
+     binary_dir.mkdir()
+     executable(
+         binary_dir / "aws",
+         """
+ import json, os, pathlib, sys
+ with pathlib.Path(os.environ["AWS_LOG"]).open("a") as out:
+     out.write(json.dumps(sys.argv[1:]) + "\\n")
+ if sys.argv[-1] == ".":
+     pathlib.Path("downloaded file").write_text("downloaded")
+     print("download: s3://bucket/prefix/downloaded file to downloaded file")
+ """,
+     )
+     log = tmp_path / "commands"
+     env = dict(os.environ, PATH=f"{binary_dir}:{os.environ['PATH']}", 
AWS_LOG=str(log))
+ 
+     def run(*options, action="upload"):
+         log.unlink(missing_ok=True)
+         result = subprocess.run(
+             [
+                 sys.executable,
+                 script_root / "s3.py",
+                 "--action",
+                 action,
+                 "--bucket",
+                 "bucket",
+                 "--prefix",
+                 "/prefix/",
+                 *options,
+             ],
+             cwd=tmp_path,
+             env=env,
+             text=True,
+             capture_output=True,
+         )
+         commands = (
+             [json.loads(line) for line in log.read_text().splitlines()] if 
log.exists() else []
+         )
+         return result, commands
+ 
+     return run
+ 
+ 
+ def test_artifact_upload_aliases_and_globs(artifact_cli):
+     expected = [
+         ["s3", "cp", "--no-progress", "--recursive", "build/dir", 
"s3://bucket/prefix/build/dir"],
+         ["s3", "cp", "--no-progress", "build/dist/a.whl", 
"s3://bucket/prefix/build/dist/a.whl"],
+         [
+             "s3",
+             "cp",
+             "--no-progress",
+             "build/dist/b space.whl",
+             "s3://bucket/prefix/build/dist/b space.whl",
+         ],
+         ["s3", "cp", "--no-progress", "build/exact.so", 
"s3://bucket/prefix/build/exact.so"],
+     ]
+     for flags in (
+         ["--artifacts", "wheels", "extra"],
+         ["--artifacts", "extra", "--artifacts", "wheels"],
+         ["--bundle", "wheels", "--bundle", "extra"],
+         ["--bundle", "extra", "wheels"],
+     ):
+         result, commands = artifact_cli(*flags)
+         assert result.returncode == 0, result.stderr
+         assert commands == expected
+         assert "No artifacts matched pattern: build/no-match/*.so" in 
result.stderr
+         assert "The path doesn't exist: build/missing.so" in result.stderr
+ 
+ 
+ def test_artifact_paths_alias_and_unmatched_pattern(artifact_cli):
+     for flag in ("--paths", "--items"):
+         result, commands = artifact_cli(flag, "build/dist/*.whl", 
"./build/dist/a.whl")
+         assert result.returncode == 0, result.stderr
+         assert [command[-2] for command in commands] == [
+             "build/dist/a.whl",
+             "build/dist/b space.whl",
+         ]
+         result, commands = artifact_cli(flag, "absent/*.whl")
+         assert result.returncode == 0
+         assert commands == []
+         assert "No artifacts matched pattern: absent/*.whl" in result.stderr
+ 
+ 
+ @pytest.mark.parametrize("artifacts_flag", ["--artifacts", "--bundle"])
+ @pytest.mark.parametrize("paths_flag", ["--paths", "--items"])
+ def test_artifact_flags_are_mutually_exclusive(artifact_cli, artifacts_flag, 
paths_flag):
+     result, commands = artifact_cli(artifacts_flag, "wheels", paths_flag, 
"build/exact.so")
+     assert result.returncode == 2
+     assert "not allowed with argument" in result.stderr
+     assert commands == []
+ 
+ 
+ def test_artifact_download_copies_whole_prefix(artifact_cli, tmp_path):
+     result, commands = artifact_cli(action="download")
+     assert result.returncode == 0, result.stderr
+     assert commands == [["s3", "cp", "--no-progress", "--recursive", 
"s3://bucket/prefix", "."]]
+     downloaded = tmp_path / "downloaded file"
+     assert downloaded.read_text() == "downloaded"
+     assert downloaded.stat().st_mode & 0o111
+ 
+ 
+ @pytest.mark.parametrize("absolute", [False, True])
+ def test_legacy_artifact_literal_paths(artifact_cli, tmp_path, absolute):
+     outside = tmp_path.parent / "outside.so"
+     outside.write_text("artifact")
+     path = str(outside) if absolute else "../outside.so"
+     result, commands = artifact_cli("--items", path)
+     assert result.returncode == 0, result.stderr
+     assert commands == [["s3", "cp", "--no-progress", path, 
"s3://bucket/prefix/" + path]]
+ 
+ 
+ def test_recursive_artifact_overlap(artifact_cli):
+     result, commands = artifact_cli("--paths", "build/dir", "build/dir/*", 
"build/dir/nested")
+     assert result.returncode == 0, result.stderr
+     assert commands == [
+         ["s3", "cp", "--no-progress", "--recursive", "build/dir", 
"s3://bucket/prefix/build/dir"]
+     ]
+ 
+ 
+ def test_artifact_empty_directory_and_unknown_name(artifact_cli, tmp_path):
+     (tmp_path / "empty").mkdir()
+     result, commands = artifact_cli("--paths", "empty")
+     assert result.returncode != 0
+     assert "Cannot upload empty folder" in result.stderr
+     assert commands == []
+     result, commands = artifact_cli("--artifacts", "unknown")
+     assert result.returncode != 0
+     assert "Unknown artifact" in result.stderr
+     assert commands == []
+ 
+ 
+ def test_current_pipeline_artifact_options(artifact_cli, tmp_path):
+     # c14bc80 passes repeated --bundle names; the new Jenkinsfiles use 
--artifacts.
+     registry = load_s3().load_artifacts()
+     (tmp_path / "ci/jenkins/artifacts.py").write_text("ARTIFACTS = " + 
repr(registry))
+     for entries in registry.values():
+         for item in entries:
+             path = tmp_path / item
+             path.parent.mkdir(parents=True, exist_ok=True)
 -            path.write_text("artifact")
++            if item == "build/wheels":
++                path.mkdir(exist_ok=True)
++                (path / "apache_tvm.whl").write_text("wheel")
++            else:
++                path.write_text("artifact")
+     for names in (
+         ["tvm_lib", "cpptest"],
+         ["tvm_lib", "tvm_lib_gpu_extra", "cpptest"],
+         ["tvm_runtime"],
++        ["wheels"],
++        ["wheels", "jvm_runtime"],
+     ):
+         old = [option for name in names for option in ("--bundle", name)]
+         old_result, old_commands = artifact_cli(*old)
+         new_result, new_commands = artifact_cli("--artifacts", *names)
+         assert old_result.returncode == new_result.returncode == 0
+         assert old_commands == new_commands
+         assert [command[-2] for command in new_commands] == sorted(
+             {item for name in names for item in registry[name]}
+         )
diff --cc tests/python/ci/test_wheel_artifacts.py
index fa707f15d9,0000000000..a8adeef068
mode 100644,000000..100644
--- a/tests/python/ci/test_wheel_artifacts.py
+++ b/tests/python/ci/test_wheel_artifacts.py
@@@ -1,309 -1,0 +1,309 @@@
 +# 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 ast
 +import importlib.util
 +import json
 +import os
 +import re
 +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 not any("./3rdparty/tvm-ffi" in script for script in scripts)
 +    assert "--no-build-isolation --offline" in wheel
 +    assert any("task_python_install_wheels.sh" in script for script in 
scripts)
 +    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/jenkins/scripts/s3.py",
 +        "ci/jenkins/scripts/retry.sh",
-         "ci/jenkins/data.py",
++        "ci/jenkins/artifacts.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")
 +    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/jenkins/scripts/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
 +
 +
 [email protected](
 +    "host_limit,caller_limit,expected", [(None, None, "2"), ("7", None, "7"), 
("7", "9", "9")]
 +)
 +def test_local_parallel_limit_crosses_docker_boundary(
 +    tmp_path, monkeypatch, host_limit, caller_limit, expected
 +):
 +    ci = load_local_ci()
 +    calls = []
 +    monkeypatch.setattr(ci, "check_docker", lambda: None)
 +    monkeypatch.setattr(ci, "cmd", lambda command: calls.append(command))
 +    monkeypatch.setattr(ci, "SCRIPT_DIR", tmp_path / "scripts")
 +    monkeypatch.setattr(ci, "REPO_ROOT", tmp_path)
 +    monkeypatch.setattr(ci, "NPROC", 8)
 +    monkeypatch.setenv("CI_NUM_EXECUTORS", "3")
 +    if host_limit is None:
 +        monkeypatch.delenv("CMAKE_BUILD_PARALLEL_LEVEL", raising=False)
 +    else:
 +        monkeypatch.setenv("CMAKE_BUILD_PARALLEL_LEVEL", host_limit)
 +    env = {} if caller_limit is None else {"CMAKE_BUILD_PARALLEL_LEVEL": 
caller_limit}
 +    ci.docker("build", "ci_cpu", ["true"], env, False)
 +    args = calls[0]
 +    index = args.index(f"CMAKE_BUILD_PARALLEL_LEVEL={expected}")
 +    assert args[index - 1] == "--env"
 +
 +
 [email protected](
 +    "script", ["task_python_unittest.sh", "task_python_docs.sh", 
"task_web_wasm.sh"]
 +)
 [email protected]("wheel_mode", [False, True])
 +def test_shared_scripts_preserve_legacy_and_wheel_modes(tmp_path, script, 
wheel_mode):
 +    scripts = tmp_path / "tests/scripts"
 +    scripts.mkdir(parents=True)
 +    shutil.copy(ROOT / "tests/scripts" / script, scripts / script)
 +    # Substitute only the install boundary; execute the real caller's mode 
routing.
 +    (scripts / "task_python_install_wheels.sh").write_text('echo WHEEL_SETUP 
>> "$CALL_LOG"\n')
 +    for directory in ["docs", "web", "bin"]:
 +        (tmp_path / directory).mkdir()
 +    for tool in ["uv", "python3", "make"]:
 +        binary = tmp_path / "bin" / tool
 +        binary.write_text(
 +            f'#!/bin/sh\necho "{tool}:$*" >> "$CALL_LOG"\n'
 +            + ("exit 77\n" if tool == "make" else "")
 +        )
 +        binary.chmod(0o755)
 +    result = subprocess.run(
 +        [shutil.which("bash"), str(scripts / script)],
 +        cwd=tmp_path,
 +        env={
 +            **os.environ,
 +            "PATH": f"{tmp_path / 'bin'}:{os.environ['PATH']}",
 +            "CALL_LOG": str(tmp_path / "calls"),
 +            "TVM_TEST_USE_WHEEL": "1" if wheel_mode else "0",
 +            "IS_LOCAL": "0",
 +        },
 +        capture_output=True,
 +        text=True,
 +    )
 +    assert result.returncode == (0 if script == "task_python_unittest.sh" 
else 77), result.stderr
 +    calls = (tmp_path / "calls").read_text().splitlines()
 +    assert ("WHEEL_SETUP" in calls) == wheel_mode
 +    dependency_calls = [call for call in calls if call.startswith("uv:")]
 +    if wheel_mode:
 +        assert not dependency_calls
 +    else:
 +        assert "uv:pip install -v --target=python ./3rdparty/tvm-ffi/" in 
dependency_calls
 +
 +
 +def test_wheel_mode_does_not_fall_back_when_install_fails(tmp_path):
 +    scripts = tmp_path / "tests/scripts"
 +    scripts.mkdir(parents=True)
 +    shutil.copy(ROOT / "tests/scripts/task_python_unittest.sh", scripts)
 +    (scripts / "task_python_install_wheels.sh").write_text("return 37\n")
 +    result = subprocess.run(
 +        [shutil.which("bash"), str(scripts / "task_python_unittest.sh")],
 +        cwd=tmp_path,
 +        env={**os.environ, "TVM_TEST_USE_WHEEL": "1"},
 +        capture_output=True,
 +        text=True,
 +    )
 +    assert result.returncode == 37
 +    assert "uv pip install" not in result.stderr
 +
 +
 [email protected]("bootstrap_fails", [False, True])
 +def test_build_bootstrap_precedes_offline_build(tmp_path, monkeypatch, 
bootstrap_fails):
 +    import shlex
 +
 +    ci = load_local_ci()
 +    calls = []
 +    monkeypatch.setattr(ci, "docker", lambda **kwargs: calls.append(kwargs))
 +    command, _, _ = ci.generate_command("cpu", {}, "wheel build")
 +    command()
 +    commands = [
 +        script
 +        for script in calls[0]["scripts"]
 +        if "uv pip install" in script or "uv build" in script
 +    ]
 +    bootstrap, build = commands
 +    arguments = shlex.split(bootstrap)
 +    # The declared build requirements are a flat string list; keep this test 
usable on Python 3.10.
 +    requires = re.search(r"(?ms)^requires = (\[.*?\])", (ROOT / 
"pyproject.toml").read_text())
 +    expected = ast.literal_eval(requires[1])
 +    assert arguments[:5] == ["uv", "pip", "install", "--python", "python3"]
 +    assert arguments[5] == "--target"
 +    assert arguments[7:] == expected
 +    assert "--no-build-isolation --offline --python python3" in build
 +    assert 
build.startswith(f'PYTHONPATH="$PWD/{arguments[6]}${{PYTHONPATH:+:$PYTHONPATH}}"
 ')
 +    binary = tmp_path / "uv"
 +    binary.write_text(
 +        f"#!{sys.executable}\n"
 +        "import json, os, pathlib, sys\n"
 +        "with pathlib.Path('calls').open('a') as out: 
out.write(json.dumps(sys.argv[1:]) + '\\n')\n"
 +        "with pathlib.Path('paths').open('a') as out: 
out.write(os.environ['PYTHONPATH'] + '\\n')\n"
 +        f"sys.exit(17 if {bootstrap_fails!r} and sys.argv[1] == 'pip' else 
0)\n"
 +    )
 +    binary.chmod(0o755)
 +    result = subprocess.run(
 +        [
 +            shutil.which("bash"),
 +            "-ec",
 +            "\n".join([*commands, 'printf "%s" "$PYTHONPATH" > after-path']),
 +        ],
 +        cwd=tmp_path,
 +        env={
 +            **os.environ,
 +            "PATH": f"{tmp_path}:{os.environ['PATH']}",
 +            "PYTHONPATH": "image-marker",
 +        },
 +        capture_output=True,
 +        text=True,
 +    )
 +    assert result.returncode == (17 if bootstrap_fails else 0)
 +    recorded = [json.loads(line) for line in (tmp_path / 
"calls").read_text().splitlines()]
 +    assert [call[0] for call in recorded] == (["pip"] if bootstrap_fails else 
["pip", "build"])
 +    paths = (tmp_path / "paths").read_text().splitlines()
 +    assert paths[0] == "image-marker"
 +    if not bootstrap_fails:
 +        assert paths[1] == f"{tmp_path}/{arguments[6]}:image-marker"
 +        assert (tmp_path / "after-path").read_text() == "image-marker"
 +    else:
 +        assert not (tmp_path / "after-path").exists()

Reply via email to