Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading legacy .NET framework proj files #131

Merged
merged 1 commit into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
dotnet-version: ${{ matrix.dotnet }}
- name: Restore tools
run: dotnet tool restore
- name: Install latest F# from mono (.net 4.x)
if: runner.os == 'Linux'
run: ./install_mono_from_microsoft_deb_packages.sh
- name: Run build
run: dotnet fake build -t Pack
- name: Upload NuGet packages
Expand Down
15 changes: 15 additions & 0 deletions install_mono_from_microsoft_deb_packages.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euxo pipefail

source /etc/os-release

# required by apt-key
sudo apt install -y gnupg2
# required by apt-update when pulling from mono-project.com
sudo apt install -y ca-certificates

# taken from http://www.mono-project.com/download/stable/#download-lin
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
sudo echo "deb https://download.mono-project.com/repo/ubuntu stable-$UBUNTU_CODENAME main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list
sudo apt update
sudo apt install -y fsharp
114 changes: 98 additions & 16 deletions src/Ionide.ProjInfo/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ open Microsoft.Build.Execution
open Types
open Microsoft.Build.Graph
open System.Diagnostics
open System.Runtime.InteropServices

/// functions for .net sdk probing
module SdkDiscovery =
Expand All @@ -22,7 +23,7 @@ module SdkDiscovery =
Version: SemanticVersioning.Version
Path: DirectoryInfo }

let private execDotnet (cwd: DirectoryInfo) (binaryFullPath: FileInfo) args =
let internal execDotnet (cwd: DirectoryInfo) (binaryFullPath: FileInfo) args =
let info = ProcessStartInfo()
info.WorkingDirectory <- cwd.FullName
info.FileName <- binaryFullPath.FullName
Expand Down Expand Up @@ -109,6 +110,44 @@ module SdkDiscovery =
| true, v -> Ok v
| false, _ -> Error(dotnetBinaryPath, [ "--version" ], cwd, version)

// functions for legacy style project files
module LegacyFrameworkDiscovery =

let isLinux = RuntimeInformation.IsOSPlatform OSPlatform.Linux
let isMac = RuntimeInformation.IsOSPlatform OSPlatform.OSX
let isUnix = isLinux || isMac

let internal msbuildBinary =
if isLinux then
"/usr/bin/msbuild" |> FileInfo |> Some
elif isMac then
"/Library/Frameworks/Mono.framework/Versions/Current/Commands/msbuild" |> FileInfo |> Some
else
// taken from https://github.com/microsoft/vswhere
// vswhere.exe is guranteed to be at the following location. refer to https://github.com/Microsoft/vswhere/issues/162
let vsWhereDir =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft Visual Studio", "Installer")
|> DirectoryInfo

let vsWhereExe = Path.Combine(vsWhereDir.FullName, "vswhere.exe") |> FileInfo
// example: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe
let msbuildExe =
SdkDiscovery.execDotnet vsWhereDir vsWhereExe [ "-find"; "MSBuild\**\Bin\MSBuild.exe" ]
|> Seq.last
|> FileInfo
if msbuildExe.Exists then
msbuildExe |> Some
else
None

let internal msbuildLibPath(msbuildDir: DirectoryInfo) =
if isLinux then
"/usr/lib/mono/xbuild"
elif isMac then
"/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/xbuild"
else
// example: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild
Path.Combine(msbuildDir.FullName, "..", "..") |> Path.GetFullPath

[<RequireQualifiedAccess>]
module Init =
Expand Down Expand Up @@ -177,6 +216,21 @@ module Init =
resolveHandler <- resolveFromSdkRoot sdkRoot
AssemblyLoadContext.Default.add_Resolving resolveHandler

let internal setupForLegacyFramework (msbuildPathDir: DirectoryInfo) =
let msbuildLibPath = LegacyFrameworkDiscovery.msbuildLibPath msbuildPathDir

// gotta set some env variables so msbuild interop works
if LegacyFrameworkDiscovery.isUnix then
Environment.SetEnvironmentVariable("MSBuildBinPath", "/usr/lib/mono/msbuild/Current/bin")
Environment.SetEnvironmentVariable("FrameworkPathOverride", "/usr/lib/mono/4.5")
else
// VsInstallRoot is required for legacy project files
// example: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
let vsInstallRoot = Path.Combine(msbuildPathDir.FullName, "..", "..", "..") |> Path.GetFullPath
Environment.SetEnvironmentVariable("VsInstallRoot", vsInstallRoot)

Environment.SetEnvironmentVariable("MSBuildExtensionsPath32", ensureTrailer msbuildLibPath)

/// Initialize the MsBuild integration. Returns path to MsBuild tool that was detected by Locator. Needs to be called before doing anything else.
/// Call it again when the working directory changes.
let init (workingDirectory: DirectoryInfo) (dotnetExe: FileInfo option) =
Expand Down Expand Up @@ -240,9 +294,9 @@ module ProjectLoader =
member this.Verbosity
with set (v: LoggerVerbosity): unit = () }

let getTfm (path: string) readingProps =
let getTfm (path: string) readingProps isLegacyFrameworkProj =
let pi = ProjectInstance(path, globalProperties = readingProps, toolsVersion = null)
let tfm = pi.GetPropertyValue "TargetFramework"
let tfm = pi.GetPropertyValue (if isLegacyFrameworkProj then "TargetFrameworkVersion" else "TargetFramework")

if String.IsNullOrWhiteSpace tfm then
let tfms = pi.GetPropertyValue "TargetFrameworks"
Expand Down Expand Up @@ -308,18 +362,44 @@ module ProjectLoader =
/// </list>
/// </remarks>
/// </summary>
let designTimeBuildTargets =
[| "ResolveAssemblyReferencesDesignTime"
"ResolveProjectReferencesDesignTime"
"ResolvePackageDependenciesDesignTime"
"_GenerateCompileDependencyCache"
"_ComputeNonExistentFileProperty"
"CoreCompile" |]
let designTimeBuildTargets isLegacyFrameworkProjFile =
if isLegacyFrameworkProjFile then
[|
"_GenerateCompileDependencyCache"
"_ComputeNonExistentFileProperty"
"CoreCompile" |]
else
[|
"ResolveAssemblyReferencesDesignTime"
"ResolveProjectReferencesDesignTime"
"ResolvePackageDependenciesDesignTime"
"_GenerateCompileDependencyCache"
"_ComputeNonExistentFileProperty"
"CoreCompile" |]

let setLegacyMsbuildProperties isOldStyleProjFile =
match LegacyFrameworkDiscovery.msbuildBinary with
| Some file ->
let msbuildBinaryDir = file.Directory
Init.setupForLegacyFramework msbuildBinaryDir
| _ -> ()

let loadProject (path: string) (binaryLogs: BinaryLogGeneration) globalProperties =
try
let isLegacyFrameworkProjFile =
if path.ToLower().EndsWith ".fsproj" && File.Exists path then
let legacyProjFormatXmlns = "xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\""
let lines:seq<string> = File.ReadLines path
(Seq.tryFind (fun (line: string) -> line.Contains legacyProjFormatXmlns) lines).IsSome
else
false

let readingProps = getGlobalProps path None globalProperties
let tfm = getTfm path readingProps

if isLegacyFrameworkProjFile then
setLegacyMsbuildProperties isLegacyFrameworkProjFile

let tfm = getTfm path readingProps isLegacyFrameworkProjFile

let globalProperties = getGlobalProps path tfm globalProperties

Expand All @@ -333,8 +413,7 @@ module ProjectLoader =

let pi = pi.CreateProjectInstance()


let build = pi.Build(designTimeBuildTargets, loggers)
let build = pi.Build(designTimeBuildTargets isLegacyFrameworkProjFile, loggers)

let t = sw.ToString()

Expand Down Expand Up @@ -448,7 +527,10 @@ module ProjectLoader =
MSBuildToolsVersion = msbuildPropString "MSBuildToolsVersion" |> Option.defaultValue ""

ProjectAssetsFile = msbuildPropString "ProjectAssetsFile" |> Option.defaultValue ""
RestoreSuccess = msbuildPropBool "RestoreSuccess" |> Option.defaultValue false
RestoreSuccess =
match msbuildPropString "TargetFrameworkVersion" with
| Some _ -> true
| None -> msbuildPropBool "RestoreSuccess" |> Option.defaultValue false

Configurations = msbuildPropStringList "Configurations" |> Option.defaultValue []
TargetFrameworks = msbuildPropStringList "TargetFrameworks" |> Option.defaultValue []
Expand Down Expand Up @@ -629,7 +711,7 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri
None

let projectInstanceFactory projectPath (_globalProperties: IDictionary<string, string>) (projectCollection: ProjectCollection) =
let tfm = ProjectLoader.getTfm projectPath (dict globalProperties)
let tfm = ProjectLoader.getTfm projectPath (dict globalProperties) false
//let globalProperties = globalProperties |> Seq.toList |> List.map (fun (KeyValue(k,v)) -> (k,v))
let globalProperties = ProjectLoader.getGlobalProps projectPath tfm globalProperties
ProjectInstance(projectPath, globalProperties, toolsVersion = null, projectCollection = projectCollection)
Expand Down Expand Up @@ -676,7 +758,7 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri
)

let gbr =
GraphBuildRequestData(projects, ProjectLoader.designTimeBuildTargets, null, BuildRequestDataFlags.ReplaceExistingProjectInstance)
GraphBuildRequestData(projects, ProjectLoader.designTimeBuildTargets false, null, BuildRequestDataFlags.ReplaceExistingProjectInstance)

let bm = BuildManager.DefaultBuildManager
use sw = new StringWriter()
Expand Down
3 changes: 3 additions & 0 deletions test/Ionide.ProjInfo.Tests/Ionide.ProjInfo.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="16.10.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Ionide.ProjInfo\Ionide.ProjInfo.fsproj" />
<ProjectReference Include="..\..\src\Ionide.ProjInfo.FCS\Ionide.ProjInfo.FCS.fsproj" />
Expand Down
14 changes: 13 additions & 1 deletion test/Ionide.ProjInfo.Tests/TestAssets.fs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ let ``sample6 Netsdk Sparse/sln`` =
/// reference:
/// - net461 lib Project1A (F#)
/// - net461 lib Project1B (F#)
let ``sample7 Oldsdk projs`` =
let ``sample7 legacy framework multi-project`` =
{ ProjDir = "sample7-oldsdk-projs"
AssemblyName = "MultiProject1"
ProjectFile = "m1"/"MultiProject1.fsproj"
Expand All @@ -158,6 +158,18 @@ let ``sample7 Oldsdk projs`` =
ProjectReferences = [] }
] }

/// legacy framework net461 console project
/// reference:
/// - net461 lib Project1A (F#)
let ``sample7 legacy framework project`` =
{ ProjDir = "sample7-oldsdk-projs"
AssemblyName = "Project1A"
ProjectFile = "a"/"Project1A.fsproj"
TargetFrameworks = Map.ofList [
"net45", sourceFiles ["Project1A.fs"]
]
ProjectReferences = [] }

/// dotnet sdk, one netstandard2.0 lib n1 with advanced solution explorer configurations
let ``sample8 NetSdk Explorer`` =
{ ProjDir = "sample8-netsdk-explorer"
Expand Down
113 changes: 112 additions & 1 deletion test/Ionide.ProjInfo.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,115 @@ module ExpectNotification =
let watchNotifications logger loader =
NotificationWatcher(loader, logNotification logger)

let testLegacyFrameworkProject toolsPath workspaceLoader isRelease (workspaceFactory: ToolsPath * (string * string) list -> IWorkspaceLoader) =
testCase
|> withLog
(sprintf "can load legacy project - %s - isRelease is %b" workspaceLoader isRelease)
(fun logger fs ->

let testDir = inDir fs "a"
copyDirFromAssets fs ``sample7 legacy framework project``.ProjDir testDir

let projPath = testDir / (``sample7 legacy framework project``.ProjectFile)
let projDir = Path.GetDirectoryName projPath

let config =
if isRelease then
"Release"
else
"Debug"

let props = [ ("Configuration", config) ]
let loader = workspaceFactory (toolsPath, props)

let watcher = watchNotifications logger loader

let parsed = loader.LoadProjects [ projPath ] |> Seq.toList

[ loading projPath
loaded projPath ]
|> expectNotifications watcher.Notifications

let [ _; WorkspaceProjectState.Loaded (n1Loaded, _, _) ] = watcher.Notifications

let n1Parsed = parsed |> expectFind projPath "first is a lib"

let expectedSources =
[ projDir / "Project1A.fs" ]
|> List.map Path.GetFullPath

Expect.equal parsed.Length 1 "console and lib"
Expect.equal n1Parsed n1Loaded "notificaton and parsed should be the same"
Expect.equal n1Parsed.SourceFiles expectedSources "check sources"
)

let testLegacyFrameworkMultiProject toolsPath workspaceLoader isRelease (workspaceFactory: ToolsPath * (string * string) list -> IWorkspaceLoader) =
testCase
|> withLog
(sprintf "can load legacy project - %s - isRelease is %b" workspaceLoader isRelease)
(fun logger fs ->

let testDir = inDir fs "load_sample7"
copyDirFromAssets fs ``sample7 legacy framework multi-project``.ProjDir testDir

let projPath = testDir / (``sample7 legacy framework multi-project``.ProjectFile)
let projDir = Path.GetDirectoryName projPath

let [ (l1, l1Dir); (l2, l2Dir) ] =
``sample7 legacy framework multi-project``.ProjectReferences
|> List.map (fun p2p -> testDir / p2p.ProjectFile)
|> List.map Path.GetFullPath
|> List.map (fun path -> path, Path.GetDirectoryName(path))

let config =
if isRelease then
"Release"
else
"Debug"

let props = [ ("Configuration", config) ]
let loader = workspaceFactory (toolsPath, props)

let watcher = watchNotifications logger loader

let parsed = loader.LoadProjects [ projPath ] |> Seq.toList
[ loading projPath
loading l1
loaded l1
loading l2
loaded l2
loaded projPath
]
|> expectNotifications watcher.Notifications

let [ _; _; WorkspaceProjectState.Loaded (l1Loaded, _, _); _; WorkspaceProjectState.Loaded (l2Loaded, _, _); WorkspaceProjectState.Loaded (n1Loaded, _, _) ] =
watcher.Notifications

let n1Parsed = parsed |> expectFind projPath "first is a multi-project"
let n1ExpectedSources =
[ projDir / "MultiProject1.fs"]
|> List.map Path.GetFullPath

let l1Parsed = parsed |> expectFind l1 "the F# lib"
let l1ExpectedSources =
[ l1Dir / "Project1A.fs"]
|> List.map Path.GetFullPath

let l2Parsed = parsed |> expectFind l2 "the F# exe"
let l2ExpectedSources =
[ l2Dir / "Project1B.fs"]
|> List.map Path.GetFullPath

Expect.equal parsed.Length 3 "check whether all projects in the multi-project were loaded"
Expect.equal n1Parsed.SourceFiles n1ExpectedSources "check sources - N1"
Expect.equal l1Parsed.SourceFiles l1ExpectedSources "check sources - L1"
Expect.equal l2Parsed.SourceFiles l2ExpectedSources "check sources - L2"

Expect.equal l1Parsed l1Loaded "l1 notificaton and parsed should be the same"
Expect.equal l2Parsed l2Loaded "l2 notificaton and parsed should be the same"
Expect.equal n1Parsed n1Loaded "n1 notificaton and parsed should be the same"
)

let testSample2 toolsPath workspaceLoader isRelease (workspaceFactory: ToolsPath * (string * string) list -> IWorkspaceLoader) =
testCase
|> withLog (sprintf "can load sample2 - %s - isRelease is %b" workspaceLoader isRelease) (fun logger fs ->
Expand Down Expand Up @@ -1064,4 +1173,6 @@ let tests toolsPath =
SdkDiscovery.sdks (Paths.dotnetRoot.Value |> Option.defaultWith (fun _ -> failwith "unable to find dotnet binary"))

Expect.isNonEmpty sdks "should have found at least the currently-executing sdk"
} ]
}
testLegacyFrameworkProject toolsPath "can load legacy project file" false (fun (tools, props) -> WorkspaceLoader.Create(tools, globalProperties = props))
testLegacyFrameworkMultiProject toolsPath "can load legacy multi project file" false (fun (tools, props) -> WorkspaceLoader.Create(tools, globalProperties = props)) ]