Skip to content

Commit

Permalink
add experimental option to conveniently add --compilertool:/path/to/f…
Browse files Browse the repository at this point in the history
…sac to the FSI session.
  • Loading branch information
smoothdeveloper committed Nov 11, 2023
1 parent 11de177 commit 0cfe72a
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 132 deletions.
5 changes: 5 additions & 0 deletions release/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,11 @@
"markdownDescription": "An array of additional command line parameters to pass to FSI when it is started. See [the Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/fsharp-interactive-options) for an exhaustive list.",
"type": "array"
},
"FSharp.fsiUsesFSACDirectoryForCompilerTool": {
"default": false,
"markdownDescription": "EXPERIMENTAL: adds an --compiltertool:/path/to/FSAC argument to the FSI parameters so the extensions that FSAC ships are conviniently loaded in the FSI session",
"type": "bool"
},
"FSharp.fsiSdkFilePath": {
"default": "",
"description": "The path to the F# Interactive tool used by Ionide-FSharp (When using .NET SDK scripts)",
Expand Down
54 changes: 53 additions & 1 deletion src/Components/Fsi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,60 @@ module Fsi =
match dotnet with
| Ok dotnet ->
let! fsiSetting = LanguageService.fsiSdk ()
let! (_, fsacPath) = LanguageService.SdkResolution.resolvedFsacPathForAmbientSdkTargetFramework ()

let fsacPath =
if fsacPath.EndsWith ".dll" then
let lastIndex =
[ fsacPath.LastIndexOf "/"; fsacPath.LastIndexOf @"\\" ]
|> Seq.sortDescending
|> Seq.tryHead

match lastIndex with
| Some lastIndex when lastIndex > 0 -> fsacPath.Substring(0, lastIndex)
| None
| Some _ -> fsacPath
else
fsacPath

let fsiArg = defaultArg fsiSetting "fsi"
return dotnet, [| yield fsiArg; yield! parms |]

let parms =
let useFsacDirectoryAsCompilerTool =
"FSharp.fsiUsesFSACDirectoryForCompilerTool" |> Configuration.get false

let p =
node.path.join (VSCodeExtension.ionidePluginPath (), "watcher", "watcher.fsx")

let fsiParams =
let userParams =
"FSharp.fsiExtraParameters"
|> Configuration.get Array.empty<string>
|> List.ofArray

if useFsacDirectoryAsCompilerTool then
userParams @ [ $"--compilertool:{fsacPath}" ]
else
userParams

let fsiParams =
let addWatcher = "FSharp.addFsiWatcher" |> Configuration.get false

if addWatcher then
[ "--load:" + p ] @ fsiParams
else
fsiParams

if Environment.isWin then
// these flags are added to work around issues with the vscode terminal shell on windows
[ "--fsi-server-input-codepage:28591"; "--fsi-server-output-codepage:65001" ]
@ fsiParams
else
fsiParams

let args = [| fsiArg; yield! parms |]

return dotnet, args
| Error msg -> return failwith msg
}

Expand Down
275 changes: 144 additions & 131 deletions src/Core/LanguageService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,117 @@ Consider:
client <- Some cl
cl

module SdkResolution =
let fsacNetcorePath = "FSharp.fsac.netCoreDllPath" |> Configuration.get ""

let tfmForSdkVersion (v: SemVer) =
match int v.major, int v.minor with
| 3, 1 -> "netcoreapp3.1"
| 3, 0 -> "netcoreapp3.0"
| 2, 1 -> "netcoreapp2.1"
| 2, 0 -> "netcoreapp2.0"
| n, _ -> $"net{n}.0"

/// given a set of tfms and a target tfm, find the first of the set that satisfies the target.
/// if no target is found, use the 'latest' tfm
/// e.g. [net6.0, net7.0] + net8.0 -> net7.0
/// e.g. [net6.0, net7.0] + net7.0 -> net7.0
let findBestTFM (availableTFMs: string seq) (tfm: string) =
let tfmToSemVer (t: string) =
t.Replace("netcoreapp", "").Replace("net", "").Split([| '.' |], 2)
|> fun ver -> semver.parse (!! $"{ver[0]}.{ver[1]}.0")

let tfmMap =
availableTFMs
|> Seq.choose (fun tfm ->
match tfmToSemVer tfm with
| Some v -> Some(tfm, v)
| None -> None)
|> Seq.sortBy (fun (_, v) -> v.major, v.minor)

printfn $"choosing from among %A{tfmMap}"

match tfmToSemVer tfm with
| None ->
printfn "unable to parse target tfm, using latest"
Seq.last availableTFMs
| Some ver ->
tfmMap
|> Seq.skipWhile (fun (_, v) -> (semver.compare (!!v, !!ver)) = enum -1) // skip while fsac tfm is less than target tfm
|> Seq.tryHead // get first fsac tfm that is greater than or equal to target tfm
|> Option.map fst
|> Option.defaultWith (fun () -> Seq.last availableTFMs)

let probePathForTFMs (basePath: string) (tfm: string) =
let availableTFMs =
node.fs.readdirSync (!!basePath) |> Seq.filter (fun p -> p.StartsWith "net") // there are loose files in the basePath, ignore those

printfn $"Available FSAC TFMs: %A{availableTFMs}"

if availableTFMs |> Seq.contains tfm then
printfn "TFM match found"
tfm, node.path.join (basePath, tfm, "fsautocomplete.dll")
else
// find best-matching
let tfm = findBestTFM availableTFMs tfm
tfm, node.path.join (basePath, tfm, "fsautocomplete.dll")

let isNetFolder (folder: string) =
printfn $"checking folder %s{folder}"
let baseName = node.path.basename folder

baseName.StartsWith("net")
&& let stat = node.fs.statSync (!!folder) in
stat.isDirectory ()

/// locates the FSAC dll and TFM for that dll given a host TFM
let fsacPathForTfm (tfm: string) : string * string =
match fsacNetcorePath with
| null
| "" ->
// user didn't specify a path, so use FSAC from our extension
let binPath = node.path.join (VSCodeExtension.ionidePluginPath (), "bin")
probePathForTFMs binPath tfm
| userSpecified ->
if userSpecified.EndsWith ".dll" then
let tfm = node.path.basename (node.path.dirname userSpecified)
tfm, userSpecified
else
// if dir has tfm folders, probe
let filesAndFolders =
node.fs.readdirSync (!!userSpecified)
|> Seq.map (fun child -> node.path.join ([| userSpecified; child |]))

printfn $"candidates: %A{filesAndFolders}"

if filesAndFolders |> Seq.exists isNetFolder then
// tfm directories found, probe this directory like we would our own bin path
probePathForTFMs userSpecified tfm
else
// no tfm paths, try to use `fsautocomplete.dll` from this directory
let tfm = node.path.basename (node.path.dirname userSpecified)
tfm, node.path.join (userSpecified, "fsautocomplete.dll")

let sdkVersionAndTargetFramework () =
promise {
let! sdkVersionAtRootPath = sdkVersion ()

match sdkVersionAtRootPath with
| Error e ->
printfn $"Error finding dotnet version: {e}"
return failwith "Error finding dotnet version, do you have dotnet installed and on the PATH?"
| Ok sdkVersion ->
printfn "Parsed SDK version at root path: %s" sdkVersion.raw
let sdkTfm = tfmForSdkVersion sdkVersion
return sdkVersion, sdkTfm
}

let resolvedFsacPathForAmbientSdkTargetFramework () =
promise {
let! _, tfm = sdkVersionAndTargetFramework ()
return fsacPathForTfm tfm
}

let getOptions (c: ExtensionContext) : JS.Promise<Executable> =
promise {
let openTelemetryEnabled = "FSharp.openTelemetry.enabled" |> Configuration.get false
Expand Down Expand Up @@ -713,144 +824,46 @@ Consider:

let fsacAttachDebugger = "FSharp.fsac.attachDebugger" |> Configuration.get false

let fsacNetcorePath = "FSharp.fsac.netCoreDllPath" |> Configuration.get ""

let fsacSilencedLogs = "FSharp.fsac.silencedLogs" |> Configuration.get [||]

let verbose = "FSharp.verboseLogging" |> Configuration.get false

/// given a set of tfms and a target tfm, find the first of the set that satisfies the target.
/// if no target is found, use the 'latest' tfm
/// e.g. [net6.0, net7.0] + net8.0 -> net7.0
/// e.g. [net6.0, net7.0] + net7.0 -> net7.0
let findBestTFM (availableTFMs: string seq) (tfm: string) =
let tfmToSemVer (t: string) =
t.Replace("netcoreapp", "").Replace("net", "").Split([| '.' |], 2)
|> fun ver -> semver.parse (!! $"{ver[0]}.{ver[1]}.0")

let tfmMap =
availableTFMs
|> Seq.choose (fun tfm ->
match tfmToSemVer tfm with
| Some v -> Some(tfm, v)
| None -> None)
|> Seq.sortBy (fun (_, v) -> v.major, v.minor)

printfn $"choosing from among %A{tfmMap}"

match tfmToSemVer tfm with
| None ->
printfn "unable to parse target tfm, using latest"
Seq.last availableTFMs
| Some ver ->
tfmMap
|> Seq.skipWhile (fun (_, v) -> (semver.compare (!!v, !!ver)) = enum -1) // skip while fsac tfm is less than target tfm
|> Seq.tryHead // get first fsac tfm that is greater than or equal to target tfm
|> Option.map fst
|> Option.defaultWith (fun () -> Seq.last availableTFMs)

let probePathForTFMs (basePath: string) (tfm: string) =
let availableTFMs =
node.fs.readdirSync (!!basePath) |> Seq.filter (fun p -> p.StartsWith "net") // there are loose files in the basePath, ignore those

printfn $"Available FSAC TFMs: %A{availableTFMs}"

if availableTFMs |> Seq.contains tfm then
printfn "TFM match found"
tfm, node.path.join (basePath, tfm, "fsautocomplete.dll")
else
// find best-matching
let tfm = findBestTFM availableTFMs tfm
tfm, node.path.join (basePath, tfm, "fsautocomplete.dll")

let isNetFolder (folder: string) =
printfn $"checking folder %s{folder}"
let baseName = node.path.basename folder

baseName.StartsWith("net")
&& let stat = node.fs.statSync (!!folder) in
stat.isDirectory ()

/// locates the FSAC dll and TFM for that dll given a host TFM
let fsacPathForTfm (tfm: string) : string * string =
match fsacNetcorePath with
| null
| "" ->
// user didn't specify a path, so use FSAC from our extension
let binPath = node.path.join (VSCodeExtension.ionidePluginPath (), "bin")
probePathForTFMs binPath tfm
| userSpecified ->
if userSpecified.EndsWith ".dll" then
let tfm = node.path.basename (node.path.dirname userSpecified)
tfm, userSpecified
else
// if dir has tfm folders, probe
let filesAndFolders =
node.fs.readdirSync (!!userSpecified)
|> Seq.map (fun child -> node.path.join ([| userSpecified; child |]))

printfn $"candidates: %A{filesAndFolders}"

if filesAndFolders |> Seq.exists isNetFolder then
// tfm directories found, probe this directory like we would our own bin path
probePathForTFMs userSpecified tfm
else
// no tfm paths, try to use `fsautocomplete.dll` from this directory
let tfm = node.path.basename (node.path.dirname userSpecified)
tfm, node.path.join (userSpecified, "fsautocomplete.dll")

let tfmForSdkVersion (v: SemVer) =
match int v.major, int v.minor with
| 3, 1 -> "netcoreapp3.1"
| 3, 0 -> "netcoreapp3.0"
| 2, 1 -> "netcoreapp2.1"
| 2, 0 -> "netcoreapp2.0"
| n, _ -> $"net{n}.0"

let discoverDotnetArgs () =
promise {

let! sdkVersionAtRootPath = sdkVersion ()

match sdkVersionAtRootPath with
| Error e ->
printfn $"Error finding dotnet version: {e}"
return failwith "Error finding dotnet version, do you have dotnet installed and on the PATH?"
| Ok sdkVersion ->
printfn "Parsed SDK version at root path: %s" sdkVersion.raw
let sdkTfm = tfmForSdkVersion sdkVersion
printfn "Parsed SDK version to tfm: %s" sdkTfm
let fsacTfm, fsacPath = fsacPathForTfm sdkTfm
printfn "Parsed TFM to fsac path: %s" fsacPath

let userDotnetArgs = "FSharp.fsac.dotnetArgs" |> Configuration.get [||]

let hasUserRollForward =
userDotnetArgs
|> Array.tryFindIndex (fun a -> a = "--roll-forward")
|> Option.map (fun _ -> true)
|> Option.defaultValue false

let hasUserFxVersion =
userDotnetArgs
|> Array.tryFindIndex (fun a -> a = "--fx-version")
|> Option.map (fun _ -> true)
|> Option.defaultValue false

let shouldApplyImplicitRollForward =
not (hasUserFxVersion || hasUserRollForward) && sdkTfm <> fsacTfm // if the SDK doesn't match one of our FSAC TFMs, then we're in compat mode

let args = userDotnetArgs

let envVariables =
[ if shouldApplyImplicitRollForward then
"DOTNET_ROLL_FORWARD", box "LatestMajor"
match sdkVersion.prerelease with
| null -> ()
| pres when Seq.length pres > 0 -> "DOTNET_ROLL_FORWARD_TO_PRERELEASE", box 1
| _ -> () ]

return args, envVariables, fsacPath, sdkVersion
let! sdkVersion, sdkTfm = SdkResolution.sdkVersionAndTargetFramework ()
printfn "Parsed SDK version to tfm: %s" sdkTfm
let fsacTfm, fsacPath = SdkResolution.fsacPathForTfm sdkTfm
printfn "Parsed TFM to fsac path: %s" fsacPath

let userDotnetArgs = "FSharp.fsac.dotnetArgs" |> Configuration.get [||]

let hasUserRollForward =
userDotnetArgs
|> Array.tryFindIndex (fun a -> a = "--roll-forward")
|> Option.map (fun _ -> true)
|> Option.defaultValue false

let hasUserFxVersion =
userDotnetArgs
|> Array.tryFindIndex (fun a -> a = "--fx-version")
|> Option.map (fun _ -> true)
|> Option.defaultValue false

let shouldApplyImplicitRollForward =
not (hasUserFxVersion || hasUserRollForward) && sdkTfm <> fsacTfm // if the SDK doesn't match one of our FSAC TFMs, then we're in compat mode

let args = userDotnetArgs

let envVariables =
[ if shouldApplyImplicitRollForward then
"DOTNET_ROLL_FORWARD", box "LatestMajor"
match sdkVersion.prerelease with
| null -> ()
| pres when Seq.length pres > 0 -> "DOTNET_ROLL_FORWARD_TO_PRERELEASE", box 1
| _ -> () ]

return args, envVariables, fsacPath, sdkVersion
}

/// Converts true to 1 and false to 0
Expand Down

0 comments on commit 0cfe72a

Please sign in to comment.