-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add TarDirectory and Untar tasks for creating and extracting tar archives #14451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 28 commits
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
62f712a
Initial plan
Copilot 695b6fa
Add TarDirectory task, registration, and resources
Copilot c0998f2
Add TarDirectory tests and docs; verified end-to-end
Copilot d769dc6
Address code review: remove redundant null-forgiving op and fix doc g…
Copilot 6ba6a02
Error on unrecognized TarDirectory Compression instead of falling back
Copilot 4835444
Add optional Format parameter to TarDirectory (Pax/Gnu/Ustar/V7)
Copilot e804b4a
Address review: rename recursive entry variable and avoid array alloc…
Copilot ae91e65
Simplify recursive foreach tuple deconstruction in TarDirectory
Copilot a89e30a
Update TFM to net11.0 and update accompanying files
ViktorHofer fa7d097
Try to fix build and template_feed style fixes
ViktorHofer e3e476d
Fix terminal logger tests
ViktorHofer 4aa4918
Use a distinct stage2 binlog name to avoid collision with stage1
ViktorHofer d0c8407
Publish stage1 binlog alongside stage2 binlog in multi-stage builds
ViktorHofer 81ffe89
Copy entire stage1 log folder back so CI publishes all stage1 logs
ViktorHofer 81cfb85
Fix torn NuGet state in netcore bootstrap by not overlaying package N…
ViktorHofer 82da8e8
Potential fix for pull request finding
ViktorHofer 8a4f6aa
Potential fix for pull request finding
ViktorHofer 0826b41
Update cts ymls
ViktorHofer 81489a3
Merge net11.0 (#14455) to enable new BCL tar/compression APIs
Copilot 2cf0cdc
Modernize TarDirectory with .NET 11 APIs and add ZStandard compression
Copilot 6b0ebf0
Use scoped using declaration for compression stream disposal
Copilot 56c9290
Merge branch 'main' into copilot/create-tar-files-task
ViktorHofer d6d155d
Inline TryParseFormat helper into its call site in TarDirectory
Copilot 9078ac8
Remove redundant Compression* const fields in favor of TarCompression…
Copilot e23164c
Merge branch 'main' into copilot/create-tar-files-task
ViktorHofer 640ca3d
Add Untar task mirroring Unzip for extracting tar archives
Copilot bc12d42
Convert TarDirectory to typed task-parameter binding
Copilot b0c19c1
Fix CI build failure: remove unused _enumOutput field (IDE0052)
Copilot 902c727
Convert Untar to typed task-parameter binding
baronfel f15b146
Push BuildEngine3.Yield() past precondition checks in TarDirectory
baronfel b463549
Use FileInfo.OpenRead() to open source tar in Untar
baronfel 5f30643
Use FileInfo.OpenWrite() to open the destination tar in TarDirectory
baronfel f3ff1b2
Rewrite TarDirectory to a sorted TarWriter loop for deterministic ord…
baronfel ae0c30f
Add DeterministicTimestamp parameter to TarDirectory
baronfel 3826a0f
Detect symlinks via LinkTarget instead of the ReparsePoint attribute
baronfel 040eb7d
Fix tar-slip error handling in Untar
baronfel 6b538fd
Fix TarDirectory non-incremental error and clean up partial archives
baronfel 6a911b2
Rename _cancellationToken field to _cancellationTokenSource in Untar
baronfel 4088800
Add PreservePermissions parameter to Untar task
baronfel 45f850c
Always extract via runtime ExtractToFileAsync; remove PreservePermiss…
baronfel c329d40
Suppress CA1416 on Unix-only File.GetUnixFileMode test call
baronfel ab57fd3
Annotate optional DeterministicTimestamp parameter as string?
baronfel 0567404
Update TarDirectory and Untar task registrations
ViktorHofer 482900c
Range-check Unix-seconds timestamp before FromUnixTimeSeconds (B1)
baronfel cbd01c9
Truncate destination with FileMode.Create when writing tar archive (H1)
baronfel c46eb5d
Continue extraction on UnauthorizedAccessException, not just IOExcept…
baronfel 1bc209a
Guard directory-entry creation so a bad entry doesn't abort extractio…
baronfel 3a83366
Hoist FailIfNotIncremental check above destination-exists handling in…
baronfel 5e7d3c0
Only translate directory separators in tar entry names on non-Unix pl…
baronfel a49b637
Fix Untar FailIfNotIncremental to log a real error instead of a progr…
baronfel a603513
Make TarDirectory cancelable (M5)
baronfel c42b1a0
Register TarDirectory/Untar via NET TaskHost instead of runtime-condi…
baronfel 6131cba
Refactor Untar to await extraction instead of blocking per entry
baronfel 6d8366e
Make TarDirectory write pipeline async with a single top-level GetAwa…
baronfel c750775
Extract Untar destination-directory validation into TryCreateDestinat…
baronfel f1c0cc3
Extract Untar per-archive loop body into TryExtractTarballAsync
baronfel 491b107
Extract TryGetDeterministicTimestamp helper in TarDirectory
baronfel ead67c5
Register TarDirectory/Untar per Viktor's SDK-path proposal
baronfel bb98cfd
Use MSBuild version property function for SDK version gate
baronfel a6b3769
Use AssemblyFile for SDK-path TarDirectory/Untar UsingTask entries
Copilot be9f953
Move Runtime="NET" tar UsingTasks out of the default tasks file
ViktorHofer 37f8d67
Use cross-runtime-resolvable parameter types on TarDirectory and Untar
ViktorHofer c1e62b3
Add E2E test covering inbox tar task resolution from .NET Framework M…
ViktorHofer 72e27cc
Update UsingTask conditions for TarDirectory and Untar
ViktorHofer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| // 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 Shouldly; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.Build.Tasks.UnitTests | ||
| { | ||
| public class TarDirectory_Tests | ||
| { | ||
| private readonly MockEngine _mockEngine = new MockEngine(); | ||
|
|
||
| [Theory] | ||
| [InlineData(TarDirectory.TarCompression.None)] | ||
| [InlineData(TarDirectory.TarCompression.GZip)] | ||
| [InlineData(TarDirectory.TarCompression.ZStandard)] | ||
| public void CanTarDirectory(TarDirectory.TarCompression 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 FileInfo(tarFilePath), | ||
| SourceDirectory = new DirectoryInfo(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); | ||
|
|
||
| GetTarEntryNames(tarFilePath, compression) | ||
| .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 FileInfo(file.Path), | ||
| Overwrite = true, | ||
| SourceDirectory = new DirectoryInfo(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, TarDirectory.TarCompression.None) | ||
| .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 FileInfo(file.Path), | ||
| SourceDirectory = new DirectoryInfo(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 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(), | ||
| }; | ||
|
|
||
| tarDirectory.Execute().ShouldBeFalse(_mockEngine.Log); | ||
|
|
||
| _mockEngine.Log.ShouldContain("MSB4321", customMessage: _mockEngine.Log); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(TarEntryFormat.Pax)] | ||
| [InlineData(TarEntryFormat.Gnu)] | ||
| [InlineData(TarEntryFormat.Ustar)] | ||
| [InlineData(TarEntryFormat.V7)] | ||
| public void CanTarDirectoryWithFormat(TarEntryFormat format) | ||
| { | ||
| 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 FileInfo(tarFilePath), | ||
| SourceDirectory = new DirectoryInfo(sourceFolder.Path), | ||
| TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), | ||
| }; | ||
|
|
||
| tarDirectory.Execute().ShouldBeTrue(_mockEngine.Log); | ||
|
|
||
| GetTarEntryFormats(tarFilePath) | ||
| .ShouldAllBe(entryFormat => entryFormat == format, _mockEngine.Log); | ||
|
|
||
| GetTarEntryNames(tarFilePath, TarDirectory.TarCompression.None) | ||
| .ShouldBe(["3F6D2F2E3C1A4B5C8D9E0F1A2B3C4D5E.txt"]); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void UnknownFormatFallsBackToPax() | ||
| { | ||
| 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, | ||
|
|
||
| // 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().ShouldBeTrue(_mockEngine.Log); | ||
|
|
||
| GetTarEntryFormats(tarFilePath) | ||
| .ShouldAllBe(entryFormat => entryFormat == TarEntryFormat.Pax, _mockEngine.Log); | ||
| } | ||
| } | ||
|
|
||
| private static List<string> GetTarEntryNames(string tarFilePath, TarDirectory.TarCompression compression) | ||
| { | ||
| List<string> names = new List<string>(); | ||
|
|
||
| using FileStream stream = new FileStream(tarFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); | ||
|
|
||
| // Wrap the file stream in a matching decompression stream, if the archive was compressed. | ||
| Stream? decompressionStream = compression switch | ||
| { | ||
| 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)), | ||
| }; | ||
|
baronfel marked this conversation as resolved.
|
||
|
|
||
| using (decompressionStream) | ||
| { | ||
| 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) | ||
| { | ||
| names.Add(entry.Name); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return names; | ||
| } | ||
|
|
||
| private static List<TarEntryFormat> GetTarEntryFormats(string tarFilePath) | ||
| { | ||
| List<TarEntryFormat> formats = new List<TarEntryFormat>(); | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #endif | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.