diff --git a/cmake/onnxruntime_unittests.cmake b/cmake/onnxruntime_unittests.cmake
index ad6f6eb7903df..686b6b6ba1228 100644
--- a/cmake/onnxruntime_unittests.cmake
+++ b/cmake/onnxruntime_unittests.cmake
@@ -1212,6 +1212,13 @@ block()
${TEST_SRC_DIR}/common/tensor_op_test_utils.h
)
+ if (onnxruntime_USE_DNNL)
+ list(APPEND supporting_test_srcs
+ ${TEST_SRC_DIR}/common/dnnl_op_test_utils.cc
+ ${TEST_SRC_DIR}/common/dnnl_op_test_utils.h
+ )
+ endif()
+
list(APPEND onnxruntime_provider_test_srcs
${supporting_test_srcs}
${onnxruntime_unittest_main_src}
diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc
index e0b31c29a054b..961012536126b 100644
--- a/onnxruntime/core/framework/tensorprotoutils.cc
+++ b/onnxruntime/core/framework/tensorprotoutils.cc
@@ -329,7 +329,8 @@ Status TensorProtoWithExternalDataToTensorProto(
}
Status ValidateExternalDataPath(const std::filesystem::path& base_dir,
- const std::filesystem::path& location) {
+ const std::filesystem::path& location,
+ const std::filesystem::path& model_path) {
// Reject absolute paths
ORT_RETURN_IF(location.is_absolute(),
"Absolute paths not allowed for external data location");
@@ -337,14 +338,39 @@ Status ValidateExternalDataPath(const std::filesystem::path& base_dir,
// Resolve and verify the path stays within model directory
auto base_canonical = std::filesystem::weakly_canonical(base_dir);
// If the symlink exists, it resolves to the target path;
- // so if the symllink is outside the directory it would be caught here.
+ // so if the symlink is outside the directory it would be caught here.
auto resolved = std::filesystem::weakly_canonical(base_dir / location);
+
// Check that resolved path starts with base directory
auto [base_end, resolved_it] = std::mismatch(
base_canonical.begin(), base_canonical.end(),
resolved.begin(), resolved.end());
- ORT_RETURN_IF(base_end != base_canonical.end(),
- "External data path: ", location, " escapes model directory: ", base_dir);
+
+ if (base_end != base_canonical.end()) {
+ // If validation against logical base_dir fails, we check against the
+ // real (canonical) path of the model file to support symlinked models
+ // (e.g. models in Hugging Face Hub local cache).
+ if (!model_path.empty()) {
+ auto real_model_dir = std::filesystem::weakly_canonical(model_path).parent_path();
+
+ auto [real_base_end, real_resolved_it] = std::mismatch(
+ real_model_dir.begin(), real_model_dir.end(),
+ resolved.begin(), resolved.end());
+
+ if (real_base_end == real_model_dir.end()) {
+ return Status::OK();
+ }
+
+ return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL,
+ "External data path: ", location, " (resolved path: ", resolved,
+ ") escapes both model directory: ", base_dir,
+ " and real model directory: ", real_model_dir);
+ }
+
+ return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL,
+ "External data path: ", location, " (resolved path: ", resolved,
+ ") escapes model directory: ", base_dir);
+ }
}
return Status::OK();
}
diff --git a/onnxruntime/core/framework/tensorprotoutils.h b/onnxruntime/core/framework/tensorprotoutils.h
index 685fa65a73720..941cd9af34b61 100644
--- a/onnxruntime/core/framework/tensorprotoutils.h
+++ b/onnxruntime/core/framework/tensorprotoutils.h
@@ -526,16 +526,19 @@ Status TensorProtoWithExternalDataToTensorProto(
ONNX_NAMESPACE::TensorProto& new_tensor_proto);
///
-/// The functions will make sure the 'location' specified in the external data is under the 'base_dir'.
+/// Validates if the external data path is under the model directory.
+/// If the model is a symlink, it checks against both the logical model directory (base_dir)
+/// and the real/canonical directory of the model.
/// If the `base_dir` is empty, the function only ensures that `location` is not an absolute path.
///
-/// model location directory
-/// location is a string retrieved from TensorProto external data that is not
-/// an in-memory tag
-/// The function will fail if the resolved full path is not under the model directory
-/// or one of the subdirectories
+/// Logical model location directory
+/// Location string retrieved from TensorProto external data
+/// Optional path to the model file, used for canonical path validation if base_dir check fails
+/// The function will fail if the resolved full path is not under the logical model directory
+/// nor the real directory of the model path
Status ValidateExternalDataPath(const std::filesystem::path& base_dir,
- const std::filesystem::path& location);
+ const std::filesystem::path& location,
+ const std::filesystem::path& model_path = {});
#endif // !defined(SHARED_PROVIDER)
diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc
index dd3eb59b7fafb..c41dc0b288930 100644
--- a/onnxruntime/core/graph/graph.cc
+++ b/onnxruntime/core/graph/graph.cc
@@ -3771,7 +3771,7 @@ Status Graph::ConvertInitializersIntoOrtValues() {
std::unique_ptr external_data_info;
ORT_RETURN_IF_ERROR(onnxruntime::ExternalDataInfo::Create(tensor_proto.external_data(), external_data_info));
const auto& location = external_data_info->GetRelPath();
- auto st = utils::ValidateExternalDataPath(model_dir, location);
+ auto st = utils::ValidateExternalDataPath(model_dir, location, model_path);
if (!st.IsOK()) {
return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL,
"External data path validation failed for initializer: ", tensor_proto.name(),
diff --git a/onnxruntime/core/providers/shared_library/provider_api.h b/onnxruntime/core/providers/shared_library/provider_api.h
index f5421d8540db8..1ed78c89e722d 100644
--- a/onnxruntime/core/providers/shared_library/provider_api.h
+++ b/onnxruntime/core/providers/shared_library/provider_api.h
@@ -453,11 +453,6 @@ inline bool HasExternalDataInMemory(const ONNX_NAMESPACE::TensorProto& ten_proto
return g_host->Utils__HasExternalDataInMemory(ten_proto);
}
-inline Status ValidateExternalDataPath(const std::filesystem::path& base_dir,
- const std::filesystem::path& location) {
- return g_host->Utils__ValidateExternalDataPath(base_dir, location);
-}
-
} // namespace utils
namespace graph_utils {
diff --git a/onnxruntime/core/providers/shared_library/provider_interfaces.h b/onnxruntime/core/providers/shared_library/provider_interfaces.h
index aeaf05cf14591..9cbbc6234a99b 100644
--- a/onnxruntime/core/providers/shared_library/provider_interfaces.h
+++ b/onnxruntime/core/providers/shared_library/provider_interfaces.h
@@ -1004,9 +1004,6 @@ struct ProviderHost {
virtual bool Utils__HasExternalDataInMemory(const ONNX_NAMESPACE::TensorProto& ten_proto) = 0;
- virtual Status Utils__ValidateExternalDataPath(const std::filesystem::path& base_path,
- const std::filesystem::path& location) = 0;
-
// Model
virtual std::unique_ptr Model__construct(ONNX_NAMESPACE::ModelProto&& model_proto, const PathString& model_path,
const IOnnxRuntimeOpSchemaRegistryList* local_registries,
diff --git a/onnxruntime/core/session/provider_bridge_ort.cc b/onnxruntime/core/session/provider_bridge_ort.cc
index 5700a32cf5ca1..3dc2df6d78ba1 100644
--- a/onnxruntime/core/session/provider_bridge_ort.cc
+++ b/onnxruntime/core/session/provider_bridge_ort.cc
@@ -1286,11 +1286,6 @@ struct ProviderHostImpl : ProviderHost {
return onnxruntime::utils::HasExternalDataInMemory(ten_proto);
}
- Status Utils__ValidateExternalDataPath(const std::filesystem::path& base_path,
- const std::filesystem::path& location) override {
- return onnxruntime::utils::ValidateExternalDataPath(base_path, location);
- }
-
// Model (wrapped)
std::unique_ptr Model__construct(ONNX_NAMESPACE::ModelProto&& model_proto, const PathString& model_path,
const IOnnxRuntimeOpSchemaRegistryList* local_registries,
diff --git a/onnxruntime/test/python/onnxruntime_test_python_symlink_data.py b/onnxruntime/test/python/onnxruntime_test_python_symlink_data.py
new file mode 100644
index 0000000000000..ea3c0f9ca9904
--- /dev/null
+++ b/onnxruntime/test/python/onnxruntime_test_python_symlink_data.py
@@ -0,0 +1,250 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+import shutil
+import struct
+import tempfile
+import unittest
+
+import numpy as np
+from onnx import TensorProto, helper, save
+
+import onnxruntime as ort
+
+
+class TestSymLinkOnnxModelExternalData(unittest.TestCase):
+ def test_symlink_model_and_data_under_same_directory(self):
+ # The following directory structure simulates huggingface hub local cache:
+ # temp_dir/ (This corresponds to .cache/huggingface/hub/model_id/)
+ # blobs/
+ # guid1
+ # guid2
+ # snapshots/version/
+ # model.onnx -> ../../blobs/guid1
+ # data.bin -> ../../blobs/guid2
+
+ self.temp_dir = tempfile.mkdtemp()
+ try:
+ blobs_dir = os.path.join(self.temp_dir, "blobs")
+ os.makedirs(blobs_dir)
+
+ snapshots_dir = os.path.join(self.temp_dir, "snapshots", "version")
+ os.makedirs(snapshots_dir)
+
+ # Create real files in blobs
+ # We'll use the helper to create the model, but we need to control where files end up.
+ # Let's manually create the data file in blobs
+ data_blob_path = os.path.join(blobs_dir, "guid2")
+ vals = [float(i) for i in range(10)]
+ with open(data_blob_path, "wb") as f:
+ f.writelines(struct.pack("f", v) for v in vals)
+
+ # Create model in blobs (referencing "data.bin" as external data)
+ # When loaded from snapshots/version/model.onnx, ORT looks for snapshots/version/data.bin
+
+ input_ = helper.make_tensor_value_info("input", TensorProto.FLOAT, [10])
+ output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [10])
+ tensor = helper.make_tensor("external_data", TensorProto.FLOAT, [10], vals)
+ tensor.data_location = TensorProto.EXTERNAL
+ tensor.ClearField("float_data")
+ tensor.ClearField("raw_data")
+
+ k = tensor.external_data.add()
+ k.key = "location"
+ k.value = "data.bin" # Relative path
+
+ offset = tensor.external_data.add()
+ offset.key = "offset"
+ offset.value = "0"
+
+ length = tensor.external_data.add()
+ length.key = "length"
+ length.value = str(len(vals) * 4)
+
+ const_node = helper.make_node("Constant", [], ["const_out"], value=tensor)
+ add_node = helper.make_node("Add", ["input", "const_out"], ["output"])
+ graph = helper.make_graph([const_node, add_node], "test_graph", [input_], [output])
+ model = helper.make_model(graph)
+
+ model_blob_path = os.path.join(blobs_dir, "guid1")
+ save(model, model_blob_path)
+
+ # Now create symlinks in snapshots
+ model_symlink_path = os.path.join(snapshots_dir, "model.onnx")
+ data_symlink_path = os.path.join(snapshots_dir, "data.bin")
+
+ try:
+ os.symlink(model_blob_path, model_symlink_path)
+ os.symlink(data_blob_path, data_symlink_path)
+ except (OSError, NotImplementedError) as e:
+ self.skipTest(f"Skipping symlink test: symlink creation is not supported in this environment: {e}")
+
+ sess = ort.InferenceSession(model_symlink_path, providers=["CPUExecutionProvider"])
+
+ input_data = np.zeros(10, dtype=np.float32)
+ res = sess.run(["output"], {"input": input_data})
+ expected = np.array([float(i) for i in range(10)], dtype=np.float32)
+ np.testing.assert_allclose(res[0], expected)
+
+ finally:
+ shutil.rmtree(self.temp_dir)
+
+ def test_symlink_with_data_in_model_sub_dir(self):
+ # working directory structure (data is in model sub directory):
+ # temp_dir/
+ # blobs/
+ # guid1
+ # data/guid2
+ # snapshots/version/
+ # model.onnx -> ../../blobs/guid1
+ # data.bin -> ../../blobs/data/guid2
+
+ self.temp_dir = tempfile.mkdtemp()
+ try:
+ blobs_dir = os.path.join(self.temp_dir, "blobs")
+ os.makedirs(blobs_dir)
+ data_dir = os.path.join(blobs_dir, "data")
+ os.makedirs(data_dir)
+
+ snapshots_dir = os.path.join(self.temp_dir, "snapshots", "version")
+ os.makedirs(snapshots_dir)
+
+ # Create real files in blobs
+ # We'll use the helper to create the model, but we need to control where files end up.
+ # Let's manually create the data file in blobs
+ data_blob_path = os.path.join(data_dir, "guid2")
+ vals = [float(i) for i in range(10)]
+ with open(data_blob_path, "wb") as f:
+ f.writelines(struct.pack("f", v) for v in vals)
+
+ # Create model in blobs (referencing "data.bin" as external data)
+ # When loaded from snapshots/version/model.onnx, ORT looks for snapshots/version/data.bin
+
+ input_ = helper.make_tensor_value_info("input", TensorProto.FLOAT, [10])
+ output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [10])
+ tensor = helper.make_tensor("external_data", TensorProto.FLOAT, [10], vals)
+ tensor.data_location = TensorProto.EXTERNAL
+ tensor.ClearField("float_data")
+ tensor.ClearField("raw_data")
+
+ k = tensor.external_data.add()
+ k.key = "location"
+ k.value = "data.bin" # Relative path
+
+ offset = tensor.external_data.add()
+ offset.key = "offset"
+ offset.value = "0"
+
+ length = tensor.external_data.add()
+ length.key = "length"
+ length.value = str(len(vals) * 4)
+
+ const_node = helper.make_node("Constant", [], ["const_out"], value=tensor)
+ add_node = helper.make_node("Add", ["input", "const_out"], ["output"])
+ graph = helper.make_graph([const_node, add_node], "test_graph", [input_], [output])
+ model = helper.make_model(graph)
+
+ model_blob_path = os.path.join(blobs_dir, "guid1")
+ save(model, model_blob_path)
+
+ # Now create symlinks in snapshots
+ model_symlink_path = os.path.join(snapshots_dir, "model.onnx")
+ data_symlink_path = os.path.join(snapshots_dir, "data.bin")
+
+ try:
+ os.symlink(model_blob_path, model_symlink_path)
+ os.symlink(data_blob_path, data_symlink_path)
+ except (OSError, NotImplementedError) as e:
+ self.skipTest(f"Skipping symlink test: symlink creation is not supported in this environment: {e}")
+
+ sess = ort.InferenceSession(model_symlink_path, providers=["CPUExecutionProvider"])
+
+ input_data = np.zeros(10, dtype=np.float32)
+ res = sess.run(["output"], {"input": input_data})
+ expected = np.array([float(i) for i in range(10)], dtype=np.float32)
+ np.testing.assert_allclose(res[0], expected)
+
+ finally:
+ shutil.rmtree(self.temp_dir)
+
+ def test_symlink_with_data_not_in_model_sub_dir(self):
+ # working directory structure (data is not in model directory or its sub directories):
+ # temp_dir/
+ # model/
+ # guid1
+ # data/
+ # guid2
+ # snapshots/version/
+ # model.onnx -> ../../model/guid1
+ # data.bin -> ../../data/guid2
+
+ self.temp_dir = tempfile.mkdtemp()
+ try:
+ model_dir = os.path.join(self.temp_dir, "model")
+ os.makedirs(model_dir)
+ data_dir = os.path.join(self.temp_dir, "data")
+ os.makedirs(data_dir)
+
+ snapshots_dir = os.path.join(self.temp_dir, "snapshots", "version")
+ os.makedirs(snapshots_dir)
+
+ # Create real files in data_dir
+ # We'll use the helper to create the model, but we need to control where files end up.
+ # Let's manually create the data file in data_dir
+ data_blob_path = os.path.join(data_dir, "guid2")
+ vals = [float(i) for i in range(10)]
+ with open(data_blob_path, "wb") as f:
+ f.writelines(struct.pack("f", v) for v in vals)
+
+ # Create model in model_dir (referencing "data.bin" as external data)
+ # When loaded from snapshots/version/model.onnx, ORT looks for snapshots/version/data.bin
+
+ input_ = helper.make_tensor_value_info("input", TensorProto.FLOAT, [10])
+ output = helper.make_tensor_value_info("output", TensorProto.FLOAT, [10])
+ tensor = helper.make_tensor("external_data", TensorProto.FLOAT, [10], vals)
+ tensor.data_location = TensorProto.EXTERNAL
+ tensor.ClearField("float_data")
+ tensor.ClearField("raw_data")
+
+ k = tensor.external_data.add()
+ k.key = "location"
+ k.value = "data.bin" # Relative path
+
+ offset = tensor.external_data.add()
+ offset.key = "offset"
+ offset.value = "0"
+
+ length = tensor.external_data.add()
+ length.key = "length"
+ length.value = str(len(vals) * 4)
+
+ const_node = helper.make_node("Constant", [], ["const_out"], value=tensor)
+ add_node = helper.make_node("Add", ["input", "const_out"], ["output"])
+ graph = helper.make_graph([const_node, add_node], "test_graph", [input_], [output])
+ model = helper.make_model(graph)
+
+ model_blob_path = os.path.join(model_dir, "guid1")
+ save(model, model_blob_path)
+
+ # Now create symlinks in snapshots
+ model_symlink_path = os.path.join(snapshots_dir, "model.onnx")
+ data_symlink_path = os.path.join(snapshots_dir, "data.bin")
+
+ try:
+ os.symlink(model_blob_path, model_symlink_path)
+ os.symlink(data_blob_path, data_symlink_path)
+ except (OSError, NotImplementedError) as e:
+ self.skipTest(f"Skipping symlink test: symlink creation is not supported in this environment: {e}")
+
+ with self.assertRaises(Exception) as cm:
+ ort.InferenceSession(model_symlink_path, providers=["CPUExecutionProvider"])
+
+ # We expect an error about external data not under model directory or the real model directory.
+ self.assertIn("External data path validation failed", str(cm.exception))
+ finally:
+ shutil.rmtree(self.temp_dir)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/ci_build/build.py b/tools/ci_build/build.py
index a0712af35e455..c8beeef8aa509 100644
--- a/tools/ci_build/build.py
+++ b/tools/ci_build/build.py
@@ -1818,6 +1818,9 @@ def run_onnxruntime_tests(args, source_dir, ctest_path, build_dir, configs):
onnx_test = False
if onnx_test:
+ log.info("Testing Symlink ONNX Model and External Data")
+ run_subprocess([sys.executable, "onnxruntime_test_python_symlink_data.py"], cwd=cwd, dll_path=dll_path)
+
# Disable python onnx tests for TensorRT and CANN EP, because many tests are
# not supported yet.
if args.use_tensorrt or args.use_cann:
diff --git a/tools/ci_build/github/azure-pipelines/dml-nuget-packaging.yml b/tools/ci_build/github/azure-pipelines/dml-nuget-packaging.yml
index 2868963b637a8..3cf28655c36e7 100644
--- a/tools/ci_build/github/azure-pipelines/dml-nuget-packaging.yml
+++ b/tools/ci_build/github/azure-pipelines/dml-nuget-packaging.yml
@@ -75,9 +75,13 @@ extends:
DoNugetPack: 'true'
DoEsrp: ${{ parameters.DoEsrp }}
NuPackScript: |
+ python -m pip install setuptools
msbuild $(Build.SourcesDirectory)\csharp\OnnxRuntime.CSharp.proj /p:Configuration=RelWithDebInfo /t:CreatePackage /p:OrtPackageId=Microsoft.ML.OnnxRuntime.DirectML /p:IsReleaseBuild=${{ parameters.IsReleaseBuild }} /p:CurrentData=$(BuildDate) /p:CurrentTime=$(BuildTime)
+ if errorlevel 1 exit /b 1
copy $(Build.SourcesDirectory)\csharp\src\Microsoft.ML.OnnxRuntime\bin\RelWithDebInfo\*.nupkg $(Build.ArtifactStagingDirectory)
- copy $(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo\*.nupkg $(Build.ArtifactStagingDirectory)
+ if errorlevel 1 exit /b 1
+ powershell -ExecutionPolicy Bypass -File $(Build.SourcesDirectory)\tools\ci_build\github\windows\select_dml_package.ps1 -SourceDir "$(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo" -IsReleaseBuild "${{ parameters.IsReleaseBuild }}" -Action copy -DestinationDir "$(Build.ArtifactStagingDirectory)"
+ if errorlevel 1 exit /b 1
mkdir $(Build.ArtifactStagingDirectory)\testdata
copy $(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo\custom_op_library.* $(Build.ArtifactStagingDirectory)\testdata
@@ -94,13 +98,16 @@ extends:
DoEsrp: ${{ parameters.DoEsrp }}
RunTests: 'false'
NuPackScript: |
+ python -m pip install setuptools
msbuild $(Build.SourcesDirectory)\csharp\OnnxRuntime.CSharp.proj /p:Configuration=RelWithDebInfo /p:TargetArchitecture=arm64 /t:CreatePackage /p:OrtPackageId=Microsoft.ML.OnnxRuntime.DirectML /p:IsReleaseBuild=${{ parameters.IsReleaseBuild }}
- cd $(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo\
- ren Microsoft.ML.OnnxRuntime.DirectML.* win-dml-arm64.zip
+ if errorlevel 1 exit /b 1
+ powershell -ExecutionPolicy Bypass -File $(Build.SourcesDirectory)\tools\ci_build\github\windows\select_dml_package.ps1 -SourceDir "$(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo" -IsReleaseBuild "${{ parameters.IsReleaseBuild }}" -Action rename -NewName "win-dml-arm64.zip"
+ if errorlevel 1 exit /b 1
copy $(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo\win-dml-arm64.zip $(Build.ArtifactStagingDirectory)
+ if errorlevel 1 exit /b 1
mkdir $(Build.ArtifactStagingDirectory)\testdata
copy $(Build.BinariesDirectory)\RelWithDebInfo\RelWithDebInfo\custom_op_library.* $(Build.ArtifactStagingDirectory)\testdata
- template: stages/nuget_dml_packaging_stage.yml
parameters:
- DoEsrp: ${{ parameters.DoEsrp }}
\ No newline at end of file
+ DoEsrp: ${{ parameters.DoEsrp }}
diff --git a/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml b/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml
index e7a4975122784..981989f519ae4 100644
--- a/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml
+++ b/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml
@@ -68,7 +68,7 @@ steps:
# Convert the SecureString token to a plain text string for the HTTP header
# This is done just-in-time before its use.
- $plainTextToken = $secureTokenObject | ConvertFrom-SecureString -AsPlainText
+ $plainTextToken = [System.Net.NetworkCredential]::new("", $secureTokenObject).Password
Write-Host "Token converted to plain text for API call (will not be logged)."
# Part 2: Publish Symbols using internal REST API
diff --git a/tools/ci_build/github/azure-pipelines/templates/validate-package.yml b/tools/ci_build/github/azure-pipelines/templates/validate-package.yml
index 529cca4586ef6..950a5a6a34f4d 100644
--- a/tools/ci_build/github/azure-pipelines/templates/validate-package.yml
+++ b/tools/ci_build/github/azure-pipelines/templates/validate-package.yml
@@ -4,6 +4,7 @@ parameters:
PackageType: ''
PackageName: ''
PackagePath: ''
+ IsReleaseBuild: false
ScriptPath: '$(Build.SourcesDirectory)/tools/nuget/validate_package.py'
workingDirectory: "$(Build.BinariesDirectory)"
@@ -17,5 +18,5 @@ steps:
displayName: 'Validate Package'
inputs:
scriptPath: '${{parameters.ScriptPath}}'
- arguments: '--package_type ${{parameters.PackageType}} --package_name ${{parameters.PackageName}} --package_path ${{parameters.PackagePath}} --platforms_supported ${{parameters.PlatformsSupported}} --verify_nuget_signing ${{parameters.VerifyNugetSigning}}'
+ arguments: '--package_type ${{parameters.PackageType}} --package_name ${{parameters.PackageName}} --package_path ${{parameters.PackagePath}} --platforms_supported ${{parameters.PlatformsSupported}} --verify_nuget_signing ${{parameters.VerifyNugetSigning}} --is_release_build ${{parameters.IsReleaseBuild}}'
workingDirectory: ${{parameters.workingDirectory}}
diff --git a/tools/ci_build/github/windows/bundle_dml_package.ps1 b/tools/ci_build/github/windows/bundle_dml_package.ps1
index ef7f781096b25..36088e772bf2d 100644
--- a/tools/ci_build/github/windows/bundle_dml_package.ps1
+++ b/tools/ci_build/github/windows/bundle_dml_package.ps1
@@ -27,17 +27,27 @@ $arm64ExtractPath = "win-dml-arm64-unzipped"
Write-Host "Extracting $arm64ZipFile to $arm64ExtractPath..."
& $sevenZipPath x $arm64ZipFile -o"$arm64ExtractPath" -y
+# Debug: List contents of extracted arm64 zip
+Write-Host "Contents of $arm64ExtractPath (recursive):"
+Get-ChildItem -Path $arm64ExtractPath -Recurse | ForEach-Object { Write-Host " - $($_.FullName)" }
+
# 2. Find the target NuGet package.
# It finds all .nupkg files that do not contain "Managed" in their name.
-$nupkgFiles = Get-ChildItem -Path . -Recurse -Filter *.nupkg | Where-Object { $_.Name -notlike "*Managed*" }
+$nupkgFiles = Get-ChildItem -Path . -Filter *.nupkg | Where-Object { ($_.Name -notlike "*Managed*") -and ($_.Name -notlike "*.symbols.nupkg") }
+
+Write-Host "Found $($nupkgFiles.Count) candidate nupkg file(s) for bundling:"
+$nupkgFiles | ForEach-Object { Write-Host " - $($_.FullName)" }
-# 3. Validate that exactly one package was found.
-if ($nupkgFiles.Count -ne 1) {
- Write-Error "Error: Expected to find exactly one non-managed NuGet package, but found $($nupkgFiles.Count)."
+# 3. Select the best package (shortest name prefers Release over Dev, and Main over Symbols)
+if ($nupkgFiles.Count -eq 0) {
+ Write-Error "Error: No matching NuGet packages found to bundle into."
exit 1
}
-$nupkg = $nupkgFiles[0]
-Write-Host "Found package to process: $($nupkg.Name)"
+if ($nupkgFiles.Count -gt 1) {
+ Write-Warning "Found multiple packages. Selecting the one with the shortest filename as the target for bundling."
+}
+$nupkg = $nupkgFiles | Sort-Object {$_.Name.Length} | Select-Object -First 1
+Write-Host "Selected target package: $($nupkg.Name)"
# 4. Validate the package name matches the expected format.
if ($nupkg.Name -notlike "Microsoft.ML.OnnxRuntime.DirectML*.nupkg") {
@@ -61,14 +71,36 @@ New-Item -ItemType Directory -Path $tempDir | Out-Null
Write-Host "Extracting $($nupkg.Name) to $tempDir..."
& $sevenZipPath x $nupkg.FullName -o"$tempDir" -y
+# Debug: Print the .nuspec content
+$nuspecFile = Get-ChildItem -Path $tempDir -Filter *.nuspec | Select-Object -First 1
+if ($nuspecFile) {
+ Write-Host "Found manifest: $($nuspecFile.FullName)"
+ Write-Host "--- Manifest Content ---"
+ Get-Content $nuspecFile.FullName | ForEach-Object { Write-Host $_ }
+ Write-Host "------------------------"
+}
+
+# Debug: List contents of extracted target nupkg
+Write-Host "Contents of $tempDir (recursive):"
+Get-ChildItem -Path $tempDir -Recurse | ForEach-Object { Write-Host " - $($_.FullName)" }
+
# Step B: Create the new runtime directory structure.
$newRuntimePath = Join-Path $tempDir "runtimes\win-arm64\native"
+Write-Host "Ensuring destination path exists: $newRuntimePath"
New-Item -ItemType Directory -Path $newRuntimePath -Force | Out-Null
# Step C: Copy the ARM64 binaries into the new structure.
$arm64SourcePath = Join-Path . "$arm64ExtractPath\runtimes\win-arm64\native"
-Write-Host "Copying ARM64 binaries from $arm64SourcePath to $newRuntimePath..."
-Copy-Item -Path "$arm64SourcePath\*" -Destination $newRuntimePath -Recurse -Force
+if (Test-Path $arm64SourcePath) {
+ Write-Host "Copying ARM64 binaries from $arm64SourcePath to $newRuntimePath..."
+ $filesToCopy = Get-ChildItem -Path "$arm64SourcePath\*"
+ Write-Host "Files found in source: $($filesToCopy.Count)"
+ $filesToCopy | ForEach-Object { Write-Host " -> $($_.Name)" }
+ Copy-Item -Path "$arm64SourcePath\*" -Destination $newRuntimePath -Recurse -Force
+} else {
+ Write-Error "Error: ARM64 source path not found: $arm64SourcePath. Bailing out to avoid creating a broken package."
+ exit 1
+}
# Step D: Delete the original nupkg file.
Remove-Item -Path $nupkg.FullName -Force
@@ -79,6 +111,13 @@ Push-Location $tempDir
& $sevenZipPath a -tzip "$($nupkg.FullName)" ".\" -r
Pop-Location
+# Debug: Check final nupkg existence
+if (Test-Path $nupkg.FullName) {
+ Write-Host "Final package created successfully: $($nupkg.FullName)"
+ $finalSize = (Get-Item $nupkg.FullName).Length
+ Write-Host "Final package size: $finalSize bytes"
+}
+
# --- Cleanup and Final Steps ---
Write-Host "Cleaning up temporary directory $tempDir..."
Remove-Item -Recurse -Force $tempDir
@@ -91,4 +130,4 @@ Write-Host "Copying final artifact to $ArtifactStagingDirectory..."
Copy-Item -Path ".\Microsoft.ML.OnnxRuntime.DirectML*.nupkg" -Destination $ArtifactStagingDirectory -Force
Write-Host "---"
-Write-Host "Script completed successfully."
\ No newline at end of file
+Write-Host "Script completed successfully."
diff --git a/tools/ci_build/github/windows/select_dml_package.ps1 b/tools/ci_build/github/windows/select_dml_package.ps1
new file mode 100644
index 0000000000000..b6a3ed936ae23
--- /dev/null
+++ b/tools/ci_build/github/windows/select_dml_package.ps1
@@ -0,0 +1,86 @@
+# select_dml_package.ps1
+# Helper script to select the correct DML NuGet package based on build type
+# Usage: select_dml_package.ps1 -SourceDir -IsReleaseBuild -Action [-DestinationDir ] [-NewName ]
+
+param(
+ [Parameter(Mandatory=$true)]
+ [string]$SourceDir,
+
+ [Parameter(Mandatory=$true)]
+ [string]$IsReleaseBuild,
+
+ [Parameter(Mandatory=$true)]
+ [ValidateSet("copy", "rename")]
+ [string]$Action,
+
+ [Parameter(Mandatory=$false)]
+ [string]$DestinationDir,
+
+ [Parameter(Mandatory=$false)]
+ [string]$NewName
+)
+
+$ErrorActionPreference = "Stop"
+
+Write-Host "Searching for packages in: $SourceDir"
+Write-Host "IsReleaseBuild: $IsReleaseBuild"
+Write-Host "Action: $Action"
+
+# Convert string to boolean
+$isRelease = [System.Convert]::ToBoolean($IsReleaseBuild)
+
+# Find all matching packages
+$allPackages = Get-ChildItem -Path $SourceDir -Filter "Microsoft.ML.OnnxRuntime.DirectML.*.nupkg"
+Write-Host "Found $($allPackages.Count) total package(s):"
+$allPackages | ForEach-Object { Write-Host " - $($_.Name)" }
+
+# Filter packages based on build type
+$filteredPackages = $allPackages | Where-Object {
+ $name = $_.Name
+ $isSymbols = $name -like "*symbols*"
+ $isDev = $name -like "*-dev*"
+
+ if ($isSymbols) {
+ return $false
+ }
+
+ if ($isRelease) {
+ return -not $isDev
+ } else {
+ return $isDev
+ }
+}
+
+Write-Host "After filtering (isRelease=$isRelease), found $($filteredPackages.Count) matching package(s):"
+$filteredPackages | ForEach-Object { Write-Host " - $($_.Name)" }
+
+if ($filteredPackages.Count -eq 0) {
+ Write-Error "No matching package found!"
+ exit 1
+}
+
+# Select the first matching package (sorted by name length for consistency)
+$selectedPackage = $filteredPackages | Sort-Object { $_.Name.Length } | Select-Object -First 1
+Write-Host "Selected package: $($selectedPackage.FullName)"
+
+# Perform the action
+if ($Action -eq "copy") {
+ if (-not $DestinationDir) {
+ Write-Error "DestinationDir is required for copy action"
+ exit 1
+ }
+ Write-Host "Copying to: $DestinationDir"
+ Copy-Item -Path $selectedPackage.FullName -Destination $DestinationDir -Force
+ Write-Host "Copy successful."
+}
+elseif ($Action -eq "rename") {
+ if (-not $NewName) {
+ Write-Error "NewName is required for rename action"
+ exit 1
+ }
+ Write-Host "Renaming to: $NewName"
+ Rename-Item -Path $selectedPackage.FullName -NewName $NewName -Force
+ Write-Host "Rename successful."
+}
+
+exit 0
diff --git a/tools/nuget/validate_package.py b/tools/nuget/validate_package.py
index 961109c595ed5..0ad1fc07eafd7 100644
--- a/tools/nuget/validate_package.py
+++ b/tools/nuget/validate_package.py
@@ -67,6 +67,10 @@ def parse_arguments():
"--verify_nuget_signing",
help="Flag indicating if Nuget package signing is to be verified. Only accepts 'true' or 'false'",
)
+ parser.add_argument(
+ "--is_release_build",
+ help="Flag indicating if validating a release build or dev build. Only accepts 'true' or 'false'",
+ )
return parser.parse_args()
@@ -285,7 +289,14 @@ def validate_zip(args):
def validate_nuget(args):
files = glob.glob(os.path.join(args.package_path, args.package_name))
- nuget_packages_found_in_path = [i for i in files if i.endswith(".nupkg") and "Managed" not in i]
+ is_release_build = args.is_release_build and args.is_release_build.lower() == "true"
+ nuget_packages_found_in_path = [
+ i
+ for i in files
+ if i.endswith(".nupkg")
+ and "Managed" not in i
+ and ((is_release_build and "-dev" not in i) or (not is_release_build and "-dev" in i))
+ ]
if len(nuget_packages_found_in_path) != 1:
print("Nuget packages found in path: ")
print(nuget_packages_found_in_path)