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)