/
FSharp.DependencyManager.fs
271 lines (229 loc) · 13.4 KB
/
FSharp.DependencyManager.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace FSharp.DependencyManager.Nuget
open System
open System.Collections.Concurrent
open System.Diagnostics
open System.IO
open FSharp.DependencyManager.Nuget
open FSharp.DependencyManager.Nuget.Utilities
open FSharp.DependencyManager.Nuget.ProjectFile
open FSDependencyManager
module FSharpDependencyManager =
[<assembly: DependencyManagerAttribute()>]
do ()
let private concat (s:string) (v:string) : string =
match String.IsNullOrEmpty(s), String.IsNullOrEmpty(v) with
| false, false -> s + ";" + v
| false, true -> s
| true, false -> v
| _ -> ""
let validateAndFormatRestoreSources (sources:string) = [|
let items = sources.Split(';')
for item in items do
let uri = new Uri(item)
if uri.IsFile then
let directoryName = uri.LocalPath
if Directory.Exists(directoryName) then
yield sprintf """ <PropertyGroup Condition="Exists('%s')"><RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);%s</RestoreAdditionalProjectSources></PropertyGroup>""" directoryName directoryName
else
raise (Exception(SR.sourceDirectoryDoesntExist(directoryName)))
else
yield sprintf """ <PropertyGroup><RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);%s</RestoreAdditionalProjectSources></PropertyGroup>""" uri.OriginalString
|]
let formatPackageReference p =
let { Include=inc; Version=ver; RestoreSources=src; Script=script } = p
seq {
match not (String.IsNullOrEmpty(inc)), not (String.IsNullOrEmpty(ver)), not (String.IsNullOrEmpty(script)) with
| true, true, false -> yield sprintf @" <ItemGroup><PackageReference Include='%s' Version='%s' /></ItemGroup>" inc ver
| true, true, true -> yield sprintf @" <ItemGroup><PackageReference Include='%s' Version='%s' Script='%s' /></ItemGroup>" inc ver script
| true, false, false -> yield sprintf @" <ItemGroup><PackageReference Include='%s' /></ItemGroup>" inc
| true, false, true -> yield sprintf @" <ItemGroup><PackageReference Include='%s' Script='%s' /></ItemGroup>" inc script
| _ -> ()
match not (String.IsNullOrEmpty(src)) with
| true -> yield! validateAndFormatRestoreSources src
| _ -> ()
}
let parsePackageReferenceOption scriptExt (setBinLogPath: string option option -> unit) (line: string) =
let validatePackageName package packageName =
if String.Compare(packageName, package, StringComparison.OrdinalIgnoreCase) = 0 then
raise (ArgumentException(SR.cantReferenceSystemPackage(packageName)))
let rec parsePackageReferenceOption' (options: (string option * string option) list) (implicitArgumentCount: int) (packageReference: PackageReference option) =
let current =
match packageReference with
| Some p -> p
| None -> { Include = ""; Version = "*"; RestoreSources = ""; Script = "" }
match options with
| [] -> packageReference
| opt :: rest ->
let addInclude v =
validatePackageName v "mscorlib"
if scriptExt = fsxExt then
validatePackageName v "FSharp.Core"
validatePackageName v "System.ValueTuple"
validatePackageName v "NETStandard.Library"
validatePackageName v "Microsoft.NETFramework.ReferenceAssemblies"
Some { current with Include = v }
let setVersion v = Some { current with Version = v }
match opt with
| Some "include", Some v -> addInclude v |> parsePackageReferenceOption' rest implicitArgumentCount
| Some "include", None -> raise (ArgumentException(SR.requiresAValue("Include")))
| Some "version", Some v -> setVersion v |> parsePackageReferenceOption' rest implicitArgumentCount
| Some "version", None -> setVersion "*" |> parsePackageReferenceOption' rest implicitArgumentCount
| Some "restoresources", Some v -> Some { current with RestoreSources = concat current.RestoreSources v } |> parsePackageReferenceOption' rest implicitArgumentCount
| Some "restoresources", None -> raise (ArgumentException(SR.requiresAValue("RestoreSources")))
| Some "script", Some v -> Some { current with Script = v } |> parsePackageReferenceOption' rest implicitArgumentCount
| Some "bl", value ->
match value with
| Some v when v.ToLowerInvariant() = "true" -> setBinLogPath (Some None) // auto-generated logging location
| Some v when v.ToLowerInvariant() = "false" -> setBinLogPath None // no logging
| Some path -> setBinLogPath (Some (Some (path))) // explicit logging location
| None ->
// parser shouldn't get here because unkeyed values follow a different path, but for the sake of completeness and keeping the compiler happy,
// this is fine
setBinLogPath (Some None) // auto-generated logging location
parsePackageReferenceOption' rest implicitArgumentCount packageReference
| None, Some v ->
match v.ToLowerInvariant() with
| "bl" ->
// a bare 'bl' enables binary logging and is NOT interpreted as one of the positional arguments. On the off chance that the user actually wants
// to reference a package named 'bl' they still have the 'Include=bl' syntax as a fallback.
setBinLogPath (Some None) // auto-generated logging location
parsePackageReferenceOption' rest implicitArgumentCount packageReference
| _ ->
match implicitArgumentCount with
| 0 -> addInclude v
| 1 -> setVersion v
| _ -> raise (ArgumentException(SR.unableToApplyImplicitArgument(implicitArgumentCount + 1)))
|> parsePackageReferenceOption' rest (implicitArgumentCount + 1)
| _ -> parsePackageReferenceOption' rest implicitArgumentCount packageReference
let options = getOptions line
parsePackageReferenceOption' options 0 None
let parsePackageReference scriptExt (lines: string list) =
let mutable binLogPath = None
lines
|> List.choose (fun line -> parsePackageReferenceOption scriptExt (fun p -> binLogPath <- p) line)
|> List.distinct
|> (fun l -> l, binLogPath)
let parsePackageDirective scriptExt (lines: (string * string) list) =
let mutable binLogPath = None
lines
|> List.map(fun (directive, line) ->
match directive with
| "i" -> sprintf "RestoreSources=%s" line
| _ -> line)
|> List.choose (fun line -> parsePackageReferenceOption scriptExt (fun p -> binLogPath <- p) line)
|> List.distinct
|> (fun l -> l, binLogPath)
/// The results of ResolveDependencies
type ResolveDependenciesResult (success: bool, stdOut: string array, stdError: string array, resolutions: string seq, sourceFiles: string seq, roots: string seq) =
/// Succeded?
member _.Success = success
/// The resolution output log
member _.StdOut = stdOut
/// The resolution error log (* process stderror *)
member _.StdError = stdError
/// The resolution paths - the full paths to selected resolved dll's.
/// In scripts this is equivalent to #r @"c:\somepath\to\packages\ResolvedPackage\1.1.1\lib\netstandard2.0\ResolvedAssembly.dll"
member _.Resolutions = resolutions
/// The source code file paths
member _.SourceFiles = sourceFiles
/// The roots to package directories
/// This points to the root of each located package.
/// The layout of the package manager will be package manager specific.
/// however, the dependency manager dll understands the nuget package layout
/// and so if the package contains folders similar to the nuget layout then
/// the dependency manager will be able to probe and resolve any native dependencies
/// required by the nuget package.
///
/// This path is also equivalent to
/// #I @"c:\somepath\to\packages\ResolvedPackage\1.1.1\"
member _.Roots = roots
[<DependencyManagerAttribute>]
type FSharpDependencyManager (outputDir:string option) =
let key = "nuget"
let name = "MsBuild Nuget DependencyManager"
let scriptsPath =
let path = Path.Combine(Path.GetTempPath(), key, Process.GetCurrentProcess().Id.ToString() + "--"+ Guid.NewGuid().ToString())
match outputDir with
| None -> path
| Some v -> Path.Combine(path, v)
let generatedScripts = new ConcurrentDictionary<string,string>()
let deleteScripts () =
try
#if !Debug
if Directory.Exists(scriptsPath) then
Directory.Delete(scriptsPath, true)
#else
()
#endif
with | _ -> ()
let deleteAtExit =
try
if not (File.Exists(scriptsPath)) then
Directory.CreateDirectory(scriptsPath) |> ignore
true
with | _ -> false
let emitFile filename (body:string) =
try
// Create a file to write to
use sw = File.CreateText(filename)
sw.WriteLine(body)
with | _ -> ()
let prepareDependencyResolutionFiles (scriptExt: string, packageManagerTextLines: (string * string) seq, targetFrameworkMoniker: string, runtimeIdentifier: string): PackageBuildResolutionResult =
let scriptExt =
match scriptExt with
| ".csx" -> csxExt
| _ -> fsxExt
let packageReferences, binLogPath =
packageManagerTextLines
|> List.ofSeq
|> FSharpDependencyManager.parsePackageDirective scriptExt
let packageReferenceLines =
packageReferences
|> List.map FSharpDependencyManager.formatPackageReference
|> Seq.concat
let packageReferenceText = String.Join(Environment.NewLine, packageReferenceLines)
let projectPath = Path.Combine(scriptsPath, "Project.fsproj")
// Generate project files
let generateAndBuildProjectArtifacts =
let writeFile path body =
if not (generatedScripts.ContainsKey(body.GetHashCode().ToString())) then
emitFile path body
let generateProjBody =
generateProjectBody.Replace("$(TARGETFRAMEWORK)", targetFrameworkMoniker)
.Replace("$(RUNTIMEIDENTIFIER)", runtimeIdentifier)
.Replace("$(PACKAGEREFERENCES)", packageReferenceText)
.Replace("$(SCRIPTEXTENSION)", scriptExt)
writeFile projectPath generateProjBody
buildProject projectPath binLogPath
generateAndBuildProjectArtifacts
do if deleteAtExit then AppDomain.CurrentDomain.ProcessExit |> Event.add(fun _ -> deleteScripts () )
member _.Name = name
member _.Key = key
member _.HelpMessages = [|
sprintf """ #r "nuget:FSharp.Data, 3.1.2";; // %s 'FSharp.Data' %s '3.1.2'""" (SR.loadNugetPackage()) (SR.version())
sprintf """ #r "nuget:FSharp.Data";; // %s 'FSharp.Data' %s""" (SR.loadNugetPackage()) (SR.highestVersion())
|]
member this.ResolveDependencies(scriptExt: string, packageManagerTextLines: (string * string) seq, targetFrameworkMoniker: string, runtimeIdentifier: string) : obj =
let poundRprefix =
match scriptExt with
| ".csx" -> "#r \""
| _ -> "#r @\""
let generateAndBuildProjectArtifacts =
let resolutionResult = prepareDependencyResolutionFiles (scriptExt, packageManagerTextLines, targetFrameworkMoniker, runtimeIdentifier)
match resolutionResult.resolutionsFile with
| Some file ->
let resolutions = getResolutionsFromFile file
let references = (findReferencesFromResolutions resolutions) |> Array.toSeq
let scripts =
let scriptPath = resolutionResult.projectPath + scriptExt
let scriptBody = makeScriptFromReferences references poundRprefix
emitFile scriptPath scriptBody
let loads = (findLoadsFromResolutions resolutions) |> Array.toList
List.concat [ [scriptPath]; loads] |> List.toSeq
let includes = (findIncludesFromResolutions resolutions) |> Array.toSeq
ResolveDependenciesResult(resolutionResult.success, resolutionResult.stdOut, resolutionResult.stdErr, references, scripts, includes)
| None ->
let empty = Seq.empty<string>
ResolveDependenciesResult(resolutionResult.success, resolutionResult.stdOut, resolutionResult.stdErr, empty, empty, empty)
generateAndBuildProjectArtifacts :> obj