From 62f712ac9c3095e5eb445af0590fd996c8a25e35 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 07:44:07 +0000
Subject: [PATCH 01/31] Initial plan
From 695b6fa798ca98606082ba5bfcdf61338f988f27 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 07:55:47 +0000
Subject: [PATCH 02/31] Add TarDirectory task, registration, and resources
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks/Microsoft.Build.Tasks.csproj | 4 +
src/Tasks/Microsoft.Common.tasks | 2 +
src/Tasks/Resources/Strings.resx | 25 ++-
src/Tasks/Resources/xlf/Strings.cs.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.de.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.es.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.fr.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.it.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.ja.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.ko.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.pl.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.ru.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.tr.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 25 +++
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 25 +++
src/Tasks/TarDirectory.cs | 162 ++++++++++++++++++++
17 files changed, 517 insertions(+), 1 deletion(-)
create mode 100644 src/Tasks/TarDirectory.cs
diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj
index 61d3b660c9a..c011de3dbe5 100644
--- a/src/Tasks/Microsoft.Build.Tasks.csproj
+++ b/src/Tasks/Microsoft.Build.Tasks.csproj
@@ -275,6 +275,10 @@
+
+
+
+
diff --git a/src/Tasks/Microsoft.Common.tasks b/src/Tasks/Microsoft.Common.tasks
index dfc8e936e90..0df375bb732 100644
--- a/src/Tasks/Microsoft.Common.tasks
+++ b/src/Tasks/Microsoft.Common.tasks
@@ -102,6 +102,8 @@
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+
+
+ Creating tar archive from directory "{0}" to "{1}".
+
+
@@ -3260,7 +3283,7 @@
MSB3991 - MSB3999 Task: CombineTargetFrameworkInfoProperties
MSB4300 - MSB4310 Task: AddToWin32Manifest
MSB4311 - MSB4320 Task: FormatUrl
-
+ MSB4321 - MSB4330 Task: TarDirectory
MSB4000 - MSB4200 Portable targets & tasks (vsproject\flavors\portable\msbuild)
MSB9000 - MSB9900 MSBuild targets files (C++)
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index 73a76a789ed..9c9bb192656 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2672,6 +2672,31 @@
MSB3353: Nebyl zadán veřejný klíč nezbytný ke zpožděnému podepsání.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Úloha {0} se nepodporuje ve verzi MSBuildu pro .NET Core. Použijte prosím verzi MSBuildu pro .NET Framework. Další podrobnosti najdete na stránce https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index c838855ce14..047b1c114b8 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2672,6 +2672,31 @@
MSB3353: Der für die verzögerte Signierung erforderliche öffentliche Schlüssel wurde nicht angegeben.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Die Aufgabe "{0}" wird für die .NET Core-Version von MSBuild nicht unterstützt. Verwenden Sie die .NET Framework-Version von MSBuild. Weitere Informationen finden Sie unter https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index f16025a78b5..bb49a9a325d 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2672,6 +2672,31 @@
MSB3353: No se especificó la clave pública necesaria para la firma retardada.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: No se admite la tarea "{0}" en la versión de MSBuild de .NET Core. Use la versión de MSBuild de .NET Framework. Vea https://aka.ms/msbuild/MSB4803 para obtener más información.
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index 2d98ee8c877..415839f7a09 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2672,6 +2672,31 @@
MSB3353: La clé publique nécessaire à la signature différée n'a pas été spécifiée.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: La tâche "{0}" n'est pas prise en charge dans la version .NET Core de MSBuild. Utilisez la version du .NET Framework de MSBuild. Pour plus d'informations, consultez https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index e328f3c0b7c..ef328a0feeb 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2672,6 +2672,31 @@
MSB3353: chiave pubblica necessaria per la firma ritardata non specificata.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: l'attività "{0}" non è supportata nella versione .NET Core di MSBuild. Usare la versione .NET Framework di MSBuild. Per altri dettagli, vedere https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index 6188a1c8ed8..73d78f6dbd2 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2672,6 +2672,31 @@
MSB3353: 遅延署名に必要な公開キーは指定されませんでした。{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: タスク "{0}" は .NET Core バージョンの MSBuild ではサポートされていません。.NET Framework バージョンの MSBuild をご使用ください。詳細については、https://aka.ms/msbuild/MSB4803 をご覧ください。
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index 9cf4ee1fdd1..7128fe9d9da 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2672,6 +2672,31 @@
MSB3353: 서명 연기에 필요한 공개 키를 지정하지 않았습니다.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: "{0}" 작업은 MSBuild의 .NET Core 버전에서 지원되지 않습니다. MSBuild의 .NET Framework 버전을 사용하세요. 자세한 내용은 https://aka.ms/msbuild/MSB4803을 참조하세요.
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index f2899e7b2be..42605f755a4 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2672,6 +2672,31 @@
MSB3353: Klucz publiczny jest niezbędny, ponieważ nie określono znaku opóźnienia.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Zadanie „{0}” nie jest obsługiwane w wersji programu MSBuild dla platformy .NET Core. Użyj wersji programu MSBuild dla platformy .NET Framework. Zobacz https://aka.ms/msbuild/MSB4803, aby uzyskać więcej szczegółów.
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index 4a05b4d2b66..34b5e9974e6 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2672,6 +2672,31 @@
MSB3353: Chave pública necessária, pois a assinatura atrasada não foi especificada.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: a tarefa "{0}" não é compatível com a versão do .NET Core do MSBuild. Use a versão do .NET Framework do MSBuild. Confira https://aka.ms/msbuild/MSB4803 para obter mais detalhes.
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index 65f65509596..1ac076b56ec 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2672,6 +2672,31 @@
MSB3353: Не указан публичный ключ, необходимый для отложенной подписи.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: задача "{0}" не поддерживается в MSBuild версии .NET Core. Используйте MSBuild версии .NET Framework. Дополнительные сведения: https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index ea3c7d9c778..13f89962c85 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2672,6 +2672,31 @@
MSB3353: Gecikmeli imzalama için gerekli olan ortak anahtar belirtilmemiş.{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild’in .NET Core sürümünde "{0}" görevi desteklenmiyor. Lütfen MSBuild’in .NET Framework sürümünü kullanın. Daha ayrıntılı bilgi için bkz. https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index b731749c0f7..4389deb2d6d 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2672,6 +2672,31 @@
MSB3353: 未指定延迟签名所需的公钥。{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: .NET Core 版本的 MSBuild 不支持“{0}”。请使用 .NET Framework 版本的 MSBuild。有关更多详细信息,请参阅 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index a7bd6c633a5..8ed92b84373 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2672,6 +2672,31 @@
MSB3353: 未指定延遲簽署所需的公開金鑰。{StrBegin="MSB3353: "}
+
+ Creating tar archive from directory "{0}" to "{1}".
+ Creating tar archive from directory "{0}" to "{1}".
+
+
+
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ MSB4321: Failed to tar directory "{0}" because it does not exist or is inaccessible.
+ {StrBegin="MSB4321: "}
+
+
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}
+ {StrBegin="MSB4323: "}
+
+
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.
+ {StrBegin="MSB4322: "}
+
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild 的 .NET Core 版本不支援工作 "{0}"。請使用 MSBuild 的 .NET Framework 版本。如需進一步的詳細資料,請參閱 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
new file mode 100644
index 00000000000..b62dd0c481f
--- /dev/null
+++ b/src/Tasks/TarDirectory.cs
@@ -0,0 +1,162 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Formats.Tar;
+using System.IO;
+using System.IO.Compression;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.Build.Tasks
+{
+ ///
+ /// Represents a task that can create a tar archive from a directory.
+ ///
+ ///
+ /// This task uses the APIs which are only available when MSBuild
+ /// runs on .NET (not .NET Framework). It is therefore registered to run only on the .NET runtime and
+ /// is unavailable in Visual Studio / MSBuild.exe.
+ ///
+ [MSBuildMultiThreadableTask]
+ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThreadableTask
+ {
+ public const string CompressionNone = "None";
+ public const string CompressionGZip = "GZip";
+
+ ///
+ /// Gets or sets a containing the full path to the destination file to create.
+ ///
+ [Required]
+ public ITaskItem DestinationFile { get; set; } = null!;
+
+ ///
+ /// Gets or sets a value indicating whether the destination file should be overwritten.
+ ///
+ public bool Overwrite { get; set; }
+
+ ///
+ /// Gets or sets a containing the full path to the source directory to create a tar archive from.
+ ///
+ [Required]
+ public ITaskItem SourceDirectory { get; set; } = null!;
+
+ ///
+ /// Question the incremental nature of this task.
+ ///
+ /// This task does not support incremental build and will error out instead.
+ public bool FailIfNotIncremental { get; set; }
+
+ ///
+ /// Gets or sets the compression to apply to the tar archive.
+ /// Valid values are "None" (the default) and "GZip".
+ /// This parameter is optional.
+ ///
+ public string? Compression { get; set; }
+
+ ///
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
+ public override bool Execute()
+ {
+ AbsolutePath sourceDirectoryAbsolutePath = TaskEnvironment.GetAbsolutePath(SourceDirectory.ItemSpec);
+ DirectoryInfo sourceDirectory = new DirectoryInfo(sourceDirectoryAbsolutePath);
+
+ if (!sourceDirectory.Exists)
+ {
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorDirectoryDoesNotExist", sourceDirectory.FullName);
+ return false;
+ }
+
+ AbsolutePath destinationFileAbsolutePath = TaskEnvironment.GetAbsolutePath(DestinationFile.ItemSpec);
+ FileInfo destinationFile = new FileInfo(destinationFileAbsolutePath);
+
+ BuildEngine3.Yield();
+
+ try
+ {
+ if (destinationFile.Exists)
+ {
+ if (!Overwrite || FailIfNotIncremental)
+ {
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFileExists", destinationFile.FullName);
+
+ return false;
+ }
+
+ try
+ {
+ File.Delete(destinationFile.FullName);
+ }
+ catch (Exception e)
+ {
+ string lockedFileMessage = LockCheck.GetLockedFileMessage(destinationFile.FullName);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", sourceDirectory.FullName, destinationFile.FullName, e.Message, lockedFileMessage);
+
+ return false;
+ }
+ }
+
+ try
+ {
+ if (FailIfNotIncremental)
+ {
+ Log.LogErrorFromResources("TarDirectory.Comment", sourceDirectory.FullName, destinationFile.FullName);
+ }
+ else
+ {
+ Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", sourceDirectory.FullName, destinationFile.FullName);
+
+ bool useGZip = false;
+ if (!string.IsNullOrEmpty(Compression) && !TryParseCompression(Compression!, out useGZip))
+ {
+ Log.LogWarningWithCodeFromResources("TarDirectory.WarningInvalidCompression", Compression);
+ }
+
+ if (useGZip)
+ {
+ using FileStream destinationStream = new FileStream(destinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
+ using GZipStream gzipStream = new GZipStream(destinationStream, CompressionLevel.Optimal);
+ TarFile.CreateFromDirectory(sourceDirectory.FullName, gzipStream, includeBaseDirectory: false);
+ }
+ else
+ {
+ TarFile.CreateFromDirectory(sourceDirectory.FullName, destinationFile.FullName, includeBaseDirectory: false);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", sourceDirectory.FullName, destinationFile.FullName, e.Message, string.Empty);
+ }
+ }
+ finally
+ {
+ BuildEngine3.Reacquire();
+ }
+
+ return !Log.HasLoggedErrors;
+
+ static bool TryParseCompression(string compression, out bool useGZip)
+ {
+ if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionNone))
+ {
+ useGZip = false;
+ }
+ else if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionGZip)
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "gzip"))
+ {
+ useGZip = true;
+ }
+ else
+ {
+ useGZip = false;
+ return false;
+ }
+
+ return true;
+ }
+ }
+ }
+}
From c0998f2c2ea59720eb11bf7f9e2058dddfb84308 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 07:58:51 +0000
Subject: [PATCH 03/31] Add TarDirectory tests and docs; verified end-to-end
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
documentation/specs/question.md | 3 +
src/Tasks.UnitTests/TarDirectory_Tests.cs | 204 ++++++++++++++++++++++
2 files changed, 207 insertions(+)
create mode 100644 src/Tasks.UnitTests/TarDirectory_Tests.cs
diff --git a/documentation/specs/question.md b/documentation/specs/question.md
index f46aa910af8..b8edb783194 100644
--- a/documentation/specs/question.md
+++ b/documentation/specs/question.md
@@ -78,6 +78,9 @@ Error when SkipUnchangedFiles is true.
`ZipDirectory`
Error if the destination zip file doesn't exists.
+`TarDirectory`
+Error if the destination tar file doesn't exists.
+
## Common Error
- **Typographical error**. Spelling, casing, or incorrect path. Check if the target inputs and outputs real files.
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
new file mode 100644
index 00000000000..baffae4d260
--- /dev/null
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -0,0 +1,204 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// TarDirectory relies on System.Formats.Tar which is only available on .NET (not .NET Framework).
+#if NET
+
+using System;
+using System.Collections.Generic;
+using System.Formats.Tar;
+using System.IO;
+using System.IO.Compression;
+using Microsoft.Build.UnitTests;
+using Microsoft.Build.Utilities;
+using Shouldly;
+using Xunit;
+
+namespace Microsoft.Build.Tasks.UnitTests
+{
+ public class TarDirectory_Tests
+ {
+ private readonly MockEngine _mockEngine = new MockEngine();
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("None")]
+ [InlineData("GZip")]
+ [InlineData("gz")]
+ public void CanTarDirectory(string? compression)
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "6DE6060259C44DB6B145159376751C22.txt", "6DE6060259C44DB6B145159376751C22");
+ testEnvironment.CreateFile(sourceFolder, "CDA3DD8C25A54A7CAC638A444CB1EAD0.txt", "CDA3DD8C25A54A7CAC638A444CB1EAD0");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ Compression = compression,
+ DestinationFile = new TaskItem(tarFilePath),
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain(sourceFolder.Path, customMessage: _mockEngine.Log);
+ _mockEngine.Log.ShouldContain(tarFilePath, customMessage: _mockEngine.Log);
+
+ // Should not contain any warnings in the TarDirectory bucket (MSB4321 - MSB4330).
+ _mockEngine.Log.ShouldNotContain("MSB432", customMessage: _mockEngine.Log);
+
+ bool isGZip = compression is not null && !StringComparer.OrdinalIgnoreCase.Equals(compression, "None");
+
+ GetTarEntryNames(tarFilePath, isGZip)
+ .ShouldBe(
+ [
+ "6DE6060259C44DB6B145159376751C22.txt",
+ "CDA3DD8C25A54A7CAC638A444CB1EAD0.txt"
+ ],
+ ignoreOrder: true);
+ }
+ }
+
+ [Fact]
+ public void CanOverwriteExistingFile()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "F1C22D660B0D4DAAA296C1B980320B03.txt", "F1C22D660B0D4DAAA296C1B980320B03");
+ testEnvironment.CreateFile(sourceFolder, "AA825D1CB154492BAA58E1002CE1DFEB.txt", "AA825D1CB154492BAA58E1002CE1DFEB");
+
+ TransientTestFile file = testEnvironment.CreateFile(testEnvironment.DefaultTestDirectory, "test.tar", contents: "test");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DestinationFile = new TaskItem(file.Path),
+ Overwrite = true,
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain(sourceFolder.Path, customMessage: _mockEngine.Log);
+ _mockEngine.Log.ShouldContain(file.Path, customMessage: _mockEngine.Log);
+
+ GetTarEntryNames(file.Path, isGZip: false)
+ .ShouldBe(
+ [
+ "F1C22D660B0D4DAAA296C1B980320B03.txt",
+ "AA825D1CB154492BAA58E1002CE1DFEB.txt"
+ ],
+ ignoreOrder: true);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorIfDestinationExists()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder folder = testEnvironment.CreateFolder(createFolder: true);
+
+ TransientTestFile file = testEnvironment.CreateFile("foo.tar", "foo");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DestinationFile = new TaskItem(file.Path),
+ SourceDirectory = new TaskItem(folder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4322", customMessage: _mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorIfDirectoryDoesNotExist()
+ {
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DestinationFile = new TaskItem(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "test.tar")),
+ SourceDirectory = new TaskItem(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4321", customMessage: _mockEngine.Log);
+ }
+
+ [Fact]
+ public void LogsWarningForInvalidCompression()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "9E0A4E0F5C8D4F0FA0B33F79C2F0B0C1.txt", "content");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ Compression = "RandomUnsupportedValue",
+ DestinationFile = new TaskItem(tarFilePath),
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ // Invalid compression is a warning, not an error; an uncompressed tar is created instead.
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4324", customMessage: _mockEngine.Log);
+
+ GetTarEntryNames(tarFilePath, isGZip: false)
+ .ShouldBe(["9E0A4E0F5C8D4F0FA0B33F79C2F0B0C1.txt"]);
+ }
+ }
+
+ private static List GetTarEntryNames(string tarFilePath, bool isGZip)
+ {
+ List names = new List();
+
+ using FileStream stream = new FileStream(tarFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ Stream tarStream = isGZip ? new GZipStream(stream, CompressionMode.Decompress) : stream;
+
+ try
+ {
+ using TarReader reader = new TarReader(tarStream);
+ for (TarEntry? entry = reader.GetNextEntry(); entry is not null; entry = reader.GetNextEntry())
+ {
+ if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile)
+ {
+ names.Add(entry.Name);
+ }
+ }
+ }
+ finally
+ {
+ if (isGZip)
+ {
+ tarStream.Dispose();
+ }
+ }
+
+ return names;
+ }
+ }
+}
+
+#endif
From d769dc6d58ac5757897b1e5414b122c7fe7f97c3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 08:01:07 +0000
Subject: [PATCH 04/31] Address code review: remove redundant null-forgiving op
and fix doc grammar
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
documentation/specs/question.md | 2 +-
src/Tasks/TarDirectory.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/documentation/specs/question.md b/documentation/specs/question.md
index b8edb783194..61667e6781a 100644
--- a/documentation/specs/question.md
+++ b/documentation/specs/question.md
@@ -79,7 +79,7 @@ Error when SkipUnchangedFiles is true.
Error if the destination zip file doesn't exists.
`TarDirectory`
-Error if the destination tar file doesn't exists.
+Error if the destination tar file doesn't exist.
## Common Error
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index b62dd0c481f..94c8e237b4d 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -108,7 +108,7 @@ public override bool Execute()
Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", sourceDirectory.FullName, destinationFile.FullName);
bool useGZip = false;
- if (!string.IsNullOrEmpty(Compression) && !TryParseCompression(Compression!, out useGZip))
+ if (!string.IsNullOrEmpty(Compression) && !TryParseCompression(Compression, out useGZip))
{
Log.LogWarningWithCodeFromResources("TarDirectory.WarningInvalidCompression", Compression);
}
From 6ba6a02e63b283b47fe730127c4a56b5b2471d29 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 08:27:12 +0000
Subject: [PATCH 05/31] Error on unrecognized TarDirectory Compression instead
of falling back
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks.UnitTests/TarDirectory_Tests.cs | 9 ++++-----
src/Tasks/Resources/Strings.resx | 4 ++--
src/Tasks/Resources/xlf/Strings.cs.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.de.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.es.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.fr.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.it.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.ja.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.ko.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.pl.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.ru.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.tr.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 6 +++---
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 6 +++---
src/Tasks/TarDirectory.cs | 4 +++-
16 files changed, 48 insertions(+), 47 deletions(-)
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
index baffae4d260..91dbdb12686 100644
--- a/src/Tasks.UnitTests/TarDirectory_Tests.cs
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -141,7 +141,7 @@ public void LogsErrorIfDirectoryDoesNotExist()
}
[Fact]
- public void LogsWarningForInvalidCompression()
+ public void LogsErrorForInvalidCompression()
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
@@ -160,13 +160,12 @@ public void LogsWarningForInvalidCompression()
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
- // Invalid compression is a warning, not an error; an uncompressed tar is created instead.
- tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+ // Invalid compression is an error and no archive is created.
+ tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
_mockEngine.Log.ShouldContain("MSB4324", customMessage: _mockEngine.Log);
- GetTarEntryNames(tarFilePath, isGZip: false)
- .ShouldBe(["9E0A4E0F5C8D4F0FA0B33F79C2F0B0C1.txt"]);
+ File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
}
}
diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx
index d3a0ccb9c57..dda23f2b3fe 100644
--- a/src/Tasks/Resources/Strings.resx
+++ b/src/Tasks/Resources/Strings.resx
@@ -3110,8 +3110,8 @@
MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}{StrBegin="MSB4323: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index 9c9bb192656..7d001a363da 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index 047b1c114b8..b161d9eb2d8 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index bb49a9a325d..d1447ecc78d 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index 415839f7a09..6c1a9b01a37 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index ef328a0feeb..4559e776bcf 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index 73d78f6dbd2..916bdbfd1c3 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index 7128fe9d9da..df99b4317fa 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index 42605f755a4..15128822c4f 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index 34b5e9974e6..82eda17d42a 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index 1ac076b56ec..1d5ec9f7483 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index 13f89962c85..aa421830d4e 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index 4389deb2d6d..1581357bc15 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index 8ed92b84373..e0537a8cfeb 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2692,9 +2692,9 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'. Creating an uncompressed tar archive instead.
+
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 94c8e237b4d..c3c5b13b89c 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -110,7 +110,9 @@ public override bool Execute()
bool useGZip = false;
if (!string.IsNullOrEmpty(Compression) && !TryParseCompression(Compression, out useGZip))
{
- Log.LogWarningWithCodeFromResources("TarDirectory.WarningInvalidCompression", Compression);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorInvalidCompression", Compression);
+
+ return false;
}
if (useGZip)
From 4835444ce62fffe48c8a7688ee944420527b68ac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 10:07:49 +0000
Subject: [PATCH 06/31] Add optional Format parameter to TarDirectory
(Pax/Gnu/Ustar/V7)
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks.UnitTests/TarDirectory_Tests.cs | 107 ++++++++++++++++++++
src/Tasks/Resources/Strings.resx | 4 +
src/Tasks/Resources/xlf/Strings.cs.xlf | 5 +
src/Tasks/Resources/xlf/Strings.de.xlf | 5 +
src/Tasks/Resources/xlf/Strings.es.xlf | 5 +
src/Tasks/Resources/xlf/Strings.fr.xlf | 5 +
src/Tasks/Resources/xlf/Strings.it.xlf | 5 +
src/Tasks/Resources/xlf/Strings.ja.xlf | 5 +
src/Tasks/Resources/xlf/Strings.ko.xlf | 5 +
src/Tasks/Resources/xlf/Strings.pl.xlf | 5 +
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 5 +
src/Tasks/Resources/xlf/Strings.ru.xlf | 5 +
src/Tasks/Resources/xlf/Strings.tr.xlf | 5 +
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 5 +
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 5 +
src/Tasks/TarDirectory.cs | 91 ++++++++++++++++-
16 files changed, 264 insertions(+), 3 deletions(-)
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
index 91dbdb12686..a5168eee14a 100644
--- a/src/Tasks.UnitTests/TarDirectory_Tests.cs
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -169,6 +169,99 @@ public void LogsErrorForInvalidCompression()
}
}
+ [Theory]
+ [InlineData("Pax", TarEntryFormat.Pax)]
+ [InlineData("gnu", TarEntryFormat.Gnu)]
+ [InlineData("Ustar", TarEntryFormat.Ustar)]
+ [InlineData("V7", TarEntryFormat.V7)]
+ public void CanTarDirectoryWithFormat(string format, TarEntryFormat expectedFormat)
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "3F6D2F2E3C1A4B5C8D9E0F1A2B3C4D5E.txt", "content");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ Format = format,
+ DestinationFile = new TaskItem(tarFilePath),
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ GetTarEntryFormats(tarFilePath)
+ .ShouldAllBe(entryFormat => entryFormat == expectedFormat, _mockEngine.Log);
+
+ GetTarEntryNames(tarFilePath, isGZip: false)
+ .ShouldBe(["3F6D2F2E3C1A4B5C8D9E0F1A2B3C4D5E.txt"]);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorForInvalidFormat()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "8B0F5A2D6E1C4F0FB1C44F80D3F1C1D2.txt", "content");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ Format = "RandomUnsupportedValue",
+ DestinationFile = new TaskItem(tarFilePath),
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ // Invalid format is an error and no archive is created.
+ tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4325", customMessage: _mockEngine.Log);
+
+ File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorForUnknownFormat()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "1A2B3C4D5E6F70819AABBCCDDEEFF001.txt", "content");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+
+ // "Unknown" parses to TarEntryFormat.Unknown, which is not a valid archive format.
+ Format = "Unknown",
+ DestinationFile = new TaskItem(tarFilePath),
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4325", customMessage: _mockEngine.Log);
+
+ File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
+ }
+ }
+
private static List GetTarEntryNames(string tarFilePath, bool isGZip)
{
List names = new List();
@@ -197,6 +290,20 @@ private static List GetTarEntryNames(string tarFilePath, bool isGZip)
return names;
}
+
+ private static List GetTarEntryFormats(string tarFilePath)
+ {
+ List formats = new List();
+
+ using FileStream stream = new FileStream(tarFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using TarReader reader = new TarReader(stream);
+ for (TarEntry? entry = reader.GetNextEntry(); entry is not null; entry = reader.GetNextEntry())
+ {
+ formats.Add(entry.Format);
+ }
+
+ return formats;
+ }
}
}
diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx
index dda23f2b3fe..e7131fe672a 100644
--- a/src/Tasks/Resources/Strings.resx
+++ b/src/Tasks/Resources/Strings.resx
@@ -3114,6 +3114,10 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+
Creating tar archive from directory "{0}" to "{1}".
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index 7d001a363da..6a3a9b8a0fa 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Úloha {0} se nepodporuje ve verzi MSBuildu pro .NET Core. Použijte prosím verzi MSBuildu pro .NET Framework. Další podrobnosti najdete na stránce https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index b161d9eb2d8..590230382cd 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Die Aufgabe "{0}" wird für die .NET Core-Version von MSBuild nicht unterstützt. Verwenden Sie die .NET Framework-Version von MSBuild. Weitere Informationen finden Sie unter https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index d1447ecc78d..6a7b5ed46f4 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: No se admite la tarea "{0}" en la versión de MSBuild de .NET Core. Use la versión de MSBuild de .NET Framework. Vea https://aka.ms/msbuild/MSB4803 para obtener más información.
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index 6c1a9b01a37..4464f2c281a 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: La tâche "{0}" n'est pas prise en charge dans la version .NET Core de MSBuild. Utilisez la version du .NET Framework de MSBuild. Pour plus d'informations, consultez https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index 4559e776bcf..7ea59560734 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: l'attività "{0}" non è supportata nella versione .NET Core di MSBuild. Usare la versione .NET Framework di MSBuild. Per altri dettagli, vedere https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index 916bdbfd1c3..d8178563620 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: タスク "{0}" は .NET Core バージョンの MSBuild ではサポートされていません。.NET Framework バージョンの MSBuild をご使用ください。詳細については、https://aka.ms/msbuild/MSB4803 をご覧ください。
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index df99b4317fa..c0dc3ea9740 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: "{0}" 작업은 MSBuild의 .NET Core 버전에서 지원되지 않습니다. MSBuild의 .NET Framework 버전을 사용하세요. 자세한 내용은 https://aka.ms/msbuild/MSB4803을 참조하세요.
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index 15128822c4f..a42c628c844 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Zadanie „{0}” nie jest obsługiwane w wersji programu MSBuild dla platformy .NET Core. Użyj wersji programu MSBuild dla platformy .NET Framework. Zobacz https://aka.ms/msbuild/MSB4803, aby uzyskać więcej szczegółów.
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index 82eda17d42a..6ba3750729d 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: a tarefa "{0}" não é compatível com a versão do .NET Core do MSBuild. Use a versão do .NET Framework do MSBuild. Confira https://aka.ms/msbuild/MSB4803 para obter mais detalhes.
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index 1d5ec9f7483..2283c1cb47e 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: задача "{0}" не поддерживается в MSBuild версии .NET Core. Используйте MSBuild версии .NET Framework. Дополнительные сведения: https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index aa421830d4e..152b9e7027b 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild’in .NET Core sürümünde "{0}" görevi desteklenmiyor. Lütfen MSBuild’in .NET Framework sürümünü kullanın. Daha ayrıntılı bilgi için bkz. https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index 1581357bc15..bbf5a745df0 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: .NET Core 版本的 MSBuild 不支持“{0}”。请使用 .NET Framework 版本的 MSBuild。有关更多详细信息,请参阅 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index e0537a8cfeb..8c2833af074 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2697,6 +2697,11 @@
MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.{StrBegin="MSB4324: "}
+
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
+ {StrBegin="MSB4325: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild 的 .NET Core 版本不支援工作 "{0}"。請使用 MSBuild 的 .NET Framework 版本。如需進一步的詳細資料,請參閱 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index c3c5b13b89c..6b207b51c33 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -2,9 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
+using System.IO.Enumeration;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -54,6 +56,13 @@ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThread
///
public string? Compression { get; set; }
+ ///
+ /// Gets or sets the tar entry format to use for the archive.
+ /// Valid values are "Pax" (the default), "Gnu", "Ustar", and "V7".
+ /// This parameter is optional.
+ ///
+ public string? Format { get; set; }
+
///
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
@@ -115,15 +124,24 @@ public override bool Execute()
return false;
}
+ TarEntryFormat format = TarEntryFormat.Pax;
+ if (!string.IsNullOrEmpty(Format) && !TryParseFormat(Format, out format))
+ {
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorInvalidFormat", Format);
+
+ return false;
+ }
+
+ using FileStream destinationStream = new FileStream(destinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
+
if (useGZip)
{
- using FileStream destinationStream = new FileStream(destinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
using GZipStream gzipStream = new GZipStream(destinationStream, CompressionLevel.Optimal);
- TarFile.CreateFromDirectory(sourceDirectory.FullName, gzipStream, includeBaseDirectory: false);
+ CreateTarFromDirectory(sourceDirectory.FullName, gzipStream, format);
}
else
{
- TarFile.CreateFromDirectory(sourceDirectory.FullName, destinationFile.FullName, includeBaseDirectory: false);
+ CreateTarFromDirectory(sourceDirectory.FullName, destinationStream, format);
}
}
}
@@ -159,6 +177,73 @@ static bool TryParseCompression(string compression, out bool useGZip)
return true;
}
+
+ static bool TryParseFormat(string format, out TarEntryFormat tarFormat) =>
+ Enum.TryParse(format, ignoreCase: true, out tarFormat) && tarFormat != TarEntryFormat.Unknown;
+ }
+
+ ///
+ /// Writes all filesystem entries under to as a tar archive.
+ ///
+ ///
+ /// This mirrors the behavior of with
+ /// includeBaseDirectory: false, but additionally honors the requested . The
+ /// overloads that accept a are not available on the
+ /// net10.0 API surface MSBuild targets, so the directory walk is performed directly against a
+ /// .
+ ///
+ private static void CreateTarFromDirectory(string sourceDirectoryFullPath, Stream destination, TarEntryFormat format)
+ {
+ using TarWriter writer = new TarWriter(destination, format, leaveOpen: true);
+
+ foreach ((string fullPath, string entryName) in EnumerateEntries(sourceDirectoryFullPath, sourceDirectoryFullPath.Length))
+ {
+ writer.WriteEntry(fullPath, entryName);
+ }
+ }
+
+ ///
+ /// Recursively enumerates the filesystem entries under , yielding the full path and
+ /// the corresponding archive entry name for each, while avoiding recursion into directory symlinks.
+ ///
+ private static IEnumerable<(string fullPath, string entryName)> EnumerateEntries(string directory, int basePathLength)
+ {
+ // Recurse into subdirectories after yielding their own entry so that directory entries precede their contents,
+ // matching the ordering produced by TarFile.CreateFromDirectory.
+ FileSystemEnumerable<(string fullPath, string entryName, bool recurse)> enumerable = new(
+ directory,
+ (ref FileSystemEntry entry) =>
+ {
+ string fullPath = entry.ToFullPath();
+ bool isRealDirectory = entry.IsDirectory && (entry.Attributes & FileAttributes.ReparsePoint) == 0;
+ string entryName = GetEntryName(fullPath.AsSpan(basePathLength), appendDirectorySeparator: isRealDirectory);
+ return (fullPath, entryName, isRealDirectory);
+ });
+
+ foreach ((string fullPath, string entryName, bool recurse) in enumerable)
+ {
+ yield return (fullPath, entryName);
+
+ if (recurse)
+ {
+ foreach ((string fullPath, string entryName) inner in EnumerateEntries(fullPath, basePathLength))
+ {
+ yield return inner;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Converts a filesystem path that is relative to the archive root into a tar entry name using forward slashes,
+ /// appending a trailing slash for directory entries.
+ ///
+ private static string GetEntryName(ReadOnlySpan relativePath, bool appendDirectorySeparator)
+ {
+ relativePath = relativePath.TrimStart(['/', '\\']);
+ string entryName = relativePath.ToString().Replace('\\', '/');
+
+ return appendDirectorySeparator ? entryName + '/' : entryName;
}
}
}
From e804b4a09250db8c32a498bf6c946c826e5d1c95 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 10:09:34 +0000
Subject: [PATCH 07/31] Address review: rename recursive entry variable and
avoid array alloc in TrimStart
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks/TarDirectory.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 6b207b51c33..da8d49511a4 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -226,9 +226,9 @@ private static void CreateTarFromDirectory(string sourceDirectoryFullPath, Strea
if (recurse)
{
- foreach ((string fullPath, string entryName) inner in EnumerateEntries(fullPath, basePathLength))
+ foreach ((string fullPath, string entryName) childEntry in EnumerateEntries(fullPath, basePathLength))
{
- yield return inner;
+ yield return childEntry;
}
}
}
@@ -240,7 +240,7 @@ private static void CreateTarFromDirectory(string sourceDirectoryFullPath, Strea
///
private static string GetEntryName(ReadOnlySpan relativePath, bool appendDirectorySeparator)
{
- relativePath = relativePath.TrimStart(['/', '\\']);
+ relativePath = relativePath.TrimStart("/\\".AsSpan());
string entryName = relativePath.ToString().Replace('\\', '/');
return appendDirectorySeparator ? entryName + '/' : entryName;
From ae91e6521d2f10d606147810906de66cc132bd3a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 10:10:51 +0000
Subject: [PATCH 08/31] Simplify recursive foreach tuple deconstruction in
TarDirectory
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks/TarDirectory.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index da8d49511a4..e3efe91f6f6 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -226,7 +226,7 @@ private static void CreateTarFromDirectory(string sourceDirectoryFullPath, Strea
if (recurse)
{
- foreach ((string fullPath, string entryName) childEntry in EnumerateEntries(fullPath, basePathLength))
+ foreach (var childEntry in EnumerateEntries(fullPath, basePathLength))
{
yield return childEntry;
}
From a89e30ae5730ffab4559fceef093b52446e8ebf3 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 12:16:17 +0200
Subject: [PATCH 09/31] Update TFM to net11.0 and update accompanying files
---
.github/skills/running-unit-tests/SKILL.md | 18 +-
Directory.Build.props | 3 +-
Directory.Build.targets | 7 -
MSBuild.VSTest.slnx | 9 +-
azure-pipelines/cts-steps/apply-steps.yml | 6 +-
azure-pipelines/cts-steps/collect-steps.yml | 12 +-
documentation/aot/aot-trimming-strategy.md | 2 +-
documentation/aot/sdk-resolution.md | 5 +-
documentation/aot/task-factory-aot.md | 8 +-
.../sdk-resolver-host-registration-api.md | 3 +-
eng/BootStrapMsBuild.targets | 2 +-
global.json | 6 +-
.../multithreaded-task-migration/SKILL.md | 4 +-
scripts/Deploy-MSBuild.ps1 | 2 +-
scripts/cts/METRICS.md | 2 +-
scripts/cts/README.md | 12 +-
scripts/cts/_Common.ps1 | 6 +-
scripts/cts/cts.config.json | 2 +-
...ft.Build.Engine.OM.UnitTests.VSTest.csproj | 4 +-
.../Graph/ProjectGraph_Tests.cs | 18 +-
...osoft.Build.Engine.UnitTests.VSTest.csproj | 4 +-
.../ProjectCache/ProjectCacheTests.cs | 4 +-
src/Build/CompatibilitySuppressions.xml | 9 +-
...t.Build.BuildCheck.UnitTests.VSTest.csproj | 4 +-
src/Directory.BeforeCommon.targets | 2 +-
...ft.Build.Framework.UnitTests.VSTest.csproj | 4 +-
src/Framework/CompatibilitySuppressions.xml | 216 +++++++++++++++++-
.../Polyfills/AotTrimmingPolyfills.cs | 4 +-
src/Framework/README.md | 2 +-
src/MSBuild.Benchmarks/ExpanderBenchmark.cs | 6 +-
.../PartialEvaluationBenchmark.cs | 2 +-
src/MSBuild.Benchmarks/readme.md | 8 +-
.../MSBuild.Bootstrap.csproj | 2 +-
.../CoordinatorIntegration_Tests.cs | 2 +-
.../ConsoleApp/ConsoleApp.csproj | 5 +-
.../Library1/Library1.csproj | 4 +-
.../Library2/Library2.csproj | 4 +-
.../Library3/Library3.csproj | 4 +-
.../Library4/Library4.csproj | 4 +-
.../SingleProject/SingleProject.csproj | 2 +-
....Build.CommandLine.UnitTests.VSTest.csproj | 4 +-
.../StringTools.UnitTests.VSTest.csproj | 5 +-
.../TaskAnalyzer.Tests.csproj | 2 +-
.../AssemblyFolderCollection_Tests.cs | 4 +-
...rosoft.Build.Tasks.UnitTests.VSTest.csproj | 4 +-
src/Tasks/CompatibilitySuppressions.xml | 62 ++++-
src/UnitTests.Shared/RunnerUtilities.cs | 4 +-
...ft.Build.Utilities.UnitTests.VSTest.csproj | 4 +-
src/Utilities/CompatibilitySuppressions.xml | 48 +++-
src/Utilities/README.md | 2 +-
.../Microsoft.Build.AotValidation.csproj | 2 +-
src/aot-validation/ObjectModelAotTests.cs | 8 +-
src/aot-validation/README.md | 4 +-
53 files changed, 450 insertions(+), 126 deletions(-)
diff --git a/.github/skills/running-unit-tests/SKILL.md b/.github/skills/running-unit-tests/SKILL.md
index bf10838a3db..daa5aa9d5cc 100644
--- a/.github/skills/running-unit-tests/SKILL.md
+++ b/.github/skills/running-unit-tests/SKILL.md
@@ -21,7 +21,7 @@ Use a **fast, scoped** `dotnet test` loop while iterating, and the **full `build
These are configured in `Directory.Build.props` (repo root), `src/Directory.Build.targets`, and `src/Shared/UnitTests/xunit.runner.json`. They apply to both `dotnet test` and `build.cmd -test` unless noted otherwise:
-- **Multi-targeting**: test projects target `net472` *and* `net10.0` on Windows (`net10.0` only on Linux/macOS). `dotnet test` runs the suite **once per TFM**.
+- **Multi-targeting**: test projects target .NETFramework and .NETCoreApp on Windows (.NETCoreApp only on Linux/macOS). `dotnet test` runs the suite **once per TFM**.
- **Single-threaded by default**: `xunit.runner.json` sets `maxParallelThreads: 1` and `parallelizeTestCollections: false`. Many tests mutate process-global state (env vars, cwd, SDK resolvers), so this is intentional.
- **Auto trait filters**: platform/TFM-inappropriate tests are filtered out via `--filter-not-trait Category=...` (e.g., `nonwindowstests`, `failing`, `nonnetcoreapptests`). Don't try to "fix" tests that appear skipped because of these.
- **Coverage on non-Windows**: `--coverage --coverage-settings Coverage.config` is appended unconditionally to `XunitOptions` in `src/Directory.Build.targets`. There is no MSBuild property switch to disable it from `dotnet test` — to skip coverage, run the test exe directly without `--coverage`.
@@ -42,26 +42,26 @@ For comprehensive MTP vs VSTest flag reference, see the `run-tests` skill from t
Aim for sub-30s iterations.
-> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with `dotnet` (`src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj`); on Linux/macOS use `/` and shell line continuations (`\`), and the exe path becomes `artifacts/bin//Debug/net10.0/` (no `.exe`).
+> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with `dotnet` (`src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj`); on Linux/macOS use `/` and shell line continuations (`\`), and the exe path becomes `artifacts/bin//Debug/net11.0/` (no `.exe`).
1. **Run via `dotnet test` with a single TFM and filter** (incremental build handles rebuilding automatically):
```powershell
dotnet test src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj `
- -f net10.0 -- --filter-method "*MyFeature*"
+ -f net11.0 -- --filter-method "*MyFeature*"
```
2. **Or run the test exe directly** (fastest — no SDK overhead; build first if source changed):
```powershell
- dotnet build src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net10.0
- artifacts\bin\Microsoft.Build.Tasks.UnitTests\Debug\net10.0\Microsoft.Build.Tasks.UnitTests.exe `
+ dotnet build src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net11.0
+ artifacts\bin\Microsoft.Build.Tasks.UnitTests\Debug\net11.0\Microsoft.Build.Tasks.UnitTests.exe `
--filter-method "*MyFeature*" --no-progress
```
- The exe path is TFM-specific: `Debug\net10.0\...exe` for net10.0, `Debug\net472\...exe` for net472. Switching TFM without rebuilding silently runs stale binaries.
+ The exe path is TFM-specific: `Debug\net11.0\...exe` for net11.0, `Debug\net472\...exe` for net472. Switching TFM without rebuilding silently runs stale binaries.
### Speeding up the dev loop further
These trade safety for speed — use during iteration, **revert before final validation**:
-- **Single TFM**: pass `-f net10.0`. Halves runtime on Windows by skipping `net472`.
+-- **Single TFM**: pass `-f net11.0`. Halves runtime on Windows by skipping `net472`.
- **Temporarily relax single-threaded execution**: drop a `xunit.runner.json` next to the test project (or override the existing one) with:
```json
{
@@ -122,8 +122,8 @@ Match the source area you changed to its `*.UnitTests` project:
| Scenario | Command |
|----------|---------|
-| Fast scoped dev loop | `dotnet test -f net10.0 -- --filter-method "*X*"` |
-| Direct test exe | `artifacts\bin\\Debug\net10.0\.exe --filter-method "*X*" --no-progress` |
+| Fast scoped dev loop | `dotnet test -f net11.0 -- --filter-method "*X*"` |
+| Direct test exe | `artifacts\bin\\Debug\net11.0\.exe --filter-method "*X*" --no-progress` |
| Single test by name | `dotnet test -- --filter-method "*MyTestMethod*"` |
| Final per-project validation | `dotnet test -c Release` (all TFMs) |
| Final full validation | `.\build.cmd -test -c Release` / `./build.sh --test -c Release` |
diff --git a/Directory.Build.props b/Directory.Build.props
index 99d68ed310b..c44a3a518ca 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -32,14 +32,13 @@
- net10.0
+ net11.0$(NetCurrent)
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 8aa6ca9426c..729254699cb 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -12,13 +12,6 @@
false
-
-
- $(ApiCompatAdditionalSearchPaths);$(NuGetPackageRoot)microsoft.visualstudio.telemetry\$(MicrosoftVisualStudioTelemetryVersion)\lib\$(FullFrameworkTFM)
-
-
diff --git a/MSBuild.VSTest.slnx b/MSBuild.VSTest.slnx
index 23421c82a66..a4812a6f245 100644
--- a/MSBuild.VSTest.slnx
+++ b/MSBuild.VSTest.slnx
@@ -6,15 +6,16 @@
Samples/Package which aren't part of the test surface) plus the
sibling *.UnitTests.VSTest.csproj wrappers. The wrappers import their
original test csproj but force the VSTest runner stack (CTS does not
- yet work with Microsoft.Testing.Platform) and pin the TFM to net10.0
- so a single `dotnet build MSBuild.VSTest.slnx` succeeds.
+ yet work with Microsoft.Testing.Platform) and pin the TFM to
+ $(LatestDotNetCoreForMSBuild) so a single `dotnet build MSBuild.VSTest.slnx`
+ succeeds.
MSBuild.slnx is intentionally left untouched so developer / IDE /
existing PR pipeline behavior is unchanged — this slnx is only built
by azure-pipelines/cts-collect.yml and cts-apply.yml.
- KNOWN COVERAGE GAP: the wrappers run net10.0 only. The regular PR
- pipeline runs both net10.0 and net472 (Windows). The CTS pipeline is
+ KNOWN COVERAGE GAP: the wrappers run $(LatestDotNetCoreForMSBuild) only. The regular PR
+ pipeline runs both $(LatestDotNetCoreForMSBuild) and net472 (Windows). The CTS pipeline is
parallel and non-blocking, so net472 signal is still provided by the
existing PR pipeline — but CTS does not add incrementality for the
net472 leg today. Tracked for follow-up; see scripts/cts/README.md.
diff --git a/azure-pipelines/cts-steps/apply-steps.yml b/azure-pipelines/cts-steps/apply-steps.yml
index 5398327630f..5e021bdb3c1 100644
--- a/azure-pipelines/cts-steps/apply-steps.yml
+++ b/azure-pipelines/cts-steps/apply-steps.yml
@@ -64,11 +64,11 @@ steps:
version: 8.0.x
- task: UseDotNet@2
- # net10.0 runtime for vstest's testhost.exe; see collect-steps.yml.
- displayName: Install .NET 10 runtime (testhost)
+ # For vstest's testhost.exe; see collect-steps.yml.
+ displayName: Install .NET runtime (testhost)
inputs:
packageType: runtime
- version: 10.0.x
+ useGlobalJson: true
includePreviewVersions: true
- task: NuGetAuthenticate@1
diff --git a/azure-pipelines/cts-steps/collect-steps.yml b/azure-pipelines/cts-steps/collect-steps.yml
index b582b7a6b09..a0f38c0c1f0 100644
--- a/azure-pipelines/cts-steps/collect-steps.yml
+++ b/azure-pipelines/cts-steps/collect-steps.yml
@@ -43,17 +43,15 @@ steps:
- task: UseDotNet@2
# vstest spawns testhost.exe (an apphost) per test DLL; the test DLLs
- # target net10.0, so testhost needs Microsoft.NETCore.App 10.0.x in the
- # agent hostedtoolcache. We can't use `useGlobalJson: true` here because
- # this repo's global.json uses Arcade's `tools.dotnet` field rather than
- # the standard `sdk.version`, which UseDotNet@2 can't parse. The full
- # SDK install used to compile the slnx is handled by eng/common/build.ps1
+ # target $(LatestDotNetCoreForMSBuild), so testhost needs a matching
+ # Microsoft.NETCore.App version in the agent hostedtoolcache.
+ # The full SDK install used to compile the slnx is handled by eng/common/build.ps1
# which puts it in the repo-local .dotnet/ -- but apphosts don't look
# there, only at DOTNET_ROOT.
- displayName: Install .NET 10 runtime (testhost)
+ displayName: Install .NET runtime (testhost)
inputs:
packageType: runtime
- version: 10.0.x
+ useGlobalJson: true
includePreviewVersions: true
- task: NuGetAuthenticate@1
diff --git a/documentation/aot/aot-trimming-strategy.md b/documentation/aot/aot-trimming-strategy.md
index 3e5d26ea229..cb655d4b5b0 100644
--- a/documentation/aot/aot-trimming-strategy.md
+++ b/documentation/aot/aot-trimming-strategy.md
@@ -134,7 +134,7 @@ Apply the catalog with this loop:
localize unavoidable DAM suppressions to the smallest member, and update the tracker when a row
moves or disappears.
5. **Verify the touched slice.** A change that affects the analyzed engine must keep
- `Microsoft.Build` at 0 warnings / 0 errors on both `net10.0` and `net472`, publish and run the
+ `Microsoft.Build` at 0 warnings / 0 errors on all TFMs, publish and run the
AOT harness when the closure changes, and run the targeted tests for the switch/path being moved.
---
diff --git a/documentation/aot/sdk-resolution.md b/documentation/aot/sdk-resolution.md
index a067bad512f..d1fd15e11da 100644
--- a/documentation/aot/sdk-resolution.md
+++ b/documentation/aot/sdk-resolution.md
@@ -182,7 +182,8 @@ resolver that would have to be **dynamically loaded** in a host that cannot do s
> `Project`/`ProjectInstance`/`ProjectGraph`/`ProjectCollection` surface, the
> `SdkResolverService_Tests` switch tests, and the `aot-validation` harness's
> `Evaluation_InBoxSdkResolvesReflectionFree` test (the harness's `#pragma warning disable IL2026`
-> is gone). `Microsoft.Build` builds warning-free for `net10.0`; the harness is green under Native AOT.
+> is gone). `Microsoft.Build` builds warning-free for the .NETCoreApp TFM; the harness is green
+> under Native AOT.
### 2.1 Goal and shape
@@ -346,7 +347,7 @@ see the
RUC is gone and in-box resolution is reflection-free), and a test asserting that a
NuGet-SDK project throws the observable error under AOT. These replace the harness's current
`#pragma warning disable IL2026`.
-* **Warning check**: rebuild `Microsoft.Build` for `net10.0` and confirm zero new IL warnings
+* **Warning check**: rebuild `Microsoft.Build` for .NETCoreApp and confirm zero new IL warnings
- the `[FeatureGuard]` satisfies the analyzer and the leaves keep their RUC.
### 2.7 Payoff and risk
diff --git a/documentation/aot/task-factory-aot.md b/documentation/aot/task-factory-aot.md
index b07e0f3b094..52891ee98af 100644
--- a/documentation/aot/task-factory-aot.md
+++ b/documentation/aot/task-factory-aot.md
@@ -95,7 +95,7 @@ authors toward the built-in factories — relevant below.
## 3. Built-in factories (what actually runs on .NET)
-| Factory | Implements | net10.0 behavior | Reflection used |
+| Factory | Implements | .NETCoreApp behavior | Reflection used |
|---|---|---|---|
| `AssemblyTaskFactory` (`src/Build/Instance/TaskFactories/`) | `ITaskFactory3` | **Active — the main path** | `TypeLoader.Load` (assembly load + type resolution), then `Activator.CreateInstance` via `TaskLoader.CreateTask`, or a `TaskHostTask` for out-of-proc |
| `IntrinsicTaskFactory` (`.../IntrinsicTasks/`) | `ITaskFactory` | Active | None — `new MSBuild()` / `new CallTarget()` directly |
@@ -235,11 +235,11 @@ The contract is annotated honestly on both sides:
the factory contract stays honest end to end.
With both sides annotated, a host that calls `ITaskFactory.CreateTask`/`Initialize`
-through the interface gets an honest IL2026, and the build is clean on both `net10.0`
-and `net472` under warnings-as-errors (0 IL warnings, 0 warnings, 0 errors). The
+through the interface gets an honest IL2026, and the build is clean on both TFMs under
+warnings-as-errors (0 IL warnings, 0 warnings, 0 errors). The
attributes compile on `net472`/`netstandard2.0` via the internal polyfill in
`src/Framework/Polyfills/AotTrimmingPolyfills.cs` (the trim analyzer only runs on
-`net10.0`).
+the .NETCoreApp TFM).
### Consequences accepted
diff --git a/documentation/specs/sdk-resolver-host-registration-api.md b/documentation/specs/sdk-resolver-host-registration-api.md
index 26916261174..18f1cd9b0fa 100644
--- a/documentation/specs/sdk-resolver-host-registration-api.md
+++ b/documentation/specs/sdk-resolver-host-registration-api.md
@@ -231,5 +231,4 @@ The primary design has been implemented:
- **Tests.** Four tests in
[`SdkResolverService_Tests.cs`](../../src/Build.UnitTests/BackEnd/SdkResolverService_Tests.cs) cover the
null-argument throw, idempotent re-registration, `Priority`-ordered inclusion in
- `GetDefaultResolvers()`, and an end-to-end resolve through `SdkResolverService`. They run on both
- `net10.0` and `net472`.
+ `GetDefaultResolvers()`, and an end-to-end resolve through `SdkResolverService`. They run on both TFMs.
diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets
index 2db60792f06..5fd8e5d148a 100644
--- a/eng/BootStrapMsBuild.targets
+++ b/eng/BootStrapMsBuild.targets
@@ -301,7 +301,7 @@
DestinationFiles="@(FreshlyBuiltNetBinaries->'$(InstallDir)sdk\$(BootstrapSdkVersion)\%(RecursiveDir)%(Filename)%(Extension)')" />
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
index b42248022db..a03962c2213 100644
--- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
+++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
@@ -3029,7 +3029,7 @@ public void FullGraphModeLoadsAllTargetFrameworksIgnoringSetTargetFramework()
TransientTestFile project1 = env.CreateFile("project1.proj", """
- net472;net10.0
+ net472;net11.0TargetFrameworkTargetFrameworks
@@ -3061,12 +3061,14 @@ public void FullGraphModeLoadsAllTargetFrameworksIgnoringSetTargetFramework()
// In Full mode, all target frameworks should be loaded despite SetTargetFramework constraint
sorted.Count.ShouldBe(4);
- // Find the inner builds for net10.0 and net472
- var net10Build = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path &&
- n.ProjectInstance.GlobalProperties.TryGetValue("TargetFramework", out var tf) && tf == "net10.0");
- net10Build.ShouldNotBeNull("Should load inner build for net10.0");
- net10Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
- net10Build.ProjectReferences.ShouldBeEmpty();
+ // Find the inner builds for net11.0 and net472
+ var net11Build = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path &&
+ n.ProjectInstance.GlobalProperties.TryGetValue("TargetFramework", out var tf) && tf == "net11.0");
+ net11Build.ShouldNotBeNull("Should load inner build for net11.0");
+ net11Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
+ net11Build.ProjectReferences.ShouldBeEmpty();
+ net11Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
+ net11Build.ProjectReferences.ShouldBeEmpty();
var net472Build = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path &&
n.ProjectInstance.GlobalProperties.TryGetValue("TargetFramework", out var tf) && tf == "net472");
@@ -3094,7 +3096,7 @@ public void FullGraphModeIgnoresSetTargetFrameworkForSingleTargetProject()
TransientTestFile project1 = env.CreateFile("project1.proj", """
- net10.0
+ net11.0
""");
diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.VSTest.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.VSTest.csproj
index 261082feabd..05ab7ae106b 100644
--- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.VSTest.csproj
+++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.VSTest.csproj
@@ -23,10 +23,10 @@
-
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs b/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
index 74e5a5bf932..fb63d65db14 100644
--- a/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
+++ b/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
@@ -29,8 +29,8 @@ namespace Microsoft.Build.Engine.UnitTests.ProjectCache
public class ProjectCacheTests : IDisposable
{
// The sample plugin and the helper apps built by these tests target the runtime MSBuild itself
- // targets (LatestDotNetCoreForMSBuild), which can lag the bootstrap SDK's TFM (e.g. MSBuild on
- // net10.0 overlaid onto a net11.0 SDK). Derive the TFM from the runtime actually executing these
+ // targets (LatestDotNetCoreForMSBuild), which can lag the bootstrap SDK's TFM.
+ // Derive the TFM from the runtime actually executing these
// tests rather than from BootstrapSdkVersion, so the plugin/app paths resolve to the built output.
private static string s_currentTargetNETFramework = $"net{Environment.Version.Major}.0";
diff --git a/src/Build/CompatibilitySuppressions.xml b/src/Build/CompatibilitySuppressions.xml
index 0497d618a92..251b6996460 100644
--- a/src/Build/CompatibilitySuppressions.xml
+++ b/src/Build/CompatibilitySuppressions.xml
@@ -1,3 +1,8 @@
-
+
-
\ No newline at end of file
+
+
+ PKV006
+ net10.0
+
+
\ No newline at end of file
diff --git a/src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.VSTest.csproj b/src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.VSTest.csproj
index d8d55f4b44d..d095d0035f6 100644
--- a/src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.VSTest.csproj
+++ b/src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.VSTest.csproj
@@ -23,10 +23,10 @@
-
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets
index 8e954e73e7c..fd84107b368 100644
--- a/src/Directory.BeforeCommon.targets
+++ b/src/Directory.BeforeCommon.targets
@@ -88,7 +88,7 @@
$(DefineConstants);FEATURE_SYMLINK_TARGET
-
+ $(DefineConstants);FEATURE_PIPEOPTIONS_CURRENTUSERONLY
diff --git a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.VSTest.csproj b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.VSTest.csproj
index 4ac0f40943b..244f855d43f 100644
--- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.VSTest.csproj
+++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.VSTest.csproj
@@ -23,10 +23,10 @@
-
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/Framework/CompatibilitySuppressions.xml b/src/Framework/CompatibilitySuppressions.xml
index d61bb3c6b1f..7b93562b7fb 100644
--- a/src/Framework/CompatibilitySuppressions.xml
+++ b/src/Framework/CompatibilitySuppressions.xml
@@ -1,6 +1,216 @@
-
+
+
+ CP0001
+ T:System.Collections.Generic.IReadOnlySet`1
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Diagnostics.CodeAnalysis.ExperimentalAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Diagnostics.CodeAnalysis.UnscopedRefAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Index
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Range
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.CallerArgumentExpressionAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.CollectionBuilderAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.IsExternalInit
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.CompilerServices.RequiredMemberAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.Versioning.OSPlatformAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.Versioning.SupportedOSPlatformAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.Versioning.SupportedOSPlatformGuardAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0001
+ T:System.Runtime.Versioning.UnsupportedOSPlatformGuardAttribute
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Execution.BuildRequestDataFlags
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.AssemblyLoadingContext
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.FeatureStatus
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.LoggerVerbosity
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.MessageImportance
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.Profiler.EvaluationLocationKind
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.Profiler.EvaluationPass
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.RegisteredTaskObjectLifetime
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.TargetBuiltReason
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.TargetSkipReason
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Framework.TaskParameterMessageKind
+ ref/net10.0/Microsoft.Build.Framework.dll
+ ref/netstandard2.0/Microsoft.Build.Framework.dll
+ true
+ PKV004.NETCoreApp,Version=v2.0
@@ -49,4 +259,8 @@
PKV004Xamarin.XboxOne,Version=v0.0
+
+ PKV006
+ net10.0
+
\ No newline at end of file
diff --git a/src/Framework/Polyfills/AotTrimmingPolyfills.cs b/src/Framework/Polyfills/AotTrimmingPolyfills.cs
index 4a6fddf2e3a..1e0f22adcfa 100644
--- a/src/Framework/Polyfills/AotTrimmingPolyfills.cs
+++ b/src/Framework/Polyfills/AotTrimmingPolyfills.cs
@@ -6,14 +6,14 @@
// (IsAotCompatible) on its net8.0+ build, but the annotations themselves are written
// unconditionally so they remain in the public metadata for every target framework.
//
-// On net10.0 these types come from the runtime. On net472 and netstandard2.0 they are not
+// On .NETCoreApp these types come from the runtime. On net472 and netstandard2.0 they are not
// available to reference: several dependencies (System.Text.Json, System.Collections.Immutable,
// Microsoft.IO.Redist, ...) embed their own *internal* copies of these attributes, so the
// types appear "already declared" - PolySharp therefore skips generating them, yet those
// copies are inaccessible from this assembly. Defining our own internal copies here makes the
// annotations resolve. The trim/AOT analyzer never runs on net472 or netstandard2.0, so these
// definitions only need to exist for the annotations to compile - the trimmer recognizes them
-// by full type name on net10.0 where the real runtime types are used.
+// by full type name on .NETCoreApp where the real runtime types are used.
#if !NET
namespace System.Diagnostics.CodeAnalysis
diff --git a/src/Framework/README.md b/src/Framework/README.md
index 4bb298146ff..28b5a8ed468 100644
--- a/src/Framework/README.md
+++ b/src/Framework/README.md
@@ -6,6 +6,6 @@ The items in this namespace are primarily base-level classes and interfaces shar
[`ITask`](https://docs.microsoft.com/dotnet/api/microsoft.build.framework.itask), and [`ILogger`](https://docs.microsoft.com/dotnet/api/microsoft.build.framework.ilogger).
### netstandard2.0 target
-The `netstandard2.0` target of this build is configured only to output reference assemblies; at runtime MSBuild will be `net10.0` or `net472`. Please use the `net10.0`-targeted assemblies for .NET 10+ scenarios.
+The `netstandard2.0` target of this build is configured only to output reference assemblies; at runtime MSBuild will be `net11.0` or `net472`. Please use the `net11.0`-targeted assemblies for .NET 11+ scenarios.
For context, see https://github.com/dotnet/msbuild/pull/6148
\ No newline at end of file
diff --git a/src/MSBuild.Benchmarks/ExpanderBenchmark.cs b/src/MSBuild.Benchmarks/ExpanderBenchmark.cs
index beeb4bb27a6..467d2029444 100644
--- a/src/MSBuild.Benchmarks/ExpanderBenchmark.cs
+++ b/src/MSBuild.Benchmarks/ExpanderBenchmark.cs
@@ -80,10 +80,10 @@ public void GlobalSetup()
// Add well-known properties used in expressions.
properties.Set(ProjectPropertyInstance.Create("Configuration", "Release"));
properties.Set(ProjectPropertyInstance.Create("Platform", "AnyCPU"));
- properties.Set(ProjectPropertyInstance.Create("OutputPath", @"bin\Release\net10.0"));
+ properties.Set(ProjectPropertyInstance.Create("OutputPath", @"bin\Release\net11.0"));
properties.Set(ProjectPropertyInstance.Create("RootNamespace", "MyProject.Core"));
properties.Set(ProjectPropertyInstance.Create("AssemblyName", "MyProject.Core"));
- properties.Set(ProjectPropertyInstance.Create("TargetFramework", "net10.0"));
+ properties.Set(ProjectPropertyInstance.Create("TargetFramework", "net11.0"));
// --- Items ---
var itemBag = new ItemDictionary();
@@ -99,7 +99,7 @@ public void GlobalSetup()
for (int i = 0; i < ItemCount / 2; i++)
{
var item = new ProjectItemInstance(project, "Reference", $"System.Lib{i}", project.FullPath);
- item.SetMetadata("HintPath", $@"packages\lib{i}\lib\net10.0\System.Lib{i}.dll");
+ item.SetMetadata("HintPath", $@"packages\lib{i}\lib\net11.0\System.Lib{i}.dll");
item.SetMetadata("Private", "true");
itemBag.Add(item);
}
diff --git a/src/MSBuild.Benchmarks/PartialEvaluationBenchmark.cs b/src/MSBuild.Benchmarks/PartialEvaluationBenchmark.cs
index 51bdd2f4a1b..ce2e87d7241 100644
--- a/src/MSBuild.Benchmarks/PartialEvaluationBenchmark.cs
+++ b/src/MSBuild.Benchmarks/PartialEvaluationBenchmark.cs
@@ -28,7 +28,7 @@ public class PartialEvaluationBenchmark
///
/// Number of <Target> elements to register during the final evaluation pass. This is
/// the pass that a properties-only evaluation skips entirely. A restored dotnet new console
- /// (net10.0) evaluates ~500 targets, so 506 reflects a realistic SDK-style project.
+ /// (.NETCoreApp) evaluates ~500 targets, so 506 reflects a realistic SDK-style project.
///
[Params(506)]
public int TargetCount { get; set; }
diff --git a/src/MSBuild.Benchmarks/readme.md b/src/MSBuild.Benchmarks/readme.md
index e21364acf2f..edfef3c1d2a 100644
--- a/src/MSBuild.Benchmarks/readme.md
+++ b/src/MSBuild.Benchmarks/readme.md
@@ -16,19 +16,19 @@ dotnet run -c Release
```
cd src/MSBuild.Benchmarks
dotnet run -c Release -f net472
-dotnet run -c Release -f net10.0
+dotnet run -c Release -f net11.0
```
### Filter to a Specific Benchmark Class
```
-dotnet run -c Release -f net10.0 -- --filter "*ItemSpecModifiersBenchmark*"
+dotnet run -c Release -f net11.0 -- --filter "*ItemSpecModifiersBenchmark*"
```
### Filter to a Single Benchmark Method
```
-dotnet run -c Release -f net10.0 -- --filter "*ItemSpecModifiersBenchmark.IncludeOnly"
+dotnet run -c Release -f net11.0 -- --filter "*ItemSpecModifiersBenchmark.IncludeOnly"
```
## Command-Line Options
@@ -41,5 +41,5 @@ dotnet run -c Release -f net10.0 -- --filter "*ItemSpecModifiersBenchmark.Includ
These custom options can be combined with any BenchmarkDotNet options:
```
-dotnet run -c Release -f net10.0 -- --filter "*ItemSpecModifiersBenchmark*" --job short --disable-ngen
+dotnet run -c Release -f net11.0 -- --filter "*ItemSpecModifiersBenchmark*" --job short --disable-ngen
```
diff --git a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj
index 4839c0dcbfa..0073c907449 100644
--- a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj
+++ b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj
@@ -44,7 +44,7 @@
-
+
diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs
index 91a8c3fb9ad..de8d9b6d02d 100644
--- a/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs
+++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorIntegration_Tests.cs
@@ -141,7 +141,7 @@ public async Task NuGetStaticGraphRestore_InheritsCoordinatorGrant_DoesNotDeadlo
"""
- net10.0
+ net11.0
""");
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
index e9c22e44cdd..3a65f23ed2c 100644
--- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
@@ -1,11 +1,14 @@
+
Exe
- net10.0
+ net11.0latest
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj
index 77cb2792044..c00294ab817 100644
--- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj
@@ -1,6 +1,7 @@
+
- net10.0
+ net11.0latest
@@ -8,4 +9,5 @@
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj
index 77cb2792044..c00294ab817 100644
--- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj
@@ -1,6 +1,7 @@
+
- net10.0
+ net11.0latest
@@ -8,4 +9,5 @@
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj
index 53502388eb3..62f73134df9 100644
--- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj
@@ -1,6 +1,8 @@
+
- net10.0
+ net11.0latest
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj
index 53502388eb3..62f73134df9 100644
--- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj
@@ -1,6 +1,8 @@
+
- net10.0
+ net11.0latest
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj
index 5c0a78df5ac..b51a0083ddc 100644
--- a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj
@@ -2,7 +2,7 @@
Exe
- net10.0
+ net11.0enableenable
diff --git a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.VSTest.csproj b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.VSTest.csproj
index c0afb02ae2f..e953557b3da 100644
--- a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.VSTest.csproj
+++ b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.VSTest.csproj
@@ -23,10 +23,10 @@
-
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/StringTools.UnitTests/StringTools.UnitTests.VSTest.csproj b/src/StringTools.UnitTests/StringTools.UnitTests.VSTest.csproj
index 3460a56240b..bfffd765d66 100644
--- a/src/StringTools.UnitTests/StringTools.UnitTests.VSTest.csproj
+++ b/src/StringTools.UnitTests/StringTools.UnitTests.VSTest.csproj
@@ -23,11 +23,10 @@
-
-
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj b/src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj
index 75cdb4a6f3a..545d18ff8e0 100644
--- a/src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj
+++ b/src/TaskAnalyzer.Tests/TaskAnalyzer.Tests.csproj
@@ -1,7 +1,7 @@
- net10.0
+ $(LatestDotNetCoreForMSBuild)falsetrue
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/Tasks/CompatibilitySuppressions.xml b/src/Tasks/CompatibilitySuppressions.xml
index fafd80dcb79..8e78b2b06e0 100644
--- a/src/Tasks/CompatibilitySuppressions.xml
+++ b/src/Tasks/CompatibilitySuppressions.xml
@@ -1,4 +1,4 @@
-
+
@@ -67,6 +67,62 @@
ref/netstandard2.0/Microsoft.Build.Tasks.Core.dllref/net472/Microsoft.Build.Tasks.Core.dll
+
+ CP0008
+ T:Microsoft.Build.Tasks.AssemblyDependency.RarNodeShutdownReason
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.Bootstrapper.BuildMessageSeverity
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.Bootstrapper.ComponentsLocation
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyIdentity.FullNameFlags
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyReferenceType
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.ManifestUtilities.OutputMessageType
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.ManifestUtilities.UpdateMode
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Tasks.Deployment.ManifestUtilities.UpdateUnit
+ ref/net10.0/Microsoft.Build.Tasks.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Tasks.Core.dll
+ true
+ PKV004.NETCoreApp,Version=v2.0
@@ -115,4 +171,8 @@
PKV004Xamarin.XboxOne,Version=v0.0
+
+ PKV006
+ net10.0
+
\ No newline at end of file
diff --git a/src/UnitTests.Shared/RunnerUtilities.cs b/src/UnitTests.Shared/RunnerUtilities.cs
index b18820da5e3..36a29902091 100644
--- a/src/UnitTests.Shared/RunnerUtilities.cs
+++ b/src/UnitTests.Shared/RunnerUtilities.cs
@@ -57,7 +57,7 @@ public static string BootstrapMSBuildCommand
// The dotnet host that ships inside the bootstrap layout. When we launch the bootstrapped MSBuild we must
// point DOTNET_HOST_PATH at this host (rather than an ambient dotnet on PATH such as the repo-local .dotnet)
// so that any task hosts it spawns resolve their runtime from the bootstrap. The bootstrap can target an
- // earlier runtime (e.g. net10.0) than the SDK overlaid into .dotnet, so a task host resolving .dotnet would
+ // earlier runtime than the SDK overlaid into .dotnet, so a task host resolving .dotnet would
// fail to find its runtime. See eng/BootStrapMsBuild.targets.
private static readonly string s_bootstrapDotnetHostPath = EnvironmentProvider.GetDotnetExePathFromFolder(BootstrapMsBuildBinaryLocation);
@@ -132,7 +132,7 @@ public static string ExecBootstrapedMSBuild(
/// exports DOTNET_ROOT/DOTNET_HOST_PATH/DOTNET_INSTALL_DIR pointing at the stage 1 bootstrap (it is the build tool
/// for stage 2). Without this override those leak into the stage 2 test run, so the launched stage 2 bootstrapped
/// MSBuild would resolve the stage 1 SDK/runtime and spawn a mismatched task host, failing with MSB4216. The
- /// bootstrap can also target an earlier runtime (e.g. net10.0) than the SDK overlaid into .dotnet, so an ambient
+ /// bootstrap can also target an earlier runtime than the SDK overlaid into .dotnet, so an ambient
/// dotnet (e.g. the repo-local .dotnet) may not contain the runtime the bootstrap targets.
///
private static Dictionary GetMSBuildEnvironmentVariables(bool useBootstrapHost)
diff --git a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.VSTest.csproj b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.VSTest.csproj
index 15ff891eae2..50458b41d49 100644
--- a/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.VSTest.csproj
+++ b/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.VSTest.csproj
@@ -23,10 +23,10 @@
-
- net10.0
+ $(LatestDotNetCoreForMSBuild)
\ No newline at end of file
diff --git a/src/Utilities/CompatibilitySuppressions.xml b/src/Utilities/CompatibilitySuppressions.xml
index d61bb3c6b1f..4f3258182c2 100644
--- a/src/Utilities/CompatibilitySuppressions.xml
+++ b/src/Utilities/CompatibilitySuppressions.xml
@@ -1,6 +1,48 @@
-
+
+
+ CP0008
+ T:Microsoft.Build.Utilities.DotNetFrameworkArchitecture
+ ref/net10.0/Microsoft.Build.Utilities.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Utilities.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Utilities.HostObjectInitializationStatus
+ ref/net10.0/Microsoft.Build.Utilities.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Utilities.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Utilities.MultipleVersionSupport
+ ref/net10.0/Microsoft.Build.Utilities.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Utilities.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Utilities.SDKType
+ ref/net10.0/Microsoft.Build.Utilities.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Utilities.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Utilities.TargetDotNetFrameworkVersion
+ ref/net10.0/Microsoft.Build.Utilities.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Utilities.Core.dll
+ true
+
+
+ CP0008
+ T:Microsoft.Build.Utilities.VisualStudioVersion
+ ref/net10.0/Microsoft.Build.Utilities.Core.dll
+ ref/netstandard2.0/Microsoft.Build.Utilities.Core.dll
+ true
+ PKV004.NETCoreApp,Version=v2.0
@@ -49,4 +91,8 @@
PKV004Xamarin.XboxOne,Version=v0.0
+
+ PKV006
+ net10.0
+
\ No newline at end of file
diff --git a/src/Utilities/README.md b/src/Utilities/README.md
index 6e2c876f06d..80e1c001b84 100644
--- a/src/Utilities/README.md
+++ b/src/Utilities/README.md
@@ -7,6 +7,6 @@ This package contains `Microsoft.Build.Utilities.Core.dll`, which defines helper
* [`Logger`](https://docs.microsoft.com/dotnet/api/microsoft.build.utilities.logger), a base class for custom logging functionality.
### netstandard2.0 target
-The `netstandard2.0` target of this build is configured only to output reference assemblies; at runtime MSBuild will be `net10.0` or `net472`. Please use the `net10.0`-targeted assemblies for .NET 10+ scenarios.
+The `netstandard2.0` target of this build is configured only to output reference assemblies; at runtime MSBuild will be `net11.0` or `net472`. Please use the `net11.0`-targeted assemblies for .NET 10+ scenarios.
For context, see https://github.com/dotnet/msbuild/pull/6148
\ No newline at end of file
diff --git a/src/aot-validation/Microsoft.Build.AotValidation.csproj b/src/aot-validation/Microsoft.Build.AotValidation.csproj
index 8efd462d251..fa850107e31 100644
--- a/src/aot-validation/Microsoft.Build.AotValidation.csproj
+++ b/src/aot-validation/Microsoft.Build.AotValidation.csproj
@@ -14,7 +14,7 @@
Exe
- net10.0
+ net11.0enableenablelatest
diff --git a/src/aot-validation/ObjectModelAotTests.cs b/src/aot-validation/ObjectModelAotTests.cs
index 722452acc0f..088e773b00a 100644
--- a/src/aot-validation/ObjectModelAotTests.cs
+++ b/src/aot-validation/ObjectModelAotTests.cs
@@ -49,7 +49,7 @@ public void Construction_CreateAndReadProjectRootElement()
public void Evaluation_InMemoryProject_PropertiesAndItems()
{
ProjectRootElement root = ProjectRootElement.Create();
- root.AddProperty("TargetFramework", "net10.0");
+ root.AddProperty("TargetFramework", "net11.0");
root.AddProperty("OutputType", "Exe");
root.AddItem("Compile", "A.cs");
root.AddItem("Compile", "B.cs");
@@ -57,7 +57,7 @@ public void Evaluation_InMemoryProject_PropertiesAndItems()
using var collection = new ProjectCollection();
var project = new Project(root, globalProperties: null, toolsVersion: null, collection);
- Assert.AreEqual("net10.0", project.GetPropertyValue("TargetFramework"));
+ Assert.AreEqual("net11.0", project.GetPropertyValue("TargetFramework"));
Assert.AreEqual("Exe", project.GetPropertyValue("OutputType"));
Assert.AreEqual(2, project.GetItems("Compile").Count);
}
@@ -116,7 +116,7 @@ public void ProjectInstance_FromFile_MirrorsTestDiscoveryAndReleaseLocator()
"""
- net10.0
+ net11.0truetrue
@@ -129,7 +129,7 @@ public void ProjectInstance_FromFile_MirrorsTestDiscoveryAndReleaseLocator()
using var collection = new ProjectCollection();
var instance = ProjectInstance.FromFile(proj, new ProjectOptions { ProjectCollection = collection });
- Assert.AreEqual("net10.0", instance.GetPropertyValue("TargetFramework"));
+ Assert.AreEqual("net11.0", instance.GetPropertyValue("TargetFramework"));
Assert.AreEqual("true", instance.GetPropertyValue("PublishRelease"));
Assert.AreEqual("true", instance.GetPropertyValue("IsTestProject"));
Assert.AreEqual(1, instance.GetItems("Compile").Count());
diff --git a/src/aot-validation/README.md b/src/aot-validation/README.md
index 55e32b66e1e..906a7ba1e11 100644
--- a/src/aot-validation/README.md
+++ b/src/aot-validation/README.md
@@ -28,11 +28,11 @@ From the repository root, using the repo's pinned SDK (`.dotnet\dotnet.exe`):
```powershell
# Fast JIT pass (build + run the MTP test host)
.\.dotnet\dotnet.exe build src\aot-validation\Microsoft.Build.AotValidation.csproj
-.\src\aot-validation\bin\Debug\net10.0\win-x64\Microsoft.Build.AotValidation.exe
+.\src\aot-validation\bin\Debug\net11.0\win-x64\Microsoft.Build.AotValidation.exe
# Native AOT pass (the real validation): publish, then run the native exe
.\.dotnet\dotnet.exe publish src\aot-validation\Microsoft.Build.AotValidation.csproj -r win-x64 -c Release
-.\src\aot-validation\bin\Release\net10.0\win-x64\publish\Microsoft.Build.AotValidation.exe
+.\src\aot-validation\bin\Release\net11.0\win-x64\publish\Microsoft.Build.AotValidation.exe
```
Native AOT publishing requires the Visual Studio C++ toolchain (the MSVC linker). A run prints the
From fa7d0973e6cc9bf84fe66db532ed6dd5105313e1 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 13:51:23 +0200
Subject: [PATCH 10/31] Try to fix build and template_feed style fixes
---
template_feed/Microsoft.Build.Templates.csproj | 11 +----------
.../content/Microsoft.CheckTemplate/Check1.cs | 1 -
.../Company.CheckTemplate.csproj | 1 +
.../Company.CheckTemplate.props | 5 +++--
.../Microsoft.CheckTemplate/Directory.Packages.props | 4 +++-
5 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/template_feed/Microsoft.Build.Templates.csproj b/template_feed/Microsoft.Build.Templates.csproj
index d56a04b4c27..3ad62b7c214 100644
--- a/template_feed/Microsoft.Build.Templates.csproj
+++ b/template_feed/Microsoft.Build.Templates.csproj
@@ -1,26 +1,17 @@
- Microsoft.Build.Templates
+ $(LatestDotNetCoreForMSBuild)MicrosoftCommon Microsoft Build Templates
- https://github.com/dotnet/msbuildTemplate
- $(LatestDotNetCoreForMSBuild)false$(NoWarn);NU5128truetrue
-
-
- false
-
-
-
-
diff --git a/template_feed/content/Microsoft.CheckTemplate/Check1.cs b/template_feed/content/Microsoft.CheckTemplate/Check1.cs
index 194e451d533..adfd680d894 100644
--- a/template_feed/content/Microsoft.CheckTemplate/Check1.cs
+++ b/template_feed/content/Microsoft.CheckTemplate/Check1.cs
@@ -23,7 +23,6 @@ public override void Initialize(ConfigurationContext configurationContext)
// configurationContext to be used only if check needs external configuration data.
}
-
public override void RegisterActions(IBuildCheckRegistrationContext registrationContext)
{
registrationContext.RegisterEvaluatedPropertiesAction(EvaluatedPropertiesAction);
diff --git a/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.csproj b/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.csproj
index 75c007b9d49..c764b4a6c0c 100644
--- a/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.csproj
+++ b/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.csproj
@@ -41,4 +41,5 @@
+
diff --git a/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.props b/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.props
index 167b0912cf7..122baaf5090 100644
--- a/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.props
+++ b/template_feed/content/Microsoft.CheckTemplate/Company.CheckTemplate.props
@@ -1,6 +1,7 @@
-
+
- $([MSBuild]::RegisterBuildCheck($(MSBuildThisFileDirectory)Company.CheckTemplate.dll))
+ $([MSBuild]::RegisterBuildCheck($(MSBuildThisFileDirectory)Company.CheckTemplate.dll))
+
diff --git a/template_feed/content/Microsoft.CheckTemplate/Directory.Packages.props b/template_feed/content/Microsoft.CheckTemplate/Directory.Packages.props
index 92c952cfd3d..7c1c763ce88 100644
--- a/template_feed/content/Microsoft.CheckTemplate/Directory.Packages.props
+++ b/template_feed/content/Microsoft.CheckTemplate/Directory.Packages.props
@@ -1,9 +1,11 @@
-
+
true
+
+
From e3e476d8aef62b166880de52586ae3eb0ee35b5f Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 13:52:48 +0200
Subject: [PATCH 11/31] Fix terminal logger tests
---
...eportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt | 2 +-
...dReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt | 2 +-
...ortsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt
index 5d32134141c..b265adc45d3 100644
--- a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt
+++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Linux.verified.txt
@@ -1,4 +1,4 @@
-]9;4;3;\ project [36;1mnet10.0[m [35;1mwin-x64[m [32;1msucceeded[m (0.2s) → ]8;;file:///src\/src/project.dll]8;;\
+]9;4;3;\ project [36;1mnet11.0[m [35;1mwin-x64[m [32;1msucceeded[m (0.2s) → ]8;;file:///src\/src/project.dll]8;;\
[?25l[1F
[?25h
Build [32;1msucceeded[m in 5.0s
diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt
index e4caad3cc16..3fd962fc7a1 100644
--- a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt
+++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.OSX.verified.txt
@@ -1,4 +1,4 @@
- project [36;1mnet10.0[m [35;1mwin-x64[m [32;1msucceeded[m (0.2s) → ]8;;file:///src\/src/project.dll]8;;\
+ project [36;1mnet11.0[m [35;1mwin-x64[m [32;1msucceeded[m (0.2s) → ]8;;file:///src\/src/project.dll]8;;\
[?25l[1F
[?25h
Build [32;1msucceeded[m in 5.0s
diff --git a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt
index 9b473ace8ee..853d5402138 100644
--- a/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt
+++ b/src/Build.UnitTests/Snapshots/TerminalLogger_Tests.ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier.Windows.verified.txt
@@ -1,4 +1,4 @@
-]9;4;3;\ project [36;1mnet10.0[m [35;1mwin-x64[m [32;1msucceeded[m (0.2s) → ]8;;file:///C:/src\C:\src\project.dll]8;;\
+]9;4;3;\ project [36;1mnet11.0[m [35;1mwin-x64[m [32;1msucceeded[m (0.2s) → ]8;;file:///C:/src\C:\src\project.dll]8;;\
[?25l[1F
[?25h
Build [32;1msucceeded[m in 5.0s
From 4aa49180cbbbcafc5bf1dd576fb37c0213a51967 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:04:06 +0200
Subject: [PATCH 12/31] Use a distinct stage2 binlog name to avoid collision
with stage1
The stage1 and stage2 CI builds both default their binary log to Build.binlog, so publishing them to the same artifacts location collides. Declare ci/binaryLog/excludeCIBinarylog as first-class parameters, forward them to both stage builds, and pass -binaryLogName Build.stage2.binlog to stage2 when a binlog will actually be produced.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2a6b38b1-0da7-488f-9938-3ac1051e15e2
---
eng/build.ps1 | 24 ++++++++++++++++++++++++
eng/build.sh | 35 +++++++++++++++++++++++++++++++++++
2 files changed, 59 insertions(+)
diff --git a/eng/build.ps1 b/eng/build.ps1
index 0489c33d31b..f7e57b7ee0a 100644
--- a/eng/build.ps1
+++ b/eng/build.ps1
@@ -3,6 +3,9 @@ Param(
[string] $msbuildEngine,
[string] $configuration = "Debug",
[switch] $test,
+ [switch] $ci,
+ [switch][Alias('bl')] $binaryLog,
+ [switch][Alias('nobl')] $excludeCIBinarylog,
[switch] $stage2,
[string] $stage2Arguments = "",
[Parameter(ValueFromRemainingArguments=$true)][String[]]$properties
@@ -20,6 +23,17 @@ if ($msbuildEngine) {
$commonBuildArgs += $msbuildEngine
}
+# Forward the binary-log-related switches to both the stage 1 and stage 2 builds.
+if ($ci) {
+ $commonBuildArgs += '-ci'
+}
+if ($binaryLog) {
+ $commonBuildArgs += '-binaryLog'
+}
+if ($excludeCIBinarylog) {
+ $commonBuildArgs += '-excludeCIBinarylog'
+}
+
# Supplying stage2Arguments implies a multi-stage build
if ($stage2Arguments) {
$stage2 = $true
@@ -121,6 +135,16 @@ $stage2Args = @(if ($stage2Arguments) { $stage2Arguments -split '\s+' | Where-Ob
$stage2BuildArgs = $commonBuildArgs
+# Give the stage 2 binary log a distinct name so it doesn't collide with the stage 1 binlog
+# (both otherwise default to Build.binlog) when CI publishes them to the same artifacts location.
+# Only do this when a binary log will actually be produced, so we don't force one to be created:
+# Arcade emits a binlog for CI builds (-ci) or when -binaryLog is passed explicitly, unless it's
+# suppressed with -excludeCIBinarylog.
+if (($ci -or $binaryLog) -and -not $excludeCIBinarylog) {
+ $stage2BuildArgs += '-binaryLogName'
+ $stage2BuildArgs += 'Build.stage2.binlog'
+}
+
# Only run tests in stage2 when supplying the '-test' switch in a multi-stage build.
if ($test) {
$stage2BuildArgs += '-test'
diff --git a/eng/build.sh b/eng/build.sh
index 6c7fd326c78..4383340f42b 100755
--- a/eng/build.sh
+++ b/eng/build.sh
@@ -2,6 +2,9 @@
configuration="Debug"
test=false
+ci=false
+binary_log=false
+exclude_ci_binary_log=false
stage2=false
stage2Arguments=
properties=
@@ -30,6 +33,18 @@ while [[ $# -gt 0 ]]; do
test=true
shift 1
;;
+ --ci)
+ ci=true
+ shift 1
+ ;;
+ --binarylog|-bl)
+ binary_log=true
+ shift 1
+ ;;
+ --excludecibinarylog|-nobl)
+ exclude_ci_binary_log=true
+ shift 1
+ ;;
--stage2)
stage2=true
shift 1
@@ -52,6 +67,17 @@ build_script="$scriptroot/common/build.sh"
# Arguments common to the stage1 and stage2 builds, including any caller-supplied properties.
common_build_args="--configuration $configuration $properties"
+# Forward the binary-log-related switches to both the stage 1 and stage 2 builds.
+if [ "$ci" = true ]; then
+ common_build_args="$common_build_args --ci"
+fi
+if [ "$binary_log" = true ]; then
+ common_build_args="$common_build_args --binaryLog"
+fi
+if [ "$exclude_ci_binary_log" = true ]; then
+ common_build_args="$common_build_args --excludeCIBinarylog"
+fi
+
build_args="$common_build_args"
# If the caller requested a multi-stage build, add the --prepareMachine switch to the stage 1 build so that it kills any lingering processes from stage 1 before stage 2 starts.
@@ -119,6 +145,15 @@ export DOTNET_INSTALL_DIR="$bootstrap_root/core"
stage2_build_args="$common_build_args"
+# Give the stage 2 binary log a distinct name so it doesn't collide with the stage 1 binlog
+# (both otherwise default to Build.binlog) when CI publishes them to the same artifacts location.
+# Only do this when a binary log will actually be produced, so we don't force one to be created:
+# Arcade emits a binlog for CI builds (--ci) or when --binaryLog is passed explicitly, unless it's
+# suppressed with --excludeCIBinarylog.
+if { [ "$ci" = true ] || [ "$binary_log" = true ]; } && [ "$exclude_ci_binary_log" != true ]; then
+ stage2_build_args="$stage2_build_args --binaryLogName Build.stage2.binlog"
+fi
+
# Only run tests in stage2 when supplying the '--test' switch in a multi-stage build.
if [ "$test" = true ]; then
stage2_build_args="$stage2_build_args --test"
From d0c8407dc0bf395dfea6175f749d9a52900b6c9c Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:30:20 +0200
Subject: [PATCH 13/31] Publish stage1 binlog alongside stage2 binlog in
multi-stage builds
Moving the stage 1 artifacts aside relocated its Build.binlog out of the published artifacts/log directory, so CI only published the stage 2 binlog. Copy the stage 1 binlog back as Build.stage1.binlog right after the move (best-effort; skipped when no binlog was produced).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2a6b38b1-0da7-488f-9938-3ac1051e15e2
---
eng/BootStrapMsBuild.targets | 10 +++++-----
eng/build.ps1 | 10 ++++++++++
eng/build.sh | 10 ++++++++++
3 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets
index 5fd8e5d148a..f10f83bd4cf 100644
--- a/eng/BootStrapMsBuild.targets
+++ b/eng/BootStrapMsBuild.targets
@@ -21,13 +21,13 @@
AfterTargets="AfterBuild"
Condition="'$(CreateBootstrap)' != 'false'"/>
+
-
-
+ Directories="$(BootstrapDestination)"
+ ContinueOnError="true"/>
+
@@ -71,8 +71,8 @@
-
+
diff --git a/eng/build.ps1 b/eng/build.ps1
index f7e57b7ee0a..8ab292edeae 100644
--- a/eng/build.ps1
+++ b/eng/build.ps1
@@ -80,6 +80,16 @@ $BootstrapRoot = Join-Path $Stage1BinDir "bootstrap"
Remove-Item -Force -Recurse $Stage1Dir -ErrorAction SilentlyContinue
Move-Item -Path $ArtifactsDir -Destination $Stage1Dir -Force
+# The move above relocated the stage 1 binlog out of the published $ArtifactsDir\log directory. Copy it
+# back so CI publishes it alongside the stage 2 binlog. Best-effort: never fail the build if the stage 1
+# binlog isn't there (e.g. when no binary log was produced).
+$stage1Binlog = Join-Path $Stage1Dir "log\$configuration\Build.binlog"
+if (Test-Path $stage1Binlog) {
+ $publishedLogDir = Join-Path $ArtifactsDir "log\$configuration"
+ New-Item -ItemType Directory -Force -Path $publishedLogDir | Out-Null
+ Copy-Item -Path $stage1Binlog -Destination (Join-Path $publishedLogDir 'Build.stage1.binlog') -Force -ErrorAction SilentlyContinue
+}
+
# Mirror of tools.ps1 GetDefaultMSBuildEngine: presence of tools.vs => 'vs', else tools.dotnet => 'dotnet'.
$GlobalJson = Get-Content -Raw -Path (Join-Path $RepoRoot 'global.json') | ConvertFrom-Json
if (-not $msbuildEngine) {
diff --git a/eng/build.sh b/eng/build.sh
index 4383340f42b..f4a801e4acc 100755
--- a/eng/build.sh
+++ b/eng/build.sh
@@ -120,6 +120,16 @@ bootstrap_root="$stage1_bin_dir/bootstrap"
rm -rf "$stage1_dir"
mv "$artifacts_dir" "$stage1_dir"
+# The move above relocated the stage 1 binlog out of the published $artifacts_dir/log directory. Copy it
+# back so CI publishes it alongside the stage 2 binlog. Best-effort: never fail the build if the stage 1
+# binlog isn't there (e.g. when no binary log was produced).
+stage1_binlog="$stage1_dir/log/$configuration/Build.binlog"
+if [ -f "$stage1_binlog" ]; then
+ published_log_dir="$artifacts_dir/log/$configuration"
+ mkdir -p "$published_log_dir"
+ cp -f "$stage1_binlog" "$published_log_dir/Build.stage1.binlog" || true
+fi
+
build_tool_path="$bootstrap_root/core/dotnet"
build_tool_command="msbuild"
export DOTNET_ROOT="$bootstrap_root/core"
From 81ffe899d0d1a74ba61d261cd6589fb22951d4a2 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:33:50 +0200
Subject: [PATCH 14/31] Copy entire stage1 log folder back so CI publishes all
stage1 logs
Instead of copying just the stage 1 binlog, copy the whole stage 1 log directory back into artifacts/log after the move, so CI publishes all stage 1 logs alongside the stage 2 ones. Runs before the stage 2 build, so it won't clobber stage 2 output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2a6b38b1-0da7-488f-9938-3ac1051e15e2
---
eng/build.ps1 | 17 +++++++++--------
eng/build.sh | 16 ++++++++--------
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/eng/build.ps1 b/eng/build.ps1
index 8ab292edeae..f0c313a29e1 100644
--- a/eng/build.ps1
+++ b/eng/build.ps1
@@ -80,14 +80,15 @@ $BootstrapRoot = Join-Path $Stage1BinDir "bootstrap"
Remove-Item -Force -Recurse $Stage1Dir -ErrorAction SilentlyContinue
Move-Item -Path $ArtifactsDir -Destination $Stage1Dir -Force
-# The move above relocated the stage 1 binlog out of the published $ArtifactsDir\log directory. Copy it
-# back so CI publishes it alongside the stage 2 binlog. Best-effort: never fail the build if the stage 1
-# binlog isn't there (e.g. when no binary log was produced).
-$stage1Binlog = Join-Path $Stage1Dir "log\$configuration\Build.binlog"
-if (Test-Path $stage1Binlog) {
- $publishedLogDir = Join-Path $ArtifactsDir "log\$configuration"
- New-Item -ItemType Directory -Force -Path $publishedLogDir | Out-Null
- Copy-Item -Path $stage1Binlog -Destination (Join-Path $publishedLogDir 'Build.stage1.binlog') -Force -ErrorAction SilentlyContinue
+# The move above relocated the stage 1 log directory (including its binlog) out of the published
+# $ArtifactsDir\log location. Copy the whole log folder back so CI publishes the stage 1 logs alongside
+# the stage 2 ones. This runs before the stage 2 build, so it won't clobber any stage 2 output. The
+# stage 1 binlog keeps its default Build.binlog name, distinct from the stage 2 Build.stage2.binlog.
+# Best-effort: never fail the build if the stage 1 log folder isn't there (e.g. when no logs were produced).
+$stage1LogDir = Join-Path $Stage1Dir 'log'
+if (Test-Path $stage1LogDir) {
+ New-Item -ItemType Directory -Force -Path $ArtifactsDir | Out-Null
+ Copy-Item -Path $stage1LogDir -Destination $ArtifactsDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Mirror of tools.ps1 GetDefaultMSBuildEngine: presence of tools.vs => 'vs', else tools.dotnet => 'dotnet'.
diff --git a/eng/build.sh b/eng/build.sh
index f4a801e4acc..ea670438fba 100755
--- a/eng/build.sh
+++ b/eng/build.sh
@@ -120,14 +120,14 @@ bootstrap_root="$stage1_bin_dir/bootstrap"
rm -rf "$stage1_dir"
mv "$artifacts_dir" "$stage1_dir"
-# The move above relocated the stage 1 binlog out of the published $artifacts_dir/log directory. Copy it
-# back so CI publishes it alongside the stage 2 binlog. Best-effort: never fail the build if the stage 1
-# binlog isn't there (e.g. when no binary log was produced).
-stage1_binlog="$stage1_dir/log/$configuration/Build.binlog"
-if [ -f "$stage1_binlog" ]; then
- published_log_dir="$artifacts_dir/log/$configuration"
- mkdir -p "$published_log_dir"
- cp -f "$stage1_binlog" "$published_log_dir/Build.stage1.binlog" || true
+# The move above relocated the stage 1 log directory (including its binlog) out of the published
+# $artifacts_dir/log location. Copy the whole log folder back so CI publishes the stage 1 logs alongside
+# the stage 2 ones. This runs before the stage 2 build, so it won't clobber any stage 2 output. The
+# stage 1 binlog keeps its default Build.binlog name, distinct from the stage 2 Build.stage2.binlog.
+# Best-effort: never fail the build if the stage 1 log folder isn't there (e.g. when no logs were produced).
+if [ -d "$stage1_dir/log" ]; then
+ mkdir -p "$artifacts_dir"
+ cp -rf "$stage1_dir/log" "$artifacts_dir/" || true
fi
build_tool_path="$bootstrap_root/core/dotnet"
From 81cfb85950bc9b91b1928b488eea4ca7fbb4dbc9 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:09:18 +0200
Subject: [PATCH 15/31] Fix torn NuGet state in netcore bootstrap by not
overlaying package NuGet tooling
BootstrapNetCore copied the older NuGet.Build.Tasks package family (a toolset dependency, e.g. 7.10.0-rc.26) over the acquired SDK's root, while the SDK's pack tooling under Sdks\Microsoft.NET.Sdk\tools kept its bundled version (rc.36111). This tore restore (package version) from pack (SDK version).
The AcquireSdk-downloaded SDK already ships a complete, internally-aligned NuGet stack (restore + pack + resolver). Per the design intent in Version.Details.xml (bind to the host SDK's NuGet at runtime), stop overlaying the package versions and also exclude our NuGet.Build.Tasks.Console.* from the freshly-built binaries so the SDK's aligned tooling wins. Verified: full build succeeds with 0 warnings, all NuGet.Build.Tasks*.dll now report rc.36111, and restore+pack via the bootstrap dotnet succeed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2a6b38b1-0da7-488f-9938-3ac1051e15e2
---
eng/BootStrapMsBuild.targets | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets
index f10f83bd4cf..8a99144d8d6 100644
--- a/eng/BootStrapMsBuild.targets
+++ b/eng/BootStrapMsBuild.targets
@@ -284,7 +284,10 @@
-
+
+
@@ -292,11 +295,13 @@
-
-
-
+
From 82da8e898df5451fe29f958e7376f463246a224a Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:54:03 +0200
Subject: [PATCH 16/31] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
src/Build.UnitTests/Graph/ProjectGraph_Tests.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
index a03962c2213..bfb335c3d2b 100644
--- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
+++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
@@ -3067,8 +3067,6 @@ public void FullGraphModeLoadsAllTargetFrameworksIgnoringSetTargetFramework()
net11Build.ShouldNotBeNull("Should load inner build for net11.0");
net11Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
net11Build.ProjectReferences.ShouldBeEmpty();
- net11Build.ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
- net11Build.ProjectReferences.ShouldBeEmpty();
var net472Build = sorted.FirstOrDefault(n => n.ProjectInstance.FullPath == project1.Path &&
n.ProjectInstance.GlobalProperties.TryGetValue("TargetFramework", out var tf) && tf == "net472");
From 8a4f6aa4a106b2371c174a97c9a00c89354524d8 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:54:14 +0200
Subject: [PATCH 17/31] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
src/Utilities/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Utilities/README.md b/src/Utilities/README.md
index 80e1c001b84..bedca9eba7c 100644
--- a/src/Utilities/README.md
+++ b/src/Utilities/README.md
@@ -7,6 +7,6 @@ This package contains `Microsoft.Build.Utilities.Core.dll`, which defines helper
* [`Logger`](https://docs.microsoft.com/dotnet/api/microsoft.build.utilities.logger), a base class for custom logging functionality.
### netstandard2.0 target
-The `netstandard2.0` target of this build is configured only to output reference assemblies; at runtime MSBuild will be `net11.0` or `net472`. Please use the `net11.0`-targeted assemblies for .NET 10+ scenarios.
+The `netstandard2.0` target of this build is configured only to output reference assemblies; at runtime MSBuild will be `net11.0` or `net472`. Please use the `net11.0`-targeted assemblies for .NET 11+ scenarios.
For context, see https://github.com/dotnet/msbuild/pull/6148
\ No newline at end of file
From 0826b41adc1074684647a9be52c285e00bdd286f Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:56:49 +0200
Subject: [PATCH 18/31] Update cts ymls
---
azure-pipelines/cts-steps/apply-steps.yml | 2 +-
azure-pipelines/cts-steps/collect-steps.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/azure-pipelines/cts-steps/apply-steps.yml b/azure-pipelines/cts-steps/apply-steps.yml
index 5e021bdb3c1..7cd0ff32e50 100644
--- a/azure-pipelines/cts-steps/apply-steps.yml
+++ b/azure-pipelines/cts-steps/apply-steps.yml
@@ -68,7 +68,7 @@ steps:
displayName: Install .NET runtime (testhost)
inputs:
packageType: runtime
- useGlobalJson: true
+ version: 11.0.x
includePreviewVersions: true
- task: NuGetAuthenticate@1
diff --git a/azure-pipelines/cts-steps/collect-steps.yml b/azure-pipelines/cts-steps/collect-steps.yml
index a0f38c0c1f0..1a93f4ba8c2 100644
--- a/azure-pipelines/cts-steps/collect-steps.yml
+++ b/azure-pipelines/cts-steps/collect-steps.yml
@@ -51,7 +51,7 @@ steps:
displayName: Install .NET runtime (testhost)
inputs:
packageType: runtime
- useGlobalJson: true
+ version: 11.0.x
includePreviewVersions: true
- task: NuGetAuthenticate@1
From 2cf0cdc671209a30a31528cf4e75df46f0518063 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:52:40 +0000
Subject: [PATCH 19/31] Modernize TarDirectory with .NET 11 APIs and add
ZStandard compression
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks.UnitTests/TarDirectory_Tests.cs | 38 ++++---
src/Tasks/Resources/Strings.resx | 2 +-
src/Tasks/Resources/xlf/Strings.cs.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.de.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.es.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.fr.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.it.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.ja.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.ko.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.pl.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.ru.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.tr.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 4 +-
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 4 +-
src/Tasks/TarDirectory.cs | 106 ++++++--------------
16 files changed, 80 insertions(+), 118 deletions(-)
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
index a5168eee14a..705303fd4d6 100644
--- a/src/Tasks.UnitTests/TarDirectory_Tests.cs
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -25,6 +25,8 @@ public class TarDirectory_Tests
[InlineData("None")]
[InlineData("GZip")]
[InlineData("gz")]
+ [InlineData("ZStandard")]
+ [InlineData("zstd")]
public void CanTarDirectory(string? compression)
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
@@ -53,9 +55,7 @@ public void CanTarDirectory(string? compression)
// Should not contain any warnings in the TarDirectory bucket (MSB4321 - MSB4330).
_mockEngine.Log.ShouldNotContain("MSB432", customMessage: _mockEngine.Log);
- bool isGZip = compression is not null && !StringComparer.OrdinalIgnoreCase.Equals(compression, "None");
-
- GetTarEntryNames(tarFilePath, isGZip)
+ GetTarEntryNames(tarFilePath, compression)
.ShouldBe(
[
"6DE6060259C44DB6B145159376751C22.txt",
@@ -91,7 +91,7 @@ public void CanOverwriteExistingFile()
_mockEngine.Log.ShouldContain(sourceFolder.Path, customMessage: _mockEngine.Log);
_mockEngine.Log.ShouldContain(file.Path, customMessage: _mockEngine.Log);
- GetTarEntryNames(file.Path, isGZip: false)
+ GetTarEntryNames(file.Path, compression: null)
.ShouldBe(
[
"F1C22D660B0D4DAAA296C1B980320B03.txt",
@@ -198,7 +198,7 @@ public void CanTarDirectoryWithFormat(string format, TarEntryFormat expectedForm
GetTarEntryFormats(tarFilePath)
.ShouldAllBe(entryFormat => entryFormat == expectedFormat, _mockEngine.Log);
- GetTarEntryNames(tarFilePath, isGZip: false)
+ GetTarEntryNames(tarFilePath, compression: null)
.ShouldBe(["3F6D2F2E3C1A4B5C8D9E0F1A2B3C4D5E.txt"]);
}
}
@@ -262,16 +262,29 @@ public void LogsErrorForUnknownFormat()
}
}
- private static List GetTarEntryNames(string tarFilePath, bool isGZip)
+ private static List GetTarEntryNames(string tarFilePath, string? compression)
{
List names = new List();
using FileStream stream = new FileStream(tarFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
- Stream tarStream = isGZip ? new GZipStream(stream, CompressionMode.Decompress) : stream;
- try
+ // Wrap the file stream in a matching decompression stream, if the archive was compressed.
+ Stream? decompressionStream = compression switch
+ {
+ null => null,
+ _ when StringComparer.OrdinalIgnoreCase.Equals(compression, "None") => null,
+ _ when StringComparer.OrdinalIgnoreCase.Equals(compression, "GZip")
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "gzip") => new GZipStream(stream, CompressionMode.Decompress),
+ _ when StringComparer.OrdinalIgnoreCase.Equals(compression, "ZStandard")
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "zstd")
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "zst") => new ZstandardStream(stream, CompressionMode.Decompress),
+ _ => throw new ArgumentException($"Unexpected compression '{compression}'.", nameof(compression)),
+ };
+
+ using (decompressionStream)
{
- using TarReader reader = new TarReader(tarStream);
+ using TarReader reader = new TarReader(decompressionStream ?? stream);
for (TarEntry? entry = reader.GetNextEntry(); entry is not null; entry = reader.GetNextEntry())
{
if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile)
@@ -280,13 +293,6 @@ private static List GetTarEntryNames(string tarFilePath, bool isGZip)
}
}
}
- finally
- {
- if (isGZip)
- {
- tarStream.Dispose();
- }
- }
return names;
}
diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx
index e7131fe672a..47048f16d25 100644
--- a/src/Tasks/Resources/Strings.resx
+++ b/src/Tasks/Resources/Strings.resx
@@ -3111,7 +3111,7 @@
{StrBegin="MSB4323: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index 6a3a9b8a0fa..a9d7ca3e37d 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index 590230382cd..5a4a8345883 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index 6a7b5ed46f4..1527372271f 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index 4464f2c281a..0b77315c1db 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index 7ea59560734..253e99a51ec 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index d8178563620..896aed55a12 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index c0dc3ea9740..30dbf64e920 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index a42c628c844..ddbd1f3988d 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index 6ba3750729d..06747378ae8 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index 2283c1cb47e..d674a2bb4ba 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index 152b9e7027b..0236f4f5ea8 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index bbf5a745df0..8af23334ffe 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index 8c2833af074..48284afb885 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2693,8 +2693,8 @@
{StrBegin="MSB4322: "}
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None' and 'GZip'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
+ MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.{StrBegin="MSB4324: "}
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index e3efe91f6f6..73b876850bb 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -2,11 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
-using System.IO.Enumeration;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -25,6 +23,7 @@ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThread
{
public const string CompressionNone = "None";
public const string CompressionGZip = "GZip";
+ public const string CompressionZStandard = "ZStandard";
///
/// Gets or sets a containing the full path to the destination file to create.
@@ -51,7 +50,7 @@ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThread
///
/// Gets or sets the compression to apply to the tar archive.
- /// Valid values are "None" (the default) and "GZip".
+ /// Valid values are "None" (the default), "GZip", and "ZStandard".
/// This parameter is optional.
///
public string? Compression { get; set; }
@@ -116,8 +115,7 @@ public override bool Execute()
{
Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", sourceDirectory.FullName, destinationFile.FullName);
- bool useGZip = false;
- if (!string.IsNullOrEmpty(Compression) && !TryParseCompression(Compression, out useGZip))
+ if (!TryParseCompression(Compression, out TarCompression compression))
{
Log.LogErrorWithCodeFromResources("TarDirectory.ErrorInvalidCompression", Compression);
@@ -134,14 +132,19 @@ public override bool Execute()
using FileStream destinationStream = new FileStream(destinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
- if (useGZip)
+ // Wrap the destination stream in the requested compression, if any. The tar archive is always
+ // written to the (optionally compressed) stream using the new .NET 11 overload that honors the
+ // requested TarEntryFormat.
+ Stream? compressionStream = compression switch
{
- using GZipStream gzipStream = new GZipStream(destinationStream, CompressionLevel.Optimal);
- CreateTarFromDirectory(sourceDirectory.FullName, gzipStream, format);
- }
- else
+ TarCompression.GZip => new GZipStream(destinationStream, CompressionLevel.Optimal),
+ TarCompression.ZStandard => new ZstandardStream(destinationStream, CompressionLevel.Optimal),
+ _ => null,
+ };
+
+ using (compressionStream)
{
- CreateTarFromDirectory(sourceDirectory.FullName, destinationStream, format);
+ TarFile.CreateFromDirectory(sourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
}
}
}
@@ -157,21 +160,28 @@ public override bool Execute()
return !Log.HasLoggedErrors;
- static bool TryParseCompression(string compression, out bool useGZip)
+ static bool TryParseCompression(string? compression, out TarCompression result)
{
- if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionNone))
+ if (string.IsNullOrEmpty(compression)
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionNone))
{
- useGZip = false;
+ result = TarCompression.None;
}
else if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionGZip)
|| StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
|| StringComparer.OrdinalIgnoreCase.Equals(compression, "gzip"))
{
- useGZip = true;
+ result = TarCompression.GZip;
+ }
+ else if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionZStandard)
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "zstd")
+ || StringComparer.OrdinalIgnoreCase.Equals(compression, "zst"))
+ {
+ result = TarCompression.ZStandard;
}
else
{
- useGZip = false;
+ result = TarCompression.None;
return false;
}
@@ -183,67 +193,13 @@ static bool TryParseFormat(string format, out TarEntryFormat tarFormat) =>
}
///
- /// Writes all filesystem entries under to as a tar archive.
- ///
- ///
- /// This mirrors the behavior of with
- /// includeBaseDirectory: false, but additionally honors the requested . The
- /// overloads that accept a are not available on the
- /// net10.0 API surface MSBuild targets, so the directory walk is performed directly against a
- /// .
- ///
- private static void CreateTarFromDirectory(string sourceDirectoryFullPath, Stream destination, TarEntryFormat format)
- {
- using TarWriter writer = new TarWriter(destination, format, leaveOpen: true);
-
- foreach ((string fullPath, string entryName) in EnumerateEntries(sourceDirectoryFullPath, sourceDirectoryFullPath.Length))
- {
- writer.WriteEntry(fullPath, entryName);
- }
- }
-
- ///
- /// Recursively enumerates the filesystem entries under , yielding the full path and
- /// the corresponding archive entry name for each, while avoiding recursion into directory symlinks.
+ /// Identifies the compression to apply to the tar archive stream.
///
- private static IEnumerable<(string fullPath, string entryName)> EnumerateEntries(string directory, int basePathLength)
+ private enum TarCompression
{
- // Recurse into subdirectories after yielding their own entry so that directory entries precede their contents,
- // matching the ordering produced by TarFile.CreateFromDirectory.
- FileSystemEnumerable<(string fullPath, string entryName, bool recurse)> enumerable = new(
- directory,
- (ref FileSystemEntry entry) =>
- {
- string fullPath = entry.ToFullPath();
- bool isRealDirectory = entry.IsDirectory && (entry.Attributes & FileAttributes.ReparsePoint) == 0;
- string entryName = GetEntryName(fullPath.AsSpan(basePathLength), appendDirectorySeparator: isRealDirectory);
- return (fullPath, entryName, isRealDirectory);
- });
-
- foreach ((string fullPath, string entryName, bool recurse) in enumerable)
- {
- yield return (fullPath, entryName);
-
- if (recurse)
- {
- foreach (var childEntry in EnumerateEntries(fullPath, basePathLength))
- {
- yield return childEntry;
- }
- }
- }
- }
-
- ///
- /// Converts a filesystem path that is relative to the archive root into a tar entry name using forward slashes,
- /// appending a trailing slash for directory entries.
- ///
- private static string GetEntryName(ReadOnlySpan relativePath, bool appendDirectorySeparator)
- {
- relativePath = relativePath.TrimStart("/\\".AsSpan());
- string entryName = relativePath.ToString().Replace('\\', '/');
-
- return appendDirectorySeparator ? entryName + '/' : entryName;
+ None,
+ GZip,
+ ZStandard,
}
}
}
From 6b0ebf02f3d6e3c5c86d21d833c1982cad658e95 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:57:50 +0000
Subject: [PATCH 20/31] Use scoped using declaration for compression stream
disposal
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks/TarDirectory.cs | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 73b876850bb..4b21a3244d3 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -135,17 +135,14 @@ public override bool Execute()
// Wrap the destination stream in the requested compression, if any. The tar archive is always
// written to the (optionally compressed) stream using the new .NET 11 overload that honors the
// requested TarEntryFormat.
- Stream? compressionStream = compression switch
+ using Stream? compressionStream = compression switch
{
TarCompression.GZip => new GZipStream(destinationStream, CompressionLevel.Optimal),
TarCompression.ZStandard => new ZstandardStream(destinationStream, CompressionLevel.Optimal),
_ => null,
};
- using (compressionStream)
- {
- TarFile.CreateFromDirectory(sourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
- }
+ TarFile.CreateFromDirectory(sourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
}
}
catch (Exception e)
From d6d155d6648249b7c36065e624389863a7a8ab35 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Jul 2026 07:34:32 +0000
Subject: [PATCH 21/31] Inline TryParseFormat helper into its call site in
TarDirectory
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks/TarDirectory.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 4b21a3244d3..5caea4f0a5d 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -123,7 +123,8 @@ public override bool Execute()
}
TarEntryFormat format = TarEntryFormat.Pax;
- if (!string.IsNullOrEmpty(Format) && !TryParseFormat(Format, out format))
+ if (!string.IsNullOrEmpty(Format)
+ && (!Enum.TryParse(Format, ignoreCase: true, out format) || format == TarEntryFormat.Unknown))
{
Log.LogErrorWithCodeFromResources("TarDirectory.ErrorInvalidFormat", Format);
@@ -184,9 +185,6 @@ static bool TryParseCompression(string? compression, out TarCompression result)
return true;
}
-
- static bool TryParseFormat(string format, out TarEntryFormat tarFormat) =>
- Enum.TryParse(format, ignoreCase: true, out tarFormat) && tarFormat != TarEntryFormat.Unknown;
}
///
From 9078ac8148449d616d3937cda4d104efcbeab405 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Jul 2026 07:40:30 +0000
Subject: [PATCH 22/31] Remove redundant Compression* const fields in favor of
TarCompression enum
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks/TarDirectory.cs | 25 +++++++++----------------
1 file changed, 9 insertions(+), 16 deletions(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 5caea4f0a5d..520c9a2990c 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -21,10 +21,6 @@ namespace Microsoft.Build.Tasks
[MSBuildMultiThreadableTask]
public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThreadableTask
{
- public const string CompressionNone = "None";
- public const string CompressionGZip = "GZip";
- public const string CompressionZStandard = "ZStandard";
-
///
/// Gets or sets a containing the full path to the destination file to create.
///
@@ -160,30 +156,27 @@ public override bool Execute()
static bool TryParseCompression(string? compression, out TarCompression result)
{
- if (string.IsNullOrEmpty(compression)
- || StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionNone))
+ if (string.IsNullOrEmpty(compression))
{
result = TarCompression.None;
+ return true;
}
- else if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionGZip)
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
+
+ if (StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
|| StringComparer.OrdinalIgnoreCase.Equals(compression, "gzip"))
{
result = TarCompression.GZip;
+ return true;
}
- else if (StringComparer.OrdinalIgnoreCase.Equals(compression, CompressionZStandard)
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "zstd")
+
+ if (StringComparer.OrdinalIgnoreCase.Equals(compression, "zstd")
|| StringComparer.OrdinalIgnoreCase.Equals(compression, "zst"))
{
result = TarCompression.ZStandard;
- }
- else
- {
- result = TarCompression.None;
- return false;
+ return true;
}
- return true;
+ return Enum.TryParse(compression, ignoreCase: true, out result) && Enum.IsDefined(result);
}
}
From 640ca3dd6f419e86f04b33ed65c1855bc8a04708 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:43:05 +0000
Subject: [PATCH 23/31] Add Untar task mirroring Unzip for extracting tar
archives
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Tasks.UnitTests/Untar_Tests.cs | 289 ++++++++++++++
src/Tasks/Microsoft.Build.Tasks.csproj | 2 +
src/Tasks/Microsoft.Common.tasks | 2 +
src/Tasks/Resources/Strings.resx | 52 +++
src/Tasks/Resources/xlf/Strings.cs.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.de.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.es.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.fr.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.it.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.ja.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.ko.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.pl.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.ru.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.tr.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 65 ++++
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 65 ++++
src/Tasks/Untar.cs | 407 ++++++++++++++++++++
18 files changed, 1597 insertions(+)
create mode 100644 src/Tasks.UnitTests/Untar_Tests.cs
create mode 100644 src/Tasks/Untar.cs
diff --git a/src/Tasks.UnitTests/Untar_Tests.cs b/src/Tasks.UnitTests/Untar_Tests.cs
new file mode 100644
index 00000000000..581c22b22a5
--- /dev/null
+++ b/src/Tasks.UnitTests/Untar_Tests.cs
@@ -0,0 +1,289 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Untar relies on System.Formats.Tar which is only available on .NET (not .NET Framework).
+#if NET
+
+using System;
+using System.IO;
+using Microsoft.Build.UnitTests;
+using Microsoft.Build.Utilities;
+using Shouldly;
+using Xunit;
+
+namespace Microsoft.Build.Tasks.UnitTests
+{
+ public class Untar_Tests
+ {
+ private readonly MockEngine _mockEngine = new MockEngine();
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("None")]
+ [InlineData("GZip")]
+ [InlineData("ZStandard")]
+ public void CanUntar(string? compression)
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "F1.txt", "F1");
+ testEnvironment.CreateFile(sourceFolder, "F2.txt", "F2");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SkipUnchangedFiles = false,
+ SourceFiles = [new TaskItem(tarFilePath)],
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ File.ReadAllText(Path.Combine(destination.Path, "F1.txt")).ShouldBe("F1");
+ File.ReadAllText(Path.Combine(destination.Path, "F2.txt")).ShouldBe("F2");
+ }
+ }
+
+ [Fact]
+ public void CanUntarWithIncludeFilter()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "included.txt", "included");
+ testEnvironment.CreateFile(sourceFolder, "excluded.txt", "excluded");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SkipUnchangedFiles = false,
+ SourceFiles = [new TaskItem(tarFilePath)],
+ Include = "included.txt",
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ File.Exists(Path.Combine(destination.Path, "included.txt")).ShouldBeTrue(_mockEngine.Log);
+ File.Exists(Path.Combine(destination.Path, "excluded.txt")).ShouldBeFalse(_mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void CanUntarWithExcludeFilter()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "kept.txt", "kept");
+ testEnvironment.CreateFile(sourceFolder, "dropped.txt", "dropped");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SkipUnchangedFiles = false,
+ SourceFiles = [new TaskItem(tarFilePath)],
+ Exclude = "dropped.txt",
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ File.Exists(Path.Combine(destination.Path, "kept.txt")).ShouldBeTrue(_mockEngine.Log);
+ File.Exists(Path.Combine(destination.Path, "dropped.txt")).ShouldBeFalse(_mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void SkipsUnchangedFiles()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "unchanged.txt", "unchanged");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar CreateTask() => new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SkipUnchangedFiles = true,
+ SourceFiles = [new TaskItem(tarFilePath)],
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ CreateTask().Execute().ShouldBeTrue(_mockEngine.Log);
+
+ // A second extraction should skip the unchanged file.
+ CreateTask().Execute().ShouldBeTrue(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain(nameof(Untar.SkipUnchangedFiles), customMessage: _mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void CanOverwriteExistingFile()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "file.txt", "new-content");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(destination, "file.txt", "old-content");
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SkipUnchangedFiles = false,
+ SourceFiles = [new TaskItem(tarFilePath)],
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ File.ReadAllText(Path.Combine(destination.Path, "file.txt")).ShouldBe("new-content");
+ }
+ }
+
+ [Fact]
+ public void LogsErrorIfSourceFileDoesNotExist()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SourceFiles = [new TaskItem(Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "missing.tar"))],
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4332", customMessage: _mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorForCorruptArchive()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder folder = testEnvironment.CreateFolder(createFolder: true);
+ TransientTestFile corrupt = testEnvironment.CreateFile(folder, "corrupt.tar", "this is not a tar archive");
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SourceFiles = [new TaskItem(corrupt.Path)],
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4333", customMessage: _mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorIfIncludeContainsPropertyReferences()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "file.txt", "file");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ SourceFiles = [new TaskItem(tarFilePath)],
+ Include = "$(Include)",
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4338", customMessage: _mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void FailIfNotIncrementalLogsError()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+ testEnvironment.CreateFile(sourceFolder, "file.txt", "file");
+
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+
+ TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
+
+ Untar untar = new Untar
+ {
+ BuildEngine = _mockEngine,
+ DestinationFolder = new TaskItem(destination.Path),
+ FailIfNotIncremental = true,
+ SkipUnchangedFiles = false,
+ SourceFiles = [new TaskItem(tarFilePath)],
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ untar.Execute().ShouldBeFalse(_mockEngine.Log);
+ }
+ }
+
+ private string CreateTar(TestEnvironment testEnvironment, TransientTestFolder sourceFolder, string? compression)
+ {
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ Compression = compression,
+ DestinationFile = new TaskItem(tarFilePath),
+ SourceDirectory = new TaskItem(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ return tarFilePath;
+ }
+ }
+}
+
+#endif
diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj
index c011de3dbe5..13ebfcbcf79 100644
--- a/src/Tasks/Microsoft.Build.Tasks.csproj
+++ b/src/Tasks/Microsoft.Build.Tasks.csproj
@@ -278,6 +278,8 @@
+
+
diff --git a/src/Tasks/Microsoft.Common.tasks b/src/Tasks/Microsoft.Common.tasks
index 0df375bb732..8e1fcd7b3c8 100644
--- a/src/Tasks/Microsoft.Common.tasks
+++ b/src/Tasks/Microsoft.Common.tasks
@@ -104,6 +104,8 @@
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+ Entry is outside the target directory: "{0}"
+
+
+ Untarring file "{0}" to "{1}".
+
+
@@ -3288,6 +3339,7 @@
MSB4300 - MSB4310 Task: AddToWin32Manifest
MSB4311 - MSB4320 Task: FormatUrl
MSB4321 - MSB4330 Task: TarDirectory
+ MSB4331 - MSB4340 Task: Untar
MSB4000 - MSB4200 Portable targets & tasks (vsproject\flavors\portable\msbuild)
MSB9000 - MSB9900 MSBuild targets files (C++)
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index a9d7ca3e37d..5f785e8308c 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2807,6 +2807,71 @@
MSB3394: Nelze zrušit registraci knihovny typů {0}, protože není zaregistrována.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Rozzipování ze souboru {0} do souboru {1} neproběhlo, protože parametr {2} byl v projektu nastaven na hodnotu {3} a velikosti souborů a časová razítka se shodují.
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index 5a4a8345883..34291d0378e 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2807,6 +2807,71 @@
MSB3394: Die Typbibliothek "{0}" ist nicht registriert und kann daher nicht aus der Registrierung entfernt werden.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Die Datei "{0}" wurde nicht in die Datei "{1}" entzippt, weil der Parameter "{2}" im Projekt auf "{3}" festgelegt war und die Größen und Zeitstempel der Dateien übereinstimmen.
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index 1527372271f..e2136963977 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2807,6 +2807,71 @@
MSB3394: La biblioteca de tipos "{0}" no está registrada, por lo que no puede anularse su registro.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.No se descomprimió del archivo "{0}" en el archivo "{1}" porque el parámetro "{2}" se estableció como "{3}" en el proyecto y los tamaños y las marcas de tiempo de los archivos coinciden.
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index 0b77315c1db..b221c325991 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2807,6 +2807,71 @@
MSB3394: La bibliothèque de types "{0}" n'est pas inscrite, impossible d'annuler l'inscription.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Impossible de décompresser le fichier "{0}" vers le fichier "{1}", car le paramètre "{2}" a la valeur "{3}" dans le projet, et les tailles et horodatages des fichiers correspondent.
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index 253e99a51ec..dd8b1e07307 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2807,6 +2807,71 @@
MSB3394: non è possibile annullare la registrazione della libreria dei tipi "{0}" perché non è registrata.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Non è stato possibile decomprimere il file "{0}" nel file "{1}". Il parametro "{2}" è stato impostato su "{3}" nel progetto e le dimensioni e il timestamp dei file corrispondono.
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index 896aed55a12..4d1a83c981d 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2807,6 +2807,71 @@
MSB3394: タイプ ライブラリ "{0}" は登録されていないため、登録解除できません。{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match."{2}" パラメーターがプロジェクトで "{3}" に設定されているため、またファイルのサイズとタイムスタンプが一致するため、ファイル "{0}" からファイル "{1}" に解凍しませんでした。
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index 30dbf64e920..3fba7227eaa 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2807,6 +2807,71 @@
MSB3394: 형식 라이브러리 "{0}"은(는) 등록되어 있지 않으므로 등록을 취소할 수 없습니다.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match."{2}" 매개 변수가 프로젝트에 "{3}"(으)로 설정되었고 파일 크기와 타임스탬프가 일치하기 때문에 "{0}" 파일에서 "{1}" 파일로 압축을 풀 수 없습니다.
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index ddbd1f3988d..4dad3c3ac6a 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2807,6 +2807,71 @@
MSB3394: Biblioteka typów „{0}” nie jest zarejestrowana, nie można wyrejestrować.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Nie wykonano rozpakowywania z pliku „{0}” do pliku „{1}”, ponieważ parametr „{2}” w projekcie został ustawiony na wartość „{3}”, a rozmiary plików i sygnatury czasowe pasują do siebie.
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index 06747378ae8..9679456cf39 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2807,6 +2807,71 @@
MSB3394: A biblioteca de tipos "{0}" não está registrada; não é possível cancelar o registro.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Não foi possível descompactar o arquivo "{0}" para o arquivo "{1}", pois o parâmetro "{2}" foi definido como "{3}" no projeto, e os tamanhos de arquivos e os carimbos de data/hora não correspondem.
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index d674a2bb4ba..4a4a4ef35e5 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2807,6 +2807,71 @@
MSB3394: Библиотека типов "{0}" не зарегистрирована. Невозможно отменить ее регистрацию.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Не удалось выполнить распаковку из файла "{0}" в файл "{1}", так как для параметра "{2}" в проекте было задано значение "{3}", а размеры файлов и отметки времени совпадают.
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index 0236f4f5ea8..bfc4fe8df94 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2807,6 +2807,71 @@
MSB3394: "{0}" tür kitaplığı kaydettirilmemiş, kaydı kaldırılamıyor.{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.Projede "{2}" parametresi "{3}" olarak ayarlandığından ve dosya boyutlarıyla zaman damgaları eşleştiğinden "{0}" dosyasını "{1}" dosyasına çıkarma işlemi gerçekleştirilmedi.
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index 8af23334ffe..a314ff72114 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2807,6 +2807,71 @@
MSB3394: 类型库“{0}”未注册,无法注销。{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.未从文件“{0}”解压缩到文件“{1}”,因为“{2}”参数在项目中设置为“{3}”,而两个文件的大小及时间戳一致。
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index 48284afb885..c7d435a53f8 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2807,6 +2807,71 @@
MSB3394: 類型程式庫 "{0}" 並未註冊,因此無法移除註冊。{StrBegin="MSB3394: "}
+
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+ Did not untar entry "{0}" because its entry type "{1}" is not supported.
+
+
+
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+ Did not untar from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.
+
+
+
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+ Did not untar file "{0}" because it didn't match the include filter or because it matched the exclude filter.
+
+
+
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ MSB4331: Failed to untar to directory "{0}" because it could not be created. {1}
+ {StrBegin="MSB4331: "}
+
+
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ MSB4336: Failed to untar file "{0}" to "{1}". {2}
+ {StrBegin="MSB4336: "}
+
+
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ MSB4335: Failed to untar file "{0}" because destination file "{1}" is read-only and could not be made writable. {2} {3}
+ {StrBegin="MSB4335: "}
+
+
+ MSB4333: Failed to open tar file "{0}". {1}
+ MSB4333: Failed to open tar file "{0}". {1}
+ {StrBegin="MSB4333: "}
+
+
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ MSB4334: Failed to untar file "{0}" to "{1}" because it is outside the destination directory.
+ {StrBegin="MSB4334: "}
+
+
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ MSB4332: Failed to untar file "{0}" because the file does not exist or is inaccessible.
+ {StrBegin="MSB4332: "}
+
+
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ MSB4337: Failed to parse pattern "{0}" because it contains an invalid path character.
+ {StrBegin="MSB4337: "}
+
+
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ MSB4338: Failed to parse pattern "{0}" because it contains a property reference which isn't supported.
+ {StrBegin="MSB4338: "}
+
+
+ Untarring file "{0}" to "{1}".
+ Untarring file "{0}" to "{1}".
+
+
+
+ Entry is outside the target directory: "{0}"
+ Entry is outside the target directory: "{0}"
+
+ Did not unzip from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match.並未從檔案 "{0}" 解壓縮到檔案 "{1}",因為在專案中的 "{2}" 參數原先設定為 "{3}",且檔案的大小與時間戳記相符。
diff --git a/src/Tasks/Untar.cs b/src/Tasks/Untar.cs
new file mode 100644
index 00000000000..0dd7c0597b1
--- /dev/null
+++ b/src/Tasks/Untar.cs
@@ -0,0 +1,407 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Formats.Tar;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Shared;
+using Microsoft.Build.Shared.FileSystem;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.Build.Tasks
+{
+ ///
+ /// Represents a task that can extract a tar archive, optionally compressed with GZip or ZStandard.
+ ///
+ ///
+ /// This task uses the APIs which are only available when MSBuild
+ /// runs on .NET (not .NET Framework). It is therefore registered to run only on the .NET runtime and
+ /// is unavailable in Visual Studio / MSBuild.exe.
+ ///
+ [MSBuildMultiThreadableTask]
+ public sealed class Untar : TaskExtension, ICancelableTask, IIncrementalTask, IMultiThreadableTask
+ {
+ // We pick a value that is the largest multiple of 4096 that is still smaller than the large object heap threshold (85K).
+ // The CopyTo/CopyToAsync buffer is short-lived and is likely to be collected at Gen0, and it offers a significant
+ // improvement in Copy performance.
+ private const int _DefaultCopyBufferSize = 81920;
+
+ ///
+ /// Stores a used for cancellation.
+ ///
+ private readonly CancellationTokenSource _cancellationToken = new CancellationTokenSource();
+
+ ///
+ /// Stores the include patterns after parsing.
+ ///
+ private string[] _includePatterns = [];
+
+ ///
+ /// Stores the exclude patterns after parsing.
+ ///
+ private string[] _excludePatterns = [];
+
+ ///
+ /// Gets or sets a with a destination folder path to untar the files to.
+ ///
+ [Required]
+ public ITaskItem DestinationFolder { get; set; } = null!;
+
+ ///
+ /// Gets or sets a value that indicates whether read-only files should be overwritten.
+ ///
+ public bool OverwriteReadOnlyFiles { get; set; }
+
+ ///
+ /// Gets or sets a value that indicates whether files should be skipped if the destination is unchanged.
+ ///
+ public bool SkipUnchangedFiles { get; set; } = true;
+
+ ///
+ /// Gets or sets an array of objects containing the paths to tar archive files to untar.
+ /// The compression (none, GZip, or ZStandard) is detected automatically from the archive contents.
+ ///
+ [Required]
+ public ITaskItem[] SourceFiles { get; set; } = null!;
+
+ ///
+ /// Gets or sets an MSBuild glob expression that specifies which files to include being untarred from the archive.
+ ///
+ public string? Include { get; set; }
+
+ ///
+ /// Gets or sets an MSBuild glob expression that specifies which files to exclude from being untarred from the archive.
+ ///
+ public string? Exclude { get; set; }
+
+ ///
+ /// Question the incremental nature of this task.
+ ///
+ /// This task does not support incremental build and will error out instead.
+ public bool FailIfNotIncremental { get; set; }
+
+ ///
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
+ ///
+ public void Cancel()
+ {
+ _cancellationToken.Cancel();
+ }
+
+ ///
+ public override bool Execute()
+ {
+ DirectoryInfo destinationDirectory;
+ try
+ {
+ AbsolutePath destinationPath = TaskEnvironment.GetAbsolutePath(DestinationFolder.ItemSpec);
+ destinationDirectory = Directory.CreateDirectory(destinationPath);
+ }
+ catch (Exception e)
+ {
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotCreateDestinationDirectory", DestinationFolder.ItemSpec, e.Message);
+
+ return false;
+ }
+
+ BuildEngine3.Yield();
+
+ try
+ {
+ ParseIncludeExclude();
+
+ if (!Log.HasLoggedErrors)
+ {
+ foreach (ITaskItem sourceFile in SourceFiles.TakeWhile(i => !_cancellationToken.IsCancellationRequested))
+ {
+ AbsolutePath sourceFilePath;
+ try
+ {
+ sourceFilePath = TaskEnvironment.GetAbsolutePath(sourceFile.ItemSpec);
+ }
+ catch (Exception)
+ {
+ Log.LogErrorWithCodeFromResources("Untar.ErrorFileDoesNotExist", sourceFile.ItemSpec);
+ continue;
+ }
+
+ if (!FileSystems.Default.FileExists(sourceFilePath))
+ {
+ Log.LogErrorWithCodeFromResources("Untar.ErrorFileDoesNotExist", sourceFile.ItemSpec);
+ continue;
+ }
+
+ try
+ {
+ using FileStream stream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 0x1000, useAsync: false);
+
+ // Detect and unwrap any compression applied to the tar archive. The decompression stream (if any)
+ // and the TarReader are disposed by the enclosing using statements below.
+ using Stream? decompressionStream = CreateDecompressionStream(stream);
+#pragma warning disable CA2000 // Dispose objects before losing scope because the using declaration disposes the TarReader.
+ using TarReader reader = new TarReader(decompressionStream ?? stream, leaveOpen: true);
+#pragma warning restore CA2000 // Dispose objects before losing scope
+
+ try
+ {
+ Extract(reader, destinationDirectory);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception e)
+ {
+ // Should only be thrown if the archive could not be read (corrupt file, etc).
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotOpenFile", sourceFile.ItemSpec, e.Message);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception e)
+ {
+ // Should only be thrown if the archive could not be opened (Access denied, corrupt file, etc).
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotOpenFile", sourceFile.ItemSpec, e.Message);
+ }
+ }
+ }
+ }
+ finally
+ {
+ BuildEngine3.Reacquire();
+ }
+
+ return !_cancellationToken.IsCancellationRequested && !Log.HasLoggedErrors;
+ }
+
+ ///
+ /// Creates a decompression stream around if the archive is compressed.
+ ///
+ /// The seekable positioned at the start of the archive.
+ ///
+ /// A decompression when GZip or ZStandard compression is detected; otherwise ,
+ /// in which case the archive should be read directly from .
+ ///
+ private static Stream? CreateDecompressionStream(FileStream stream)
+ {
+ Span magic = stackalloc byte[4];
+ int read = stream.Read(magic);
+ stream.Position = 0;
+
+ // GZip magic number: 0x1F 0x8B.
+ if (read >= 2 && magic[0] == 0x1F && magic[1] == 0x8B)
+ {
+ return new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true);
+ }
+
+ // ZStandard magic number: 0x28 0xB5 0x2F 0xFD.
+ if (read >= 4 && magic[0] == 0x28 && magic[1] == 0xB5 && magic[2] == 0x2F && magic[3] == 0xFD)
+ {
+ return new ZstandardStream(stream, CompressionMode.Decompress, leaveOpen: true);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Extracts all entries to the specified directory.
+ ///
+ /// The containing the entries to extract.
+ /// The to extract entries to.
+ private void Extract(TarReader reader, DirectoryInfo destinationDirectory)
+ {
+ AbsolutePath fullDestinationDirectoryPath = TaskEnvironment.GetAbsolutePath(FileUtilities.EnsureTrailingSlash(destinationDirectory.FullName)).GetCanonicalForm();
+
+ for (TarEntry? tarEntry = reader.GetNextEntry(); tarEntry is not null && !_cancellationToken.IsCancellationRequested; tarEntry = reader.GetNextEntry())
+ {
+ string entryName = tarEntry.Name;
+
+ if (ShouldSkipEntry(entryName))
+ {
+ Log.LogMessageFromResources(MessageImportance.Low, "Untar.DidNotUntarBecauseOfFilter", entryName);
+ continue;
+ }
+
+ AbsolutePath fullDestinationPath = TaskEnvironment.GetAbsolutePath(Path.Combine(destinationDirectory.FullName, entryName)).GetCanonicalForm();
+ ErrorUtilities.VerifyThrowInvalidOperation(fullDestinationPath.Value.StartsWith(fullDestinationDirectoryPath, FileUtilities.PathComparison), "Untar.TarSlipExploit", fullDestinationPath);
+
+ FileInfo destinationPath = new(fullDestinationPath);
+
+ // Directory entries and entries whose name refers to a directory should be created and skipped.
+ if (tarEntry.EntryType is TarEntryType.Directory || Path.GetFileName(destinationPath.FullName).Length == 0)
+ {
+ Directory.CreateDirectory(destinationPath.FullName);
+ continue;
+ }
+
+ // Only regular files are extracted. Other entry types (symbolic/hard links, devices, etc.) are skipped.
+ if (tarEntry.EntryType is not (TarEntryType.RegularFile or TarEntryType.V7RegularFile))
+ {
+ Log.LogMessageFromResources(MessageImportance.Low, "Untar.DidNotUntarBecauseOfEntryType", entryName, tarEntry.EntryType.ToString());
+ continue;
+ }
+
+ if (!destinationPath.FullName.StartsWith(destinationDirectory.FullName, StringComparison.OrdinalIgnoreCase))
+ {
+ // TarFile.ExtractToDirectory() throws an IOException for this but since we're extracting one file at a time
+ // for logging and cancellation, we need to check for it ourselves.
+ Log.LogErrorFromResources("Untar.ErrorExtractingResultsInFilesOutsideDestination", destinationPath.FullName, destinationDirectory.FullName);
+ continue;
+ }
+
+ if (ShouldSkipEntry(tarEntry, destinationPath))
+ {
+ Log.LogMessageFromResources(MessageImportance.Low, "Untar.DidNotUntarBecauseOfFileMatch", entryName, destinationPath.FullName, nameof(SkipUnchangedFiles), "true");
+ continue;
+ }
+ else if (FailIfNotIncremental)
+ {
+ Log.LogErrorFromResources("Untar.FileComment", entryName, destinationPath.FullName);
+ continue;
+ }
+
+ try
+ {
+ destinationPath.Directory?.Create();
+ }
+ catch (Exception e)
+ {
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotCreateDestinationDirectory", destinationPath.DirectoryName, e.Message);
+ continue;
+ }
+
+ if (OverwriteReadOnlyFiles && destinationPath.Exists && destinationPath.IsReadOnly)
+ {
+ try
+ {
+ destinationPath.IsReadOnly = false;
+ }
+ catch (Exception e)
+ {
+ string lockedFileMessage = LockCheck.GetLockedFileMessage(destinationPath.FullName);
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotMakeFileWriteable", entryName, destinationPath.FullName, e.Message, lockedFileMessage);
+ continue;
+ }
+ }
+
+ try
+ {
+ Log.LogMessageFromResources(MessageImportance.Normal, "Untar.FileComment", entryName, destinationPath.FullName);
+
+ FileStreamOptions fileStreamOptions = new()
+ {
+ Access = FileAccess.Write,
+ Mode = FileMode.Create,
+ Share = FileShare.None,
+ BufferSize = 0x1000
+ };
+
+ const UnixFileMode OwnershipPermissions =
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
+
+ // Restore Unix permissions.
+ // For security, limit to ownership permissions, and respect umask (through UnixCreateMode).
+ UnixFileMode mode = tarEntry.Mode & OwnershipPermissions;
+ if (mode != UnixFileMode.None && NativeMethodsShared.IsUnixLike)
+ {
+ fileStreamOptions.UnixCreateMode = mode;
+ }
+
+ using (FileStream destination = new FileStream(destinationPath.FullName, fileStreamOptions))
+ {
+ if (tarEntry.DataStream is Stream dataStream)
+ {
+#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks
+ dataStream.CopyToAsync(destination, _DefaultCopyBufferSize, _cancellationToken.Token)
+ .ConfigureAwait(continueOnCapturedContext: false)
+ .GetAwaiter()
+ .GetResult();
+#pragma warning restore CA2025
+ }
+ }
+
+ destinationPath.LastWriteTimeUtc = tarEntry.ModificationTime.UtcDateTime;
+ }
+ catch (IOException e)
+ {
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotExtractFile", entryName, destinationPath.FullName, e.Message);
+ }
+ }
+ }
+
+ ///
+ /// Determines whether or not an entry should be skipped when untarring by filtering.
+ ///
+ /// The full name of the entry in the tar archive.
+ /// true if the entry should be skipped, otherwise false.
+ private bool ShouldSkipEntry(string entryName)
+ {
+ bool result = false;
+
+ if (_includePatterns.Length > 0)
+ {
+ result = _includePatterns.All(pattern => !FileMatcher.IsMatch(FileMatcher.Normalize(entryName), pattern));
+ }
+
+ if (_excludePatterns.Length > 0)
+ {
+ result |= _excludePatterns.Any(pattern => FileMatcher.IsMatch(FileMatcher.Normalize(entryName), pattern));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Determines whether or not an entry should be skipped when untarring.
+ ///
+ /// The object containing information about the entry in the tar archive.
+ /// A object containing information about the destination file.
+ /// true if the entry should be skipped, otherwise false.
+ private bool ShouldSkipEntry(TarEntry tarEntry, FileInfo fileInfo)
+ {
+ return SkipUnchangedFiles
+ && fileInfo.Exists
+ && tarEntry.ModificationTime.UtcDateTime == fileInfo.LastWriteTimeUtc
+ && tarEntry.Length == fileInfo.Length;
+ }
+
+ private void ParseIncludeExclude()
+ {
+ ParsePattern(Include, out _includePatterns);
+ ParsePattern(Exclude, out _excludePatterns);
+ }
+
+ private void ParsePattern(string? pattern, out string[] patterns)
+ {
+ patterns = [];
+ if (!string.IsNullOrWhiteSpace(pattern))
+ {
+ if (FileMatcher.HasPropertyOrItemReferences(pattern))
+ {
+ // Supporting property references would require access to Expander which is unavailable in Microsoft.Build.Tasks
+ Log.LogErrorWithCodeFromResources("Untar.ErrorParsingPatternPropertyReferences", pattern);
+ }
+ else if (pattern.AsSpan().IndexOfAny(FileUtilities.InvalidPathChars) >= 0)
+ {
+ Log.LogErrorWithCodeFromResources("Untar.ErrorParsingPatternInvalidPath", pattern);
+ }
+ else
+ {
+ patterns = pattern.Contains(';')
+ ? pattern.Split([';'], StringSplitOptions.RemoveEmptyEntries).Select(FileMatcher.Normalize).ToArray()
+ : [pattern];
+ }
+ }
+ }
+ }
+}
From bc12d42a105fb745cebfc8059b28cbc09f3715bc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Jul 2026 20:47:44 +0000
Subject: [PATCH 24/31] Convert TarDirectory to typed task-parameter binding
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
.../BackEnd/TaskBuilderTestTask.cs | 27 ++++
.../BackEnd/TaskExecutionHost_Tests.cs | 43 ++++++
src/Framework/ValueTypeParser.cs | 13 ++
src/Tasks.UnitTests/TarDirectory_Tests.cs | 136 +++++-------------
src/Tasks.UnitTests/Untar_Tests.cs | 27 ++--
src/Tasks/Resources/Strings.resx | 8 --
src/Tasks/Resources/xlf/Strings.cs.xlf | 10 --
src/Tasks/Resources/xlf/Strings.de.xlf | 10 --
src/Tasks/Resources/xlf/Strings.es.xlf | 10 --
src/Tasks/Resources/xlf/Strings.fr.xlf | 10 --
src/Tasks/Resources/xlf/Strings.it.xlf | 10 --
src/Tasks/Resources/xlf/Strings.ja.xlf | 10 --
src/Tasks/Resources/xlf/Strings.ko.xlf | 10 --
src/Tasks/Resources/xlf/Strings.pl.xlf | 10 --
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 10 --
src/Tasks/Resources/xlf/Strings.ru.xlf | 10 --
src/Tasks/Resources/xlf/Strings.tr.xlf | 10 --
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 10 --
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 10 --
src/Tasks/TarDirectory.cs | 92 ++++--------
20 files changed, 154 insertions(+), 322 deletions(-)
diff --git a/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs b/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs
index 398af99a80e..d3bc1e90b09 100644
--- a/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs
+++ b/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs
@@ -66,6 +66,16 @@ public CustomStruct(IConvertible value)
///
internal static readonly CustomStruct[] s_customStructArray = new CustomStruct[] { new CustomStruct(43), new CustomStruct(44) };
+ ///
+ /// An enum used to validate engine-level binding of string values to enum task parameters.
+ ///
+ public enum TestTaskEnum
+ {
+ None,
+ First,
+ Second,
+ }
+
///
/// The task host.
///
@@ -241,6 +251,11 @@ public CustomStruct(IConvertible value)
///
private AbsolutePath _absolutePathOutput;
+ ///
+ /// The value set for the EnumParam.
+ ///
+ private TestTaskEnum _enumOutput;
+
///
/// The value for the AbsolutePathArrayOutput.
///
@@ -701,6 +716,18 @@ public AbsolutePath AbsolutePathParam
}
}
+ ///
+ /// An enum parameter, used to validate engine-level binding of string values to enum task parameters.
+ ///
+ public TestTaskEnum EnumParam
+ {
+ set
+ {
+ _enumOutput = value;
+ _testTaskHost?.ParameterSet("EnumParam", value);
+ }
+ }
+
///
/// An AbsolutePath array parameter.
///
diff --git a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
index 122ea6506a5..8c6654d9699 100644
--- a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
@@ -700,6 +700,49 @@ public void TestSetTaskItemIntArrayParamEmptyItem()
#endregion
+ #region Enum Params
+
+ ///
+ /// Validate that setting an enum parameter binds the string value (case-insensitively) to the enum member.
+ ///
+ [Fact]
+ public void TestSetEnumParam()
+ {
+ ValidateTaskParameter("EnumParam", "Second", TaskBuilderTestTask.TestTaskEnum.Second);
+ }
+
+ ///
+ /// Validate that enum binding is case-insensitive.
+ ///
+ [Fact]
+ public void TestSetEnumParamCaseInsensitive()
+ {
+ ValidateTaskParameter("EnumParam", "first", TaskBuilderTestTask.TestTaskEnum.First);
+ }
+
+ ///
+ /// Validate that setting the parameter with an empty value does not cause it to be set.
+ ///
+ [Fact]
+ public void TestSetEnumParamEmptyAttribute()
+ {
+ ValidateTaskParameterNotSet("EnumParam", "");
+ }
+
+ ///
+ /// Validate that setting an enum parameter to a value that does not map to a defined member is an error.
+ ///
+ [Fact]
+ public void TestSetEnumParamInvalidValue()
+ {
+ var parameters = GetStandardParametersDictionary(true);
+ parameters["EnumParam"] = ("NotADefinedValue", ElementLocation.Create("foo.proj"));
+
+ Should.Throw(() => _host.SetTaskParameters(parameters));
+ }
+
+ #endregion
+
#region FileInfo Params
///
diff --git a/src/Framework/ValueTypeParser.cs b/src/Framework/ValueTypeParser.cs
index d5672cdf62b..123bc72b692 100644
--- a/src/Framework/ValueTypeParser.cs
+++ b/src/Framework/ValueTypeParser.cs
@@ -94,6 +94,19 @@ public static object Parse(string value, Type targetType)
return value;
}
+ // Enums bind by their (case-insensitive) member name, and values that do not map to a
+ // defined member are rejected so the engine surfaces a clear parameter-binding error.
+ if (targetType.IsEnum)
+ {
+ object result = Enum.Parse(targetType, value, ignoreCase: true);
+ if (!Enum.IsDefined(targetType, result))
+ {
+ throw new ArgumentException($"'{value}' is not a defined value of enum type {targetType.Name}.", nameof(value));
+ }
+
+ return result;
+ }
+
// Everything else (numeric types, char, etc.) is converted via Convert.ChangeType, pinned
// to InvariantCulture. This is deliberately permissive: a future analyzer is expected to
// steer authors toward the first-class supported types above and away from relying on this
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
index 705303fd4d6..5fea691b5aa 100644
--- a/src/Tasks.UnitTests/TarDirectory_Tests.cs
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -10,7 +10,6 @@
using System.IO;
using System.IO.Compression;
using Microsoft.Build.UnitTests;
-using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
@@ -21,13 +20,10 @@ public class TarDirectory_Tests
private readonly MockEngine _mockEngine = new MockEngine();
[Theory]
- [InlineData(null)]
- [InlineData("None")]
- [InlineData("GZip")]
- [InlineData("gz")]
- [InlineData("ZStandard")]
- [InlineData("zstd")]
- public void CanTarDirectory(string? compression)
+ [InlineData(TarDirectory.TarCompression.None)]
+ [InlineData(TarDirectory.TarCompression.GZip)]
+ [InlineData(TarDirectory.TarCompression.ZStandard)]
+ public void CanTarDirectory(TarDirectory.TarCompression compression)
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
@@ -42,8 +38,8 @@ public void CanTarDirectory(string? compression)
{
BuildEngine = _mockEngine,
Compression = compression,
- DestinationFile = new TaskItem(tarFilePath),
- SourceDirectory = new TaskItem(sourceFolder.Path),
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -80,9 +76,9 @@ public void CanOverwriteExistingFile()
TarDirectory tarDirectory = new TarDirectory
{
BuildEngine = _mockEngine,
- DestinationFile = new TaskItem(file.Path),
+ DestinationFile = new FileInfo(file.Path),
Overwrite = true,
- SourceDirectory = new TaskItem(sourceFolder.Path),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -91,7 +87,7 @@ public void CanOverwriteExistingFile()
_mockEngine.Log.ShouldContain(sourceFolder.Path, customMessage: _mockEngine.Log);
_mockEngine.Log.ShouldContain(file.Path, customMessage: _mockEngine.Log);
- GetTarEntryNames(file.Path, compression: null)
+ GetTarEntryNames(file.Path, TarDirectory.TarCompression.None)
.ShouldBe(
[
"F1C22D660B0D4DAAA296C1B980320B03.txt",
@@ -113,8 +109,8 @@ public void LogsErrorIfDestinationExists()
TarDirectory tarDirectory = new TarDirectory
{
BuildEngine = _mockEngine,
- DestinationFile = new TaskItem(file.Path),
- SourceDirectory = new TaskItem(folder.Path),
+ DestinationFile = new FileInfo(file.Path),
+ SourceDirectory = new DirectoryInfo(folder.Path),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -130,8 +126,8 @@ public void LogsErrorIfDirectoryDoesNotExist()
TarDirectory tarDirectory = new TarDirectory
{
BuildEngine = _mockEngine,
- DestinationFile = new TaskItem(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "test.tar")),
- SourceDirectory = new TaskItem(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))),
+ DestinationFile = new FileInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "test.tar")),
+ SourceDirectory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -140,41 +136,12 @@ public void LogsErrorIfDirectoryDoesNotExist()
_mockEngine.Log.ShouldContain("MSB4321", customMessage: _mockEngine.Log);
}
- [Fact]
- public void LogsErrorForInvalidCompression()
- {
- using (TestEnvironment testEnvironment = TestEnvironment.Create())
- {
- TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
-
- testEnvironment.CreateFile(sourceFolder, "9E0A4E0F5C8D4F0FA0B33F79C2F0B0C1.txt", "content");
-
- string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
-
- TarDirectory tarDirectory = new TarDirectory
- {
- BuildEngine = _mockEngine,
- Compression = "RandomUnsupportedValue",
- DestinationFile = new TaskItem(tarFilePath),
- SourceDirectory = new TaskItem(sourceFolder.Path),
- TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
- };
-
- // Invalid compression is an error and no archive is created.
- tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
-
- _mockEngine.Log.ShouldContain("MSB4324", customMessage: _mockEngine.Log);
-
- File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
- }
- }
-
[Theory]
- [InlineData("Pax", TarEntryFormat.Pax)]
- [InlineData("gnu", TarEntryFormat.Gnu)]
- [InlineData("Ustar", TarEntryFormat.Ustar)]
- [InlineData("V7", TarEntryFormat.V7)]
- public void CanTarDirectoryWithFormat(string format, TarEntryFormat expectedFormat)
+ [InlineData(TarEntryFormat.Pax)]
+ [InlineData(TarEntryFormat.Gnu)]
+ [InlineData(TarEntryFormat.Ustar)]
+ [InlineData(TarEntryFormat.V7)]
+ public void CanTarDirectoryWithFormat(TarEntryFormat format)
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
@@ -188,52 +155,23 @@ public void CanTarDirectoryWithFormat(string format, TarEntryFormat expectedForm
{
BuildEngine = _mockEngine,
Format = format,
- DestinationFile = new TaskItem(tarFilePath),
- SourceDirectory = new TaskItem(sourceFolder.Path),
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
GetTarEntryFormats(tarFilePath)
- .ShouldAllBe(entryFormat => entryFormat == expectedFormat, _mockEngine.Log);
+ .ShouldAllBe(entryFormat => entryFormat == format, _mockEngine.Log);
- GetTarEntryNames(tarFilePath, compression: null)
+ GetTarEntryNames(tarFilePath, TarDirectory.TarCompression.None)
.ShouldBe(["3F6D2F2E3C1A4B5C8D9E0F1A2B3C4D5E.txt"]);
}
}
[Fact]
- public void LogsErrorForInvalidFormat()
- {
- using (TestEnvironment testEnvironment = TestEnvironment.Create())
- {
- TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
-
- testEnvironment.CreateFile(sourceFolder, "8B0F5A2D6E1C4F0FB1C44F80D3F1C1D2.txt", "content");
-
- string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
-
- TarDirectory tarDirectory = new TarDirectory
- {
- BuildEngine = _mockEngine,
- Format = "RandomUnsupportedValue",
- DestinationFile = new TaskItem(tarFilePath),
- SourceDirectory = new TaskItem(sourceFolder.Path),
- TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
- };
-
- // Invalid format is an error and no archive is created.
- tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
-
- _mockEngine.Log.ShouldContain("MSB4325", customMessage: _mockEngine.Log);
-
- File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
- }
- }
-
- [Fact]
- public void LogsErrorForUnknownFormat()
+ public void UnknownFormatFallsBackToPax()
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
@@ -247,22 +185,21 @@ public void LogsErrorForUnknownFormat()
{
BuildEngine = _mockEngine,
- // "Unknown" parses to TarEntryFormat.Unknown, which is not a valid archive format.
- Format = "Unknown",
- DestinationFile = new TaskItem(tarFilePath),
- SourceDirectory = new TaskItem(sourceFolder.Path),
+ // TarEntryFormat.Unknown is not a real archive format; the task falls back to the Pax default.
+ Format = TarEntryFormat.Unknown,
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
- tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
-
- _mockEngine.Log.ShouldContain("MSB4325", customMessage: _mockEngine.Log);
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
- File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
+ GetTarEntryFormats(tarFilePath)
+ .ShouldAllBe(entryFormat => entryFormat == TarEntryFormat.Pax, _mockEngine.Log);
}
}
- private static List GetTarEntryNames(string tarFilePath, string? compression)
+ private static List GetTarEntryNames(string tarFilePath, TarDirectory.TarCompression compression)
{
List names = new List();
@@ -271,14 +208,9 @@ private static List GetTarEntryNames(string tarFilePath, string? compres
// Wrap the file stream in a matching decompression stream, if the archive was compressed.
Stream? decompressionStream = compression switch
{
- null => null,
- _ when StringComparer.OrdinalIgnoreCase.Equals(compression, "None") => null,
- _ when StringComparer.OrdinalIgnoreCase.Equals(compression, "GZip")
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "gzip") => new GZipStream(stream, CompressionMode.Decompress),
- _ when StringComparer.OrdinalIgnoreCase.Equals(compression, "ZStandard")
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "zstd")
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "zst") => new ZstandardStream(stream, CompressionMode.Decompress),
+ TarDirectory.TarCompression.None => null,
+ TarDirectory.TarCompression.GZip => new GZipStream(stream, CompressionMode.Decompress),
+ TarDirectory.TarCompression.ZStandard => new ZstandardStream(stream, CompressionMode.Decompress),
_ => throw new ArgumentException($"Unexpected compression '{compression}'.", nameof(compression)),
};
diff --git a/src/Tasks.UnitTests/Untar_Tests.cs b/src/Tasks.UnitTests/Untar_Tests.cs
index 581c22b22a5..be6ab69494a 100644
--- a/src/Tasks.UnitTests/Untar_Tests.cs
+++ b/src/Tasks.UnitTests/Untar_Tests.cs
@@ -18,11 +18,10 @@ public class Untar_Tests
private readonly MockEngine _mockEngine = new MockEngine();
[Theory]
- [InlineData(null)]
- [InlineData("None")]
- [InlineData("GZip")]
- [InlineData("ZStandard")]
- public void CanUntar(string? compression)
+ [InlineData(TarDirectory.TarCompression.None)]
+ [InlineData(TarDirectory.TarCompression.GZip)]
+ [InlineData(TarDirectory.TarCompression.ZStandard)]
+ public void CanUntar(TarDirectory.TarCompression compression)
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
@@ -59,7 +58,7 @@ public void CanUntarWithIncludeFilter()
testEnvironment.CreateFile(sourceFolder, "included.txt", "included");
testEnvironment.CreateFile(sourceFolder, "excluded.txt", "excluded");
- string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder);
TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
@@ -89,7 +88,7 @@ public void CanUntarWithExcludeFilter()
testEnvironment.CreateFile(sourceFolder, "kept.txt", "kept");
testEnvironment.CreateFile(sourceFolder, "dropped.txt", "dropped");
- string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder);
TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
@@ -118,7 +117,7 @@ public void SkipsUnchangedFiles()
TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
testEnvironment.CreateFile(sourceFolder, "unchanged.txt", "unchanged");
- string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder);
TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
@@ -148,7 +147,7 @@ public void CanOverwriteExistingFile()
TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
testEnvironment.CreateFile(sourceFolder, "file.txt", "new-content");
- string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder);
TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: true);
testEnvironment.CreateFile(destination, "file.txt", "old-content");
@@ -221,7 +220,7 @@ public void LogsErrorIfIncludeContainsPropertyReferences()
TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
testEnvironment.CreateFile(sourceFolder, "file.txt", "file");
- string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder);
TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
@@ -248,7 +247,7 @@ public void FailIfNotIncrementalLogsError()
TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
testEnvironment.CreateFile(sourceFolder, "file.txt", "file");
- string tarFilePath = CreateTar(testEnvironment, sourceFolder, compression: null);
+ string tarFilePath = CreateTar(testEnvironment, sourceFolder);
TransientTestFolder destination = testEnvironment.CreateFolder(createFolder: false);
@@ -266,7 +265,7 @@ public void FailIfNotIncrementalLogsError()
}
}
- private string CreateTar(TestEnvironment testEnvironment, TransientTestFolder sourceFolder, string? compression)
+ private string CreateTar(TestEnvironment testEnvironment, TransientTestFolder sourceFolder, TarDirectory.TarCompression compression = TarDirectory.TarCompression.None)
{
string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
@@ -274,8 +273,8 @@ private string CreateTar(TestEnvironment testEnvironment, TransientTestFolder so
{
BuildEngine = _mockEngine,
Compression = compression,
- DestinationFile = new TaskItem(tarFilePath),
- SourceDirectory = new TaskItem(sourceFolder.Path),
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx
index f8d94ad96ef..bda78c9e41f 100644
--- a/src/Tasks/Resources/Strings.resx
+++ b/src/Tasks/Resources/Strings.resx
@@ -3110,14 +3110,6 @@
MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}{StrBegin="MSB4323: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
-
Creating tar archive from directory "{0}" to "{1}".
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index 5f785e8308c..64bca91481f 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Úloha {0} se nepodporuje ve verzi MSBuildu pro .NET Core. Použijte prosím verzi MSBuildu pro .NET Framework. Další podrobnosti najdete na stránce https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index 34291d0378e..e5861050646 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Die Aufgabe "{0}" wird für die .NET Core-Version von MSBuild nicht unterstützt. Verwenden Sie die .NET Framework-Version von MSBuild. Weitere Informationen finden Sie unter https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index e2136963977..7f4f7df54f7 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: No se admite la tarea "{0}" en la versión de MSBuild de .NET Core. Use la versión de MSBuild de .NET Framework. Vea https://aka.ms/msbuild/MSB4803 para obtener más información.
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index b221c325991..a46595b4f64 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: La tâche "{0}" n'est pas prise en charge dans la version .NET Core de MSBuild. Utilisez la version du .NET Framework de MSBuild. Pour plus d'informations, consultez https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index dd8b1e07307..abca15d6ad2 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: l'attività "{0}" non è supportata nella versione .NET Core di MSBuild. Usare la versione .NET Framework di MSBuild. Per altri dettagli, vedere https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index 4d1a83c981d..a219687cf6c 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: タスク "{0}" は .NET Core バージョンの MSBuild ではサポートされていません。.NET Framework バージョンの MSBuild をご使用ください。詳細については、https://aka.ms/msbuild/MSB4803 をご覧ください。
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index 3fba7227eaa..fa1e4b279c1 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: "{0}" 작업은 MSBuild의 .NET Core 버전에서 지원되지 않습니다. MSBuild의 .NET Framework 버전을 사용하세요. 자세한 내용은 https://aka.ms/msbuild/MSB4803을 참조하세요.
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index 4dad3c3ac6a..0c986ecd84e 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Zadanie „{0}” nie jest obsługiwane w wersji programu MSBuild dla platformy .NET Core. Użyj wersji programu MSBuild dla platformy .NET Framework. Zobacz https://aka.ms/msbuild/MSB4803, aby uzyskać więcej szczegółów.
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index 9679456cf39..dc9d7227d0e 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: a tarefa "{0}" não é compatível com a versão do .NET Core do MSBuild. Use a versão do .NET Framework do MSBuild. Confira https://aka.ms/msbuild/MSB4803 para obter mais detalhes.
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index 4a4a4ef35e5..544b9275a79 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: задача "{0}" не поддерживается в MSBuild версии .NET Core. Используйте MSBuild версии .NET Framework. Дополнительные сведения: https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index bfc4fe8df94..34de691f1f6 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild’in .NET Core sürümünde "{0}" görevi desteklenmiyor. Lütfen MSBuild’in .NET Framework sürümünü kullanın. Daha ayrıntılı bilgi için bkz. https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index a314ff72114..5782ea784cb 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: .NET Core 版本的 MSBuild 不支持“{0}”。请使用 .NET Framework 版本的 MSBuild。有关更多详细信息,请参阅 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index c7d435a53f8..6a51ce375bc 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2692,16 +2692,6 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
-
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- MSB4324: Unsupported compression '{0}'. Valid values are 'None', 'GZip', and 'ZStandard'.
- {StrBegin="MSB4324: "}
-
-
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- MSB4325: Unsupported tar format '{0}'. Valid values are 'Pax', 'Gnu', 'Ustar', and 'V7'.
- {StrBegin="MSB4325: "}
- MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild 的 .NET Core 版本不支援工作 "{0}"。請使用 MSBuild 的 .NET Framework 版本。如需進一步的詳細資料,請參閱 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 520c9a2990c..22db22771b7 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -22,10 +22,10 @@ namespace Microsoft.Build.Tasks
public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThreadableTask
{
///
- /// Gets or sets a containing the full path to the destination file to create.
+ /// Gets or sets the full path to the destination file to create.
///
[Required]
- public ITaskItem DestinationFile { get; set; } = null!;
+ public FileInfo DestinationFile { get; set; } = null!;
///
/// Gets or sets a value indicating whether the destination file should be overwritten.
@@ -33,10 +33,10 @@ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThread
public bool Overwrite { get; set; }
///
- /// Gets or sets a containing the full path to the source directory to create a tar archive from.
+ /// Gets or sets the full path to the source directory to create a tar archive from.
///
[Required]
- public ITaskItem SourceDirectory { get; set; } = null!;
+ public DirectoryInfo SourceDirectory { get; set; } = null!;
///
/// Question the incremental nature of this task.
@@ -46,56 +46,50 @@ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThread
///
/// Gets or sets the compression to apply to the tar archive.
- /// Valid values are "None" (the default), "GZip", and "ZStandard".
+ /// The default is .
/// This parameter is optional.
///
- public string? Compression { get; set; }
+ public TarCompression Compression { get; set; } = TarCompression.None;
///
/// Gets or sets the tar entry format to use for the archive.
- /// Valid values are "Pax" (the default), "Gnu", "Ustar", and "V7".
+ /// The default is .
/// This parameter is optional.
///
- public string? Format { get; set; }
+ public TarEntryFormat Format { get; set; } = TarEntryFormat.Pax;
///
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
public override bool Execute()
{
- AbsolutePath sourceDirectoryAbsolutePath = TaskEnvironment.GetAbsolutePath(SourceDirectory.ItemSpec);
- DirectoryInfo sourceDirectory = new DirectoryInfo(sourceDirectoryAbsolutePath);
-
- if (!sourceDirectory.Exists)
+ if (!SourceDirectory.Exists)
{
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorDirectoryDoesNotExist", sourceDirectory.FullName);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorDirectoryDoesNotExist", SourceDirectory.FullName);
return false;
}
- AbsolutePath destinationFileAbsolutePath = TaskEnvironment.GetAbsolutePath(DestinationFile.ItemSpec);
- FileInfo destinationFile = new FileInfo(destinationFileAbsolutePath);
-
BuildEngine3.Yield();
try
{
- if (destinationFile.Exists)
+ if (DestinationFile.Exists)
{
if (!Overwrite || FailIfNotIncremental)
{
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFileExists", destinationFile.FullName);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFileExists", DestinationFile.FullName);
return false;
}
try
{
- File.Delete(destinationFile.FullName);
+ File.Delete(DestinationFile.FullName);
}
catch (Exception e)
{
- string lockedFileMessage = LockCheck.GetLockedFileMessage(destinationFile.FullName);
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", sourceDirectory.FullName, destinationFile.FullName, e.Message, lockedFileMessage);
+ string lockedFileMessage = LockCheck.GetLockedFileMessage(DestinationFile.FullName);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", SourceDirectory.FullName, DestinationFile.FullName, e.Message, lockedFileMessage);
return false;
}
@@ -105,46 +99,33 @@ public override bool Execute()
{
if (FailIfNotIncremental)
{
- Log.LogErrorFromResources("TarDirectory.Comment", sourceDirectory.FullName, destinationFile.FullName);
+ Log.LogErrorFromResources("TarDirectory.Comment", SourceDirectory.FullName, DestinationFile.FullName);
}
else
{
- Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", sourceDirectory.FullName, destinationFile.FullName);
-
- if (!TryParseCompression(Compression, out TarCompression compression))
- {
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorInvalidCompression", Compression);
-
- return false;
- }
+ Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", SourceDirectory.FullName, DestinationFile.FullName);
- TarEntryFormat format = TarEntryFormat.Pax;
- if (!string.IsNullOrEmpty(Format)
- && (!Enum.TryParse(Format, ignoreCase: true, out format) || format == TarEntryFormat.Unknown))
- {
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorInvalidFormat", Format);
-
- return false;
- }
+ // Unknown is only reachable if it was explicitly set; fall back to the Pax default.
+ TarEntryFormat format = Format == TarEntryFormat.Unknown ? TarEntryFormat.Pax : Format;
- using FileStream destinationStream = new FileStream(destinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
+ using FileStream destinationStream = new FileStream(DestinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
// Wrap the destination stream in the requested compression, if any. The tar archive is always
// written to the (optionally compressed) stream using the new .NET 11 overload that honors the
// requested TarEntryFormat.
- using Stream? compressionStream = compression switch
+ using Stream? compressionStream = Compression switch
{
TarCompression.GZip => new GZipStream(destinationStream, CompressionLevel.Optimal),
TarCompression.ZStandard => new ZstandardStream(destinationStream, CompressionLevel.Optimal),
_ => null,
};
- TarFile.CreateFromDirectory(sourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
+ TarFile.CreateFromDirectory(SourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
}
}
catch (Exception e)
{
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", sourceDirectory.FullName, destinationFile.FullName, e.Message, string.Empty);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", SourceDirectory.FullName, DestinationFile.FullName, e.Message, string.Empty);
}
}
finally
@@ -153,37 +134,12 @@ public override bool Execute()
}
return !Log.HasLoggedErrors;
-
- static bool TryParseCompression(string? compression, out TarCompression result)
- {
- if (string.IsNullOrEmpty(compression))
- {
- result = TarCompression.None;
- return true;
- }
-
- if (StringComparer.OrdinalIgnoreCase.Equals(compression, "gz")
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "gzip"))
- {
- result = TarCompression.GZip;
- return true;
- }
-
- if (StringComparer.OrdinalIgnoreCase.Equals(compression, "zstd")
- || StringComparer.OrdinalIgnoreCase.Equals(compression, "zst"))
- {
- result = TarCompression.ZStandard;
- return true;
- }
-
- return Enum.TryParse(compression, ignoreCase: true, out result) && Enum.IsDefined(result);
- }
}
///
/// Identifies the compression to apply to the tar archive stream.
///
- private enum TarCompression
+ public enum TarCompression
{
None,
GZip,
From b0c19c1e5040132781b4ae8abe3b1d9d8a102d2a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Jul 2026 09:45:48 +0000
Subject: [PATCH 25/31] Fix CI build failure: remove unused _enumOutput field
(IDE0052)
Co-authored-by: ViktorHofer <7412651+ViktorHofer@users.noreply.github.com>
---
src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs b/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs
index d3bc1e90b09..40c74ce79ab 100644
--- a/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs
+++ b/src/Build.UnitTests/BackEnd/TaskBuilderTestTask.cs
@@ -251,11 +251,6 @@ public enum TestTaskEnum
///
private AbsolutePath _absolutePathOutput;
- ///
- /// The value set for the EnumParam.
- ///
- private TestTaskEnum _enumOutput;
-
///
/// The value for the AbsolutePathArrayOutput.
///
@@ -723,7 +718,6 @@ public TestTaskEnum EnumParam
{
set
{
- _enumOutput = value;
_testTaskHost?.ParameterSet("EnumParam", value);
}
}
From 902c727430947aee15ee2ecefaaf2ffc268ecf1f Mon Sep 17 00:00:00 2001
From: Chet Husk
Date: Mon, 27 Jul 2026 15:22:32 -0500
Subject: [PATCH 26/31] Convert Untar to typed task-parameter binding
Change DestinationFolder from ITaskItem to DirectoryInfo and SourceFiles
from ITaskItem[] to FileInfo[], matching the typed binding already applied
to TarDirectory. The MSBuild engine roots relative paths for these strongly
typed parameters via TaskEnvironment during parameter binding, so the manual
GetAbsolutePath calls in Execute are no longer needed. TaskEnvironment is
retained for tar-slip canonicalization in Extract.
Tests updated to construct DirectoryInfo/FileInfo instead of TaskItem.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d5c6bb3-d469-4226-9cef-986eca1f511f
---
src/Tasks.UnitTests/Untar_Tests.cs | 37 +++++++++++++++---------------
src/Tasks/Untar.cs | 36 ++++++++++-------------------
2 files changed, 30 insertions(+), 43 deletions(-)
diff --git a/src/Tasks.UnitTests/Untar_Tests.cs b/src/Tasks.UnitTests/Untar_Tests.cs
index be6ab69494a..6d5bc158a75 100644
--- a/src/Tasks.UnitTests/Untar_Tests.cs
+++ b/src/Tasks.UnitTests/Untar_Tests.cs
@@ -7,7 +7,6 @@
using System;
using System.IO;
using Microsoft.Build.UnitTests;
-using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
@@ -36,9 +35,9 @@ public void CanUntar(TarDirectory.TarCompression compression)
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
+ DestinationFolder = new DirectoryInfo(destination.Path),
SkipUnchangedFiles = false,
- SourceFiles = [new TaskItem(tarFilePath)],
+ SourceFiles = [new FileInfo(tarFilePath)],
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -65,9 +64,9 @@ public void CanUntarWithIncludeFilter()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
+ DestinationFolder = new DirectoryInfo(destination.Path),
SkipUnchangedFiles = false,
- SourceFiles = [new TaskItem(tarFilePath)],
+ SourceFiles = [new FileInfo(tarFilePath)],
Include = "included.txt",
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -95,9 +94,9 @@ public void CanUntarWithExcludeFilter()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
+ DestinationFolder = new DirectoryInfo(destination.Path),
SkipUnchangedFiles = false,
- SourceFiles = [new TaskItem(tarFilePath)],
+ SourceFiles = [new FileInfo(tarFilePath)],
Exclude = "dropped.txt",
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -124,9 +123,9 @@ public void SkipsUnchangedFiles()
Untar CreateTask() => new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
+ DestinationFolder = new DirectoryInfo(destination.Path),
SkipUnchangedFiles = true,
- SourceFiles = [new TaskItem(tarFilePath)],
+ SourceFiles = [new FileInfo(tarFilePath)],
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -155,9 +154,9 @@ public void CanOverwriteExistingFile()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
+ DestinationFolder = new DirectoryInfo(destination.Path),
SkipUnchangedFiles = false,
- SourceFiles = [new TaskItem(tarFilePath)],
+ SourceFiles = [new FileInfo(tarFilePath)],
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -177,8 +176,8 @@ public void LogsErrorIfSourceFileDoesNotExist()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
- SourceFiles = [new TaskItem(Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "missing.tar"))],
+ DestinationFolder = new DirectoryInfo(destination.Path),
+ SourceFiles = [new FileInfo(Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "missing.tar"))],
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -201,8 +200,8 @@ public void LogsErrorForCorruptArchive()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
- SourceFiles = [new TaskItem(corrupt.Path)],
+ DestinationFolder = new DirectoryInfo(destination.Path),
+ SourceFiles = [new FileInfo(corrupt.Path)],
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -227,8 +226,8 @@ public void LogsErrorIfIncludeContainsPropertyReferences()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
- SourceFiles = [new TaskItem(tarFilePath)],
+ DestinationFolder = new DirectoryInfo(destination.Path),
+ SourceFiles = [new FileInfo(tarFilePath)],
Include = "$(Include)",
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
@@ -254,10 +253,10 @@ public void FailIfNotIncrementalLogsError()
Untar untar = new Untar
{
BuildEngine = _mockEngine,
- DestinationFolder = new TaskItem(destination.Path),
+ DestinationFolder = new DirectoryInfo(destination.Path),
FailIfNotIncremental = true,
SkipUnchangedFiles = false,
- SourceFiles = [new TaskItem(tarFilePath)],
+ SourceFiles = [new FileInfo(tarFilePath)],
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};
diff --git a/src/Tasks/Untar.cs b/src/Tasks/Untar.cs
index 0dd7c0597b1..a2db96e6b10 100644
--- a/src/Tasks/Untar.cs
+++ b/src/Tasks/Untar.cs
@@ -47,10 +47,10 @@ public sealed class Untar : TaskExtension, ICancelableTask, IIncrementalTask, IM
private string[] _excludePatterns = [];
///
- /// Gets or sets a with a destination folder path to untar the files to.
+ /// Gets or sets a with a destination folder path to untar the files to.
///
[Required]
- public ITaskItem DestinationFolder { get; set; } = null!;
+ public DirectoryInfo DestinationFolder { get; set; } = null!;
///
/// Gets or sets a value that indicates whether read-only files should be overwritten.
@@ -63,11 +63,11 @@ public sealed class Untar : TaskExtension, ICancelableTask, IIncrementalTask, IM
public bool SkipUnchangedFiles { get; set; } = true;
///
- /// Gets or sets an array of objects containing the paths to tar archive files to untar.
+ /// Gets or sets an array of objects containing the paths to tar archive files to untar.
/// The compression (none, GZip, or ZStandard) is detected automatically from the archive contents.
///
[Required]
- public ITaskItem[] SourceFiles { get; set; } = null!;
+ public FileInfo[] SourceFiles { get; set; } = null!;
///
/// Gets or sets an MSBuild glob expression that specifies which files to include being untarred from the archive.
@@ -100,12 +100,11 @@ public override bool Execute()
DirectoryInfo destinationDirectory;
try
{
- AbsolutePath destinationPath = TaskEnvironment.GetAbsolutePath(DestinationFolder.ItemSpec);
- destinationDirectory = Directory.CreateDirectory(destinationPath);
+ destinationDirectory = Directory.CreateDirectory(DestinationFolder.FullName);
}
catch (Exception e)
{
- Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotCreateDestinationDirectory", DestinationFolder.ItemSpec, e.Message);
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotCreateDestinationDirectory", DestinationFolder.FullName, e.Message);
return false;
}
@@ -118,28 +117,17 @@ public override bool Execute()
if (!Log.HasLoggedErrors)
{
- foreach (ITaskItem sourceFile in SourceFiles.TakeWhile(i => !_cancellationToken.IsCancellationRequested))
+ foreach (FileInfo sourceFile in SourceFiles.TakeWhile(i => !_cancellationToken.IsCancellationRequested))
{
- AbsolutePath sourceFilePath;
- try
- {
- sourceFilePath = TaskEnvironment.GetAbsolutePath(sourceFile.ItemSpec);
- }
- catch (Exception)
- {
- Log.LogErrorWithCodeFromResources("Untar.ErrorFileDoesNotExist", sourceFile.ItemSpec);
- continue;
- }
-
- if (!FileSystems.Default.FileExists(sourceFilePath))
+ if (!FileSystems.Default.FileExists(sourceFile.FullName))
{
- Log.LogErrorWithCodeFromResources("Untar.ErrorFileDoesNotExist", sourceFile.ItemSpec);
+ Log.LogErrorWithCodeFromResources("Untar.ErrorFileDoesNotExist", sourceFile.FullName);
continue;
}
try
{
- using FileStream stream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 0x1000, useAsync: false);
+ using FileStream stream = new FileStream(sourceFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 0x1000, useAsync: false);
// Detect and unwrap any compression applied to the tar archive. The decompression stream (if any)
// and the TarReader are disposed by the enclosing using statements below.
@@ -159,7 +147,7 @@ public override bool Execute()
catch (Exception e)
{
// Should only be thrown if the archive could not be read (corrupt file, etc).
- Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotOpenFile", sourceFile.ItemSpec, e.Message);
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotOpenFile", sourceFile.FullName, e.Message);
}
}
catch (OperationCanceledException)
@@ -169,7 +157,7 @@ public override bool Execute()
catch (Exception e)
{
// Should only be thrown if the archive could not be opened (Access denied, corrupt file, etc).
- Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotOpenFile", sourceFile.ItemSpec, e.Message);
+ Log.LogErrorWithCodeFromResources("Untar.ErrorCouldNotOpenFile", sourceFile.FullName, e.Message);
}
}
}
From f15b1461d6ce5ca4dc4eb2defa06efeab7b9a6cc Mon Sep 17 00:00:00 2001
From: Chet Husk
Date: Mon, 27 Jul 2026 15:28:36 -0500
Subject: [PATCH 27/31] Push BuildEngine3.Yield() past precondition checks in
TarDirectory
Move the source-directory, destination-file overwrite, File.Delete, and
FailIfNotIncremental precondition checks ahead of BuildEngine3.Yield() so
that early-return failure paths no longer pre-emptively yield and reacquire
the build engine node. Only the actual tar-writing work now runs under the
yield/try/finally. All logging and return values are preserved.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d5c6bb3-d469-4226-9cef-986eca1f511f
---
src/Tasks/TarDirectory.cs | 95 +++++++++++++++++++--------------------
1 file changed, 47 insertions(+), 48 deletions(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 22db22771b7..dcc9b4777c3 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -69,65 +69,64 @@ public override bool Execute()
return false;
}
- BuildEngine3.Yield();
-
- try
+ // Evaluate all preconditions before yielding so that failures (which do no real work) don't
+ // pay the cost of yielding and reacquiring the build engine node.
+ if (DestinationFile.Exists)
{
- if (DestinationFile.Exists)
+ if (!Overwrite || FailIfNotIncremental)
{
- if (!Overwrite || FailIfNotIncremental)
- {
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFileExists", DestinationFile.FullName);
-
- return false;
- }
-
- try
- {
- File.Delete(DestinationFile.FullName);
- }
- catch (Exception e)
- {
- string lockedFileMessage = LockCheck.GetLockedFileMessage(DestinationFile.FullName);
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", SourceDirectory.FullName, DestinationFile.FullName, e.Message, lockedFileMessage);
-
- return false;
- }
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFileExists", DestinationFile.FullName);
+
+ return false;
}
try
{
- if (FailIfNotIncremental)
- {
- Log.LogErrorFromResources("TarDirectory.Comment", SourceDirectory.FullName, DestinationFile.FullName);
- }
- else
- {
- Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", SourceDirectory.FullName, DestinationFile.FullName);
-
- // Unknown is only reachable if it was explicitly set; fall back to the Pax default.
- TarEntryFormat format = Format == TarEntryFormat.Unknown ? TarEntryFormat.Pax : Format;
-
- using FileStream destinationStream = new FileStream(DestinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
-
- // Wrap the destination stream in the requested compression, if any. The tar archive is always
- // written to the (optionally compressed) stream using the new .NET 11 overload that honors the
- // requested TarEntryFormat.
- using Stream? compressionStream = Compression switch
- {
- TarCompression.GZip => new GZipStream(destinationStream, CompressionLevel.Optimal),
- TarCompression.ZStandard => new ZstandardStream(destinationStream, CompressionLevel.Optimal),
- _ => null,
- };
-
- TarFile.CreateFromDirectory(SourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
- }
+ File.Delete(DestinationFile.FullName);
}
catch (Exception e)
{
- Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", SourceDirectory.FullName, DestinationFile.FullName, e.Message, string.Empty);
+ string lockedFileMessage = LockCheck.GetLockedFileMessage(DestinationFile.FullName);
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", SourceDirectory.FullName, DestinationFile.FullName, e.Message, lockedFileMessage);
+
+ return false;
}
}
+
+ if (FailIfNotIncremental)
+ {
+ Log.LogErrorFromResources("TarDirectory.Comment", SourceDirectory.FullName, DestinationFile.FullName);
+
+ return false;
+ }
+
+ BuildEngine3.Yield();
+
+ try
+ {
+ Log.LogMessageFromResources(MessageImportance.High, "TarDirectory.Comment", SourceDirectory.FullName, DestinationFile.FullName);
+
+ // Unknown is only reachable if it was explicitly set; fall back to the Pax default.
+ TarEntryFormat format = Format == TarEntryFormat.Unknown ? TarEntryFormat.Pax : Format;
+
+ using FileStream destinationStream = new FileStream(DestinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
+
+ // Wrap the destination stream in the requested compression, if any. The tar archive is always
+ // written to the (optionally compressed) stream using the new .NET 11 overload that honors the
+ // requested TarEntryFormat.
+ using Stream? compressionStream = Compression switch
+ {
+ TarCompression.GZip => new GZipStream(destinationStream, CompressionLevel.Optimal),
+ TarCompression.ZStandard => new ZstandardStream(destinationStream, CompressionLevel.Optimal),
+ _ => null,
+ };
+
+ TarFile.CreateFromDirectory(SourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
+ }
+ catch (Exception e)
+ {
+ Log.LogErrorWithCodeFromResources("TarDirectory.ErrorFailed", SourceDirectory.FullName, DestinationFile.FullName, e.Message, string.Empty);
+ }
finally
{
BuildEngine3.Reacquire();
From b463549f43d812178c532ee94bd0ef97135a3a33 Mon Sep 17 00:00:00 2001
From: Chet Husk
Date: Mon, 27 Jul 2026 15:30:35 -0500
Subject: [PATCH 28/31] Use FileInfo.OpenRead() to open source tar in Untar
Now that SourceFiles is a FileInfo[], replace the explicit FileStream
construction with sourceFile.OpenRead(), which is functionally identical
(FileMode.Open/FileAccess.Read/FileShare.Read with the default 4096 buffer
and synchronous I/O) but reads cleaner.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d5c6bb3-d469-4226-9cef-986eca1f511f
---
src/Tasks/Untar.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Tasks/Untar.cs b/src/Tasks/Untar.cs
index a2db96e6b10..24bdb7ea7a0 100644
--- a/src/Tasks/Untar.cs
+++ b/src/Tasks/Untar.cs
@@ -127,7 +127,7 @@ public override bool Execute()
try
{
- using FileStream stream = new FileStream(sourceFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 0x1000, useAsync: false);
+ using FileStream stream = sourceFile.OpenRead();
// Detect and unwrap any compression applied to the tar archive. The decompression stream (if any)
// and the TarReader are disposed by the enclosing using statements below.
From 5f3064326351d43f53a1c2044fa8c703c9d7a1a9 Mon Sep 17 00:00:00 2001
From: Chet Husk
Date: Mon, 27 Jul 2026 15:47:33 -0500
Subject: [PATCH 29/31] Use FileInfo.OpenWrite() to open the destination tar in
TarDirectory
Replace the explicit FileStream construction with DestinationFile.OpenWrite().
It matches the write-only FileAccess and FileShare.None we used; the FileMode
difference (OpenOrCreate vs Create) is safe here because Execute deletes any
existing destination (or errors when Overwrite is false) before this line, so
the file never exists at this point and OpenWrite creates a fresh one. Added a
comment documenting that precondition.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d5c6bb3-d469-4226-9cef-986eca1f511f
---
src/Tasks/TarDirectory.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index dcc9b4777c3..88ed811efd4 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -109,7 +109,9 @@ public override bool Execute()
// Unknown is only reachable if it was explicitly set; fall back to the Pax default.
TarEntryFormat format = Format == TarEntryFormat.Unknown ? TarEntryFormat.Pax : Format;
- using FileStream destinationStream = new FileStream(DestinationFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None);
+ // The destination is guaranteed not to exist at this point: Execute deletes any existing
+ // file (or errors when Overwrite is false) before yielding, so OpenWrite creates a fresh file.
+ using FileStream destinationStream = DestinationFile.OpenWrite();
// Wrap the destination stream in the requested compression, if any. The tar archive is always
// written to the (optionally compressed) stream using the new .NET 11 overload that honors the
From f3ff1b25f756b9cca2b95fa0b1ce65c126a2f96d Mon Sep 17 00:00:00 2001
From: Chet Husk
Date: Mon, 27 Jul 2026 16:27:16 -0500
Subject: [PATCH 30/31] Rewrite TarDirectory to a sorted TarWriter loop for
deterministic ordering
Replace the one-shot TarFile.CreateFromDirectory call with a manual TarWriter loop that enumerates the source directory and writes entries in ordinal-sorted entry-name order. Per-entry metadata is written exactly as CreateFromDirectory would (via WriteEntry(fullPath, entryName)); only the entry order changes, making the produced archive reproducible regardless of filesystem enumeration order. Directory symlinks are not recursed into, matching CreateFromDirectory.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d5c6bb3-d469-4226-9cef-986eca1f511f
---
src/Tasks.UnitTests/TarDirectory_Tests.cs | 47 ++++++++++++++++++
src/Tasks/TarDirectory.cs | 59 +++++++++++++++++++++--
2 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
index 5fea691b5aa..b6b9d2a19f9 100644
--- a/src/Tasks.UnitTests/TarDirectory_Tests.cs
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -199,6 +199,53 @@ public void UnknownFormatFallsBackToPax()
}
}
+ [Fact]
+ public void WritesEntriesInDeterministicSortedOrder()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ // Create files and a nested directory in a deliberately non-sorted order to prove the task
+ // reorders them rather than relying on the filesystem enumeration order.
+ testEnvironment.CreateFile(sourceFolder, "zebra.txt", "z");
+ testEnvironment.CreateFile(sourceFolder, "apple.txt", "a");
+ TransientTestFolder nestedFolder = testEnvironment.CreateFolder(Path.Combine(sourceFolder.Path, "middle"), createFolder: true);
+ testEnvironment.CreateFile(nestedFolder, "inner.txt", "i");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ // Entries must be emitted in ordinal-sorted order, and a directory must always precede its
+ // contents (its name ends in '/', which is a prefix of everything inside it).
+ GetAllTarEntryNames(tarFilePath)
+ .ShouldBe(["apple.txt", "middle/", "middle/inner.txt", "zebra.txt"]);
+ }
+ }
+
+ private static List GetAllTarEntryNames(string tarFilePath)
+ {
+ List names = new List();
+
+ using FileStream stream = new FileStream(tarFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using TarReader reader = new TarReader(stream);
+ for (TarEntry? entry = reader.GetNextEntry(); entry is not null; entry = reader.GetNextEntry())
+ {
+ names.Add(entry.Name);
+ }
+
+ return names;
+ }
+
private static List GetTarEntryNames(string tarFilePath, TarDirectory.TarCompression compression)
{
List names = new List();
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 88ed811efd4..847a52dac1f 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
@@ -114,8 +115,8 @@ public override bool Execute()
using FileStream destinationStream = DestinationFile.OpenWrite();
// Wrap the destination stream in the requested compression, if any. The tar archive is always
- // written to the (optionally compressed) stream using the new .NET 11 overload that honors the
- // requested TarEntryFormat.
+ // written to the (optionally compressed) stream, and the TarWriter is created with the requested
+ // TarEntryFormat so every entry is emitted in that format.
using Stream? compressionStream = Compression switch
{
TarCompression.GZip => new GZipStream(destinationStream, CompressionLevel.Optimal),
@@ -123,7 +124,15 @@ public override bool Execute()
_ => null,
};
- TarFile.CreateFromDirectory(SourceDirectory.FullName, compressionStream ?? destinationStream, includeBaseDirectory: false, format);
+ // Write the archive entry-by-entry (rather than the one-shot TarFile.CreateFromDirectory) so that the
+ // entries are emitted in a deterministic, ordinal-sorted order. Per-entry metadata is written exactly
+ // as TarFile.CreateFromDirectory would via WriteEntry(fullPath, entryName); only the order is affected.
+ using TarWriter writer = new TarWriter(compressionStream ?? destinationStream, format, leaveOpen: true);
+
+ foreach ((string fullPath, string entryName) in EnumerateEntriesInDeterministicOrder())
+ {
+ writer.WriteEntry(fullPath, entryName);
+ }
}
catch (Exception e)
{
@@ -137,6 +146,50 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}
+ ///
+ /// Enumerates every filesystem entry under paired with the name it should be
+ /// given inside the archive, sorted by entry name using an ordinal comparison so that the archive is written
+ /// in a deterministic, reproducible order regardless of how the underlying filesystem enumerates directory
+ /// contents. This mirrors the entry naming of
+ /// (relative, forward-slash separated, directories suffixed with '/', base directory excluded).
+ ///
+ private List<(string FullPath, string EntryName)> EnumerateEntriesInDeterministicOrder()
+ {
+ string basePath = FileUtilities.EnsureTrailingSlash(SourceDirectory.FullName);
+
+ List<(string FullPath, string EntryName)> entries = [];
+ CollectEntries(SourceDirectory, basePath, entries);
+
+ // Order determinism: sort by the in-archive entry name using an ordinal comparison. Because a
+ // directory's entry name ends in '/', it is always a prefix of the names of everything it contains,
+ // so directories sort ahead of their own contents, preserving the parent-before-children ordering
+ // that tar expects for restoring directory timestamps.
+ entries.Sort(static (left, right) => string.CompareOrdinal(left.EntryName, right.EntryName));
+
+ return entries;
+ }
+
+ ///
+ /// Recursively collects the filesystem entries under into .
+ /// Directory symlinks and junctions are recorded as entries but are not recursed into, matching the behavior of
+ /// and avoiding reparse-point cycles.
+ ///
+ private static void CollectEntries(DirectoryInfo directory, string basePath, List<(string FullPath, string EntryName)> entries)
+ {
+ foreach (FileSystemInfo info in directory.EnumerateFileSystemInfos())
+ {
+ bool isRealDirectory = info is DirectoryInfo && (info.Attributes & FileAttributes.ReparsePoint) == 0;
+
+ string relativePath = info.FullName.Substring(basePath.Length).Replace('\\', '/');
+ entries.Add((info.FullName, isRealDirectory ? relativePath + "/" : relativePath));
+
+ if (isRealDirectory)
+ {
+ CollectEntries((DirectoryInfo)info, basePath, entries);
+ }
+ }
+ }
+
///
/// Identifies the compression to apply to the tar archive stream.
///
From ae0c30f7fa8a9098848402f8ebedda143d0821ad Mon Sep 17 00:00:00 2001
From: Chet Husk
Date: Mon, 27 Jul 2026 16:31:15 -0500
Subject: [PATCH 31/31] Add DeterministicTimestamp parameter to TarDirectory
Introduce an optional DeterministicTimestamp string parameter that stamps
every archive entry with a single caller-supplied timestamp in place of the
source files' last-write times, producing byte-reproducible tarballs.
The value may be an integer number of seconds since the Unix epoch (the
SOURCE_DATE_EPOCH form) or an RFC 3339 date-time, parsed culture-invariantly
via TryParseTimestamp (ported from NuGet's deterministic-timestamp handling).
When set, entries are constructed manually so the modification time can be
overridden; the source file's Unix mode is preserved, while Unix owner ids
default to 0 (matching Windows behavior and the reproducible-tar convention).
When empty, entries keep their source last-write times; ordering is always
deterministic regardless.
An invalid value fails the task with the new MSB4324
TarDirectory.InvalidDeterministicTimestamp resource (and its xlf locales).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d5c6bb3-d469-4226-9cef-986eca1f511f
---
src/Tasks.UnitTests/TarDirectory_Tests.cs | 111 ++++++++++++++++
src/Tasks/Resources/Strings.resx | 4 +
src/Tasks/Resources/xlf/Strings.cs.xlf | 5 +
src/Tasks/Resources/xlf/Strings.de.xlf | 5 +
src/Tasks/Resources/xlf/Strings.es.xlf | 5 +
src/Tasks/Resources/xlf/Strings.fr.xlf | 5 +
src/Tasks/Resources/xlf/Strings.it.xlf | 5 +
src/Tasks/Resources/xlf/Strings.ja.xlf | 5 +
src/Tasks/Resources/xlf/Strings.ko.xlf | 5 +
src/Tasks/Resources/xlf/Strings.pl.xlf | 5 +
src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 5 +
src/Tasks/Resources/xlf/Strings.ru.xlf | 5 +
src/Tasks/Resources/xlf/Strings.tr.xlf | 5 +
src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 5 +
src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 5 +
src/Tasks/TarDirectory.cs | 139 ++++++++++++++++++--
16 files changed, 311 insertions(+), 8 deletions(-)
diff --git a/src/Tasks.UnitTests/TarDirectory_Tests.cs b/src/Tasks.UnitTests/TarDirectory_Tests.cs
index b6b9d2a19f9..4aa757bdea2 100644
--- a/src/Tasks.UnitTests/TarDirectory_Tests.cs
+++ b/src/Tasks.UnitTests/TarDirectory_Tests.cs
@@ -232,6 +232,117 @@ public void WritesEntriesInDeterministicSortedOrder()
}
}
+ [Theory]
+ [InlineData("1704067200")]
+ [InlineData("2024-01-01T00:00:00Z")]
+ public void DeterministicTimestampStampsEveryEntry(string deterministicTimestamp)
+ {
+ DateTimeOffset expected = DateTimeOffset.FromUnixTimeSeconds(1704067200);
+
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "a.txt", "a");
+ TransientTestFolder nestedFolder = testEnvironment.CreateFolder(Path.Combine(sourceFolder.Path, "nested"), createFolder: true);
+ testEnvironment.CreateFile(nestedFolder, "b.txt", "b");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DeterministicTimestamp = deterministicTimestamp,
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ GetTarEntryModificationTimes(tarFilePath)
+ .ShouldAllBe(modificationTime => modificationTime == expected, _mockEngine.Log);
+ }
+ }
+
+ [Fact]
+ public void DeterministicTimestampProducesByteIdenticalArchives()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "z.txt", "z");
+ testEnvironment.CreateFile(sourceFolder, "a.txt", "a");
+ TransientTestFolder nestedFolder = testEnvironment.CreateFolder(Path.Combine(sourceFolder.Path, "nested"), createFolder: true);
+ testEnvironment.CreateFile(nestedFolder, "b.txt", "b");
+
+ byte[] first = TarWithDeterministicTimestamp(testEnvironment, sourceFolder.Path, "1704067200");
+ byte[] second = TarWithDeterministicTimestamp(testEnvironment, sourceFolder.Path, "1704067200");
+
+ second.ShouldBe(first);
+ }
+ }
+
+ [Fact]
+ public void LogsErrorForInvalidDeterministicTimestamp()
+ {
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFolder sourceFolder = testEnvironment.CreateFolder(createFolder: true);
+
+ testEnvironment.CreateFile(sourceFolder, "a.txt", "a");
+
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DeterministicTimestamp = "not-a-timestamp",
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceFolder.Path),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log);
+
+ _mockEngine.Log.ShouldContain("MSB4324", customMessage: _mockEngine.Log);
+ File.Exists(tarFilePath).ShouldBeFalse(_mockEngine.Log);
+ }
+ }
+
+ private byte[] TarWithDeterministicTimestamp(TestEnvironment testEnvironment, string sourceDirectory, string deterministicTimestamp)
+ {
+ string tarFilePath = Path.Combine(testEnvironment.CreateFolder(createFolder: true).Path, "test.tar");
+
+ TarDirectory tarDirectory = new TarDirectory
+ {
+ BuildEngine = _mockEngine,
+ DeterministicTimestamp = deterministicTimestamp,
+ DestinationFile = new FileInfo(tarFilePath),
+ SourceDirectory = new DirectoryInfo(sourceDirectory),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log);
+
+ return File.ReadAllBytes(tarFilePath);
+ }
+
+ private static List GetTarEntryModificationTimes(string tarFilePath)
+ {
+ List modificationTimes = new List();
+
+ using FileStream stream = new FileStream(tarFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using TarReader reader = new TarReader(stream);
+ for (TarEntry? entry = reader.GetNextEntry(); entry is not null; entry = reader.GetNextEntry())
+ {
+ modificationTimes.Add(entry.ModificationTime);
+ }
+
+ return modificationTimes;
+ }
+
private static List GetAllTarEntryNames(string tarFilePath)
{
List names = new List();
diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx
index bda78c9e41f..a1e92a59d25 100644
--- a/src/Tasks/Resources/Strings.resx
+++ b/src/Tasks/Resources/Strings.resx
@@ -3110,6 +3110,10 @@
MSB4323: Failed to tar directory "{0}" to file "{1}". {2} {3}{StrBegin="MSB4323: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+
Creating tar archive from directory "{0}" to "{1}".
diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf
index 64bca91481f..565eef107fa 100644
--- a/src/Tasks/Resources/xlf/Strings.cs.xlf
+++ b/src/Tasks/Resources/xlf/Strings.cs.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Úloha {0} se nepodporuje ve verzi MSBuildu pro .NET Core. Použijte prosím verzi MSBuildu pro .NET Framework. Další podrobnosti najdete na stránce https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf
index e5861050646..1d8e0738505 100644
--- a/src/Tasks/Resources/xlf/Strings.de.xlf
+++ b/src/Tasks/Resources/xlf/Strings.de.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Die Aufgabe "{0}" wird für die .NET Core-Version von MSBuild nicht unterstützt. Verwenden Sie die .NET Framework-Version von MSBuild. Weitere Informationen finden Sie unter https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf
index 7f4f7df54f7..26f8e8ed1e7 100644
--- a/src/Tasks/Resources/xlf/Strings.es.xlf
+++ b/src/Tasks/Resources/xlf/Strings.es.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: No se admite la tarea "{0}" en la versión de MSBuild de .NET Core. Use la versión de MSBuild de .NET Framework. Vea https://aka.ms/msbuild/MSB4803 para obtener más información.
diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf
index a46595b4f64..796a73b7950 100644
--- a/src/Tasks/Resources/xlf/Strings.fr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.fr.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: La tâche "{0}" n'est pas prise en charge dans la version .NET Core de MSBuild. Utilisez la version du .NET Framework de MSBuild. Pour plus d'informations, consultez https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf
index abca15d6ad2..e13f642f7e5 100644
--- a/src/Tasks/Resources/xlf/Strings.it.xlf
+++ b/src/Tasks/Resources/xlf/Strings.it.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: l'attività "{0}" non è supportata nella versione .NET Core di MSBuild. Usare la versione .NET Framework di MSBuild. Per altri dettagli, vedere https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf
index a219687cf6c..162e80d2b55 100644
--- a/src/Tasks/Resources/xlf/Strings.ja.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ja.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: タスク "{0}" は .NET Core バージョンの MSBuild ではサポートされていません。.NET Framework バージョンの MSBuild をご使用ください。詳細については、https://aka.ms/msbuild/MSB4803 をご覧ください。
diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf
index fa1e4b279c1..2d0a43df686 100644
--- a/src/Tasks/Resources/xlf/Strings.ko.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ko.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: "{0}" 작업은 MSBuild의 .NET Core 버전에서 지원되지 않습니다. MSBuild의 .NET Framework 버전을 사용하세요. 자세한 내용은 https://aka.ms/msbuild/MSB4803을 참조하세요.
diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf
index 0c986ecd84e..51bd1639cde 100644
--- a/src/Tasks/Resources/xlf/Strings.pl.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pl.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: Zadanie „{0}” nie jest obsługiwane w wersji programu MSBuild dla platformy .NET Core. Użyj wersji programu MSBuild dla platformy .NET Framework. Zobacz https://aka.ms/msbuild/MSB4803, aby uzyskać więcej szczegółów.
diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
index dc9d7227d0e..7ea672dc557 100644
--- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: a tarefa "{0}" não é compatível com a versão do .NET Core do MSBuild. Use a versão do .NET Framework do MSBuild. Confira https://aka.ms/msbuild/MSB4803 para obter mais detalhes.
diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf
index 544b9275a79..3bd0fe8232e 100644
--- a/src/Tasks/Resources/xlf/Strings.ru.xlf
+++ b/src/Tasks/Resources/xlf/Strings.ru.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: задача "{0}" не поддерживается в MSBuild версии .NET Core. Используйте MSBuild версии .NET Framework. Дополнительные сведения: https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf
index 34de691f1f6..ef666ff227a 100644
--- a/src/Tasks/Resources/xlf/Strings.tr.xlf
+++ b/src/Tasks/Resources/xlf/Strings.tr.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild’in .NET Core sürümünde "{0}" görevi desteklenmiyor. Lütfen MSBuild’in .NET Framework sürümünü kullanın. Daha ayrıntılı bilgi için bkz. https://aka.ms/msbuild/MSB4803.
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
index 5782ea784cb..5406be42bdb 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: .NET Core 版本的 MSBuild 不支持“{0}”。请使用 .NET Framework 版本的 MSBuild。有关更多详细信息,请参阅 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
index 6a51ce375bc..4d5e1a2083c 100644
--- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf
@@ -2692,6 +2692,11 @@
MSB4322: Failed to create tar file "{0}" because it already exists. Please delete or rename the file and try again.{StrBegin="MSB4322: "}
+
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ MSB4324: The DeterministicTimestamp value "{0}" is not valid. Specify an RFC 3339 date-time (for example, "2024-01-01T00:00:00Z") or an integer number of seconds since the Unix epoch.
+ {StrBegin="MSB4324: "}
+ MSB4803: The task "{0}" is not supported on the .NET Core version of MSBuild. Please use the .NET Framework version of MSBuild. See https://aka.ms/msbuild/MSB4803 for further details.MSB4803: MSBuild 的 .NET Core 版本不支援工作 "{0}"。請使用 MSBuild 的 .NET Framework 版本。如需進一步的詳細資料,請參閱 https://aka.ms/msbuild/MSB4803。
diff --git a/src/Tasks/TarDirectory.cs b/src/Tasks/TarDirectory.cs
index 847a52dac1f..5a5b81b82b0 100644
--- a/src/Tasks/TarDirectory.cs
+++ b/src/Tasks/TarDirectory.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Formats.Tar;
+using System.Globalization;
using System.IO;
using System.IO.Compression;
using Microsoft.Build.Framework;
@@ -59,6 +60,17 @@ public sealed class TarDirectory : TaskExtension, IIncrementalTask, IMultiThread
///
public TarEntryFormat Format { get; set; } = TarEntryFormat.Pax;
+ ///
+ /// Gets or sets an optional timestamp to stamp on every entry in the archive in place of the source files'
+ /// last-write times. Supplying this value makes the produced archive reproducible across machines and runs.
+ /// The value may be an RFC 3339 date-time (for example, 2024-01-01T00:00:00Z) or an integer number of
+ /// seconds since the Unix epoch (for example, 1704067200, which is also the form of SOURCE_DATE_EPOCH).
+ /// When empty, each entry keeps its source file's last-write time (entries are always written in a
+ /// deterministic order regardless of this value).
+ /// This parameter is optional.
+ ///
+ public string DeterministicTimestamp { get; set; } = string.Empty;
+
///
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
@@ -101,6 +113,19 @@ public override bool Execute()
return false;
}
+ DateTimeOffset? deterministicTimestamp = null;
+ if (!string.IsNullOrEmpty(DeterministicTimestamp))
+ {
+ if (!TryParseTimestamp(DeterministicTimestamp, out DateTimeOffset parsedTimestamp))
+ {
+ Log.LogErrorWithCodeFromResources("TarDirectory.InvalidDeterministicTimestamp", DeterministicTimestamp);
+
+ return false;
+ }
+
+ deterministicTimestamp = parsedTimestamp;
+ }
+
BuildEngine3.Yield();
try
@@ -125,13 +150,21 @@ public override bool Execute()
};
// Write the archive entry-by-entry (rather than the one-shot TarFile.CreateFromDirectory) so that the
- // entries are emitted in a deterministic, ordinal-sorted order. Per-entry metadata is written exactly
- // as TarFile.CreateFromDirectory would via WriteEntry(fullPath, entryName); only the order is affected.
+ // entries are emitted in a deterministic, ordinal-sorted order. When a deterministic timestamp is
+ // supplied, each entry is constructed manually so its modification time can be overridden; otherwise
+ // per-entry metadata is written exactly as TarFile.CreateFromDirectory would via WriteEntry.
using TarWriter writer = new TarWriter(compressionStream ?? destinationStream, format, leaveOpen: true);
- foreach ((string fullPath, string entryName) in EnumerateEntriesInDeterministicOrder())
+ foreach ((FileSystemInfo info, string entryName) in EnumerateEntriesInDeterministicOrder())
{
- writer.WriteEntry(fullPath, entryName);
+ if (deterministicTimestamp is DateTimeOffset timestamp)
+ {
+ WriteStampedEntry(writer, format, info, entryName, timestamp);
+ }
+ else
+ {
+ writer.WriteEntry(info.FullName, entryName);
+ }
}
}
catch (Exception e)
@@ -153,11 +186,11 @@ public override bool Execute()
/// contents. This mirrors the entry naming of
/// (relative, forward-slash separated, directories suffixed with '/', base directory excluded).
///
- private List<(string FullPath, string EntryName)> EnumerateEntriesInDeterministicOrder()
+ private List<(FileSystemInfo Info, string EntryName)> EnumerateEntriesInDeterministicOrder()
{
string basePath = FileUtilities.EnsureTrailingSlash(SourceDirectory.FullName);
- List<(string FullPath, string EntryName)> entries = [];
+ List<(FileSystemInfo Info, string EntryName)> entries = [];
CollectEntries(SourceDirectory, basePath, entries);
// Order determinism: sort by the in-archive entry name using an ordinal comparison. Because a
@@ -174,14 +207,14 @@ public override bool Execute()
/// Directory symlinks and junctions are recorded as entries but are not recursed into, matching the behavior of
/// and avoiding reparse-point cycles.
///
- private static void CollectEntries(DirectoryInfo directory, string basePath, List<(string FullPath, string EntryName)> entries)
+ private static void CollectEntries(DirectoryInfo directory, string basePath, List<(FileSystemInfo Info, string EntryName)> entries)
{
foreach (FileSystemInfo info in directory.EnumerateFileSystemInfos())
{
bool isRealDirectory = info is DirectoryInfo && (info.Attributes & FileAttributes.ReparsePoint) == 0;
string relativePath = info.FullName.Substring(basePath.Length).Replace('\\', '/');
- entries.Add((info.FullName, isRealDirectory ? relativePath + "/" : relativePath));
+ entries.Add((info, isRealDirectory ? relativePath + "/" : relativePath));
if (isRealDirectory)
{
@@ -190,6 +223,96 @@ private static void CollectEntries(DirectoryInfo directory, string basePath, Lis
}
}
+ ///
+ /// Parses a value. The value may be an integer number of seconds since the
+ /// Unix epoch, or an RFC 3339 date-time. Parsing is culture-invariant and always resolves to a UTC instant.
+ ///
+ private static bool TryParseTimestamp(string value, out DateTimeOffset timestamp)
+ {
+ // A bare integer is interpreted as the number of seconds since the Unix epoch. This matches the
+ // SOURCE_DATE_EPOCH convention and NuGet's deterministic-timestamp handling.
+ if (long.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out long unixTimeSeconds))
+ {
+ timestamp = DateTimeOffset.FromUnixTimeSeconds(unixTimeSeconds);
+
+ return true;
+ }
+
+ return DateTimeOffset.TryParseExact(value, s_timestampFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out timestamp);
+ }
+
+ ///
+ /// Writes a single filesystem entry to the archive, stamping it with in place of the
+ /// source file's last-write time. The entry is constructed manually (rather than via )
+ /// so its modification time can be overridden. The source file's Unix mode is preserved; Unix owner ids default
+ /// to 0, which both matches Windows behavior and is the conventional choice for a reproducible archive.
+ ///
+ private static void WriteStampedEntry(TarWriter writer, TarEntryFormat format, FileSystemInfo info, string entryName, DateTimeOffset timestamp)
+ {
+ bool isSymbolicLink = (info.Attributes & FileAttributes.ReparsePoint) != 0;
+ bool isDirectory = info is DirectoryInfo && !isSymbolicLink;
+
+ TarEntryType entryType = (isDirectory, isSymbolicLink, format) switch
+ {
+ (true, _, _) => TarEntryType.Directory,
+ (_, true, _) => TarEntryType.SymbolicLink,
+ (_, _, TarEntryFormat.V7) => TarEntryType.V7RegularFile,
+ _ => TarEntryType.RegularFile,
+ };
+
+ TarEntry entry = CreateEntry(format, entryType, entryName);
+ entry.ModificationTime = timestamp;
+
+ // Preserve the source's Unix permissions (for example, executable bits). This information does not exist on
+ // Windows, where the entry keeps the default mode for its type.
+ if (!isSymbolicLink && !OperatingSystem.IsWindows())
+ {
+ entry.Mode = info.UnixFileMode;
+ }
+
+ FileStream? dataStream = null;
+ try
+ {
+ if (isSymbolicLink)
+ {
+ entry.LinkName = info.LinkTarget ?? string.Empty;
+ }
+ else if (!isDirectory)
+ {
+ dataStream = ((FileInfo)info).OpenRead();
+ entry.DataStream = dataStream;
+ }
+
+ writer.WriteEntry(entry);
+ }
+ finally
+ {
+ dataStream?.Dispose();
+ }
+ }
+
+ ///
+ /// Creates a of the concrete type that matches .
+ ///
+ private static TarEntry CreateEntry(TarEntryFormat format, TarEntryType entryType, string entryName) => format switch
+ {
+ TarEntryFormat.V7 => new V7TarEntry(entryType, entryName),
+ TarEntryFormat.Ustar => new UstarTarEntry(entryType, entryName),
+ TarEntryFormat.Gnu => new GnuTarEntry(entryType, entryName),
+ _ => new PaxTarEntry(entryType, entryName),
+ };
+
+ ///
+ /// The RFC 3339 date-time formats accepted for , mirroring NuGet's
+ /// deterministic-timestamp parsing.
+ ///
+ private static readonly string[] s_timestampFormats =
+ [
+ "yyyy-MM-dd'T'HH:mm:ss'Z'",
+ "yyyy-MM-dd'T'HH:mm:sszzz",
+ "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK",
+ ];
+
///
/// Identifies the compression to apply to the tar archive stream.
///