Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageVersion Include="Microsoft.Build" Version="$(MSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.Build.Framework" Version="$(MSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="$(MSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions MSBuildPrediction.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
Directory.build.rsp = Directory.build.rsp
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
LICENSE.txt = LICENSE.txt
NuGet.config = NuGet.config
README.md = README.md
Expand Down
124 changes: 86 additions & 38 deletions src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public sealed class CopyTaskPredictor : IProjectPredictor
{
private const string CopyTaskName = "Copy";
private const string CopyTaskSourceFiles = "SourceFiles";
private const string CopyTaskSourceFolders = "SourceFolders";
private const string CopyTaskDestinationFiles = "DestinationFiles";
private const string CopyTaskDestinationFolder = "DestinationFolder";

Expand Down Expand Up @@ -81,55 +82,70 @@ private static void ParseCopyTask(
{
if (projectInstance.EvaluateConditionCarefully(task.Condition))
{
var inputs = new FileExpressionList(
task.Parameters[CopyTaskSourceFiles],
projectInstance,
task);
if (inputs.NumExpressions == 0)
bool hasSourceFiles = task.Parameters.TryGetValue(CopyTaskSourceFiles, out string sourceFiles) && !string.IsNullOrEmpty(sourceFiles);
bool hasSourceFolders = task.Parameters.TryGetValue(CopyTaskSourceFolders, out string sourceFolders) && !string.IsNullOrEmpty(sourceFolders);
bool hasDestinationFiles = task.Parameters.TryGetValue(CopyTaskDestinationFiles, out string destinationFiles) && !string.IsNullOrEmpty(destinationFiles);
bool hasDestinationFolder = task.Parameters.TryGetValue(CopyTaskDestinationFolder, out string destinationFolder) && !string.IsNullOrEmpty(destinationFolder);

// The task will nop if there are no sources.
if (!hasSourceFiles && !hasSourceFolders)
{
continue;
}

foreach (var file in inputs.DedupedFiles)
// The task will error if there is no destination
if (!hasDestinationFiles && !hasDestinationFolder)
{
predictionReporter.ReportInputFile(file);
continue;
}

bool hasDestinationFolder = task.Parameters.TryGetValue(
CopyTaskDestinationFolder,
out string destinationFolder);
bool hasDestinationFiles = task.Parameters.TryGetValue(
CopyTaskDestinationFiles,
out string destinationFiles);

if (hasDestinationFiles || hasDestinationFolder)
// The task will error if both destination types are used.
if (hasDestinationFolder && hasDestinationFiles)
{
// Having both is an MSBuild violation, which it will complain about.
if (hasDestinationFolder && hasDestinationFiles)
{
continue;
}
continue;
}

string destination = destinationFolder ?? destinationFiles;
// SourceFolders and DestinationFiles can't be used together.
if (hasSourceFolders && hasDestinationFiles)
{
continue;
}

var outputs = new FileExpressionList(destination, projectInstance, task);
var inputs = EvaluateExpression(hasSourceFolders ? sourceFolders : sourceFiles, projectInstance, task);
if (inputs.NumExpressions == 0)
{
continue;
}

// When using batch tokens, the user should specify exactly one total token, and it must appear in both the input and output.
// Doing otherwise should be a BuildCop error. If not using batch tokens, then any number of other tokens is fine.
if ((outputs.NumBatchExpressions == 1 && outputs.NumExpressions == 1 &&
inputs.NumBatchExpressions == 1 && inputs.NumExpressions == 1) ||
(outputs.NumBatchExpressions == 0 && inputs.NumBatchExpressions == 0))
foreach (string file in inputs.Paths)
{
if (hasSourceFolders)
{
ProcessOutputs(inputs, outputs, hasDestinationFolder, predictionReporter);
predictionReporter.ReportInputDirectory(file);
}
else
{
// Ignore case we cannot handle.
predictionReporter.ReportInputFile(file);
}
}

var outputs = EvaluateExpression(hasDestinationFolder ? destinationFolder : destinationFiles, projectInstance, task);
if (outputs.NumExpressions == 0)
{
continue;
}

// When using batch tokens, the user should specify exactly one total token, and it must appear in both the input and output.
// If not using batch tokens, then any number of other tokens is fine.
if ((outputs.NumBatchExpressions == 1 && outputs.NumExpressions == 1 &&
inputs.NumBatchExpressions == 1 && inputs.NumExpressions == 1) ||
(outputs.NumBatchExpressions == 0 && inputs.NumBatchExpressions == 0))
{
ProcessOutputs(inputs.Paths, outputs.Paths, hasDestinationFolder, predictionReporter);
}
else
{
// Ignore malformed case.
// Ignore case we cannot handle.
}
}
}
Expand All @@ -146,46 +162,78 @@ private static void ParseCopyTask(
/// <param name="copyTaskSpecifiesDestinationFolder">True if the user has specified DestinationFolder.</param>
/// <param name="predictionReporter">A reporter to report predictions to.</param>
private static void ProcessOutputs(
FileExpressionList inputs,
FileExpressionList outputs,
List<string> inputs,
List<string> outputs,
bool copyTaskSpecifiesDestinationFolder,
ProjectPredictionReporter predictionReporter)
{
for (int i = 0; i < inputs.DedupedFiles.Count; i++)
for (int i = 0; i < inputs.Count; i++)
{
string predictedOutputDirectory;

// If the user specified a destination folder, they could have specified an expression that evaluates to
// either exactly one or N folders. We need to handle each case.
if (copyTaskSpecifiesDestinationFolder)
{
if (outputs.DedupedFiles.Count == 0)
if (outputs.Count == 0)
{
// Output files couldn't be parsed, bail out.
break;
}

// If output directories isn't 1 or N, bail out.
if (inputs.DedupedFiles.Count != outputs.DedupedFiles.Count && outputs.DedupedFiles.Count > 1)
if (inputs.Count != outputs.Count && outputs.Count > 1)
{
break;
}

predictedOutputDirectory = outputs.DedupedFiles.Count == 1 ? outputs.DedupedFiles[0] : outputs.DedupedFiles[i];
predictedOutputDirectory = outputs.Count == 1 ? outputs[0] : outputs[i];
}
else
{
if (i >= outputs.DedupedFiles.Count)
if (i >= outputs.Count)
{
break;
}

// The output list is a set of files. Predict their directories.
predictedOutputDirectory = Path.GetDirectoryName(outputs.DedupedFiles[i]);
predictedOutputDirectory = Path.GetDirectoryName(outputs[i]);
}

predictionReporter.ReportOutputDirectory(predictedOutputDirectory);
}
}

private static (List<string> Paths, int NumExpressions, int NumBatchExpressions) EvaluateExpression(string rawFileListString, ProjectInstance project, ProjectTaskInstance task)
{
List<string> expressions = rawFileListString.SplitStringList();
int numBatchExpressions = 0;

List<string> paths = new();
HashSet<string> seenPaths = new(PathComparer.Instance);
foreach (string expression in expressions)
{
List<string> evaluatedFiles = FileExpression.EvaluateExpression(expression, project, task, out bool isBatched);
if (isBatched)
{
numBatchExpressions++;
}

foreach (string file in evaluatedFiles)
{
if (string.IsNullOrWhiteSpace(file))
{
continue;
}

if (seenPaths.Add(file))
{
paths.Add(file);
}
}
}

return (paths, expressions.Count, numBatchExpressions);
}
}
}
72 changes: 0 additions & 72 deletions src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs

This file was deleted.

17 changes: 17 additions & 0 deletions src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,22 @@ public void TargetUnevaluatableConditionInCopy()
var predictor = new CopyTaskPredictor();
ParseAndVerifyProject("TargetUnevaluatableConditionInCopy.csproj", predictor, expectedInputFiles, null, null, expectedOutputDirectories);
}

[Fact]
public void SourceFolders()
{
PredictedItem[] expectedInputDirectories =
{
new PredictedItem(@"Copy", nameof(CopyTaskPredictor)),
};

PredictedItem[] expectedOutputDirectories =
{
new PredictedItem(@"target\Debug\x64\folder", nameof(CopyTaskPredictor)),
};

var predictor = new CopyTaskPredictor();
ParseAndVerifyProject("SourceFolders.csproj", predictor, null, expectedInputDirectories, null, expectedOutputDirectories);
}
}
}
25 changes: 25 additions & 0 deletions src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="CopyFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{0000000A-0000-00AA-AA00-0AA00A00A00A}</ProjectGuid>
<OutputType>Exe</OutputType>
<OutDir>objd\amd64</OutDir>
<RootNamespace>SomeNamespace</RootNamespace>
<AssemblyName>SomeName</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="SomeFile.cs" />
</ItemGroup>
<ItemGroup>
<FoldersToCopy Include="copy" />
</ItemGroup>
<PropertyGroup>
<Dest>target\$(Configuration)\$(Platform)\folder</Dest>
</PropertyGroup>
<Target Name="CopyFiles">
<Copy SourceFolders="@(FoldersToCopy)" DestinationFolder="$(Dest)" />
</Target>
</Project>