diff --git a/src/Paket.Core/ProjectFile.fs b/src/Paket.Core/ProjectFile.fs index 24e4ba2488..3994583300 100644 --- a/src/Paket.Core/ProjectFile.fs +++ b/src/Paket.Core/ProjectFile.fs @@ -1,4 +1,4 @@ -namespace Paket +namespace Paket open Paket.Domain open Paket.Logging @@ -125,6 +125,223 @@ type ProjectFile = if !isPaketNode = paketOnes then yield node] + member this.GetPropertyWithDefaults propertyName defaultProperties = + let rec handleElement (data : Map) (node : XmlNode) = + let processPlaceholders (data : Map) text = + let getPlaceholderValue (name:string) = + // Change "$(Configuration)" to "Configuration", + // then find in the data map + let name = name.Substring(2, name.Length - 3) + match data.TryFind(name) with + | None -> "" + | Some s -> s + + let replacePlaceholder (s:string) (m:System.Text.RegularExpressions.Match) = + let front = s.Substring(0, m.Index) + let value = getPlaceholderValue m.Value + let back = s.Substring(m.Index + m.Length) + front + value + back + + // The placeholder name must be a valid XML node name, + // else where would its value be defined? + let regex = @"\$\([a-zA-Z_\-\:][a-zA-Z0-9_\.\-\:]*\)" + + System.Text.RegularExpressions.Regex.Matches(text, regex) + |> fun x -> System.Linq.Enumerable.Cast(x) + |> Seq.toArray + |> Array.rev + |> Array.fold replacePlaceholder text + + let conditionMatches data condition = + let rec parseWord (data:System.Text.StringBuilder) (input:string) index inQuotes = + if input.Length <= index + then + if data.Length > 0 && not inQuotes then Some(data.ToString(), index) + else None + else + let c = input.[index] + let gtz = data.Length > 0 + match gtz, inQuotes, c with + | false, false, ' ' -> parseWord data input (index + 1) false + | false, false, '\'' -> parseWord data input (index + 1) true + | _, true, '\'' -> Some(data.ToString(), index + 1) + | true, false, ' ' -> Some(data.ToString(), index + 1) + | _, true, c -> parseWord (data.Append(c)) input (index + 1) true + | _, false, c -> parseWord (data.Append(c)) input (index + 1) false + + let rec parseComparison (data:System.Text.StringBuilder) (input:string) index = + let isCompChar c = c = '<' || c = '>' || c = '!' || c = '=' + if input.Length <= index + then None + else + let c = input.[index] + if data.Length = 0 && c = ' ' + then parseComparison data input (index + 1) + elif data.Length = 2 && isCompChar c + then None + elif isCompChar c + then parseComparison (data.Append(c)) input (index + 1) + else + let s = data.ToString() + let valid = [ "=="; "!="; "<"; ">"; "<="; ">=" ] + match (valid |> List.tryFind ((=) s)) with + | None -> None + | Some(_) -> Some(s, index) + + let parseCondition (data:System.Text.StringBuilder) (input:string) index = + if input.Length <= index + then None + else + data.Clear() |> ignore + match parseWord data input index false with + | None -> None + | Some(left, index) -> + data.Clear() |> ignore + let comp = parseComparison data input index + match comp with + | None -> None + | Some(comp, index) -> + data.Clear() |> ignore + match parseWord data input index false with + | None -> None + | Some(right, index) -> + Some(left, comp, right, index) + + let rec parseAndOr (data:System.Text.StringBuilder) (input:string) index = + if input.Length <= index + then None + else + let c = input.[index] + if data.Length = 0 && c = ' ' + then parseAndOr data input (index + 1) + elif c = ' ' then + let s = data.ToString() + if s.Equals("and", StringComparison.OrdinalIgnoreCase) then Some("and", index) + elif s.Equals("or", StringComparison.OrdinalIgnoreCase) then Some("or", index) + else None + else parseAndOr (data.Append(c)) input (index + 1) + + let rec containsMoreText (input:string) index = + if input.Length <= index then false + else + match input.[index] with + | ' ' -> containsMoreText input (index + 1) + | _ -> true + + let rec parseFullCondition data (sb:System.Text.StringBuilder) (input:string) index = + if input.Length <= index + then data + else + match data with + | None -> None + | Some(data) -> + sb.Clear() |> ignore + let andOr, index = + match data with + | [] -> None, index + | _ -> + let moreText = containsMoreText input index + match (parseAndOr sb input index), moreText with + | None, false -> None, index + | Some(andOr, index), _ -> Some(andOr), index + | None, true -> failwith "Could not parse condition; multiple conditions found with no \"AND\" or \"OR\" between them." + sb.Clear() |> ignore + let nextCondition = parseCondition sb input index + let moreText = containsMoreText input index + match nextCondition, moreText with + | None, true -> None + | None, false -> Some(data) + | Some(left, comp, right, index), _ -> + let data = Some <| data @[(andOr, left, comp, right)] + parseFullCondition data sb input index + + let allConditions = parseFullCondition (Some([])) (System.Text.StringBuilder()) condition 0 + + let rec handleConditions xs lastCondition = + match xs with + | [] -> lastCondition + | (cond, left, comp, right)::xs -> + let left = processPlaceholders data left + let right = processPlaceholders data right + let inline doComp l r = + match comp with + | "==" -> l = r + | "!=" -> l <> r + | ">" -> l > r + | "<" -> l < r + | "<=" -> l <= r + | ">=" -> l >= r + | _ -> failwithf "%s is not a valid comparison operator" comp + + let result = + match comp with + | "==" | "!=" -> doComp left right + | _ -> + match System.Int64.TryParse(left), System.Int64.TryParse(right) with + | (true, l), (true, r) -> doComp l r + | _ -> false + + match lastCondition, cond with + | _, None -> handleConditions xs result + | true, Some("and") -> handleConditions xs result + | _, Some("or") -> handleConditions xs (lastCondition || result) + | _ -> false + + match allConditions with + | None -> false + | Some(conditions) -> handleConditions conditions true + + + let addData data (node:XmlNode) = + let text = processPlaceholders data node.InnerText + // Note that using Map.add overrides the value assigned + // to this key if it already exists in the map; so long + // as we process nodes top-to-bottom, this matches the + // behavior of MSBuild. + Map.add node.Name text data + + let handleConditionalElement data node = + node + |> getAttribute "Condition" + |> function + | None -> + node + |> getChildNodes + |> Seq.fold handleElement data + | Some s -> + if not (conditionMatches data s) + then data + else + if node.ChildNodes.Count > 0 then + node + |> getChildNodes + |> Seq.fold handleElement data + else + data + + match node.Name with + | "PropertyGroup" -> handleConditionalElement data node + // Don't handle these yet + | "Choose" | "Import" | "ItemGroup" | "ProjectExtensions" | "Target" | "UsingTask" -> data + // Any other node types are intended to be values being defined + | _ -> + node + |> getAttribute "Condition" + |> function + | None -> addData data node + | Some s -> + if not (conditionMatches data s) + then data + else addData data node + + this.Document + |> getDescendants "PropertyGroup" + |> Seq.fold handleElement defaultProperties + |> Map.tryFind propertyName + + member this.GetProperty propertyName = + this.GetPropertyWithDefaults propertyName Map.empty + member this.Name = FileInfo(this.FileName).Name member this.NameWithoutExtension = Path.GetFileNameWithoutExtension this.Name @@ -699,14 +916,10 @@ type ProjectFile = |> Seq.head member this.GetTargetFrameworkIdentifier() = - seq { for outputType in this.Document |> getDescendants "TargetFrameworkIdentifier" -> - outputType.InnerText } - |> Seq.tryHead + this.GetProperty "TargetFrameworkIdentifier" member this.GetTargetFrameworkProfile() = - seq {for outputType in this.Document |> getDescendants "TargetFrameworkProfile" -> - outputType.InnerText } - |> Seq.tryHead + this.GetProperty "TargetFrameworkProfile" member this.GetTargetProfile() = match this.GetTargetFrameworkProfile() with @@ -715,21 +928,21 @@ type ProjectFile = | Some profile when String.IsNullOrWhiteSpace profile |> not -> KnownTargetProfiles.FindPortableProfile profile | _ -> - let framework = - seq {for outputType in this.Document |> getDescendants "TargetFrameworkVersion" -> - outputType.InnerText } - |> Seq.choose (fun s -> - let prefix = - match this.GetTargetFrameworkIdentifier() with - | None -> "net" - | Some x -> x - - prefix + s.Replace("v","") - |> FrameworkDetection.Extract) - |> Seq.tryHead - - SinglePlatform(defaultArg framework (DotNetFramework(FrameworkVersion.V4))) - + let prefix = + match this.GetTargetFrameworkIdentifier() with + | None -> "net" + | Some x -> x + let framework = this.GetProperty "TargetFrameworkVersion" + let defaultResult = SinglePlatform(DotNetFramework(FrameworkVersion.V4)) + match framework with + | None -> defaultResult + | Some s -> + let detectedFramework = + prefix + s.Replace("v","") + |> FrameworkDetection.Extract + match detectedFramework with + | None -> defaultResult + | Some x -> SinglePlatform(x) member this.AddImportForPaketTargets(relativeTargetsPath) = match this.Document @@ -752,220 +965,9 @@ type ProjectFile = else "Content" member this.GetOutputDirectory buildConfiguration = - let rec handleElement (data : Map) (node : XmlNode) = - let processPlaceholders (data : Map) text = - let getPlaceholderValue (name:string) = - // Change "$(Configuration)" to "Configuration", - // then find in the data map - let name = name.Substring(2, name.Length - 3) - match data.TryFind(name) with - | None -> "" - | Some s -> s - - let replacePlaceholder (s:string) (m:System.Text.RegularExpressions.Match) = - let front = s.Substring(0, m.Index) - let value = getPlaceholderValue m.Value - let back = s.Substring(m.Index + m.Length) - front + value + back - - // The placeholder name must be a valid XML node name, - // else where would its value be defined? - let regex = @"\$\([a-zA-Z_\-\:][a-zA-Z0-9_\.\-\:]*\)" - - System.Text.RegularExpressions.Regex.Matches(text, regex) - |> fun x -> System.Linq.Enumerable.Cast(x) - |> Seq.toArray - |> Array.rev - |> Array.fold replacePlaceholder text - - let conditionMatches data condition = - let rec parseWord (data:System.Text.StringBuilder) (input:string) index inQuotes = - if input.Length <= index - then - if data.Length > 0 && not inQuotes then Some(data.ToString(), index) - else None - else - let c = input.[index] - let gtz = data.Length > 0 - match gtz, inQuotes, c with - | false, false, ' ' -> parseWord data input (index + 1) false - | false, false, '\'' -> parseWord data input (index + 1) true - | _, true, '\'' -> Some(data.ToString(), index + 1) - | true, false, ' ' -> Some(data.ToString(), index + 1) - | _, true, c -> parseWord (data.Append(c)) input (index + 1) true - | _, false, c -> parseWord (data.Append(c)) input (index + 1) false - - let rec parseComparison (data:System.Text.StringBuilder) (input:string) index = - let isCompChar c = c = '<' || c = '>' || c = '!' || c = '=' - if input.Length <= index - then None - else - let c = input.[index] - if data.Length = 0 && c = ' ' - then parseComparison data input (index + 1) - elif data.Length = 2 && isCompChar c - then None - elif isCompChar c - then parseComparison (data.Append(c)) input (index + 1) - else - let s = data.ToString() - let valid = [ "=="; "!="; "<"; ">"; "<="; ">=" ] - match (valid |> List.tryFind ((=) s)) with - | None -> None - | Some(_) -> Some(s, index) - - let parseCondition (data:System.Text.StringBuilder) (input:string) index = - if input.Length <= index - then None - else - data.Clear() |> ignore - match parseWord data input index false with - | None -> None - | Some(left, index) -> - data.Clear() |> ignore - let comp = parseComparison data input index - match comp with - | None -> None - | Some(comp, index) -> - data.Clear() |> ignore - match parseWord data input index false with - | None -> None - | Some(right, index) -> - Some(left, comp, right, index) - - let rec parseAndOr (data:System.Text.StringBuilder) (input:string) index = - if input.Length <= index - then None - else - let c = input.[index] - if data.Length = 0 && c = ' ' - then parseAndOr data input (index + 1) - elif c = ' ' then - let s = data.ToString() - if s.Equals("and", StringComparison.OrdinalIgnoreCase) then Some("and", index) - elif s.Equals("or", StringComparison.OrdinalIgnoreCase) then Some("or", index) - else None - else parseAndOr (data.Append(c)) input (index + 1) - - let rec containsMoreText (input:string) index = - if input.Length <= index then false - else - match input.[index] with - | ' ' -> containsMoreText input (index + 1) - | _ -> true - - let rec parseFullCondition data (sb:System.Text.StringBuilder) (input:string) index = - if input.Length <= index - then data - else - match data with - | None -> None - | Some(data) -> - sb.Clear() |> ignore - let andOr, index = - match data with - | [] -> None, index - | _ -> - let moreText = containsMoreText input index - match (parseAndOr sb input index), moreText with - | None, false -> None, index - | Some(andOr, index), _ -> Some(andOr), index - | None, true -> failwith "Could not parse condition; multiple conditions found with no \"AND\" or \"OR\" between them." - sb.Clear() |> ignore - let nextCondition = parseCondition sb input index - let moreText = containsMoreText input index - match nextCondition, moreText with - | None, true -> None - | None, false -> Some(data) - | Some(left, comp, right, index), _ -> - let data = Some <| data @[(andOr, left, comp, right)] - parseFullCondition data sb input index - - let allConditions = parseFullCondition (Some([])) (System.Text.StringBuilder()) condition 0 - - let rec handleConditions xs lastCondition = - match xs with - | [] -> lastCondition - | (cond, left, comp, right)::xs -> - let left = processPlaceholders data left - let right = processPlaceholders data right - let inline doComp l r = - match comp with - | "==" -> l = r - | "!=" -> l <> r - | ">" -> l > r - | "<" -> l < r - | "<=" -> l <= r - | ">=" -> l >= r - | _ -> failwithf "%s is not a valid comparision operator" comp - - let result = - match comp with - | "==" | "!=" -> doComp left right - | _ -> - match System.Int64.TryParse(left), System.Int64.TryParse(right) with - | (true, l), (true, r) -> doComp l r - | _ -> false - - match lastCondition, cond with - | _, None -> handleConditions xs result - | true, Some("and") -> handleConditions xs result - | _, Some("or") -> handleConditions xs (lastCondition || result) - | _ -> false - - match allConditions with - | None -> false - | Some(conditions) -> handleConditions conditions true - - - let addData data (node:XmlNode) = - let text = processPlaceholders data node.InnerText - // Note that using Map.add overrides the value assigned - // to this key if it already exists in the map; so long - // as we process nodes top-to-bottom, this matches the - // behavior of MSBuild. - Map.add node.Name text data - - let handleConditionalElement data node = - node - |> getAttribute "Condition" - |> function - | None -> - node - |> getChildNodes - |> Seq.fold handleElement data - | Some s -> - if not (conditionMatches data s) - then data - else - if node.ChildNodes.Count > 0 then - node - |> getChildNodes - |> Seq.fold handleElement data - else - data - - match node.Name with - | "PropertyGroup" -> handleConditionalElement data node - // Don't handle these yet - | "Choose" | "Import" | "ItemGroup" | "ProjectExtensions" | "Target" | "UsingTask" -> data - // Any other node types are intended to be values being defined - | _ -> - node - |> getAttribute "Condition" - |> function - | None -> addData data node - | Some s -> - if not (conditionMatches data s) - then data - else addData data node - let startingData = Map.empty.Add("Configuration", buildConfiguration) - this.Document - |> getDescendants "PropertyGroup" - |> Seq.fold handleElement startingData - |> Map.tryFind "OutputPath" + this.GetPropertyWithDefaults "OutputPath" startingData |> function | None -> failwithf "Unable to find %s output path node in file %s" buildConfiguration this.FileName | Some s -> s.TrimEnd [|'\\'|] |> normalizePath diff --git a/tests/Paket.Tests/ProjectFile/OutputSpecs.fs b/tests/Paket.Tests/ProjectFile/OutputSpecs.fs index fc8e45b17f..ca60736cfb 100644 --- a/tests/Paket.Tests/ProjectFile/OutputSpecs.fs +++ b/tests/Paket.Tests/ProjectFile/OutputSpecs.fs @@ -38,6 +38,11 @@ let ``should detect output path for proj file`` ProjectFile.TryLoad(sprintf "./ProjectFile/TestData/%s.fsprojtest" project).Value.GetOutputDirectory configuration |> shouldEqual (System.IO.Path.Combine(@"bin", configuration) |> normalizePath) +[] +let ``should detect framework profile for ProjectWithConditions file`` () = + ProjectFile.TryLoad("./ProjectFile/TestData/ProjectWithConditions.fsprojtest").Value.GetTargetProfile() + |> shouldEqual (SinglePlatform(DotNetFramework(FrameworkVersion.V4_6))) + [] let ``should detect assembly name for Project1 proj file`` () = ProjectFile.TryLoad("./ProjectFile/TestData/Project1.fsprojtest").Value.GetAssemblyName() diff --git a/tests/Paket.Tests/ProjectFile/TestData/ProjectWithConditions.fsprojtest b/tests/Paket.Tests/ProjectFile/TestData/ProjectWithConditions.fsprojtest index c919b916b9..7525a8c02a 100644 --- a/tests/Paket.Tests/ProjectFile/TestData/ProjectWithConditions.fsprojtest +++ b/tests/Paket.Tests/ProjectFile/TestData/ProjectWithConditions.fsprojtest @@ -9,10 +9,10 @@ Library Paket.Tests Paket.Tests - v4.5 + v4.6 + Client 4.3.0.0 Paket.Tests - ..\..\ true