-
Notifications
You must be signed in to change notification settings - Fork 525
/
DependenciesFile.fs
402 lines (345 loc) · 20.7 KB
/
DependenciesFile.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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
namespace Paket
open System
open System.IO
open Paket
open Paket.Logging
open Paket.Requirements
open Paket.ModuleResolver
open Paket.PackageResolver
open Paket.PackageSources
/// [omit]
type InstallOptions =
{ Strict : bool
OmitContent : bool }
static member Default = { Strict = false; OmitContent = false}
/// [omit]
module DependenciesFileParser =
let private basicOperators = ["~>";"==";"<=";">=";"=";">";"<"]
let private operators = basicOperators @ (basicOperators |> List.map (fun o -> "!" + o))
let parseResolverStrategy (text : string) = if text.StartsWith "!" then ResolverStrategy.Min else ResolverStrategy.Max
let twiddle(minimum:string) =
let promote index (values:string array) =
let parsed, number = Int32.TryParse values.[index]
if parsed then values.[index] <- (number + 1).ToString()
if values.Length > 1 then values.[values.Length - 1] <- "0"
values
let parts = minimum.Split '.'
let penultimateItem = Math.Max(parts.Length - 2, 0)
let promoted = parts |> promote penultimateItem
String.Join(".", promoted)
let parseVersionRequirement (text : string) : VersionRequirement =
let parsePrerelease(texts:string seq) =
let texts = texts |> Seq.filter ((<>) "")
if Seq.isEmpty texts then PreReleaseStatus.No else
if Seq.head(texts).ToLower() = "prerelease" then PreReleaseStatus.All else
PreReleaseStatus.Concrete(texts |> Seq.toList)
if text = "" || text = null then VersionRequirement(VersionRange.AtLeast("0"),PreReleaseStatus.No) else
match text.Split(' ') |> Array.toList with
| ">=" :: v1 :: "<" :: v2 :: rest -> VersionRequirement(VersionRange.Range(VersionRangeBound.Including,SemVer.Parse v1,SemVer.Parse v2,VersionRangeBound.Excluding),parsePrerelease rest)
| ">=" :: v1 :: "<=" :: v2 :: rest -> VersionRequirement(VersionRange.Range(VersionRangeBound.Including,SemVer.Parse v1,SemVer.Parse v2,VersionRangeBound.Including),parsePrerelease rest)
| "~>" :: v1 :: ">=" :: v2 :: rest -> VersionRequirement(VersionRange.Range(VersionRangeBound.Including,SemVer.Parse v2,SemVer.Parse(twiddle v1),VersionRangeBound.Excluding),parsePrerelease rest)
| "~>" :: v1 :: ">" :: v2 :: rest -> VersionRequirement(VersionRange.Range(VersionRangeBound.Excluding,SemVer.Parse v2,SemVer.Parse(twiddle v1),VersionRangeBound.Excluding),parsePrerelease rest)
| ">" :: v1 :: "<" :: v2 :: rest -> VersionRequirement(VersionRange.Range(VersionRangeBound.Excluding,SemVer.Parse v1,SemVer.Parse v2,VersionRangeBound.Excluding),parsePrerelease rest)
| ">" :: v1 :: "<=" :: v2 :: rest -> VersionRequirement(VersionRange.Range(VersionRangeBound.Excluding,SemVer.Parse v1,SemVer.Parse v2,VersionRangeBound.Including),parsePrerelease rest)
| _ ->
let splitVersion (text:string) =
match basicOperators |> List.tryFind(text.StartsWith) with
| Some token -> token, text.Replace(token + " ", "").Split(' ') |> Array.toList
| None -> "=", text.Split(' ') |> Array.toList
try
match splitVersion text with
| "==", version :: rest -> VersionRequirement(VersionRange.OverrideAll(SemVer.Parse version),parsePrerelease rest)
| ">=", version :: rest -> VersionRequirement(VersionRange.AtLeast(version),parsePrerelease rest)
| ">", version :: rest -> VersionRequirement(VersionRange.GreaterThan(SemVer.Parse version),parsePrerelease rest)
| "<", version :: rest -> VersionRequirement(VersionRange.LessThan(SemVer.Parse version),parsePrerelease rest)
| "<=", version :: rest -> VersionRequirement(VersionRange.Maximum(SemVer.Parse version),parsePrerelease rest)
| "~>", minimum :: rest -> VersionRequirement(VersionRange.Between(minimum,twiddle minimum),parsePrerelease rest)
| _, version :: rest -> VersionRequirement(VersionRange.Exactly(version),parsePrerelease rest)
| _ -> failwithf "could not parse version range \"%s\"" text
with
| _ -> failwithf "could not parse version range \"%s\"" text
let parseDependencyLine (line:string) =
let rec parseDepLine start acc =
if start >= line.Length then acc
else
match line.[start] with
| ' ' -> parseDepLine (start+1) acc
| '"' ->
match line.IndexOf('"', start+1) with
| -1 -> failwithf "Unclosed quote in line '%s'" line
| ind -> parseDepLine (ind+1) (line.Substring(start+1, ind-start-1)::acc)
| _ ->
match line.IndexOf(' ', start+1) with
| -1 -> line.Substring(start)::acc
| ind -> parseDepLine (ind+1) (line.Substring(start, ind-start)::acc)
parseDepLine 0 []
|> List.rev
|> List.toArray
let private ``parse git source`` trimmed origin originTxt =
let parts = parseDependencyLine trimmed
let getParts (projectSpec:string) =
match projectSpec.Split [|':'; '/'|] with
| [| owner; project |] -> owner, project, None
| [| owner; project; commit |] -> owner, project, Some commit
| _ -> failwithf "invalid %s specification:%s %s" originTxt Environment.NewLine trimmed
match parts with
| [| _; projectSpec; fileSpec |] -> origin, getParts projectSpec, fileSpec
| [| _; projectSpec; |] -> origin, getParts projectSpec, RemoteDownload.FullProjectSourceFileName
| _ -> failwithf "invalid %s specification:%s %s" originTxt Environment.NewLine trimmed
let private ``parse http source`` trimmed =
let parts = parseDependencyLine trimmed
let getParts (projectSpec:string) fileSpec =
let ``project spec`` =
match projectSpec.EndsWith("/") with
| false -> projectSpec
| true -> projectSpec.Substring(0, projectSpec.Length-1)
let splitted = ``project spec``.Split [|':'; '/'|]
let fileName = match String.IsNullOrEmpty(fileSpec) with
| true -> (splitted |> Seq.last) + ".fs"
| false -> fileSpec
match splitted |> Seq.truncate 6 |> Seq.toArray with
//SourceFile(origin(url), (owner,project, commit), path)
| [| protocol; x; y; domain |] -> HttpLink(``project spec``), (domain, domain, None), fileName
| [| protocol; x; y; domain; project |] -> HttpLink(``project spec``), (domain,project, None), fileName
| [| protocol; x; y; owner; project; details |] -> HttpLink(``project spec``), (owner,project+"/"+details, None), fileName
| _ -> failwithf "invalid http-reference specification:%s %s" Environment.NewLine trimmed
match parts with
| [| _; projectSpec; |] -> getParts projectSpec String.Empty
| [| _; projectSpec; fileSpec |] -> getParts projectSpec fileSpec
| _ -> failwithf "invalid http-reference specification:%s %s" Environment.NewLine trimmed
let private (|Remote|Package|Blank|ReferencesMode|OmitContent|SourceFile|) (line:string) =
match line.Trim() with
| _ when String.IsNullOrWhiteSpace line -> Blank
| trimmed when trimmed.StartsWith "source" -> Remote(PackageSource.Parse(trimmed))
| trimmed when trimmed.StartsWith "nuget" ->
let parts = trimmed.Replace("nuget","").Trim().Replace("\"", "").Split([|' '|],StringSplitOptions.RemoveEmptyEntries) |> Seq.toList
let isVersion(text:string) =
match Int32.TryParse(text.[0].ToString()) with
| true,_ -> true
| _ -> false
match parts with
| name :: operator1 :: version1 :: operator2 :: version2 :: rest
when List.exists ((=) operator1) operators && List.exists ((=) operator2) operators -> Package(name,operator1 + " " + version1 + " " + operator2 + " " + version2 + " " + String.Join(" ",rest))
| name :: operator :: version :: rest
when List.exists ((=) operator) operators -> Package(name,operator + " " + version + " " + String.Join(" ",rest))
| name :: version :: rest when isVersion version ->
Package(name,version + " " + String.Join(" ",rest))
| name :: rest -> Package(name,">= 0 " + String.Join(" ",rest))
| name :: [] -> Package(name,">= 0")
| _ -> failwithf "could not retrieve nuget package from %s" trimmed
| trimmed when trimmed.StartsWith "references" -> ReferencesMode(trimmed.Replace("references","").Trim() = "strict")
| trimmed when trimmed.StartsWith "content" -> OmitContent(trimmed.Replace("content","").Trim() = "none")
| trimmed when trimmed.StartsWith "gist" ->
SourceFile(``parse git source`` trimmed SingleSourceFileOrigin.GistLink "gist")
| trimmed when trimmed.StartsWith "github" ->
SourceFile(``parse git source`` trimmed SingleSourceFileOrigin.GitHubLink "github")
| trimmed when trimmed.StartsWith "http" ->
SourceFile(``parse http source`` trimmed)
| _ -> Blank
let parseDependenciesFile fileName (lines:string seq) =
((0, InstallOptions.Default, [], [], []), lines)
||> Seq.fold(fun (lineNo, options, sources: PackageSource list, packages, sourceFiles: UnresolvedSourceFile list) line ->
let lineNo = lineNo + 1
try
match line with
| Remote(newSource) -> lineNo, options, sources @ [newSource], packages, sourceFiles
| Blank -> lineNo, options, sources, packages, sourceFiles
| ReferencesMode mode -> lineNo, { options with Strict = mode }, sources, packages, sourceFiles
| OmitContent omit -> lineNo, { options with OmitContent = omit }, sources, packages, sourceFiles
| Package(name,version) ->
lineNo, options, sources,
{ Sources = sources
Name = name
ResolverStrategy = parseResolverStrategy version
Parent = DependenciesFile fileName
VersionRequirement = parseVersionRequirement(version.Trim '!') } :: packages, sourceFiles
| SourceFile(origin, (owner,project, commit), path) ->
lineNo, options, sources, packages, { Owner = owner; Project = project; Commit = commit; Name = path; Origin = origin} :: sourceFiles
with
| exn -> failwithf "Error in paket.dependencies line %d%s %s" lineNo Environment.NewLine exn.Message)
|> fun (_,options,_,packages,remoteFiles) ->
fileName,
options,
packages |> List.rev,
remoteFiles |> List.rev
module DependenciesFileSerializer =
let formatVersionRange strategy (version : VersionRequirement) : string =
let prefix =
if strategy = ResolverStrategy.Min then "!"
else ""
let preReleases =
match version.PreReleases with
| No -> ""
| PreReleaseStatus.All -> "prerelease"
| Concrete list -> String.Join(" ",list)
let version =
match version.Range with
| Minimum x when strategy = ResolverStrategy.Max && x = SemVer.Parse "0" -> ""
| Minimum x -> ">= " + x.ToString()
| GreaterThan x -> "> " + x.ToString()
| Specific x -> x.ToString()
| VersionRange.Range(_, from, _, _)
when DependenciesFileParser.parseVersionRequirement ("~> " + from.ToString() + preReleases) = version ->
"~> " + from.ToString()
| _ -> version.ToString()
let text = prefix + version
if text <> "" && preReleases <> "" then text + " " + preReleases else text + preReleases
/// Allows to parse and analyze paket.dependencies files.
type DependenciesFile(fileName,options,packages : PackageRequirement list, remoteFiles : UnresolvedSourceFile list) =
let packages = packages |> Seq.toList
let dependencyMap = Map.ofSeq (packages |> Seq.map (fun p -> p.Name, p.VersionRequirement))
let sources =
packages
|> Seq.map (fun p -> p.Sources)
|> Seq.concat
|> Set.ofSeq
|> Set.toList
member __.DirectDependencies = dependencyMap
member __.Packages = packages
member __.HasPackage (name : string) = packages |> List.exists (fun p -> p.Name.ToLower() = name.ToLower())
member __.RemoteFiles = remoteFiles
member __.Options = options
member __.FileName = fileName
member __.Sources = sources
member this.Resolve(force) =
let getSha1 origin owner repo branch = RemoteDownload.getSHA1OfBranch origin owner repo branch |> Async.RunSynchronously
this.Resolve(getSha1,NuGetV2.GetVersions,NuGetV2.GetPackageDetails force)
member __.Resolve(getSha1,getVersionF, getPackageDetailsF) =
let resolveSourceFile(file:ResolvedSourceFile) : PackageRequirement list =
RemoteDownload.downloadDependenciesFile(Path.GetDirectoryName fileName, file)
|> Async.RunSynchronously
|> DependenciesFile.FromCode
|> fun df -> df.Packages
let remoteFiles = ModuleResolver.Resolve(resolveSourceFile,getSha1,remoteFiles)
let remoteDependencies =
remoteFiles
|> List.map (fun f -> f.Dependencies)
|> List.fold (fun set current -> Set.union set current) Set.empty
|> Seq.map (fun (n, v) ->
let p = packages |> Seq.last
{ p with Name = n
VersionRequirement = v })
|> Seq.toList
{ ResolvedPackages = PackageResolver.Resolve(getVersionF, getPackageDetailsF, remoteDependencies @ packages)
ResolvedSourceFiles = remoteFiles }
member __.AddAdditionionalPackage(packageName:string,version:string) =
let versionRange = DependenciesFileParser.parseVersionRequirement (version.Trim '!')
let sources =
match packages |> List.rev with
| lastPackage::_ -> lastPackage.Sources
| [] -> [PackageSources.DefaultNugetSource]
let newPackage =
{ Name = packageName
VersionRequirement = versionRange
Sources = sources
ResolverStrategy = DependenciesFileParser.parseResolverStrategy version
Parent = PackageRequirementSource.DependenciesFile fileName }
DependenciesFile(fileName,options,packages @ [newPackage], remoteFiles)
member __.AddFixedPackage(packageName:string,version:string) =
let versionRange = DependenciesFileParser.parseVersionRequirement (version.Trim '!')
let sources =
match packages |> List.rev with
| lastPackage::_ -> lastPackage.Sources
| [] -> [PackageSources.DefaultNugetSource]
let strategy =
match packages |> List.tryFind (fun p -> p.Name.ToLower() = packageName.ToLower()) with
| Some package -> package.ResolverStrategy
| None -> DependenciesFileParser.parseResolverStrategy version
let newPackage =
{ Name = packageName
VersionRequirement = versionRange
Sources = sources
ResolverStrategy = strategy
Parent = PackageRequirementSource.DependenciesFile fileName }
DependenciesFile(fileName,options,(packages |> List.filter (fun p -> p.Name.ToLower() <> packageName.ToLower())) @ [newPackage], remoteFiles)
member __.RemovePackage(packageName:string) =
let newPackages =
packages
|> List.filter (fun p -> p.Name.ToLower() <> packageName.ToLower())
DependenciesFile(fileName,options,newPackages,remoteFiles)
member this.Add(packageName,version:string) =
if this.HasPackage packageName then
traceWarnfn "%s contains package %s already. ==> Ignored" fileName packageName
this
else
if version = "" then
tracefn "Adding %s to %s" packageName fileName
else
tracefn "Adding %s %s to %s" packageName version fileName
this.AddAdditionionalPackage(packageName,version)
member this.Remove(packageName) =
if this.HasPackage packageName then
tracefn "Removing %s from %s" packageName fileName
this.RemovePackage(packageName)
else
traceWarnfn "%s doesn't contain package %s. ==> Ignored" fileName packageName
this
member this.UpdatePackageVersion(packageName, version) =
if this.HasPackage(packageName) then
let versionRequirement = DependenciesFileParser.parseVersionRequirement version
tracefn "Updating %s version to %s in %s" packageName version fileName
let packages =
this.Packages |> List.map (fun p ->
if p.Name.ToLower() = packageName.ToLower() then
{ p with VersionRequirement = versionRequirement }
else p)
DependenciesFile(this.FileName, this.Options, packages, this.RemoteFiles)
else
traceWarnfn "%s doesn't contain package %s. ==> Ignored" fileName packageName
this
member this.GetAllPackageSources() =
this.Packages
|> List.collect (fun package -> package.Sources)
|> Seq.distinct
|> Seq.toList
override __.ToString() =
let sources =
packages
|> Seq.map (fun package -> package.Sources,package)
|> Seq.groupBy fst
let formatNugetSource source =
"source " + source.Url +
match source.Authentication with
| Some (PlainTextAuthentication(username,password)) ->
sprintf " username: \"%s\" password: \"%s\"" username password
| Some (EnvVarAuthentication(usernameVar,passwordVar)) ->
sprintf " username: \"%s\" password: \"%s\"" usernameVar.Variable passwordVar.Variable
| _ -> ""
let all =
let hasReportedSource = ref false
let hasReportedFirst = ref false
let hasReportedSecond = ref false
[ if options.Strict then yield "references strict"
if options.OmitContent then yield "content none"
for sources, packages in sources do
for source in sources do
hasReportedSource := true
match source with
| Nuget source -> yield formatNugetSource source
| LocalNuget source -> yield "source " + source
for _,package in packages do
if (not !hasReportedFirst) && !hasReportedSource then
yield ""
hasReportedFirst := true
let version = DependenciesFileSerializer.formatVersionRange package.ResolverStrategy package.VersionRequirement
yield sprintf "nuget %s%s" package.Name (if version <> "" then " " + version else "")
for remoteFile in remoteFiles do
if (not !hasReportedSecond) && !hasReportedFirst then
yield ""
hasReportedSecond := true
yield sprintf "github %s" (remoteFile.ToString())]
String.Join(Environment.NewLine, all)
member this.Save() =
File.WriteAllText(fileName, this.ToString())
tracefn "Dependencies files saved to %s" fileName
static member FromCode(code:string) : DependenciesFile =
DependenciesFile(DependenciesFileParser.parseDependenciesFile "" <| code.Replace("\r\n","\n").Replace("\r","\n").Split('\n'))
static member ReadFromFile fileName : DependenciesFile =
verbosefn "Parsing %s" fileName
DependenciesFile(DependenciesFileParser.parseDependenciesFile fileName <| File.ReadAllLines fileName)
/// Find the matching lock file to a dependencies file
static member FindLockfile(dependenciesFileName) =
let fi = FileInfo(dependenciesFileName)
FileInfo(Path.Combine(fi.Directory.FullName, fi.Name.Replace(fi.Extension,"") + ".lock"))
/// Find the matching lock file to a dependencies file
member this.FindLockfile() = DependenciesFile.FindLockfile this.FileName