diff --git a/Directory.Packages.props b/Directory.Packages.props
index c0f9bea..4f2ffaf 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,7 +10,7 @@
-
+
diff --git a/MSBuildPrediction.sln b/MSBuildPrediction.sln
index 147f7e0..3e1a1d5 100644
--- a/MSBuildPrediction.sln
+++ b/MSBuildPrediction.sln
@@ -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
diff --git a/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs b/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs
index 7ae0c35..3ca334c 100644
--- a/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs
+++ b/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs
@@ -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";
@@ -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.
}
}
}
@@ -146,12 +162,12 @@ private static void ParseCopyTask(
/// True if the user has specified DestinationFolder.
/// A reporter to report predictions to.
private static void ProcessOutputs(
- FileExpressionList inputs,
- FileExpressionList outputs,
+ List inputs,
+ List outputs,
bool copyTaskSpecifiesDestinationFolder,
ProjectPredictionReporter predictionReporter)
{
- for (int i = 0; i < inputs.DedupedFiles.Count; i++)
+ for (int i = 0; i < inputs.Count; i++)
{
string predictedOutputDirectory;
@@ -159,33 +175,65 @@ private static void ProcessOutputs(
// 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 Paths, int NumExpressions, int NumBatchExpressions) EvaluateExpression(string rawFileListString, ProjectInstance project, ProjectTaskInstance task)
+ {
+ List expressions = rawFileListString.SplitStringList();
+ int numBatchExpressions = 0;
+
+ List paths = new();
+ HashSet seenPaths = new(PathComparer.Instance);
+ foreach (string expression in expressions)
+ {
+ List 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);
+ }
}
}
\ No newline at end of file
diff --git a/src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs b/src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs
deleted file mode 100644
index 0c2699f..0000000
--- a/src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System.Collections.Generic;
-using Microsoft.Build.Execution;
-
-namespace Microsoft.Build.Prediction.Predictors.CopyTask
-{
- ///
- /// Contains a parsed list of file expressions as well as the list of files derived from evaluating said
- /// expressions.
- ///
- internal sealed class FileExpressionList
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The unprocessed list of file expressions.
- /// The project where the expression list exists.
- /// The task where the expression list exists.
- public FileExpressionList(string rawFileListString, ProjectInstance project, ProjectTaskInstance task)
- {
- List expressions = rawFileListString.SplitStringList();
- NumExpressions = expressions.Count;
-
- var seenFiles = new HashSet(PathComparer.Instance);
- foreach (string expression in expressions)
- {
- List evaluatedFiles = FileExpression.EvaluateExpression(expression, project, task, out bool isBatched);
- if (isBatched)
- {
- NumBatchExpressions++;
- }
-
- foreach (string file in evaluatedFiles)
- {
- if (string.IsNullOrWhiteSpace(file))
- {
- continue;
- }
-
- if (seenFiles.Add(file))
- {
- DedupedFiles.Add(file);
- }
-
- AllFiles.Add(file);
- }
- }
- }
-
- ///
- /// Gets the set of all files in all of the expanded expressions. May include duplicates.
- ///
- public List AllFiles { get; } = new List();
-
- ///
- /// Gets the set of all files in the expanded expressions. Duplicates are removed.
- ///
- public List DedupedFiles { get; } = new List();
-
- ///
- /// Gets the total number of expressions in the file list.
- ///
- public int NumExpressions { get; }
-
- ///
- /// Gets the number of batch expressions in the file list.
- ///
- public int NumBatchExpressions { get; }
- }
-}
\ No newline at end of file
diff --git a/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs b/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs
index 185bd97..ec08181 100644
--- a/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs
+++ b/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs
@@ -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);
+ }
}
}
\ No newline at end of file
diff --git a/src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj b/src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj
new file mode 100644
index 0000000..794041a
--- /dev/null
+++ b/src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj
@@ -0,0 +1,25 @@
+
+
+
+ {0000000A-0000-00AA-AA00-0AA00A00A00A}
+ Exe
+ objd\amd64
+ SomeNamespace
+ SomeName
+
+
+
+
+
+
+
+
+
+
+
+ target\$(Configuration)\$(Platform)\folder
+
+
+
+
+
\ No newline at end of file