diff --git a/src/fsharp/FSharp.Build/FSBuild.txt b/src/fsharp/FSharp.Build/FSBuild.txt
index 99209176b85..b429a789bdc 100644
--- a/src/fsharp/FSharp.Build/FSBuild.txt
+++ b/src/fsharp/FSharp.Build/FSBuild.txt
@@ -1,2 +1,6 @@
# FSharp.Build resource strings
toolpathUnknown,"ToolPath is unknown; specify the path to the tool."
+mapSourceRootsContainsDuplicate,"SourceRoot contains duplicate items '%s' with conflicting metadata '%s': '%s' and '%s'"
+mapSourceRootsPathMustEndWithSlashOrBackslash,"SourceRoot paths are required to end with a slash or backslash: '%s'"
+mapSourceRootsNoTopLevelSourceRoot,"SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true"
+mapSourceRootsNoSuchTopLevelSourceRoot,"The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '%s'"
\ No newline at end of file
diff --git a/src/fsharp/FSharp.Build/FSharp.Build.fsproj b/src/fsharp/FSharp.Build/FSharp.Build.fsproj
index 77e910e4db8..c62b2c2d93e 100644
--- a/src/fsharp/FSharp.Build/FSharp.Build.fsproj
+++ b/src/fsharp/FSharp.Build/FSharp.Build.fsproj
@@ -28,6 +28,7 @@
+
diff --git a/src/fsharp/FSharp.Build/MapSourceRoots.fs b/src/fsharp/FSharp.Build/MapSourceRoots.fs
new file mode 100644
index 00000000000..84310352b8d
--- /dev/null
+++ b/src/fsharp/FSharp.Build/MapSourceRoots.fs
@@ -0,0 +1,191 @@
+namespace FSharp.Build
+
+open System
+open System.IO
+open Microsoft.Build.Framework
+open Microsoft.Build.Utilities
+open System.Collections.Generic
+
+(*
+ This type is a translation of the matching MapSourceRoots task in Roslyn,
+ which is planned to move to a shared location at some point in the future.
+
+ Until then, this version will be used. The exact source used is:
+ https://github.com/dotnet/roslyn/blob/69d3fb733e6c74a41c118bf905739163cf5aef2a/src/Compilers/Core/MSBuildTask/MapSourceRoots.cs,
+ with matching targets usage at:
+ https://github.com/dotnet/roslyn/blob/69d3fb733e6c74a41c118bf905739163cf5aef2a/src/Compilers/Core/MSBuildTask/Microsoft.Managed.Core.targets#L79-L127
+
+*)
+
+module Utilities =
+ ///
+ /// Copied from msbuild. ItemSpecs are normalized using this method.
+ ///
+ let FixFilePath (path: string) =
+ if String.IsNullOrEmpty(path) || Path.DirectorySeparatorChar = '\\'
+ then path
+ else path.Replace('\\', '/');
+
+///
+/// Given a list of SourceRoot items produces a list of the same items with added MappedPath metadata that
+/// contains calculated deterministic source path for each SourceRoot.
+///
+///
+/// Does not perform any path validation.
+///
+/// The MappedPath is either the path (ItemSpec) itself, when is false,
+/// or a calculated deterministic source path (starting with prefix '/_/', '/_1/', etc.), otherwise.
+///
+type MapSourceRoots () =
+ inherit Task ()
+
+ static let MappedPath = "MappedPath"
+ static let SourceControl = "SourceControl"
+ static let NestedRoot = "NestedRoot"
+ static let ContainingRoot = "ContainingRoot"
+ static let RevisionId = "RevisionId"
+ static let SourceLinkUrl = "SourceLinkUrl"
+ static let knownMetadataNames =
+ [
+ SourceControl
+ RevisionId
+ NestedRoot
+ ContainingRoot
+ MappedPath
+ SourceLinkUrl
+ ]
+
+ static let (|NullOrEmpty|HasValue|) (s: string) = if String.IsNullOrEmpty s then NullOrEmpty else HasValue s
+ static let ensureEndsWithSlash (path: string) =
+ if path.EndsWith "/"
+ then path
+ else path + "/"
+
+ static let endsWithDirectorySeparator (path: string) =
+ if path.Length = 0
+ then false
+ else
+ let endChar = path.[path.Length - 1]
+ endChar = Path.DirectorySeparatorChar || endChar = Path.AltDirectorySeparatorChar
+
+ static let reportConflictingWellKnownMetadata (log: TaskLoggingHelper) (l: ITaskItem) (r: ITaskItem) =
+ for name in knownMetadataNames do
+ match l.GetMetadata name, r.GetMetadata name with
+ | HasValue lValue, HasValue rValue when lValue <> rValue ->
+ log.LogWarning(FSBuild.SR.mapSourceRootsContainsDuplicate(r.ItemSpec, name, lValue, rValue))
+ | _, _ -> ()
+
+
+ static member PerformMapping (log: TaskLoggingHelper) (sourceRoots: ITaskItem []) deterministic =
+ let mappedSourceRoots = ResizeArray<_>()
+ let rootByItemSpec = Dictionary();
+
+ for sourceRoot in sourceRoots do
+ // The SourceRoot is required to have a trailing directory separator.
+ // We do not append one implicitly as we do not know which separator to append on Windows.
+ // The usage of SourceRoot might be sensitive to what kind of separator is used (e.g. in SourceLink where it needs
+ // to match the corresponding separators used in paths given to the compiler).
+ if not (endsWithDirectorySeparator sourceRoot.ItemSpec)
+ then
+ log.LogError(FSBuild.SR.mapSourceRootsPathMustEndWithSlashOrBackslash sourceRoot.ItemSpec)
+
+ match rootByItemSpec.TryGetValue sourceRoot.ItemSpec with
+ | true, existingRoot ->
+ reportConflictingWellKnownMetadata log existingRoot sourceRoot
+ sourceRoot.CopyMetadataTo existingRoot
+ | false, _ ->
+ rootByItemSpec.[sourceRoot.ItemSpec] <- sourceRoot
+ mappedSourceRoots.Add sourceRoot
+
+ if log.HasLoggedErrors
+ then None
+ else
+ if deterministic
+ then
+ let topLevelMappedPaths = Dictionary<_,_>()
+ let setTopLevelMappedPaths isSourceControlled =
+
+ let mapNestedRootIfEmpty (root: ITaskItem) =
+ let localPath = root.ItemSpec
+ match root.GetMetadata NestedRoot with
+ | NullOrEmpty ->
+ // root isn't nested
+ if topLevelMappedPaths.ContainsKey(localPath)
+ then
+ log.LogError(FSBuild.SR.mapSourceRootsContainsDuplicate(localPath, NestedRoot, "", ""));
+ else
+ let index = topLevelMappedPaths.Count;
+ let mappedPath = "/_" + (if index = 0 then "" else string index) + "/"
+ topLevelMappedPaths.[localPath] <- mappedPath
+ root.SetMetadata(MappedPath, mappedPath)
+ | HasValue _ -> ()
+
+ for root in mappedSourceRoots do
+ match root.GetMetadata SourceControl with
+ | HasValue v when isSourceControlled -> mapNestedRootIfEmpty root
+ | NullOrEmpty when not isSourceControlled -> mapNestedRootIfEmpty root
+ | _ -> ()
+
+ // assign mapped paths to process source-controlled top-level roots first:
+ setTopLevelMappedPaths true
+
+ // then assign mapped paths to other source-controlled top-level roots:
+ setTopLevelMappedPaths false
+
+ if topLevelMappedPaths.Count = 0
+ then
+ log.LogError(FSBuild.SR.mapSourceRootsNoTopLevelSourceRoot ())
+ else
+ // finally, calculate mapped paths of nested roots:
+ for root in mappedSourceRoots do
+ match root.GetMetadata NestedRoot with
+ | HasValue nestedRoot ->
+ match root.GetMetadata ContainingRoot with
+ | HasValue containingRoot ->
+ // The value of ContainingRoot metadata is a file path that is compared with ItemSpec values of SourceRoot items.
+ // Since the paths in ItemSpec have backslashes replaced with slashes on non-Windows platforms we need to do the same for ContainingRoot.
+ match topLevelMappedPaths.TryGetValue(Utilities.FixFilePath(containingRoot)) with
+ | true, mappedTopLevelPath ->
+ root.SetMetadata(MappedPath, mappedTopLevelPath + ensureEndsWithSlash(nestedRoot.Replace('\\', '/')));
+ | false, _ ->
+ log.LogError(FSBuild.SR.mapSourceRootsNoSuchTopLevelSourceRoot containingRoot)
+ | NullOrEmpty ->
+ log.LogError(FSBuild.SR.mapSourceRootsNoSuchTopLevelSourceRoot "")
+ | NullOrEmpty -> ()
+ else
+ for root in mappedSourceRoots do
+ root.SetMetadata(MappedPath, root.ItemSpec)
+
+ if log.HasLoggedErrors then None else Some (mappedSourceRoots.ToArray())
+
+
+ ///
+ /// SourceRoot items with the following optional well-known metadata:
+ ///
+ /// SourceControl Indicates name of the source control system the source root is tracked by (e.g. Git, TFVC, etc.), if any.
+ /// NestedRoot If a value is specified the source root is nested (e.g. git submodule). The value is a path to this root relative to the containing root.
+ /// ContainingRoot Identifies another source root item that this source root is nested under.
+ ///
+ ///
+ []
+ member val SourceRoots: ITaskItem[] = null with get, set
+
+ ///
+ /// True if the mapped paths should be deterministic.
+ ///
+ member val Deterministic = false with get, set
+
+ ///
+ /// SourceRoot items with MappedPath metadata set.
+ /// Items listed in that have the same ItemSpec will be merged into a single item in this list.
+ ///
+ []
+ member val MappedSourceRoots: ITaskItem[] = null with get, set
+
+ override this.Execute() =
+ match MapSourceRoots.PerformMapping this.Log this.SourceRoots this.Deterministic with
+ | None ->
+ false
+ | Some mappings ->
+ this.MappedSourceRoots <- mappings
+ true
\ No newline at end of file
diff --git a/src/fsharp/FSharp.Build/Microsoft.FSharp.NetSdk.targets b/src/fsharp/FSharp.Build/Microsoft.FSharp.NetSdk.targets
index 7fa9d899362..a04f7854de5 100644
--- a/src/fsharp/FSharp.Build/Microsoft.FSharp.NetSdk.targets
+++ b/src/fsharp/FSharp.Build/Microsoft.FSharp.NetSdk.targets
@@ -12,7 +12,8 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
-->
-
+
+
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
@@ -90,7 +91,74 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
$(FSharpToolsDirectory)/$(FSharpDesignTimeProtocol)/%(_ResolvedOutputFiles.NearestTargetFramework)/%(_ResolvedOutputFiles.FileName)%(_ResolvedOutputFiles.Extension)
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+ <_MappedSourceRoot Remove="@(_MappedSourceRoot)" />
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.cs.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.cs.xlf
index 374f1770bbb..bd142be8836 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.cs.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.cs.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
Parametr ToolPath není známý. Zadejte cestu k nástroji.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.de.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.de.xlf
index c5a6cb42c9f..88548526599 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.de.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.de.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath unbekannt. Geben Sie den Pfad zum Tool an.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.es.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.es.xlf
index 4791cd16b2c..180307863c5 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.es.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.es.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath se desconoce; especifique la ruta de acceso a la herramienta.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.fr.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.fr.xlf
index 79ddfd7ab24..cd98018c017 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.fr.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.fr.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath est inconnu, spécifiez le chemin de l'outil.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.it.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.it.xlf
index a7dd116bb44..a694adff73b 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.it.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.it.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
Il valore di ToolPath è sconosciuto. Specificare il percorso dello strumento.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ja.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ja.xlf
index e26503cb96f..c4638dc9e5b 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ja.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ja.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath が不明です。ツールンパスを指定します。
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ko.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ko.xlf
index 4c141658672..767de522e96 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ko.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ko.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath를 알 수 없습니다. 도구 경로를 지정하세요.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pl.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pl.xlf
index b7734b58a75..91acbeb6626 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pl.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pl.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
Właściwość ToolPath jest nieznana. Określ ścieżkę do narzędzia.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pt-BR.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pt-BR.xlf
index 865c593fd07..2f4a683f5a2 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pt-BR.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.pt-BR.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath desconhecido. Especifique o caminho para a ferramenta.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ru.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ru.xlf
index b1fef639ee0..e960f420a6c 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ru.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.ru.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
Путь к инструменту (ToolPath) неизвестен, укажите путь к инструменту.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.tr.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.tr.xlf
index 4dd43b35531..3a8ad0841ed 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.tr.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.tr.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath bilinmiyor; aracın yolunu belirtin.
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hans.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hans.xlf
index bf5016a3685..a4e15083f21 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hans.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hans.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath 未知;请指定工具的路径。
diff --git a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hant.xlf b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hant.xlf
index 0e30a5cfeca..6b18b193fe4 100644
--- a/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hant.xlf
+++ b/src/fsharp/FSharp.Build/xlf/FSBuild.txt.zh-Hant.xlf
@@ -2,6 +2,26 @@
+
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+ SourceRoot contains duplicate items '{0}' with conflicting metadata '{1}': '{2}' and '{3}'
+
+
+
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+ The value of SourceRoot.ContainingRoot was not found in SourceRoot items, or the corresponding item is not a top-level source root: '{0}'
+
+
+
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+ SourceRoot items must include at least one top-level (not nested) item when DeterministicSourcePaths is true
+
+
+
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+ SourceRoot paths are required to end with a slash or backslash: '{0}'
+
+
ToolPath is unknown; specify the path to the tool.
ToolPath 未知; 請指定工具的路徑。
diff --git a/tests/FSharp.Build.UnitTests/FSharp.Build.UnitTests.fsproj b/tests/FSharp.Build.UnitTests/FSharp.Build.UnitTests.fsproj
index 0a91d3d70c1..ffc6aa91e4b 100644
--- a/tests/FSharp.Build.UnitTests/FSharp.Build.UnitTests.fsproj
+++ b/tests/FSharp.Build.UnitTests/FSharp.Build.UnitTests.fsproj
@@ -12,6 +12,7 @@
+
diff --git a/tests/FSharp.Build.UnitTests/MapSourceRootsTests.fs b/tests/FSharp.Build.UnitTests/MapSourceRootsTests.fs
new file mode 100644
index 00000000000..a5d852b2625
--- /dev/null
+++ b/tests/FSharp.Build.UnitTests/MapSourceRootsTests.fs
@@ -0,0 +1,419 @@
+
+namespace FSharp.Build.UnitTests
+
+open Microsoft.Build.Framework
+open Microsoft.Build.Utilities
+open FSharp.Build
+open NUnit.Framework
+open System.Collections.Generic
+
+type MockEngine() =
+ member val Errors = ResizeArray() with get
+ member val Warnings = ResizeArray() with get
+ member val Custom = ResizeArray() with get
+ member val Messages = ResizeArray() with get
+
+ interface IBuildEngine with
+ member this.BuildProjectFile(projectFileName: string, targetNames: string [], globalProperties: System.Collections.IDictionary, targetOutputs: System.Collections.IDictionary): bool =
+ failwith "Not Implemented"
+ member this.ColumnNumberOfTaskNode: int = 0
+ member this.ContinueOnError: bool = true
+ member this.LineNumberOfTaskNode: int = 0
+ member this.LogCustomEvent(e: CustomBuildEventArgs): unit =
+ this.Custom.Add e
+ failwith "Not Implemented"
+ member this.LogErrorEvent(e: BuildErrorEventArgs): unit =
+ this.Errors.Add e
+ member this.LogMessageEvent(e: BuildMessageEventArgs): unit =
+ this.Messages.Add e
+ member this.LogWarningEvent(e: BuildWarningEventArgs): unit =
+ this.Warnings.Add e
+ member this.ProjectFileOfTaskNode: string = ""
+
+type SourceRoot =
+ SourceRoot of
+ path: string *
+ props: list *
+ expectedProps: list
+
+
+/// these tests are ported from https://github.com/dotnet/roslyn/blob/093ea477717001c58be6231cf2a793f4245cbf72/src/Compilers/Core/MSBuildTaskTests/MapSourceRootTests.cs
+/// Same scenarios, slightly different setup/teardown
+[]
+type MapSourceRootsTests() =
+
+ let assertNoErrors (t: MapSourceRoots) =
+ let engine = t.BuildEngine :?> MockEngine
+ let errors = engine.Errors
+ Assert.AreEqual(0, errors.Count, sprintf "Expected no errors, but found the following: %A" errors)
+ let newTask () =
+ MapSourceRoots(BuildEngine = MockEngine())
+ let toTaskItem (SourceRoot(path, props, _)) =
+ let dict = Dictionary()
+ for (k, v) in props do dict.Add(k, v)
+ TaskItem(path, dict) :> ITaskItem
+ let checkExpectations position (SourceRoot(path, _, expectedProps), mapping: ITaskItem) =
+ Assert.AreEqual(Utilities.FixFilePath path, mapping.ItemSpec, sprintf "expected paths to be the same while checking position %d" position)
+ for (key, value) in expectedProps do
+ Assert.AreEqual(value, mapping.GetMetadata(key), sprintf "expected values for metadata key %s to be the same while checking position %d" key position)
+
+ let successfulTest items =
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) true
+
+ assertNoErrors task
+
+ match outputs with
+ | None ->
+ Assert.Fail("Expected to get some mappings back from this scenario")
+ | Some mappings ->
+ Array.zip items mappings
+ |> Array.iteri checkExpectations
+
+ []
+ member this.``basic deterministic scenarios`` () =
+ let items =
+ [|
+ SourceRoot(@"c:\packages\SourcePackage1\", [], ["MappedPath", @"/_1/"])
+ SourceRoot(@"/packages/SourcePackage2/", [], ["MappedPath", @"/_2/"])
+ SourceRoot(@"c:\MyProjects\MyProject\", ["SourceControl", "Git"], [
+ "SourceControl", "Git"
+ "MappedPath", @"/_/"
+ ])
+ SourceRoot(@"c:\MyProjects\MyProject\a\b\", [
+ "SourceControl", "Git"
+ "NestedRoot", "a/b"
+ "ContainingRoot", @"c:\MyProjects\MyProject\"
+ "some metadata", "some value"
+ ], [
+ "SourceControl", "Git"
+ "some metadata", "some value"
+ "MappedPath", @"/_/a/b/"
+ ])
+ |]
+
+ successfulTest items
+
+
+ []
+ member this.``invalid chars`` () =
+ let items =
+ [|
+ SourceRoot(@"!@#:;$%^&*()_+|{}\", [], ["MappedPath", @"/_1/"])
+ SourceRoot(@"****/", ["SourceControl", "Git"], [
+ "MappedPath", @"/_/"
+ "SourceControl", "Git"
+ ])
+ SourceRoot(@"****\|||:;\", [
+ "SourceControl", "Git"
+ "NestedRoot","|||:;"
+ "ContainingRoot", @"****/"
+ ], [
+ "MappedPath", @"/_/|||:;/"
+ "SourceControl", "Git"
+ ])
+ |]
+ successfulTest items
+
+ []
+ member this.``input paths must end with separator`` () =
+ let items =
+ [|
+ SourceRoot(@"C:\", [], [])
+ SourceRoot(@"C:/", [], [])
+ SourceRoot(@"C:", [], [])
+ SourceRoot(@"C", [], [])
+ |]
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) true
+
+ match outputs with
+ | None ->
+ let errors = (task.BuildEngine :?> MockEngine).Errors
+ Assert.AreEqual(2, errors.Count, "Should have had some errors with path mappings")
+ let expectedErrors = ["'C:'"; "'C'"]
+ let errorMessages = errors |> Seq.map (fun e -> e.Message)
+
+ Assert.IsTrue(errorMessages |> Seq.forall (fun error -> error.Contains("end with a slash or backslash")))
+
+ expectedErrors
+ |> Seq.iter (fun expectedErrorPath ->
+ Assert.IsTrue(errorMessages |> Seq.exists (fun err -> err.EndsWith expectedErrorPath),
+ sprintf "expected an error to end with '%s', none did.\nMessages were:\n%A" expectedErrorPath errorMessages)
+ )
+ | Some mappings ->
+ Assert.Fail("Expected to fail on the inputs")
+
+ []
+ member this.``nested roots separators`` () =
+ let items =
+ [|
+ SourceRoot(@"c:\MyProjects\MyProject\", [], [
+ "MappedPath", @"/_/"
+ ])
+ SourceRoot(@"c:\MyProjects\MyProject\a\a\", [
+ "NestedRoot", @"a/a/"
+ "ContainingRoot", @"c:\MyProjects\MyProject\"
+ ], [
+ "MappedPath", @"/_/a/a/"
+ ])
+ SourceRoot(@"c:\MyProjects\MyProject\a\b\", [
+ "NestedRoot", @"a/b\"
+ "ContainingRoot", @"c:\MyProjects\MyProject\"
+ ],[
+ "MappedPath", @"/_/a/b/"
+ ])
+ SourceRoot(@"c:\MyProjects\MyProject\a\c\", [
+ "NestedRoot", @"a\c"
+ "ContainingRoot", @"c:\MyProjects\MyProject\"
+ ], [
+ "MappedPath", @"/_/a/c/"
+ ])
+ |]
+
+ successfulTest items
+
+ []
+ member this.``sourceroot case sensitivity``() =
+ let items = [|
+ SourceRoot(@"c:\packages\SourcePackage1\", [], ["MappedPath", @"/_/"])
+ SourceRoot(@"C:\packages\SourcePackage1\", [], ["MappedPath", @"/_1/"])
+ SourceRoot(@"c:\packages\SourcePackage2\", [], ["MappedPath", @"/_2/"])
+ |]
+
+ successfulTest items
+
+ []
+ member this.``recursion error`` () =
+ let path1 = Utilities.FixFilePath @"c:\MyProjects\MyProject\a\1\"
+ let path2 = Utilities.FixFilePath @"c:\MyProjects\MyProject\a\2\"
+ let path3 = Utilities.FixFilePath @"c:\MyProjects\MyProject\"
+ let items =
+ [|
+ SourceRoot(path1, [
+ "ContainingRoot", path2
+ "NestedRoot", "a/1"
+ ], [])
+ SourceRoot(path2, [
+ "ContainingRoot", path1
+ "NestedRoot", "a/2"
+ ], [])
+ SourceRoot(path3, [], [])
+ |]
+
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) true
+
+ match outputs with
+ | None ->
+ let errors = (task.BuildEngine :?> MockEngine).Errors
+ Assert.AreEqual(2, errors.Count, "Should have had some errors with path mappings")
+ let expectedErrors = [path2; path1] |> List.map (sprintf "'%s'")
+ let errorMessages = errors |> Seq.map (fun e -> e.Message)
+
+ Assert.IsTrue(errorMessages |> Seq.forall (fun error -> error.Contains("ContainingRoot was not found in SourceRoot items")),
+ sprintf "Expected to have the same type of errors but had %A" errorMessages
+ )
+
+ expectedErrors
+ |> Seq.iter (fun expectedErrorPath ->
+ Assert.IsTrue(errorMessages |> Seq.exists (fun err -> err.EndsWith expectedErrorPath), sprintf "expected an error to end with '%s', none did.\nMessages were:\n%A" expectedErrorPath errorMessages)
+ )
+ | Some mappings ->
+ Assert.Fail("Expected to fail on the inputs")
+
+ []
+ []
+ []
+ member this.``metadata merge 1`` (deterministic: bool) =
+ let path1 = Utilities.FixFilePath @"c:\packages\SourcePackage1\"
+ let path2 = Utilities.FixFilePath @"c:\packages\SourcePackage2\"
+ let path3 = Utilities.FixFilePath @"c:\packages\SourcePackage3\"
+
+ let items = [|
+ SourceRoot(path1, [
+ "NestedRoot", @"NR1A"
+ "ContainingRoot", path3
+ "RevisionId", "RevId1"
+ "SourceControl", "git"
+ "MappedPath", "MP1"
+ "SourceLinkUrl", "URL1"
+ ], [])
+ SourceRoot(path1, [
+ "NestedRoot", @"NR1B"
+ "ContainingRoot", @"CR"
+ "RevisionId", "RevId2"
+ "SourceControl", "tfvc"
+ "MappedPath", "MP2"
+ "SourceLinkUrl", "URL2"
+ ], [])
+ SourceRoot(path2, [
+ "NestedRoot", @"NR2"
+ "SourceControl", "git"
+ ], [])
+ SourceRoot(path2, [
+ "ContainingRoot", path3
+ "SourceControl", "git"
+ ], [])
+ SourceRoot(path3, [], [])
+ |]
+
+ /// because this test isn't one to one we have to put the expecations in another structure
+ let actualExpectations = [|
+ SourceRoot(path1, [], [
+ "SourceControl", "git"
+ "RevisionId", "RevId1"
+ "NestedRoot", "NR1A"
+ "ContainingRoot", path3
+ "MappedPath", if deterministic then "/_/NR1A/" else path1
+ "SourceLinkUrl", "URL1"
+ ])
+ SourceRoot(path2, [], [
+ "SourceControl", "git"
+ "RevisionId", ""
+ "NestedRoot", "NR2"
+ "ContainingRoot", path3
+ "MappedPath", if deterministic then "/_/NR2/" else path2
+ "SourceLinkUrl", ""
+ ])
+ SourceRoot(path3, [], [
+ "SourceControl", ""
+ "RevisionId", ""
+ "NestedRoot", ""
+ "ContainingRoot", ""
+ "MappedPath", if deterministic then "/_/" else path3
+ "SourceLinkUrl", ""
+ ])
+ |]
+
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) deterministic
+
+ assertNoErrors task
+
+ match outputs with
+ | None ->
+ Assert.Fail("Expected to get some mappings back from this scenario")
+ | Some mappings ->
+ let warnings = (task.BuildEngine :?> MockEngine).Warnings |> Seq.map (fun w -> w.Message)
+
+ Assert.AreEqual(6, Seq.length warnings)
+ Assert.IsTrue(warnings |> Seq.forall (fun w -> w.Contains "duplicate items"))
+
+ [
+ "SourceControl", "git", "tfvc"
+ "RevisionId", "RevId1", "RevId2"
+ "NestedRoot", "NR1A", "NR1B"
+ "ContainingRoot", path3, "CR"
+ "MappedPath", "MP1", "MP2"
+ "SourceLinkUrl", "URL1", "URL2"
+ ]
+ |> List.iter (fun (key, lval, rval) ->
+ Assert.IsTrue(
+ (warnings |> Seq.exists (fun warn -> warn.Contains(sprintf "SourceRoot contains duplicate items '%s' with conflicting metadata '%s': '%s' and '%s'" path1 key lval rval))),
+ sprintf "Expected to find an error message for %s comparing %s and %s, but got %A" key lval rval warnings
+ )
+ )
+
+ Array.zip actualExpectations mappings
+ |> Array.iteri checkExpectations
+
+ []
+ member this.``missing containing root`` () =
+ let items = [|
+ SourceRoot(@"c:\MyProjects\MYPROJECT\", [], [])
+ SourceRoot(@"c:\MyProjects\MyProject\a\b\", [
+ "SourceControl", "Git"
+ "NestedRoot", "a/b"
+ "ContainingRoot", @"c:\MyProjects\MyProject\"
+ ], []
+ )
+ |]
+
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) true
+
+ match outputs with
+ | None ->
+ let errors = (task.BuildEngine :?> MockEngine).Errors
+ Assert.AreEqual(1, errors.Count, "Should have had some errors with path mappings")
+ let expectedErrors = [@"c:\MyProjects\MyProject\"] |> List.map (sprintf "'%s'")
+ let errorMessages = errors |> Seq.map (fun e -> e.Message)
+
+ Assert.IsTrue(errorMessages |> Seq.forall (fun error -> error.Contains("corresponding item is not a top-level source root")),
+ sprintf "Expected to have the same type of errors but had %A" errorMessages
+ )
+
+ expectedErrors
+ |> Seq.iter (fun expectedErrorPath ->
+ Assert.IsTrue(errorMessages |> Seq.exists (fun err -> err.EndsWith expectedErrorPath), sprintf "expected an error to end with '%s', none did.\nMessages were:\n%A" expectedErrorPath errorMessages)
+ )
+ | Some mappings ->
+ Assert.Fail("Expected to fail on the inputs")
+
+ []
+ member this.``no containing root`` () =
+ let items = [|
+ SourceRoot(@"c:\MyProjects\MyProject\", [], [])
+ SourceRoot(@"c:\MyProjects\MyProject\a\b\", [
+ "SourceControl", "Git"
+ "NestedRoot", "a/b"
+ ], [])
+ |]
+
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) true
+
+ match outputs with
+ | None ->
+ let errors = (task.BuildEngine :?> MockEngine).Errors
+ Assert.AreEqual(1, errors.Count, "Should have had some errors with path mappings")
+ let expectedErrors = [@""] |> List.map (sprintf "'%s'")
+ let errorMessages = errors |> Seq.map (fun e -> e.Message)
+
+ Assert.IsTrue(errorMessages |> Seq.forall (fun error -> error.Contains("corresponding item is not a top-level source root")),
+ sprintf "Expected to have the same type of errors but had %A" errorMessages
+ )
+
+ expectedErrors
+ |> Seq.iter (fun expectedErrorPath ->
+ Assert.IsTrue(errorMessages |> Seq.exists (fun err -> err.EndsWith expectedErrorPath), sprintf "expected an error to end with '%s', none did.\nMessages were:\n%A" expectedErrorPath errorMessages)
+ )
+ | Some mappings ->
+ Assert.Fail("Expected to fail on the inputs")
+
+ []
+ []
+ []
+ member this.``no top level source root`` (deterministic: bool) =
+ let path1 = Utilities.FixFilePath @"c:\MyProjects\MyProject\a\b\"
+ let items = [|
+ SourceRoot(path1, [
+ "ContainingRoot", path1
+ "NestedRoot", "a/b"
+ ], [
+ "SourceControl", ""
+ "RevisionId", ""
+ "NestedRoot", "a/b"
+ "ContainingRoot", path1
+ "MappedPath", path1
+ "SourceLinkUrl", ""
+ ])
+ |]
+
+ let task = newTask()
+ let outputs = MapSourceRoots.PerformMapping task.Log (items |> Array.map toTaskItem) deterministic
+
+ match outputs, deterministic with
+ | Some _, true ->
+ Assert.Fail "Expected to fail when deterministic"
+ | None, true ->
+ let errors = (task.BuildEngine :?> MockEngine).Errors
+ Assert.AreEqual(1, errors.Count, "Should have had some errors with path mappings")
+ let error = errors.[0].Message
+ Assert.IsTrue(error.Contains "when DeterministicSourcePaths is true")
+ | None, false ->
+ Assert.Fail (sprintf "Expected to succeed when not deterministic")
+ | Some mappings, false ->
+ Array.zip items mappings
+ |> Array.iteri checkExpectations