Skip to content
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

Add Artifact file renaming feature #437

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/Artifacts.UnitTests/ArtifactsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectItem> 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<ProjectItem> 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);
}
}
}
6 changes: 5 additions & 1 deletion src/Artifacts.UnitTests/CustomProjectCreatorTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/Artifacts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -175,6 +176,7 @@ The `<Artifact />` 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**

Expand Down
30 changes: 25 additions & 5 deletions src/Artifacts/Tasks/Robocopy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private void CopyItems(IList<RobocopyMetadata> items)
if (hasWildcards || isRecursive)
{
string match = GetMatchString(items);
CopySearch(items, isRecursive, match, source, null);
CopySearch(items, isRecursive, match, source, subDirectory: null);
}
else
{
Expand All @@ -111,8 +111,8 @@ private void CopyItems(IList<RobocopyMetadata> 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);
}
Expand Down Expand Up @@ -191,9 +191,29 @@ private IEnumerable<IList<RobocopyMetadata>> GetBuckets()
IList<RobocopyMetadata> allSources = new List<RobocopyMetadata>();
IList<IList<RobocopyMetadata>> allBuckets = new List<IList<RobocopyMetadata>>();

foreach (ITaskItem item in Sources ?? Enumerable.Empty<ITaskItem>())
int sourceLength = Sources?.Length ?? 0;
List<string> 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);
}
Expand Down
7 changes: 5 additions & 2 deletions src/Artifacts/Tasks/RobocopyMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = { '?', '*' };

Expand All @@ -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; }
Expand All @@ -61,7 +63,7 @@ private RobocopyMetadata()

private bool OnlyNewer { get; set; }

public static bool TryParse(ITaskItem item, TaskLoggingHelper log, Func<string, bool> directoryExists, out RobocopyMetadata metadata)
public static bool TryParse(ITaskItem item, TaskLoggingHelper log, string renamedFile, Func<string, bool> directoryExists, out RobocopyMetadata metadata)
{
metadata = null;

Expand Down Expand Up @@ -97,6 +99,7 @@ public static bool TryParse(ITaskItem item, TaskLoggingHelper log, Func<string,
VerifyExists = item.GetMetadataBoolean(nameof(VerifyExists)),
AlwaysCopy = item.GetMetadataBoolean(nameof(AlwaysCopy), defaultValue: false),
OnlyNewer = item.GetMetadataBoolean(nameof(OnlyNewer), defaultValue: false),
RenamedFile = renamedFile,
};

foreach (string destination in item.GetMetadata("DestinationFolder").Split(DestinationSplitter, StringSplitOptions.RemoveEmptyEntries).Select(d => d.Trim()))
Expand Down
1 change: 1 addition & 0 deletions src/Artifacts/build/Microsoft.Build.Artifacts.targets
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* $(TargetFramworks) is specified in which case the artifacts are copied in the outer build
-->
<Artifact Include="$(DefaultArtifactsSource)"
RenamedFiles="$(RenamedFiles)"
DestinationFolder="$(ArtifactsPath)"
FileMatch="$([MSBuild]::ValueOrDefault($(DefaultArtifactsFileMatch), '*exe *dll *exe.config *nupkg'))"
FileExclude="$(DefaultArtifactsFileExclude)"
Expand Down