diff --git a/src/Artifacts.UnitTests/ArtifactsTests.cs b/src/Artifacts.UnitTests/ArtifactsTests.cs index cc0c9b12..45a0a05b 100644 --- a/src/Artifacts.UnitTests/ArtifactsTests.cs +++ b/src/Artifacts.UnitTests/ArtifactsTests.cs @@ -356,5 +356,81 @@ public void UsingSdkLogic(bool appendTargetFrameworkToOutputPath) }.Select(i => Path.Combine(artifactsPath.FullName, i)), ignoreOrder: true); } + + [Fact] + public void RenamedFiles() + { + CreateFiles( + Path.Combine("bin", "Debug"), + "foo.exe", + "foo.pdb", + "foo.exe.config", + "bar.dll", + "bar.pdb", + "bar.cs"); + + DirectoryInfo artifactsPath = new DirectoryInfo(Path.Combine(TestRootPath, "artifacts")); + DirectoryInfo artifactsPath2 = new DirectoryInfo(Path.Combine(TestRootPath, "artifacts2")); + string artifactPaths = string.Concat(artifactsPath.FullName, ";", Environment.NewLine, artifactsPath2.FullName); + + ProjectCreator.Templates.SdkProjectWithArtifacts( + outputPath: Path.Combine("bin", "Debug"), + artifactsPath: artifactPaths, + defaultArtifactSource: @"bin\Debug\foo.exe;bin\Debug\bar.dll", + renamedFiles: "foo.test.exe;bar.test.dll") + .TryGetItems("Artifact", out IReadOnlyCollection artifactItems) + .TryGetPropertyValue("DefaultArtifactsSource", out string defaultArtifactsSource) + .TryBuild(out bool result, out BuildOutput buildOutput); + + result.ShouldBeTrue(buildOutput.GetConsoleLog()); + + artifactsPath.GetFiles("*", SearchOption.AllDirectories) + .Select(i => i.FullName) + .ShouldBe( + new[] + { + "foo.test.exe", + "bar.test.dll", + }.Select(i => Path.Combine(artifactsPath.FullName, i)), + ignoreOrder: true); + artifactsPath2.GetFiles("*", SearchOption.AllDirectories) + .Select(i => i.FullName) + .ShouldBe( + new[] + { + "foo.test.exe", + "bar.test.dll", + }.Select(i => Path.Combine(artifactsPath2.FullName, i)), + ignoreOrder: true); + } + + [Fact] + public void RenamedFilesWithIncorrectNumberShouldFail() + { + CreateFiles( + Path.Combine("bin", "Debug"), + "foo.exe", + "foo.pdb", + "foo.exe.config", + "bar.dll", + "bar.pdb", + "bar.cs"); + + DirectoryInfo artifactsPath = new DirectoryInfo(Path.Combine(TestRootPath, "artifacts")); + + ProjectCreator.Templates.SdkProjectWithArtifacts( + outputPath: Path.Combine("bin", "Debug"), + artifactsPath: artifactsPath.FullName, + defaultArtifactSource: @"bin\Debug\foo.exe;bin\Debug\bar.dll", + renamedFiles: "foo.test.exe") + .TryGetItems("Artifact", out IReadOnlyCollection artifactItems) + .TryGetPropertyValue("DefaultArtifactsSource", out string defaultArtifactsSource) + .TryBuild(out bool result, out BuildOutput buildOutput); + + result.ShouldBeFalse(buildOutput.GetConsoleLog()); + const string expectedMessage = @"Artifact Include 'bin\Debug\foo.exe;bin\Debug\bar.dll' length does not match with RenamedFiles 'foo.test.exe'"; + string errorMessage = buildOutput.ErrorEvents.Single().Message; + (errorMessage == expectedMessage || errorMessage == expectedMessage.Replace('\\', '/')).ShouldBeTrue(errorMessage); + } } } \ No newline at end of file diff --git a/src/Artifacts.UnitTests/CustomProjectCreatorTemplates.cs b/src/Artifacts.UnitTests/CustomProjectCreatorTemplates.cs index a987b49a..e2f00a2b 100644 --- a/src/Artifacts.UnitTests/CustomProjectCreatorTemplates.cs +++ b/src/Artifacts.UnitTests/CustomProjectCreatorTemplates.cs @@ -90,7 +90,9 @@ public static ProjectCreator SdkProjectWithArtifacts( string toolsVersion = null, string treatAsLocalProperty = null, ProjectCollection projectCollection = null, - NewProjectFileOptions? projectFileOptions = null) + NewProjectFileOptions? projectFileOptions = null, + string defaultArtifactSource = null, + string renamedFiles = null) { return ProjectCreator.Create( path, @@ -108,6 +110,8 @@ public static ProjectCreator SdkProjectWithArtifacts( .Property("AppendTargetFrameworkToOutputPath", appendTargetFrameworkToOutputPath.HasValue ? appendTargetFrameworkToOutputPath.ToString() : null) .Property("OutputPath", $"$(OutputPath)$(TargetFramework.ToLowerInvariant()){Path.DirectorySeparatorChar}", condition: "'$(AppendTargetFrameworkToOutputPath)' == 'true'") .Property("ArtifactsPath", artifactsPath) + .Property("DefaultArtifactsSource", defaultArtifactSource) + .Property("RenamedFiles", renamedFiles) .CustomAction(customAction) .Target("Build") .Target("AfterBuild", afterTargets: "Build") diff --git a/src/Artifacts/README.md b/src/Artifacts/README.md index 78c77830..3530ec68 100644 --- a/src/Artifacts/README.md +++ b/src/Artifacts/README.md @@ -151,6 +151,7 @@ The following properties control artifacts staging: | `CustomAfterArtifactsProps` | A list of custom MSBuild projects to import **after** Artifacts properties are declared.| | `CustomBeforeArtifactsTargets` | A list of custom MSBuild projects to import **before** Artifacts targets are declared.| | `CustomAfterArtifactsTargets` | A list of custom MSBuild projects to import **after** Artifacts targets are declared.| +| `RenamedFiles` | Specifies the list of files to rename on copy | | **Example** @@ -175,6 +176,7 @@ The `` items specify collections of artifacts to stage. These items | `FileMatch` | A list of one or more file filters seperated by a space or semicolon to include. Wildcards include `*` and `?` | `*`| | `FileExclude` | A list of one or more file filters seperated by a space or semicolon to exclude. Wildcards include `*` and `?` | | | `DirExclude` | A list of one or more directory filters seperated by a space or semicolon to exclude. Wildcards include `*` and `?` | | +| `RenamedFiles` | A list of files separated by a semicolon matching Include length to rename source files on copy. RenamedFiles should not contain directory as it is provided through DestinationFolder | | **Example** diff --git a/src/Artifacts/Tasks/Robocopy.cs b/src/Artifacts/Tasks/Robocopy.cs index 37b81d5a..2ef1a2b4 100644 --- a/src/Artifacts/Tasks/Robocopy.cs +++ b/src/Artifacts/Tasks/Robocopy.cs @@ -90,7 +90,7 @@ private void CopyItems(IList items) if (hasWildcards || isRecursive) { string match = GetMatchString(items); - CopySearch(items, isRecursive, match, source, null); + CopySearch(items, isRecursive, match, source, subDirectory: null); } else { @@ -111,8 +111,8 @@ private void CopyItems(IList items, DirectoryInfo source) { foreach (string destination in item.DestinationFolders) { - FileInfo destFile = new FileInfo(Path.Combine(destination, file)); - if (Verify(destFile, false, false)) + FileInfo destFile = new FileInfo(Path.Combine(destination, item.RenamedFile ?? file)); + if (Verify(destFile, shouldExist: false, verifyExists: false)) { CopyFile(sourceFile, destFile, createDirs, item); } @@ -191,9 +191,29 @@ private IEnumerable> GetBuckets() IList allSources = new List(); IList> allBuckets = new List>(); - foreach (ITaskItem item in Sources ?? Enumerable.Empty()) + int sourceLength = Sources?.Length ?? 0; + List renamedFiles; + if (sourceLength != 0) { - if (RobocopyMetadata.TryParse(item, Log, FileSystem.DirectoryExists, out RobocopyMetadata metadata)) + renamedFiles = Sources[0].GetMetadata("RenamedFiles").Split(RobocopyMetadata.DestinationSplitter, StringSplitOptions.RemoveEmptyEntries).Select(d => d.Trim()).ToList(); + if (renamedFiles.Count == 0) + { + renamedFiles = null; + } + else if (renamedFiles.Count != sourceLength) + { + Log.LogError($"Artifact Include '{string.Join(";", Sources.Select(s => s.ItemSpec))}' length does not match with RenamedFiles '{string.Join(";", renamedFiles)}'"); + return allBuckets; + } + } + else + { + renamedFiles = null; + } + + for (int i = 0; i < sourceLength; i++) + { + if (RobocopyMetadata.TryParse(Sources[i], Log, renamedFiles?[i], FileSystem.DirectoryExists, out RobocopyMetadata metadata)) { allSources.Add(metadata); } diff --git a/src/Artifacts/Tasks/RobocopyMetadata.cs b/src/Artifacts/Tasks/RobocopyMetadata.cs index 9f6bcd56..07121e50 100644 --- a/src/Artifacts/Tasks/RobocopyMetadata.cs +++ b/src/Artifacts/Tasks/RobocopyMetadata.cs @@ -15,7 +15,7 @@ namespace Microsoft.Build.Artifacts.Tasks { internal sealed class RobocopyMetadata { - private static readonly char[] DestinationSplitter = { ';' }; + internal static readonly char[] DestinationSplitter = { ';' }; private static readonly char[] MultiSplits = { '\t', ' ', '\n', '\r', ';', ',' }; private static readonly char[] Wildcards = { '?', '*' }; @@ -37,6 +37,8 @@ private RobocopyMetadata() public string[] FileMatches { get; private set; } + public string RenamedFile { get; private set; } + public Regex[] FileRegexExcludes { get; private set; } public Regex[] FileRegexMatches { get; private set; } @@ -61,7 +63,7 @@ private RobocopyMetadata() private bool OnlyNewer { get; set; } - public static bool TryParse(ITaskItem item, TaskLoggingHelper log, Func directoryExists, out RobocopyMetadata metadata) + public static bool TryParse(ITaskItem item, TaskLoggingHelper log, string renamedFile, Func directoryExists, out RobocopyMetadata metadata) { metadata = null; @@ -97,6 +99,7 @@ public static bool TryParse(ITaskItem item, TaskLoggingHelper log, Func d.Trim())) diff --git a/src/Artifacts/build/Microsoft.Build.Artifacts.targets b/src/Artifacts/build/Microsoft.Build.Artifacts.targets index 9f18f642..031ccede 100644 --- a/src/Artifacts/build/Microsoft.Build.Artifacts.targets +++ b/src/Artifacts/build/Microsoft.Build.Artifacts.targets @@ -34,6 +34,7 @@ * $(TargetFramworks) is specified in which case the artifacts are copied in the outer build -->