-
Notifications
You must be signed in to change notification settings - Fork 577
Expand file tree
/
Copy pathSdkAssemblyResolver.fs
More file actions
502 lines (408 loc) · 20.8 KB
/
Copy pathSdkAssemblyResolver.fs
File metadata and controls
502 lines (408 loc) · 20.8 KB
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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
module Fake.Runtime.SdkAssemblyResolver
open System
open System.IO
open System.Net
open System.Net.Http
open System.Threading
open System.Runtime.InteropServices
open Fake.Core
open Fake.IO.FileSystemOperators
open Fake.DotNet
open Fake.Runtime
open Newtonsoft.Json
open Paket
open Microsoft.Deployment.DotNet.Releases
/// here we will pin Fake runner execution framework to .NET 6 as in `SdkVersion`
/// We will also try to resolve the current SDK that the runner is executing, if it is the same as pinned
/// one then we will use runtime assemblies from that SDK version on its installation on disk. Otherwise,
/// we will default to NetStandard2.0 assemblies. We will download them since they are packaged in a NuGet
/// package extract them a and reference them.
/// the resolution of runtime version for the selected SDK is as follows; we will use the dotnet official release
/// package to get the releases for pinned framework version, and get the runtime. If the accessing the network
/// is not possible, then we will use a cached releases file.
type SdkAssemblyResolver(logLevel: Trace.VerboseLevel) =
// following environment variables are used in testing for different scenarios that the SDK resolver
// could encounter, they are not intended to be used other than that!
let CustomDotNetHostPath =
Environment.environVarOrDefault "FAKE_SDK_RESOLVER_CUSTOM_DOTNET_PATH" ""
let RuntimeResolverResolveMethod =
Environment.environVarOrDefault "FAKE_SDK_RESOLVER_RUNTIME_VERSION_RESOLVE_METHOD" ""
// Defaults still .NET 6.0 but could be overriden with .NET 8.0 or even comma-separated "6.0,8.0"
let RuntimeAssemblyVersions =
let versions =
Environment.environVarOrDefault "FAKE_SDK_RESOLVER_CUSTOM_DOTNET_VERSION" "6.0"
versions.Split([| ','; ';' |]) |> Array.toList
member this.LogLevel = logLevel
member this.SdkVersionRaws = RuntimeAssemblyVersions
member this.SdkVersions =
RuntimeAssemblyVersions
|> List.map (fun v ->
if String.IsNullOrEmpty v || v = "\"\"" then
ReleaseVersion "6.0.0"
elif v.Contains "." then
ReleaseVersion(v + ".0")
else
ReleaseVersion(v + ".0.0"))
member this.PaketFrameworkIdentifiers =
this.SdkVersions
|> List.map (fun thisSdk ->
match FrameworkVersion.TryParse(thisSdk.Major.ToString()) with
| Some v -> FrameworkIdentifier.DotNetFramework v
| None -> failwithf $"Paket: .NET not found: {thisSdk.Major.ToString()}")
member this.SdkVersionRaw = RuntimeAssemblyVersions |> Seq.head
member this.SdkVersion = this.SdkVersions |> Seq.head
member this.PaketFrameworkIdentifier = this.PaketFrameworkIdentifiers |> Seq.head
member this.SdkVersionFromGlobalJson = GlobalJson.tryGetSDKVersionFromGlobalJson ()
member this.IsSdkVersionFromGlobalJsonSameAsSdkVersion() =
match this.SdkVersionFromGlobalJson with
| Some version ->
this.SdkVersions
|> List.exists (fun thisSdk -> ReleaseVersion(version).Major.Equals thisSdk.Major)
| None -> false
member this.DotNetBinaryName = if Environment.isUnix then "dotnet" else "dotnet.exe"
/// <summary>
/// provides the path to the `dotnet` binary running this library, respecting various dotnet <see href="https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables#dotnet_root-dotnet_rootx86%5D">environment variables</see>.
/// Also probes the PATH and checks the default installation locations
/// </summary>
member this.ResolveDotNetRoot() =
let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
let isMac = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
let isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
let isUnix = isLinux || isMac
let potentialDotnetHostEnvVars =
[ "DOTNET_HOST_PATH", id // is a full path to dotnet binary
"DOTNET_ROOT", (fun s -> Path.Combine(s, this.DotNetBinaryName)) // needs dotnet binary appended
"DOTNET_ROOT(x86)", (fun s -> Path.Combine(s, this.DotNetBinaryName)) ] // needs dotnet binary appended
let existingEnvVarValue envVarValue =
match envVarValue with
| null
| "" -> None
| other -> Some other
let tryFindFromEnvVar () =
potentialDotnetHostEnvVars
|> List.tryPick (fun (envVar, transformer) ->
match Environment.GetEnvironmentVariable envVar |> existingEnvVarValue with
| Some varValue -> Some(transformer varValue |> FileInfo)
| None -> None)
let PATHSeparator = if isUnix then ':' else ';'
/// Fully resolve the symlink, returning a fully-resolved FileInfo
/// that is not a symlink,
/// or returning None if the file or any of its resolved targets
/// does not exist.
let rec resolveFile (fi: System.IO.FileInfo) : FileInfo option =
if not fi.Exists then None
elif isNull fi.LinkTarget then Some fi
else resolveFile (System.IO.FileInfo fi.LinkTarget)
let tryFindFromPATH () =
Environment
.GetEnvironmentVariable("PATH")
.Split(PATHSeparator, StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries)
|> Array.tryPick (fun d ->
let fi = Path.Combine(d, this.DotNetBinaryName) |> FileInfo
if fi.Exists then Some fi else None)
let tryFindFromDefaultDirs () =
let windowsPath = $"C:\\Program Files\\dotnet\\{this.DotNetBinaryName}"
let macosPath = $"/usr/local/share/dotnet/{this.DotNetBinaryName}"
let linuxPath = $"/usr/share/dotnet/{this.DotNetBinaryName}"
let tryFindFile p =
let f = FileInfo p
if f.Exists then Some f else None
if isWindows then tryFindFile windowsPath
else if isMac then tryFindFile macosPath
else if isLinux then tryFindFile linuxPath
else None
if not (String.isNullOrEmpty CustomDotNetHostPath) then
Some CustomDotNetHostPath
else
tryFindFromEnvVar ()
|> Option.orElseWith tryFindFromPATH
|> Option.orElseWith tryFindFromDefaultDirs
|> Option.bind resolveFile
|> Option.map (fun dotnetRoot -> dotnetRoot.Directory.FullName)
/// <summary>
/// provides the path to the `dotnet` binary running this library, respecting various dotnet <see href="https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables#dotnet_root-dotnet_rootx86%5D">environment variables</see>.
/// Also probes the PATH and checks the default installation locations
/// </summary>
member this.ResolveDotNetRoots() =
let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
let isMac = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
let isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
let isUnix = isLinux || isMac
let potentialDotnetHostEnvVars =
[ "DOTNET_HOST_PATH", id // is a full path to dotnet binary
"DOTNET_ROOT", (fun s -> Path.Combine(s, this.DotNetBinaryName)) // needs dotnet binary appended
"DOTNET_ROOT(x86)", (fun s -> Path.Combine(s, this.DotNetBinaryName)) ] // needs dotnet binary appended
let existingEnvVarValue envVarValue =
match envVarValue with
| null
| "" -> None
| other -> Some other
let tryFindFromEnvVar () =
potentialDotnetHostEnvVars
|> List.choose (fun (envVar, transformer) ->
match Environment.GetEnvironmentVariable envVar |> existingEnvVarValue with
| Some varValue -> Some(transformer varValue |> FileInfo)
| None -> None)
let PATHSeparator = if isUnix then ':' else ';'
/// Fully resolve the symlink, returning a fully-resolved FileInfo
/// that is not a symlink,
/// or returning None if the file or any of its resolved targets
/// does not exist.
let rec resolveFile (fi: System.IO.FileInfo) : FileInfo option =
if not fi.Exists then None
elif isNull fi.LinkTarget then Some fi
else resolveFile (System.IO.FileInfo fi.LinkTarget)
let tryFindFromPATH () =
Environment
.GetEnvironmentVariable("PATH")
.Split(PATHSeparator, StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries)
|> Array.choose (fun d ->
let fi = Path.Combine(d, this.DotNetBinaryName) |> FileInfo
if fi.Exists then Some fi else None)
let tryFindFromDefaultDirs () =
let windowsPath = $"C:\\Program Files\\dotnet\\{this.DotNetBinaryName}"
let macosPath = $"/usr/local/share/dotnet/{this.DotNetBinaryName}"
let linuxPath = $"/usr/share/dotnet/{this.DotNetBinaryName}"
let tryFindFile p =
let f = FileInfo p
if f.Exists then Some f else None
if isWindows then tryFindFile windowsPath
else if isMac then tryFindFile macosPath
else if isLinux then tryFindFile linuxPath
else None
if not (String.isNullOrEmpty CustomDotNetHostPath) then
[| CustomDotNetHostPath |]
else
let paths =
seq {
yield! tryFindFromEnvVar ()
yield! tryFindFromPATH ()
match tryFindFromDefaultDirs () with
| Some x -> yield x
| None -> ()
}
paths
|> Seq.distinct
|> Seq.choose resolveFile
|> Seq.map (fun dotnetRoot -> dotnetRoot.Directory.FullName)
|> Seq.distinct
|> Seq.toArray
member this.TryResolveSdkRuntimeVersionFromNetwork() =
if this.LogLevel.PrintVerbose then
Trace.tracefn "Trying to resolve runtime version from network.."
let sdkSet = this.SdkVersionRaws |> Set.ofList
try
task {
let! productCollection = ProductCollection.GetAsync()
let sdkVersionReleases =
productCollection
|> Seq.filter (fun product -> sdkSet |> Set.exists (fun raws -> product.ProductVersion.Equals raws))
let! releases =
sdkVersionReleases
|> Seq.map (fun rel -> rel.GetReleasesAsync())
|> System.Threading.Tasks.Task.WhenAll
return releases |> Seq.collect (fun sdks -> sdks |> Seq.toList)
}
|> Async.AwaitTask
|> Async.RunSynchronously
|> Seq.toList
|> Some
with ex ->
Trace.traceError $"Could not get SDK runtime version from network due to: {ex.Message}"
None
member this.TryResolveSdkRuntimeVersionFromCache() =
if this.LogLevel.PrintVerbose then
Trace.tracefn "Trying to resolve runtime version from cache.."
try
System.Reflection.Assembly.GetExecutingAssembly().Location
|> Path.GetDirectoryName
</> "cachedDotnetSdkReleases.json"
|> Product.GetReleasesAsync
|> Async.AwaitTask
|> Async.RunSynchronously
|> List.ofSeq
|> Some
with ex ->
Trace.traceError $"Could not get SDK runtime version from cache due to: {ex.Message}"
None
member this.GetProductReleasesForSdk(version: ReleaseVersion) =
let net60releases =
if RuntimeResolverResolveMethod = "cache" then
// for testing only!
this.TryResolveSdkRuntimeVersionFromCache()
else
// this is the default case, we will try the network, if we could not, then we will reach for cached file.
this.TryResolveSdkRuntimeVersionFromNetwork()
|> Option.orElseWith (this.TryResolveSdkRuntimeVersionFromCache)
let sdkRelease (release: ProductRelease) =
release.Sdks |> Seq.exists (fun sdk -> sdk.Version.Equals(version))
match net60releases with
| None -> []
| Some versions ->
match versions |> List.filter sdkRelease with
| [] ->
Trace.traceFAKE $"No exact match of product releases {version.ToString()} found."
match versions |> List.filter (fun release -> release.Version.Major = version.Major) with
| [] ->
Trace.traceFAKE
$"No product release found for {version.ToString()}. Maybe a pre-release? Returning all the versions."
versions
| majorMatch ->
Trace.traceFAKE $".NET {version.Major} product releases returned."
majorMatch
| foundMatch -> foundMatch
member this.GetProductReleaseForSdk(version: ReleaseVersion) =
this.GetProductReleasesForSdk version |> List.tryHead
member this.ResolveSdkRuntimeVersions() =
let versionOptions dotnetRoot (options: InternalDotNet.VersionOptions) =
// If a custom CLI path is provided, configure the version command
// to use that path. This really only accomodates a test scenarios
// in which FAKE_SDK_RESOLVER_CUSTOM_DOTNET_PATH is set.
match dotnetRoot with
| Some root ->
options.WithCommon(fun common -> { common with DotNetCliPath = root </> this.DotNetBinaryName })
| None -> options
let dotnetRoots = this.ResolveDotNetRoots()
let sdkVersions =
if Array.isEmpty dotnetRoots then
[ InternalDotNet.getVersion (versionOptions None) |> ReleaseVersion ]
else
dotnetRoots
|> Seq.map (fun dotnetRoot ->
InternalDotNet.getVersion (versionOptions (Some dotnetRoot)) |> ReleaseVersion)
|> Seq.toList
let productReleases =
sdkVersions
|> List.collect (fun sdkVersion -> this.GetProductReleasesForSdk sdkVersion)
match productReleases with
| [] ->
let versions =
String.Join(", ", sdkVersions |> Seq.map (fun sdkVersion -> sdkVersion.ToString()))
let example =
sdkVersions
|> Seq.tryHead
|> Option.map (fun sdkVersion -> sdkVersion.Major.ToString())
|> Option.defaultValue "6"
failwithf
$"Could not find a suitable .NET 6 runtime version matching SDK version: {versions} (You can also try setting environment variable FAKE_SDK_RESOLVER_CUSTOM_DOTNET_VERSION to e.g. {example}.0 )"
| releases ->
let versions =
releases
|> Seq.map (fun release -> release.Runtime.Version.ToString())
|> Seq.distinct
|> Seq.toList
if this.LogLevel.PrintVerbose then
versions
|> List.iter (fun version -> Trace.trace $"Resolved runtime version: {version}")
versions
member this.ResolveSdkRuntimeVersion() =
this.ResolveSdkRuntimeVersions() |> Seq.head
member this.SdkReferenceAssemblies() =
let runtimeVersions = this.ResolveSdkRuntimeVersions()
let referenceAssembliesPaths =
runtimeVersions
|> List.map (fun runtimeVersion ->
this.SdkVersionRaws
|> List.choose (fun rawVersion ->
this.ResolveDotNetRoot()
|> Option.map (fun dotnetRoot ->
dotnetRoot
</> "packs"
</> "Microsoft.NETCore.App.Ref"
</> runtimeVersion
</> "ref"
</> "net" + rawVersion)))
if Seq.isEmpty referenceAssembliesPaths then
failwithf "Could not find referenced assemblies, please check installed SDK and runtime versions"
else
if this.LogLevel.PrintVerbose then
let paths = String.Join(",", referenceAssembliesPaths)
Trace.tracefn $"Resolved referenced SDK paths: {paths}"
let existingReferenceAssembliesPaths =
referenceAssembliesPaths
|> List.collect (fun paths -> paths |> List.filter (fun path -> Directory.Exists path))
if this.LogLevel.PrintVerbose then
let paths = String.Join(",", existingReferenceAssembliesPaths)
Trace.tracefn $"Found referenced SDK paths: {paths}"
let referenceAssembliesPath = existingReferenceAssembliesPaths |> List.tryHead
match referenceAssembliesPath with
| Some pathFound -> Directory.GetFiles(pathFound, "*.dll") |> Seq.toList
| None ->
let paths = String.Join(",", referenceAssembliesPaths)
failwithf
"Could not find referenced assemblies in path: '%s', please check installed SDK and runtime versions"
paths
member this.NetStandard20ReferenceAssemblies
(
nuGetPackage: string,
version: string,
pathSuffix: string,
groupName: Domain.GroupName,
paketDependenciesFile: Lazy<Paket.DependenciesFile>
) =
// We need to use "real" reference assemblies as using the currently running runtime assemblies doesn't work:
// see https://github.com/fsharp/FAKE/pull/1695
// Therefore we download the reference assemblies (the NETStandard.Library or Microsoft.NETCore.App.Ref package)
// and add them in addition to what we have resolved,
// we use the sources in the paket.dependencies to give the user a chance to overwrite.
// Note: This package/version needs to updated together with our "framework" variable below and needs to
// be compatible with the runtime we are currently running on.
let rootDir = Directory.GetCurrentDirectory()
let packageName = Domain.PackageName(nuGetPackage)
let version = SemVer.Parse(version)
let existingPkg = NuGetCache.GetTargetUserNupkg packageName version
let extractedFolder =
match File.Exists existingPkg with
| true ->
// Shortcut in order to prevent requests to nuget sources if we have it downloaded already
Path.GetDirectoryName existingPkg
| false ->
let sources = paketDependenciesFile.Value.Groups.[groupName].Sources
let versions =
Paket.NuGet.GetVersions
false
None
rootDir
(PackageResolver.GetPackageVersionsParameters.ofParams sources groupName packageName)
|> Async.RunSynchronously
|> dict
let source =
match versions.TryGetValue(version) with
| true, v when v.Length > 0 -> v |> Seq.head
| _ ->
failwithf
"Could not find package '%A' with version '%A' in any package source of group '%A', but fake needs this package to compile the script"
packageName
version
groupName
let _, extractedFolder =
Paket.NuGet.DownloadAndExtractPackage(
None,
rootDir,
false,
PackagesFolderGroupConfig.NoPackagesFolder,
source,
[],
Paket.Constants.MainDependencyGroup,
packageName,
version,
PackageResolver.ResolvedPackageKind.Package,
false,
false,
false,
false
)
|> Async.RunSynchronously
extractedFolder
let sdkDir = Path.Combine(extractedFolder, pathSuffix)
Directory.GetFiles(sdkDir, "*.dll") |> Seq.toList
member this.ResolveSdkReferenceAssemblies
(
groupName: Domain.GroupName,
paketDependenciesFile: Lazy<Paket.DependenciesFile>
) =
if this.LogLevel.PrintVerbose then
let versions =
String.Join(", and", this.SdkVersions |> List.map (fun v -> $" .NET{v.Major}"))
Trace.tracefn $"Using{versions} assemblies"
this.SdkReferenceAssemblies()