diff --git a/WORKSPACE b/WORKSPACE index 41a4b4f06c..356b14efe3 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -174,17 +174,17 @@ http_archive( ################################### Official/forked mediapipe repository ######### #### Will be used on feature release -#local_repository( -# name = "mediapipe", -# path = "C:\\git\\mediapipe", -#) +local_repository( + name = "mediapipe", + path = "C:\\git\\mediapipe", +) # Remote fallback for non-local development: -git_repository( - name = "mediapipe", - remote = "https://github.com/openvinotoolkit/mediapipe", - commit = "ec9f5b3e5e765097f8fd9622f2e5d0c037351173", # Fix capi ovms handle ownership - code review1 -) +#git_repository( +# name = "mediapipe", +# remote = "https://github.com/openvinotoolkit/mediapipe", +# commit = "ec9f5b3e5e765097f8fd9622f2e5d0c037351173", # Fix capi ovms handle ownership - code review1 +#) # Protobuf for Node dependencies http_archive( diff --git a/demos/python_demos/clip_image_classification/servable/config.json b/demos/python_demos/clip_image_classification/servable/config.json index a9b68fe720..bccd7babd7 100644 --- a/demos/python_demos/clip_image_classification/servable/config.json +++ b/demos/python_demos/clip_image_classification/servable/config.json @@ -3,13 +3,14 @@ { "config": { "name": "clip", - "base_path": "/model/" + "base_path": "C:\\git\\model_server\\demos\\python_demos\\clip_image_classification\\servable\\model", + "target_device": "CPU" } }], "mediapipe_config_list": [ { "name":"python_model", - "graph_path":"/workspace/graph.pbtxt" + "graph_path":"C:\\git\\model_server\\demos\\python_demos\\clip_image_classification\\servable\\graph.pbtxt" } ] } diff --git a/demos/python_demos/clip_image_classification/servable/graph.pbtxt b/demos/python_demos/clip_image_classification/servable/graph.pbtxt index f6385d7558..f5031fc97d 100755 --- a/demos/python_demos/clip_image_classification/servable/graph.pbtxt +++ b/demos/python_demos/clip_image_classification/servable/graph.pbtxt @@ -28,7 +28,7 @@ node { output_stream: "PIXEL_VALUES_PY:pixel_values_py" node_options: { [type.googleapis.com/mediapipe.PythonExecutorCalculatorOptions]: { - handler_path: "/workspace/preprocess.py" + handler_path: "C:\\git\\model_server\\demos\\python_demos\\clip_image_classification\\servable\\preprocess.py" } } } @@ -118,7 +118,7 @@ node { output_stream: "LABEL:output_label" node_options: { [type.googleapis.com/mediapipe.PythonExecutorCalculatorOptions]: { - handler_path: "/workspace/postprocess.py" + handler_path: "C:\\git\\model_server\\demos\\python_demos\\clip_image_classification\\servable\\postprocess.py" } } } \ No newline at end of file diff --git a/demos/python_demos/clip_image_classification/servable/postprocess.py b/demos/python_demos/clip_image_classification/servable/postprocess.py index 4ca0ce8419..0e798d819d 100644 --- a/demos/python_demos/clip_image_classification/servable/postprocess.py +++ b/demos/python_demos/clip_image_classification/servable/postprocess.py @@ -16,9 +16,14 @@ from pyovms import Tensor import numpy as np -from scipy.special import softmax from tritonclient.utils import deserialize_bytes_tensor + +def softmax(x, axis=None): + x_max = np.max(x, axis=axis, keepdims=True) + e_x = np.exp(x - x_max) + return e_x / np.sum(e_x, axis=axis, keepdims=True) + class OvmsPythonModel: def initialize(self, kwargs: dict): diff --git a/demos/python_demos/clip_image_classification/servable/preprocess.py b/demos/python_demos/clip_image_classification/servable/preprocess.py index 03c0a7f61f..99c6645a3d 100644 --- a/demos/python_demos/clip_image_classification/servable/preprocess.py +++ b/demos/python_demos/clip_image_classification/servable/preprocess.py @@ -15,7 +15,6 @@ #***************************************************************************** from pyovms import Tensor -from transformers import CLIPProcessor from PIL import Image import numpy as np from io import BytesIO @@ -24,6 +23,10 @@ class OvmsPythonModel: def initialize(self, kwargs: dict): + try: + from transformers import CLIPProcessor + except ImportError: + from transformers.models.clip.processing_clip import CLIPProcessor model_id = "openai/clip-vit-base-patch16" self.processor = CLIPProcessor.from_pretrained(model_id) diff --git a/docs/windows_developer_guide.md b/docs/windows_developer_guide.md index 57aebd923b..4a49a6044e 100644 --- a/docs/windows_developer_guide.md +++ b/docs/windows_developer_guide.md @@ -75,14 +75,19 @@ This default command compiles ovms.exe without python dependencies, just C++ bin windows_build.bat ``` -Optionally, you add parameter to the windows_build.bat script +Optionally, you add parameters to the windows_build.bat script ```bat -windows_build.bat my_dir_on_c --with_python --with_tests --integrity +windows_build.bat my_dir_on_c --with_python 3.13.1 --with_tests --integrity ``` [arg1] This way you can change default dependency location directory to c:\my_dir_on_c [arg2] --with_python - this will build the ovms.exe with python dependency and support for python chat templates for GENAI LLM -[arg3] --with_tests - this will also build ovms_test.exe target -[arg4] --integrity - Add the compilation integrity flag to 0 or 1 - set the additional integritycheck compilation flag when compiling dependencies +[arg3] additional Python version (e.g. `3.13.1`) - builds a second set of Python runtime libraries (`libovmspython`, `libpython_calculators`, `pyovms`) linked against the specified Python ABI, on top of the default Python 3.12 ABI. This enables serving Python nodes from virtualenvs created with that Python version. Requires a full Python development install (with headers and `pythonXYZ.dll`) at `C:\opt\Python` (e.g. `C:\opt\Python313`). +[arg4] --with_tests - this will also build ovms_test.exe target +[arg5] --integrity - set the additional integritycheck compilation flag + +> **Note:** When arg5 is provided, the build performs three Bazel invocations: the main build (cp312 default), the extra-ABI build (e.g. cp313), and a restore build that returns the bazel-bin artifacts to cp312 linkage so that packaging picks up the correct default DLLs. + +The staged extra-ABI libraries are placed in `dist\windows\python_abi_addons\cp\` and are automatically picked up by `windows_create_package.bat --with_python`. # Running unit tests - optional The script compiles ovms_test binary with C++ only, downloads and converts test LLM models (src\tests\llm_testing). @@ -92,8 +97,12 @@ windows_test.bat The optional script compiles ovms_test binary with python support, downloads and converts test LLM models (src\tests\llm_testing) and installs Python torch and optimum. ```bat -windows_test.bat opt --with_python +windows_test.bat opt --with_python 3.13.1 ``` +[arg1] This way you can change default dependency location directory to c:\my_dir_on_c +[arg2] --with_python - compile and run tests with Python support +[arg3] optional additional Python ABI version (e.g. `3.13.1`) - sets `OVMS_PYTHON_ABI` so the Python runtime tests exercise the versioned loader path (cp313 DLLs). Requires the dev Python install at `C:\opt\Python313`. +[arg4] optional gtest filter (default `*`) # Creating deployment package This step prepares ovms.zip deployment package from the build artifacts in the dist\windows\ directory. Run this script after successful compilation. @@ -102,11 +111,30 @@ The default version creates C++ only version without Python dependency. windows_create_package.bat ``` -Optionally you can create a package with Python dependency. Note that to create valid package with Python, you need to build using `--with_python` flag in the previous step as well. +Optionally you can create a package with Python dependency. Note that to create a valid package with Python, you need to build using the `--with_python` flag in the previous step as well. ```bat windows_create_package.bat opt --with_python ``` +The package includes the default Python 3.12 embedded runtime and its libraries: +- `libovmspython.dll` / `libovmspython-cp312.dll` — Python runtime loader (cp312 fallback / cp312 explicit) +- `libpython_calculators.dll` / `libpython_calculators-cp312.dll` — MediaPipe Python calculator plugin +- `python\pyovms.pyd` and `python\cp312\pyovms.pyd` — Python binding module + +If arg5 was passed to `windows_build.bat` (e.g. `3.13.1`), the additional ABI libraries are also included: +- `libovmspython-cp313.dll`, `libpython_calculators-cp313.dll` +- `python\cp313\pyovms.pyd` + +**Selecting the active Python ABI at runtime:** `ovms.exe` detects the ABI from the `PYTHONHOME` environment variable. When started via `setupvars.bat`, `PYTHONHOME` points to the bundled `python\` directory (no version digits), so the unversioned fallback DLLs are used (cp312). To use a different ABI — e.g. when serving Python nodes from a cp313 virtualenv — set the environment before starting `ovms.exe`: +```bat +set PYTHONHOME=C:\Program Files\Python313 +set PYTHONPATH=\Lib\site-packages;\python\cp313 +``` +Or use the explicit override to bypass auto-detection: +```bat +set OVMS_PYTHON_ABI=313 +``` + # Test the Deployment You can follow the [baremetal deployment guide](deploying_server_baremetal.md) for information how to deploy and use the ovms.zip package. diff --git a/src/BUILD b/src/BUILD index c57f1e779e..058ac990dc 100644 --- a/src/BUILD +++ b/src/BUILD @@ -169,6 +169,7 @@ cc_binary( "//src/mediapipe_internal:libovms_mediapipe_runtime_owner", "//src/mediapipe_internal:libovms_mediapipe_runtime_c_api_exports", ":libovms_version_impl", + ":ovms_lib", ], linkopts = select({ "//conditions:default": ["-Wl,--allow-shlib-undefined"], @@ -713,9 +714,11 @@ ovms_cc_library( "kfs_python_tensor_bridge_hdrs", "mediapipe_executor_interface", "libovms_execution_context", + "libovms_module", "libovmsstatus", "libovmslogging", "libovms_servable_definition", + "python_runtime_version", "//src/utils:newline_delimited", ], visibility = ["//visibility:public"], @@ -871,6 +874,13 @@ ovms_cc_library( visibility = ["//visibility:public"], ) +ovms_cc_library( + name = "python_runtime_version", + hdrs = ["python_runtime_version.hpp"], + srcs = ["python_runtime_version.cpp"], + visibility = ["//visibility:public"], +) + ovms_cc_library( name = "python_calculators_plugin_loader", hdrs = ["python_calculators_plugin_loader.hpp"], @@ -880,6 +890,7 @@ ovms_cc_library( "libovmslogging", "libovmsstatus", "kfs_python_tensor_bridge", + "python_runtime_version", ], visibility = ["//visibility:public"], ) diff --git a/src/llm/BUILD b/src/llm/BUILD index a8ffbd8f00..bc1be858ed 100644 --- a/src/llm/BUILD +++ b/src/llm/BUILD @@ -643,6 +643,7 @@ ovms_cc_library( ], deps = [ "//src:libovmslogging", + "//src:python_runtime_version", ], visibility = ["//visibility:public"], ) diff --git a/src/llm/runtime_chat_template_runtime_loader.cpp b/src/llm/runtime_chat_template_runtime_loader.cpp index 36d6968967..ccde820fee 100644 --- a/src/llm/runtime_chat_template_runtime_loader.cpp +++ b/src/llm/runtime_chat_template_runtime_loader.cpp @@ -28,6 +28,7 @@ #endif #include "../logging.hpp" +#include "../python_runtime_version.hpp" namespace ovms { namespace { @@ -107,7 +108,7 @@ std::vector buildCandidates() { "./bazel-bin/src/python/libovmspython.so", }); #endif - return candidates; + return withAbiVersionedCandidates(candidates); } void initializeRuntimeState() { diff --git a/src/mediapipe_runtime_api.cpp b/src/mediapipe_runtime_api.cpp index 0c45d1d3f6..d84354fbc6 100644 --- a/src/mediapipe_runtime_api.cpp +++ b/src/mediapipe_runtime_api.cpp @@ -34,7 +34,9 @@ #include "logging.hpp" #include "kfs_python_tensor_bridge.hpp" +#include "python_runtime_version.hpp" #include "mediapipe_graph_executor_interface.hpp" +#include "server.hpp" #include "utils/newline_delimited.hpp" struct OVMS_Server_; @@ -325,10 +327,26 @@ MediapipeRuntimeApi::MediapipeRuntimeApi(PythonBackend* pythonBackend) : "bazel-bin\\src\\ovms_mediapipe_runtime_shared.dll", ".\\bazel-bin\\src\\ovms_mediapipe_runtime_shared.dll"}; + char executablePathBuf[MAX_PATH] = {0}; + DWORD executablePathBufLength = GetModuleFileNameA(nullptr, executablePathBuf, MAX_PATH); + if (executablePathBufLength > 0 && executablePathBufLength < MAX_PATH) { + std::string exePath(executablePathBuf, executablePathBufLength); + std::string exeDir = "."; + size_t separatorPos = exePath.find_last_of("\\/"); + if (separatorPos != std::string::npos) { + exeDir = exePath.substr(0, separatorPos); + } + candidates.emplace_back(exeDir + "\\ovms_mediapipe_runtime_shared.dll"); + candidates.emplace_back(exeDir + "\\src\\ovms_mediapipe_runtime_shared.dll"); + } + + candidates = withAbiVersionedCandidates(candidates); + for (const auto& candidate : candidates) { api->handle = LoadLibraryA(candidate.c_str()); if (api->handle != nullptr) { - SPDLOG_TRACE("MediaPipe runtime API loaded from: {}", candidate); + SPDLOG_INFO("MediaPipe runtime API loaded from: {}", candidate); + SPDLOG_INFO("MediaPipe runtime shared module handle = {}", fmt::ptr(api->handle)); break; } } @@ -389,14 +407,18 @@ MediapipeRuntimeApi::MediapipeRuntimeApi(PythonBackend* pythonBackend) : } if (api->setExternalServerHandle != nullptr) { - OVMS_Server* serverHandle = nullptr; - auto* status = OVMS_ServerNew(&serverHandle); - if (status != nullptr) { - OVMS_StatusDelete(status); - SPDLOG_WARN("Failed to obtain OVMS server handle for MediaPipe runtime-shared"); - } else { - api->setExternalServerHandle(static_cast(serverHandle)); - } + // The MediaPipe runtime-shared library is loaded as a separate DLL and has its + // own static/global state. Creating a fresh OVMS_Server inside this runtime would + // not point at the live server instance used by the main process; bind directly to + // the main process singleton instead. + auto* serverHandle = static_cast(&ovms::Server::instance()); + SPDLOG_INFO("MediaPipe runtime-shared is setting external OVMS server handle: live singleton ptr={} (this={})", + fmt::ptr(serverHandle), + fmt::ptr(&ovms::Server::instance())); + api->setExternalServerHandle(serverHandle); + SPDLOG_INFO("MediaPipe runtime-shared bound to main OVMS server singleton"); + } else { + SPDLOG_WARN("MediaPipe runtime-shared does not export OVMS_MPSetExternalServerHandle; it will keep creating a local server handle inside the DLL"); } } diff --git a/src/python/BUILD b/src/python/BUILD index f3cee5db8f..8532548772 100644 --- a/src/python/BUILD +++ b/src/python/BUILD @@ -494,6 +494,7 @@ ovms_cc_library( "//src:libovmslogging", "//src:libovmsstatus", "//src:libovms_module", + "//src:python_runtime_version", ], visibility = ["//visibility:public"], ) diff --git a/src/python/python_runtime_loader.cpp b/src/python/python_runtime_loader.cpp index 1d4d1ddb05..866e5ab04c 100644 --- a/src/python/python_runtime_loader.cpp +++ b/src/python/python_runtime_loader.cpp @@ -29,6 +29,7 @@ using PythonLibraryHandle = HMODULE; #include "../logging.hpp" #include "../module.hpp" +#include "../python_runtime_version.hpp" namespace ovms { @@ -96,6 +97,7 @@ Module* ensurePythonRuntimeLoaded() { "./src/python/libovmspython.so", "bazel-bin/src/python/libovmspython.so", "./bazel-bin/src/python/libovmspython.so"}; + candidates = withAbiVersionedCandidates(candidates); for (const auto& candidate : candidates) { pythonRuntimeHandle = dlopen(candidate.c_str(), RTLD_NOW | RTLD_GLOBAL); @@ -158,17 +160,28 @@ Module* ensurePythonRuntimeLoaded() { candidates.insert(candidates.end(), executableRelativeCandidates.begin(), executableRelativeCandidates.end()); candidates.insert(candidates.end(), runfilesCandidates.begin(), runfilesCandidates.end()); } + candidates = withAbiVersionedCandidates(candidates); + std::string triedCandidates; for (const auto& candidate : candidates) { pythonRuntimeHandle = LoadLibraryA(candidate.c_str()); if (pythonRuntimeHandle != nullptr) { break; } + if (!triedCandidates.empty()) + triedCandidates += ", "; + triedCandidates += candidate; } if (pythonRuntimeHandle == nullptr) { DWORD error = GetLastError(); - SPDLOG_WARN("Python runtime library libovmspython.dll failed to load: {} ({})", error, std::system_category().message(error)); + SPDLOG_WARN("Python runtime library libovmspython.dll failed to load. " + "Tried candidates: [{}]. " + "Ensure the OVMS package directory is on PATH and the correct ABI DLL exists next to ovms.exe. " + "Detected ABI tag: '{}'. Win32 error: {} ({})", + triedCandidates, + detectPythonAbiTag(), + error, std::system_category().message(error)); return nullptr; } createPythonInterpreterModuleFn = reinterpret_cast(GetProcAddress(pythonRuntimeHandle, "OVMS_createPythonInterpreterModule")); @@ -192,7 +205,19 @@ Module* ensurePythonRuntimeLoaded() { const char* pythonRuntimeValidationError = nullptr; if (!validatePythonEnvironmentFn(&pythonRuntimeValidationError)) { - SPDLOG_WARN("Python runtime environment validation failed. Ensure Python dependencies and PYTHONPATH are configured. Details: {}", + const std::string abiTag = detectPythonAbiTag(); + const char* pythonHome = std::getenv("PYTHONHOME"); + const char* pythonPath = std::getenv("PYTHONPATH"); + SPDLOG_WARN("Python runtime environment validation failed. " + "Detected ABI tag: '{}'. PYTHONHOME: '{}'. PYTHONPATH: '{}'. " + "Ensure the correct pyovms.pyd (e.g. python\\cp{}\\pyovms.pyd) is on PYTHONPATH " + "and that the Python installation directory (containing python{}.dll) is on PATH. " + "Details: {}", + abiTag.empty() ? "" : abiTag, + pythonHome != nullptr ? pythonHome : "", + pythonPath != nullptr ? pythonPath : "", + abiTag.empty() ? "312" : abiTag, + abiTag.empty() ? "312" : abiTag, pythonRuntimeValidationError != nullptr ? pythonRuntimeValidationError : "Unknown error"); createPythonInterpreterModuleFn = nullptr; validatePythonEnvironmentFn = nullptr; diff --git a/src/python_calculators_plugin_loader.cpp b/src/python_calculators_plugin_loader.cpp index 1f508ed257..4aeece9af5 100644 --- a/src/python_calculators_plugin_loader.cpp +++ b/src/python_calculators_plugin_loader.cpp @@ -16,6 +16,7 @@ #include "python_calculators_plugin_loader.hpp" #include "kfs_python_tensor_bridge.hpp" +#include "python_runtime_version.hpp" #include #include @@ -356,6 +357,8 @@ bool loadPythonCalculatorsPlugin() { } catch (...) { } + candidates = withAbiVersionedCandidates(candidates); + // CRITICAL: Expose main process symbols to plugin before loading it. // The plugin will link to a shared MediaPipe library that contains undefined // OVMS symbols (from geti calculators in the external MediaPipe fork). @@ -476,6 +479,8 @@ bool loadPythonCalculatorsPlugin() { candidates.insert(candidates.end(), runfilesCandidates.begin(), runfilesCandidates.end()); } + candidates = withAbiVersionedCandidates(candidates); + DWORD lastLoadError = ERROR_SUCCESS; for (const auto& candidate : candidates) { SetLastError(ERROR_SUCCESS); diff --git a/src/python_runtime_version.cpp b/src/python_runtime_version.cpp new file mode 100644 index 0000000000..22326c17cc --- /dev/null +++ b/src/python_runtime_version.cpp @@ -0,0 +1,95 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed 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. +//***************************************************************************** +#include "python_runtime_version.hpp" + +#include +#include +#include + +namespace ovms { + +namespace { + +// Collects a run of digits from the start of `text`, allowing '.' separators +// between digit groups (e.g. "3.13" -> "313", "313\\Lib" -> "313"). +std::string extractVersionDigits(const std::string& text) { + std::string digits; + bool sawDigit = false; + for (char c : text) { + if (std::isdigit(static_cast(c))) { + digits += c; + sawDigit = true; + } else if (c == '.' && sawDigit) { + continue; + } else { + break; + } + } + return digits; +} + +} // namespace + +std::string detectPythonAbiTag() { + if (const char* overrideTag = std::getenv("OVMS_PYTHON_ABI"); overrideTag != nullptr && overrideTag[0] != '\0') { + return std::string(overrideTag); + } + + if (const char* pythonHome = std::getenv("PYTHONHOME"); pythonHome != nullptr && pythonHome[0] != '\0') { + const std::string home(pythonHome); + std::string lowerHome(home); + for (char& c : lowerHome) { + c = static_cast(std::tolower(static_cast(c))); + } + const size_t markerPos = lowerHome.rfind("python"); + if (markerPos != std::string::npos) { + const std::string tail = home.substr(markerPos + 6); // skip past "python" + const std::string digits = extractVersionDigits(tail); + if (digits.size() >= 2) { + return digits; + } + } + } + + return std::string(); +} + +std::vector withAbiVersionedCandidates(const std::vector& baseCandidates) { + const std::string abiTag = detectPythonAbiTag(); + if (abiTag.empty()) { + return baseCandidates; + } + + std::vector versioned; + versioned.reserve(baseCandidates.size()); + for (const auto& candidatePath : baseCandidates) { + const std::filesystem::path original(candidatePath); + const std::filesystem::path parent = original.parent_path(); + const std::string versionedName = original.stem().string() + "-cp" + abiTag + original.extension().string(); + const std::filesystem::path versionedPath = parent.empty() ? std::filesystem::path(versionedName) : parent / versionedName; + versioned.push_back(versionedPath.string()); + } + + // Versioned candidates take priority; fall back to the unsuffixed names for + // backward compatibility with single-ABI builds/packages. + std::vector result; + result.reserve(versioned.size() + baseCandidates.size()); + result.insert(result.end(), versioned.begin(), versioned.end()); + result.insert(result.end(), baseCandidates.begin(), baseCandidates.end()); + return result; +} + +} // namespace ovms diff --git a/src/python_runtime_version.hpp b/src/python_runtime_version.hpp new file mode 100644 index 0000000000..a14de66b53 --- /dev/null +++ b/src/python_runtime_version.hpp @@ -0,0 +1,44 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed 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. +//***************************************************************************** +#pragma once + +#include +#include + +namespace ovms { + +// Detects the Python ABI tag (e.g. "312", "313") that should be used to select +// version-suffixed Python runtime libraries such as libovmspython-cp313.dll, +// libpython_calculators-cp313.dll or pyovms-cp313.pyd. +// +// Detection order: +// 1. OVMS_PYTHON_ABI environment variable override (e.g. "313"). +// 2. PYTHONHOME environment variable, parsed for a "python"/"Python" version +// marker (e.g. C:\opt\Python313 -> "313", /usr/lib/python3.13 -> "313"). +// +// Returns an empty string when no version marker could be determined; callers +// should then rely solely on the unsuffixed (version-agnostic) library names. +std::string detectPythonAbiTag(); + +// Given a list of candidate library paths (e.g. "libovmspython.dll", +// "src\\python\\libovmspython.dll"), returns a new list with version-suffixed +// variants (e.g. "libovmspython-cp313.dll") inserted ahead of each original +// entry when a Python ABI tag is detected via detectPythonAbiTag(). When no +// tag is detected, the original list is returned unchanged so single-ABI +// builds/packages keep working without modification. +std::vector withAbiVersionedCandidates(const std::vector& baseCandidates); + +} // namespace ovms diff --git a/src/version.hpp b/src/version.hpp index 57925cabf6..96d9490de5 100644 --- a/src/version.hpp +++ b/src/version.hpp @@ -16,8 +16,8 @@ #ifndef SRC_VERSION_HPP_ #define SRC_VERSION_HPP_ #define PROJECT_NAME "OpenVINO Model Server" -#define PROJECT_VERSION "REPLACE_PROJECT_VERSION" -#define BAZEL_BUILD_FLAGS "REPLACE_BAZEL_BUILD_FLAGS" +#define PROJECT_VERSION "2026.4.0.23505ad67" +#define BAZEL_BUILD_FLAGS "--config=win_mp_on_py_on" namespace ovms { const char* getOpenVINOVersion(); diff --git a/third_party/python/python_repo_win.bzl b/third_party/python/python_repo_win.bzl index 7a7bdbeb39..55af3ad0f5 100644 --- a/third_party/python/python_repo_win.bzl +++ b/third_party/python/python_repo_win.bzl @@ -15,8 +15,14 @@ # def _python_repository_impl(repository_ctx): - lib_path = "Python312" - version = "python312" + # OVMS_WINDOWS_PYTHON_VERSION selects which dev Python install under C:\\opt + # is used to build against (e.g. "312" -> C:\\opt\\Python312\\python312.dll). + # Pass it via `--repo_env=OVMS_WINDOWS_PYTHON_VERSION=313` to build the + # Python runtime libraries against a different ABI. Defaults to "312" to + # preserve existing behavior. + python_version_tag = repository_ctx.os.environ.get("OVMS_WINDOWS_PYTHON_VERSION", "312") + lib_path = "Python" + python_version_tag + version = "python" + python_version_tag build_file_content = """ cc_library( name = "python3-lib", @@ -29,6 +35,6 @@ cc_library( python_repository = repository_rule( implementation = _python_repository_impl, - environ = ["BASE_IMAGE"], + environ = ["BASE_IMAGE", "OVMS_WINDOWS_PYTHON_VERSION"], local=False, ) diff --git a/windows_build.bat b/windows_build.bat index 0722702c65..fcc034f51c 100644 --- a/windows_build.bat +++ b/windows_build.bat @@ -29,18 +29,32 @@ IF "%~2"=="--with_python" ( echo Building model server with Python set "bazelBuildArgs=--config=win_mp_on_py_on" set "pythonRuntimeTargets=//src/python:libpython_calculators //src/python:libovmspython" + set "additionalPythonAbi=" + + :: Accept a Python ABI in the direct form: windows_build.bat opt --with_python 3.13.1 + :: and in the legacy form: windows_build.bat opt --with_python --with_tests 3.13.1 + if not "%~3"=="" if /I not "%~3"=="--with_tests" if /I not "%~3"=="--integrity" set "additionalPythonAbi=%~3" + if not "%~4"=="" if /I not "%~4"=="--with_tests" if /I not "%~4"=="--integrity" if "!additionalPythonAbi!"=="" set "additionalPythonAbi=%~4" + if not "%~5"=="" if /I not "%~5"=="--with_tests" if /I not "%~5"=="--integrity" if "!additionalPythonAbi!"=="" set "additionalPythonAbi=%~5" ) ELSE ( echo Building model server without Python set "bazelBuildArgs=--config=win_mp_on_py_off" set "pythonRuntimeTargets=" + set "additionalPythonAbi=" ) -IF "%~3"=="--with_tests" ( +set "withTests=false" +if "%~3"=="--with_tests" set "withTests=true" +if "%~4"=="--with_tests" set "withTests=true" +if "%~5"=="--with_tests" set "withTests=true" + +set "buildTargets=//src:ovms" +if "!withTests!"=="true" set "buildTargets=!buildTargets! //src:ovms_test" +set "buildTargets=!buildTargets! //src:ovms_mediapipe_runtime_shared //third_party:espeak_ng //third_party:espeak_ng_data !pythonRuntimeTargets!" +if "!withTests!"=="true" ( echo Building model server with tests - set "buildTargets=//src:ovms //src:ovms_test //src:ovms_mediapipe_runtime_shared //third_party:espeak_ng //third_party:espeak_ng_data !pythonRuntimeTargets!" -) ELSE ( +) else ( echo Building model server without tests - set "buildTargets=//src:ovms //src:ovms_mediapipe_runtime_shared //third_party:espeak_ng //third_party:espeak_ng_data !pythonRuntimeTargets!" ) IF "%~4"=="--integrity" ( @@ -51,10 +65,19 @@ IF "%~4"=="--integrity" ( set "buildWithIntegrity=" ) -set "bazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!" +:: Optional Python ABI override: e.g. 3.13.1 or 3.13. Build the corresponding +:: libovmspython/libpython_calculators/pyovms + runtime shared library against a +:: dev Python install at C:\opt\Python. +set "additionalPythonAbi=!additionalPythonAbi!" + +:: Keep the default Python-enabled build isolated from any later extra-ABI build. +:: This avoids reusing a cached local_config_python repository from a different +:: Python minor version when the same workspace has been built for cp313 before. +set "mainBazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!" +if not "!pythonRuntimeTargets!"=="" set "mainBazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!_py312" set "openvino_dir=!BAZEL_SHORT_PATH!/openvino/runtime/cmake" -set "buildCommand=bazel %bazelStartupCmd% build %buildWithIntegrity% %bazelBuildArgs% --action_env OpenVINO_DIR=%openvino_dir% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures %buildTargets% 2>&1 | tee win_build.log" +set "buildCommand=bazel !mainBazelStartupCmd! build %buildWithIntegrity% %bazelBuildArgs% --action_env OpenVINO_DIR=%openvino_dir% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures %buildTargets% 2>&1 | tee win_build.log" set "setOvmsVersionCmd=python windows_set_ovms_version.py" :: Setting PATH environment variable based on default windows node settings: Added ovms_windows specific python settings and c:/opt and removed unused Nvidia and OCL specific tools. @@ -65,6 +88,19 @@ set "envPath=win_environment.log" set "setPythonPath=%cd%\bazel-out\x64_windows-opt\bin\src\python\binding" set "BAZEL_SH=C:\opt\msys64\usr\bin\bash.exe" +:: Remove stale MediaPipe runtime outputs before a build so a previously generated +:: default-output DLL cannot survive and win the DLL search order when the newer +:: Python ABI build is staged to the package directory. +for %%F in ( + "%cd%\bazel-bin\src\ovms_mediapipe_runtime_shared.dll" + "%cd%\bazel-bin\src\ovms_mediapipe_runtime_shared-cp313.dll" + "%cd%\bazel-out\x64_windows-opt\bin\src\ovms_mediapipe_runtime_shared.dll" + "%cd%\dist\windows\ovms\ovms_mediapipe_runtime_shared.dll" + "%cd%\dist\windows\ovms\ovms_mediapipe_runtime_shared-cp313.dll" +) do ( + if exist "%%~F" del /F /Q "%%~F" +) + :: Load chosen dependency versions from versions.mk for /f "usebackq eol=# tokens=1,3" %%A in ("%cd%\versions.mk") do ( if "%%A"=="OPENCV_VERSION" if "!opencv_version!"=="" set "opencv_version=%%B" @@ -105,10 +141,178 @@ if !errorlevel! neq 0 exit /b !errorlevel! :: Set ovms.exe --version parameters %setOvmsVersionCmd% "%bazelBuildArgs%" !BAZEL_SHORT_PATH! -:: Start bazel build +:: Start bazel build. Keep the root OVMS target explicit so the direct ABI form +:: (`windows_build.bat opt --with_python 3.13.1`) still rebuilds ovms.exe in addition +:: to the Python runtime artifacts. +echo [INFO] Building OVMS targets: !buildTargets! %buildCommand% if !errorlevel! neq 0 exit /b !errorlevel! +IF "!pythonRuntimeTargets!"=="" goto :skip_default_python_abi_stage + +:: Stage the default Python ABI outputs immediately so packaging can rely on the +:: copied artifacts instead of the mutable bazel-bin symlink, which may later +:: point at an extra-ABI build. +set "defaultPythonTag=312" +set "defaultStageDir=%cd%\dist\windows\python_abi_addons\cp!defaultPythonTag!" +if exist "!defaultStageDir!" rmdir /S /Q "!defaultStageDir!" +md "!defaultStageDir!" +if !errorlevel! neq 0 exit /b !errorlevel! + +set "defaultOutputBase=!BAZEL_SHORT_PATH!_py!defaultPythonTag!" +set "defaultBinDir=" +for /d %%d in ("!defaultOutputBase!\*") do ( + if exist "%%d\execroot\ovms\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll" ( + set "defaultBinDir=%%d\execroot\ovms\bazel-out\x64_windows-opt\bin" + ) +) +if "!defaultBinDir!"=="" ( + echo [ERROR] Could not locate default-ABI build artifacts under !defaultOutputBase! + exit /b 1 +) + +set "defaultLibOvmspython=!defaultBinDir!\src\python\libovmspython.dll" +set "defaultLibPyCalc=!defaultBinDir!\src\python\libpython_calculators.dll" +set "defaultPyovms=!defaultBinDir!\src\python\binding\pyovms.pyd" +set "defaultMpRuntime=!defaultBinDir!\src\ovms_mediapipe_runtime_shared.dll" + +copy "!defaultLibOvmspython!" "!defaultStageDir!\libovmspython-cp!defaultPythonTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!defaultLibPyCalc!" "!defaultStageDir!\libpython_calculators-cp!defaultPythonTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!defaultPyovms!" "!defaultStageDir!\pyovms.pyd" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!defaultMpRuntime!" "!defaultStageDir!\ovms_mediapipe_runtime_shared-cp!defaultPythonTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! + +echo [INFO] Staged default Python ABI cp!defaultPythonTag! runtime libraries in !defaultStageDir! + +:: NOTE: this section intentionally avoids wrapping everything in a single +:: parenthesized IF (...) block. Combining a nested "for /f ... do (...)" +:: loop and a piped "| tee" command invocation inside an outer parenthesized +:: block trips up cmd.exe's block pre-parser ("'.' was unexpected at this +:: time." / similar errors appearing only once execution reaches the block). +:: Using goto instead of nested parens sidesteps this entirely. +IF "!additionalPythonAbi!"=="" goto :skip_extra_python_abi +IF "!pythonRuntimeTargets!"=="" goto :skip_extra_python_abi + +echo Building additional Python runtime libraries for Python ABI: !additionalPythonAbi! +for /f "tokens=1,2 delims=." %%a in ("!additionalPythonAbi!") do ( + set "EXTRA_MAJOR_VER=%%a" + set "EXTRA_MINOR_VER=%%b" +) +set "extraPythonTag=!EXTRA_MAJOR_VER!!EXTRA_MINOR_VER!" +set "extraPythonHome=C:\opt\Python!extraPythonTag!" + +IF NOT EXIST "!extraPythonHome!\python!extraPythonTag!.dll" ( + echo [ERROR] Missing dev Python install for ABI !extraPythonTag!: !extraPythonHome!\python!extraPythonTag!.dll not found. + echo [ERROR] Install a full Python !additionalPythonAbi! development build to !extraPythonHome! before requesting this additional ABI build. + exit /b 1 +) + +:: IMPORTANT: two distinct Python configurations must both be overridden, or +:: pybind11-based targets will silently keep linking the primary ABI's DLL: +:: 1. --repo_env=OVMS_WINDOWS_PYTHON_VERSION selects our custom +:: @python3_windows repo (third_party/python/python_repo_win.bzl). +:: 2. --repo_env/--action_env=PYTHON_BIN_PATH + --python_path select the +:: @local_config_python repo (pybind11_bazel's python_configure), which +:: is what @pybind11//:pybind11_embed (and therefore libovmspython, +:: libpython_calculators, pyovms) actually links against for the +:: embedded interpreter. Skipping this leaves those targets linked to +:: the primary ABI's pythonXY.dll even though the build "succeeds". +:: PYTHONHOME/PATH must also be pointed at the extra Python install for +:: this invocation: local_config_python's repository rule spawns +:: "!extraPythonHome!\python.exe" to probe its version, and that process +:: inherits PYTHONHOME from the environment. windows_setupvars.bat already +:: forced PYTHONHOME to the primary ABI's install, which would make the +:: extra interpreter load the wrong stdlib and crash ("SRE module +:: mismatch"). Restore the original PATH/PYTHONHOME after this build so +:: later steps in this script are unaffected. +:: Finally, target the concrete copy_* (shared-library-producing) Bazel +:: targets, not the wrapping cc_library targets (libovmspython, +:: libpython_calculators) - requesting only the wrapper does not +:: necessarily force Bazel to relink the underlying shared library if one +:: already exists on disk, which can silently leave a stale, wrong-ABI +:: artifact in place. +set "SAVED_PATH_FOR_EXTRA_ABI=%PATH%" +set "SAVED_PYTHONHOME_FOR_EXTRA_ABI=%PYTHONHOME%" +set "PYTHONHOME=!extraPythonHome!" +set "PATH=!extraPythonHome!\;!extraPythonHome!\Scripts\;%PATH%" + +set "extraBuildTargets=//src:ovms //src/python:copy_libovmspython //src/python:copy_libpython_calculators //src/python/binding:copy_pyovms //src:ovms_mediapipe_runtime_shared" +:: Use a separate output_user_root for the extra-ABI build. This is critical: +:: pybind11_bazel's python_configure repository rule generates @local_config_python +:: with hardcoded paths to the Python include dir and .lib file. If the extra-ABI +:: build reuses the same output base as the default build, Bazel may serve the +:: cached Python312 @local_config_python even when PYTHON_BIN_PATH is overridden, +:: causing libovmspython-cpXYZ.dll to link against BOTH python312.dll (from the +:: cached @local_config_python) and pythonXYZ.dll (from @python3_windows which +:: does correctly respond to OVMS_WINDOWS_PYTHON_VERSION). The dual-runtime linkage +:: causes a fatal crash when the DLL is loaded. A separate output base forces a +:: clean @local_config_python fetch for the extra ABI. +set "extraBazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!_py!extraPythonTag!" +set "extraBuildCommand=bazel !extraBazelStartupCmd! build %buildWithIntegrity% %bazelBuildArgs% --repo_env=OVMS_WINDOWS_PYTHON_VERSION=!extraPythonTag! --repo_env=PYTHON_BIN_PATH=!extraPythonHome:\=/!/python.exe --action_env=PYTHON_BIN_PATH=!extraPythonHome:\=/!/python.exe --action_env=PYTHON_LIB_PATH=!extraPythonHome:\=/!/lib/site-packages --python_path=!extraPythonHome:\=/!/python.exe --action_env OpenVINO_DIR=%openvino_dir% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures !extraBuildTargets! 2>&1 | tee win_build_py!extraPythonTag!.log" +:: Invoke via %var% (percent expansion), NOT !var! (delayed expansion): percent +:: expansion happens during cmd.exe's initial line parsing, before pipe/redirect +:: tokenization, so the embedded "| tee" is correctly recognized as a pipeline +:: (matching how %buildCommand% is invoked above for the main build). Delayed +:: expansion (!var!) happens AFTER that parsing step, so an embedded "|" would +:: instead be passed as a literal argument to bazel.exe, causing errors like +:: "Illegal char <|> ... " from Bazel trying to resolve "|" as a path/target. +%extraBuildCommand% +set "EXTRA_BUILD_RESULT=!errorlevel!" + +set "PATH=%SAVED_PATH_FOR_EXTRA_ABI%" +set "PYTHONHOME=%SAVED_PYTHONHOME_FOR_EXTRA_ABI%" +if !EXTRA_BUILD_RESULT! neq 0 exit /b !EXTRA_BUILD_RESULT! + +:: Stage ABI-suffixed artifacts so a subsequent default-ABI build cannot +:: overwrite them, and so windows_create_package.bat can package them. +set "extraStageDir=%cd%\dist\windows\python_abi_addons\cp!extraPythonTag!" +if exist "!extraStageDir!" rmdir /S /Q "!extraStageDir!" +md "!extraStageDir!" +if !errorlevel! neq 0 exit /b !errorlevel! + +:: The extra-ABI build used a separate --output_user_root, so bazel-bin/ symlinks +:: still point to the default output base. Locate the artifacts via the extra +:: output base execroot path directly. +set "extraOutputBase=!BAZEL_SHORT_PATH!_py!extraPythonTag!" +set "extraExecroot=!extraOutputBase!\*\execroot\ovms" +:: Resolve the glob to a concrete path (the subdir contains a hash) +set "extraBinDir=" +for /d %%d in ("!extraOutputBase!\*") do ( + if exist "%%d\execroot\ovms\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll" ( + set "extraBinDir=%%d\execroot\ovms\bazel-out\x64_windows-opt\bin" + ) +) +if "!extraBinDir!"=="" ( + echo [ERROR] Could not locate extra-ABI build artifacts under !extraOutputBase! + exit /b 1 +) + +set "extraLibOvmspython=!extraBinDir!\src\python\libovmspython.dll" +set "extraLibPyCalc=!extraBinDir!\src\python\libpython_calculators.dll" +set "extraPyovms=!extraBinDir!\src\python\binding\pyovms.pyd" +set "extraMpRuntime=!extraBinDir!\src\ovms_mediapipe_runtime_shared.dll" + +copy "!extraLibOvmspython!" "!extraStageDir!\libovmspython-cp!extraPythonTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!extraLibPyCalc!" "!extraStageDir!\libpython_calculators-cp!extraPythonTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!extraPyovms!" "!extraStageDir!\pyovms.pyd" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!extraMpRuntime!" "!extraStageDir!\ovms_mediapipe_runtime_shared-cp!extraPythonTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! + +echo [INFO] Staged additional Python ABI cp!extraPythonTag! runtime libraries in !extraStageDir! + +:: With a separate output base for the extra-ABI build, the default output base +:: (bazel-bin/) was never touched. No restore build is needed. + +:skip_extra_python_abi + +:skip_default_python_abi_stage + endlocal exit /b 0 diff --git a/windows_create_package.bat b/windows_create_package.bat index 98a3c1aeb8..2dd96e2338 100644 --- a/windows_create_package.bat +++ b/windows_create_package.bat @@ -18,9 +18,50 @@ setlocal EnableExtensions EnableDelayedExpansion set "setPath=C:\opt;C:\opt\msys64\usr\bin\;%PATH%;" set "PATH=%setPath%" +set "ovms_exe_src=" +set "ovms_runtime_shared_src=" set "libovmspython_src=" set "libpython_calculators_src=" +IF "%~2"=="--with_python" ( + echo Self contained Python will be included in the package + set "with_python=true" +) ELSE ( + echo Self contained Python will not be included in the package + set "with_python=false" +) + +:: Resolve the expected Bazel outputs before copying. This allows packaging to fail +:: with a specific, actionable error when the server or Python ABI artifacts were +:: not built yet, instead of a generic "The system cannot find the file specified." +if exist %cd%\bazel-bin\src\ovms.exe ( + set "ovms_exe_src=%cd%\bazel-bin\src\ovms.exe" +) else if exist %cd%\bazel-out\x64_windows-opt\bin\src\ovms.exe ( + set "ovms_exe_src=%cd%\bazel-out\x64_windows-opt\bin\src\ovms.exe" +) +if not defined ovms_exe_src ( + echo Packaging validation failed: ovms.exe is missing from the Bazel outputs. Build the server first, e.g. with: bazel build //src:ovms + exit /b 1 +) + +:: Prefer the newest runtime-shared artifact from the staged Python ABI addon when +:: a Python build was performed. This avoids packaging a stale default-output DLL +:: left behind by a previous build or a separate output_user_root. +if /i "%with_python%"=="true" ( + if exist "%cd%\dist\windows\python_abi_addons\cp312\ovms_mediapipe_runtime_shared-cp312.dll" ( + set "ovms_runtime_shared_src=%cd%\dist\windows\python_abi_addons\cp312\ovms_mediapipe_runtime_shared-cp312.dll" + ) +) +if not defined ovms_runtime_shared_src if exist %cd%\bazel-bin\src\ovms_mediapipe_runtime_shared.dll ( + set "ovms_runtime_shared_src=%cd%\bazel-bin\src\ovms_mediapipe_runtime_shared.dll" +) else if exist %cd%\bazel-out\x64_windows-opt\bin\src\ovms_mediapipe_runtime_shared.dll ( + set "ovms_runtime_shared_src=%cd%\bazel-out\x64_windows-opt\bin\src\ovms_mediapipe_runtime_shared.dll" +) +if not defined ovms_runtime_shared_src ( + echo Packaging validation failed: ovms_mediapipe_runtime_shared.dll is missing from the Bazel outputs. Build the runtime shared library first, e.g. with: bazel build //src:ovms_mediapipe_runtime_shared + exit /b 1 +) + :: Load chosen dependency versions from versions.mk for /f "usebackq eol=# tokens=1,3" %%A in ("%cd%\versions.mk") do ( if "%%A"=="OPENCV_VERSION" if "!opencv_version!"=="" set "opencv_version=%%B" @@ -36,14 +77,6 @@ IF "%~1"=="" ( set "output_user_root=%1" ) -IF "%~2"=="--with_python" ( - echo Self contained Python will be included in the package - set "with_python=true" -) ELSE ( - echo Self contained Python will not be included in the package - set "with_python=false" -) - :: Set default USE_OV_BINARY if not set if "%OV_USE_BINARY%"=="" ( set "OV_USE_BINARY=1" @@ -55,10 +88,10 @@ if exist dist\windows\ovms ( ) md dist\windows\ovms -copy bazel-bin\src\ovms.exe dist\windows\ovms +copy "!ovms_exe_src!" dist\windows\ovms if !errorlevel! neq 0 exit /b !errorlevel! -copy %cd%\bazel-out\x64_windows-opt\bin\src\ovms_mediapipe_runtime_shared.dll dist\windows\ovms +copy "!ovms_runtime_shared_src!" "dist\windows\ovms\ovms_mediapipe_runtime_shared.dll" if !errorlevel! neq 0 exit /b !errorlevel! copy C:\%output_user_root%\openvino\runtime\bin\intel64\Release\*.dll dist\windows\ovms @@ -66,61 +99,145 @@ if !errorlevel! neq 0 exit /b !errorlevel! set "dest_dir=C:\opt" -if /i "%with_python%"=="true" ( - if exist %cd%\bazel-bin\src\python\libovmspython.dll ( - set "libovmspython_src=%cd%\bazel-bin\src\python\libovmspython.dll" - ) else if exist %cd%\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll ( - set "libovmspython_src=%cd%\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll" - ) - if not defined libovmspython_src ( - echo Missing libovmspython.dll in bazel output. Ensure //src/python:libovmspython is built. - exit /b 1 - ) +:: NOTE: this whole section intentionally avoids wrapping everything in a +:: single outer parenthesized IF (...) block, and avoids "::" comments and +:: "else if" chains inside nested blocks. cmd.exe's block pre-parser can +:: fail with errors like "'e.g.' was unexpected at this time." when deeply +:: nested parenthesized blocks contain "::" style comments or long else-if +:: chains - the error only surfaces once execution actually reaches the +:: block, not at initial parse time. Using goto plus flag variables instead +:: of nested parens sidesteps this entirely (see windows_build.bat for the +:: same fix applied to its additional-Python-ABI section). +if /i not "%with_python%"=="true" goto :skip_python_packaging - if exist %cd%\bazel-bin\src\python\libpython_calculators.dll ( - set "libpython_calculators_src=%cd%\bazel-bin\src\python\libpython_calculators.dll" - ) else if exist %cd%\bazel-out\x64_windows-opt\bin\src\python\libpython_calculators.dll ( - set "libpython_calculators_src=%cd%\bazel-out\x64_windows-opt\bin\src\python\libpython_calculators.dll" - ) - if not defined libpython_calculators_src ( - echo Missing libpython_calculators.dll in bazel output. Ensure //src/python:libpython_calculators is built. - exit /b 1 - ) +if exist "%cd%\dist\windows\python_abi_addons\cp312\libovmspython-cp312.dll" ( + set "libovmspython_src=%cd%\dist\windows\python_abi_addons\cp312\libovmspython-cp312.dll" +) else if exist %cd%\bazel-bin\src\python\libovmspython.dll ( + set "libovmspython_src=%cd%\bazel-bin\src\python\libovmspython.dll" +) else if exist %cd%\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll ( + set "libovmspython_src=%cd%\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll" +) +if not defined libovmspython_src ( + echo Missing libovmspython.dll in bazel output. Ensure //src/python:libovmspython is built. + exit /b 1 +) - :: Copy pyovms module - md dist\windows\ovms\python - copy %cd%\bazel-out\x64_windows-opt\bin\src\python\binding\pyovms.pyd dist\windows\ovms\python - if !errorlevel! neq 0 exit /b !errorlevel! +if exist "%cd%\dist\windows\python_abi_addons\cp312\libpython_calculators-cp312.dll" ( + set "libpython_calculators_src=%cd%\dist\windows\python_abi_addons\cp312\libpython_calculators-cp312.dll" +) else if exist %cd%\bazel-bin\src\python\libpython_calculators.dll ( + set "libpython_calculators_src=%cd%\bazel-bin\src\python\libpython_calculators.dll" +) else if exist %cd%\bazel-out\x64_windows-opt\bin\src\python\libpython_calculators.dll ( + set "libpython_calculators_src=%cd%\bazel-out\x64_windows-opt\bin\src\python\libpython_calculators.dll" +) +if not defined libpython_calculators_src ( + echo Missing libpython_calculators.dll in bazel output. Ensure //src/python:libpython_calculators is built. + exit /b 1 +) - :: Copy shared OVMS python runtime libraries required by ovms.exe when Python is enabled. - copy "!libovmspython_src!" dist\windows\ovms - if !errorlevel! neq 0 exit /b !errorlevel! - copy "!libpython_calculators_src!" dist\windows\ovms +:: Derive the default ABI tag from the embedded Python version (e.g. 3.12.10 -> cp312) +set "python_version=3.12.10" +for /f "tokens=1,2 delims=." %%a in ("!python_version!") do ( + set "DEFAULT_MAJOR_VER=%%a" + set "DEFAULT_MINOR_VER=%%b" +) +set "defaultAbiTag=cp!DEFAULT_MAJOR_VER!!DEFAULT_MINOR_VER!" + +:: Copy pyovms module into root python/ (default import path) and into python\cp\ for +:: symmetry with the additional-ABI addon layout (e.g. python\cp313\pyovms.pyd). +md dist\windows\ovms\python +copy %cd%\bazel-out\x64_windows-opt\bin\src\python\binding\pyovms.pyd dist\windows\ovms\python +if !errorlevel! neq 0 exit /b !errorlevel! +if exist dist\windows\ovms\python\!defaultAbiTag! ( + rmdir /s /q dist\windows\ovms\python\!defaultAbiTag! if !errorlevel! neq 0 exit /b !errorlevel! - :: Prepare self-contained python - set "python_version=3.12.10" +) +md dist\windows\ovms\python\!defaultAbiTag! +if !errorlevel! neq 0 exit /b !errorlevel! +copy %cd%\bazel-out\x64_windows-opt\bin\src\python\binding\pyovms.pyd "dist\windows\ovms\python\!defaultAbiTag!\pyovms.pyd" +if !errorlevel! neq 0 exit /b !errorlevel! - call %cd%\windows_prepare_python.bat %dest_dir% !python_version! - if !errorlevel! neq 0 ( - echo Error occurred when creating Python environment for the distribution. - exit /b !errorlevel! - ) - :: Copy whole catalog to dist folder and install dependencies required by LLM pipelines - xcopy %dest_dir%\python-!python_version!-embed-amd64 dist\windows\ovms\python /E /I /H - if !errorlevel! neq 0 ( - echo Error occurred when creating Python environment for the distribution. - exit /b !errorlevel! - ) - if not exist dist\windows\ovms\python\python312.zip ( - echo Packaging validation failed: embedded stdlib python312.zip is missing from dist\windows\ovms\python. +:: Copy shared OVMS Python runtime libraries. +:: Unversioned names (e.g. libovmspython.dll) are the loader fallback used when ABI detection +:: returns empty (standard packaged deployment via setupvars.bat). +:: Versioned names (e.g. libovmspython-cp312.dll) are symmetric with the additional-ABI addons +:: and are used as the primary candidate when PYTHONHOME encodes the version. +copy "!libovmspython_src!" "dist\windows\ovms\libovmspython.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!libovmspython_src!" "dist\windows\ovms\libovmspython-!defaultAbiTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!libpython_calculators_src!" "dist\windows\ovms\libpython_calculators.dll" +if !errorlevel! neq 0 exit /b !errorlevel! +copy "!libpython_calculators_src!" "dist\windows\ovms\libpython_calculators-!defaultAbiTag!.dll" +if !errorlevel! neq 0 exit /b !errorlevel! + +call %cd%\windows_prepare_python.bat %dest_dir% !python_version! +if !errorlevel! neq 0 ( + echo Error occurred when creating Python environment for the distribution. + exit /b !errorlevel! +) +:: Copy whole catalog to dist folder and install dependencies required by LLM pipelines +xcopy %dest_dir%\python-!python_version!-embed-amd64 dist\windows\ovms\python /E /I /H +if !errorlevel! neq 0 ( + echo Error occurred when creating Python environment for the distribution. + exit /b !errorlevel! +) +if not exist dist\windows\ovms\python\python312.zip ( + echo Packaging validation failed: embedded stdlib python312.zip is missing from dist\windows\ovms\python. + exit /b 1 +) +.\dist\windows\ovms\python\python.exe -m pip install "setuptools==80.9.0" "Jinja2==3.1.6" "MarkupSafe==3.0.2" +if !errorlevel! neq 0 ( + echo Error during Python dependencies for LLM installation. The package will not be fully functional. +) + +:: Package any additional Python ABI runtime libraries staged by +:: windows_build.bat under dist\windows\python_abi_addons\cp\, for +:: example a cp313 build produced alongside the default cp312 build above. +set "abiAddonsDir=%cd%\dist\windows\python_abi_addons" +if not exist "!abiAddonsDir!" goto :skip_python_packaging + +if /i "%with_python%"=="true" ( + if not exist "!abiAddonsDir!\cp*" ( + echo Packaging validation failed: no staged Python ABI directories were found under !abiAddonsDir!. + echo Build the default runtime and any additional ABI runtimes first, e.g. with: windows_build.bat opt --with_python 3.13.1 exit /b 1 ) - .\dist\windows\ovms\python\python.exe -m pip install "setuptools==80.9.0" "Jinja2==3.1.6" "MarkupSafe==3.0.2" - if !errorlevel! neq 0 ( - echo Error during Python dependencies for LLM installation. The package will not be fully functional. +) + +for /d %%V in ("!abiAddonsDir!\cp*") do ( + set "abiTagDir=%%~nxV" + set "abiTag=!abiTagDir:cp=!" + echo Packaging additional Python ABI: !abiTag! + + set "abiFilesOk=1" + if not exist "%%V\libovmspython-!abiTagDir!.dll" set "abiFilesOk=0" + if not exist "%%V\libpython_calculators-!abiTagDir!.dll" set "abiFilesOk=0" + if not exist "%%V\ovms_mediapipe_runtime_shared-!abiTagDir!.dll" set "abiFilesOk=0" + if not exist "%%V\pyovms.pyd" set "abiFilesOk=0" + + if "!abiFilesOk!"=="0" ( + echo Missing required staged files for ABI !abiTag! in %%V. Skipping. + ) else ( + copy "%%V\libovmspython-!abiTagDir!.dll" dist\windows\ovms + if !errorlevel! neq 0 exit /b !errorlevel! + copy "%%V\libpython_calculators-!abiTagDir!.dll" dist\windows\ovms + if !errorlevel! neq 0 exit /b !errorlevel! + copy "%%V\ovms_mediapipe_runtime_shared-!abiTagDir!.dll" dist\windows\ovms + if !errorlevel! neq 0 exit /b !errorlevel! + + if exist dist\windows\ovms\python\!abiTagDir! ( + rmdir /s /q dist\windows\ovms\python\!abiTagDir! + if !errorlevel! neq 0 exit /b !errorlevel! + ) + md dist\windows\ovms\python\!abiTagDir! + if !errorlevel! neq 0 exit /b !errorlevel! + copy "%%V\pyovms.pyd" dist\windows\ovms\python\!abiTagDir!\pyovms.pyd + if !errorlevel! neq 0 exit /b !errorlevel! ) ) +:skip_python_packaging + copy C:\%output_user_root%\openvino\runtime\3rdparty\tbb\bin\tbb12.dll dist\windows\ovms if !errorlevel! neq 0 exit /b !errorlevel! diff --git a/windows_test.bat b/windows_test.bat index 34474f4e4c..1a2b4d449a 100644 --- a/windows_test.bat +++ b/windows_test.bat @@ -38,10 +38,21 @@ set "bazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!" set "openvino_dir=!BAZEL_SHORT_PATH!/openvino/runtime/cmake" set "OVMS_MEDIA_URL_ALLOW_REDIRECTS=1" -IF "%~3"=="" ( +:: Optional arg3: additional Python ABI version (e.g. 3.13.1) - sets OVMS_PYTHON_ABI so +:: the Python runtime tests exercise the versioned loader path (e.g. cp313 DLLs). +:: The full Python dev install for that version must already be present at C:\opt\Python. +set "additionalPythonAbi=%~3" +set "OVMS_PYTHON_ABI_TAG=" +IF NOT "%~3"=="" ( + for /f "tokens=1,2 delims=." %%a in ("%~3") do ( + set "OVMS_PYTHON_ABI_TAG=%%a%%b" + ) +) + +IF "%~4"=="" ( set "gtestFilter=*" ) ELSE ( - set "gtestFilter=%3" + set "gtestFilter=%4" ) IF "%~2"=="--with_python" ( @@ -143,6 +154,12 @@ if !pytestExitCode! neq 0 ( ) echo [INFO] install_ovms_service.bat unit tests passed. +:: If an extra Python ABI was requested, expose it so runtime-loader tests exercise the cp path. +IF NOT "!OVMS_PYTHON_ABI_TAG!"=="" ( + set "OVMS_PYTHON_ABI=!OVMS_PYTHON_ABI_TAG!" + echo [INFO] OVMS_PYTHON_ABI set to !OVMS_PYTHON_ABI_TAG! for runtime tests +) + :: Start unit test echo Running: %runTest% %runTest%