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

TEST: Parse projects as .csproj #3265

Merged
merged 3 commits into from
Nov 12, 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
4 changes: 3 additions & 1 deletion src/Fable.Cli/Fable.Cli.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<AssemblyName>fable</AssemblyName>
<PackAsTool>true</PackAsTool>
<Description>F# to JS compiler</Description>
<OtherFlags>$(OtherFlags) --nowarn:3536</OtherFlags>
</PropertyGroup>
<ItemGroup Condition="'$(Pack)' == 'true'">
<Content Include="..\..\build\fable-library\**\*.*" PackagePath="fable-library\" />
Expand All @@ -36,6 +37,7 @@
<Compile Include="Main.fs" />
<Compile Include="Entry.fs" />
<Content Include="RELEASE_NOTES.md" />
<Content Include="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fable.Transforms\Fable.Transforms.fsproj" />
Expand All @@ -44,7 +46,7 @@
<Reference Include="../../lib/fcs/FSharp.Core.dll" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Buildalyzer" Version="4.1.5" />
<PackageReference Include="Buildalyzer" Version="4.1.6" />
<PackageReference Include="FSharp.SystemTextJson" Version="0.17.4" />
<PackageReference Include="source-map-sharp" Version="1.0.8" />
</ItemGroup>
Expand Down
22 changes: 12 additions & 10 deletions src/Fable.Cli/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -320,16 +320,8 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi
let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir
let watchDependencies = if cliArgs.IsWatch then Some(HashSet()) else None

let common = Path.getCommonBaseDir([currentFile; crackerResponse.FableLibDir])
let outputType =
// Everything within the Fable hidden directory will be compiled as Library. We do this since the files there will be
// compiled as part of the main project which might be a program (Exe) or library (Library).
if common.EndsWith(Naming.fableModules) then
Some "Library"
else
crackerResponse.OutputType

CompilerImpl(currentFile, project, opts, fableLibDir, ?watchDependencies=watchDependencies, ?outDir=cliArgs.OutDir, ?outType=outputType)
CompilerImpl(currentFile, project, opts, fableLibDir, crackerResponse.OutputType,
?outDir=cliArgs.OutDir, ?watchDependencies=watchDependencies)

member _.MapSourceFiles(f) =
ProjectCracked(cliArgs, crackerResponse, Array.map f sourceFiles)
Expand All @@ -343,9 +335,19 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi
// We display "parsed" because "cracked" may not be understood by users
Log.always $"Project and references ({result.ProjectOptions.SourceFiles.Length} source files) parsed in %i{ms}ms{Log.newLine}"
Log.verbose(lazy $"""F# PROJECT: %s{cliArgs.ProjectFileAsRelativePath}
FABLE LIBRARY: {result.FableLibDir}
OUTPUT TYPE: {result.OutputType}

%s{result.ProjectOptions.OtherOptions |> String.concat $"{Log.newLine} "}
%s{result.ProjectOptions.SourceFiles |> String.concat $"{Log.newLine} "}{Log.newLine}""")

// If targeting Python, make sure users are not compiling the project as library by mistake
// (imports won't work when running the code)
match cliArgs.CompilerOptions.Language, result.OutputType with
| Python, OutputType.Library ->
Log.always "Compiling project as Library. If you intend to run the code directly, please set OutputType to Exe."
| _ -> ()

let sourceFiles = result.ProjectOptions.SourceFiles |> Array.map File
ProjectCracked(cliArgs, result, sourceFiles)

Expand Down
3 changes: 2 additions & 1 deletion src/Fable.Cli/Pipeline.fs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ module Python =
| Some Py.Naming.sitePackages -> true
| _ -> false


// Everything within the Fable hidden directory will be compiled as Library. We do this since the files there will be
// compiled as part of the main project which might be a program (Exe) or library (Library).
let isLibrary = com.OutputType = OutputType.Library || Naming.isInFableModules com.CurrentFile
let isFableLibrary = isLibrary && List.contains "FABLE_LIBRARY" com.Options.Define

Expand Down
124 changes: 81 additions & 43 deletions src/Fable.Cli/ProjectCracker.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type CacheInfo =
OutDir: string option
FableLibDir: string
FableModulesDir: string
OutputType: string option
OutputType: OutputType
Exclude: string list
SourceMaps: bool
SourceMapsRoot: string option
Expand Down Expand Up @@ -59,8 +59,13 @@ type CacheInfo =
| Some other -> this.GetTimestamp() > other.GetTimestamp()

type CrackerOptions(cliArgs: CliArgs) =
let projDir = IO.Path.GetDirectoryName cliArgs.ProjectFile
let targetFramework =
match Process.runSyncWithOutput projDir "dotnet" ["--version"] with
| Naming.StartsWith "7" _ -> "net7.0"
| _ -> "net6.0"
let fableModulesDir = CrackerOptions.GetFableModulesFromProject(projDir, cliArgs.OutDir, cliArgs.NoCache)
let builtDlls = HashSet()
let fableModulesDir = CrackerOptions.GetFableModulesFromProject(cliArgs.ProjectFile, cliArgs.OutDir, cliArgs.NoCache)
let cacheInfo =
if cliArgs.NoCache then None
else CacheInfo.TryRead(fableModulesDir, cliArgs.CompilerOptions.DebugMode)
Expand All @@ -72,6 +77,7 @@ type CrackerOptions(cliArgs: CliArgs) =
member _.FableLib: string option = cliArgs.FableLibraryPath
member _.OutDir: string option = cliArgs.OutDir
member _.Configuration: string = cliArgs.Configuration
member _.TargetFramework = targetFramework
member _.Exclude: string list = cliArgs.Exclude
member _.Replace: Map<string, string> = cliArgs.Replace
member _.PrecompiledLib: string option = cliArgs.PrecompiledLib
Expand All @@ -96,10 +102,10 @@ type CrackerOptions(cliArgs: CliArgs) =
IO.Path.Combine(baseDir, Naming.fableModules)
|> Path.normalizePath

static member GetFableModulesFromProject(projFile: string, outDir: string option, noCache: bool): string =
static member GetFableModulesFromProject(projDir: string, outDir: string option, noCache: bool): string =
let fableModulesDir =
outDir
|> Option.defaultWith (fun () -> IO.Path.GetDirectoryName(projFile))
|> Option.defaultWith (fun () -> projDir)
|> CrackerOptions.GetFableModulesFromDir

if noCache then
Expand All @@ -118,7 +124,7 @@ type CrackerResponse =
FableModulesDir: string
References: string list
ProjectOptions: FSharpProjectOptions
OutputType: string option
OutputType: OutputType
PrecompiledInfo: PrecompiledInfoImpl option
CanReuseCompiledFiles: bool }

Expand Down Expand Up @@ -311,14 +317,21 @@ let getSourcesFromFablePkg (projFile: string) =
| path -> [ path ]
|> List.map Path.normalizeFullPath)

let private isUsefulOption (opt : string) =
[ "--define"
"--nowarn"
"--warnon"
let private extractUsefulOptionsAndSources (line: string) (accSources: string list, accOptions: string list) =
if line.StartsWith("-") then
// "--warnaserror" // Disable for now to prevent unexpected errors, see #2288
// "--langversion" // See getBasicCompilerArgs
]
|> List.exists opt.StartsWith
if line.StartsWith("--nowarn") || line.StartsWith("--warnon") then
accSources, line::accOptions
elif line.StartsWith("--define:") then
// When parsing the project as .csproj there will be multiple defines in the same line,
// but the F# compiler seems to accept only one per line
let defines = line.Substring(9).Split(';') |> Array.mapToList (fun d -> "--define:" + d)
accSources, defines @ accOptions
else
accSources, accOptions
else
(Path.normalizeFullPath line)::accSources, accOptions

let excludeProjRef (opts: CrackerOptions) (dllRefs: IDictionary<string,string>) (projRef: string) =
let projName = Path.GetFileNameWithoutExtension(projRef)
Expand Down Expand Up @@ -351,12 +364,8 @@ let getCrackedFsproj (opts: CrackerOptions) (projOpts: string[]) (projRefs: stri
let dllName = getDllName line
dllRefs.Add(dllName, line)
src, otherOpts
elif isUsefulOption line then
src, line::otherOpts
elif line.StartsWith("-") then
src, otherOpts
else
(Path.normalizeFullPath line)::src, otherOpts)
extractUsefulOptionsAndSources line (src, otherOpts))

let fablePkgs =
let dllRefs' = dllRefs |> Seq.map (fun (KeyValue(k,v)) -> k,v) |> Seq.toArray
Expand Down Expand Up @@ -397,7 +406,7 @@ let getProjectOptionsFromProjectFile =
if Path.IsPathRooted f then f else Path.Combine(projDir, f)
else
f
fun (opts: CrackerOptions) projFile ->
fun (opts: CrackerOptions) (projFile: string) ->
let manager =
match manager with
| Some m -> m
Expand All @@ -406,25 +415,65 @@ let getProjectOptionsFromProjectFile =
let options = AnalyzerManagerOptions(LogWriter = log)
let m = AnalyzerManager(options)
m.SetGlobalProperty("Configuration", opts.Configuration)
m.SetGlobalProperty("TargetFramework", opts.TargetFramework)
for define in opts.FableOptions.Define do
m.SetGlobalProperty(define, "true")
manager <- Some m
m

let analyzer = manager.GetProject(projFile)
// If the project targets multiple frameworks, multiple results will be returned
// For now we just take the first one with non-empty command
let result =
analyzer.Build()
let tryGetResult (getCompilerArgs: IAnalyzerResult -> string[]) (projFile: string) =
let analyzer = manager.GetProject(projFile)
let env = analyzer.EnvironmentFactory.GetBuildEnvironment(Environment.EnvironmentOptions(DesignTime=true,Restore=false))
// If the project targets multiple frameworks, multiple results will be returned
// For now we just take the first one with non-empty command
let results = analyzer.Build(env)
results
|> Seq.tryFind (fun r -> String.IsNullOrEmpty(r.Command) |> not)
|> Option.map (fun result ->
{| CompilerArguments = getCompilerArgs result
ProjectReferences = result.ProjectReferences
Properties = result.Properties |})

// Because Buildalyzer works better with .csproj, we first "dress up" the project as if it were a C# one
// and try to adapt the results. If it doesn't work, we try again to analyze the .fsproj directly
let csprojResult =
let csprojFile = projFile.Replace(".fsproj", ".csproj")
if IO.File.Exists(csprojFile) then
None
else
try
System.IO.File.Copy(projFile, csprojFile)
csprojFile
|> tryGetResult (fun r ->
// Careful, options for .csproj start with / but so do root paths in unix
let reg = System.Text.RegularExpressions.Regex(@"^\/[^\/]+?(:?:|$)")
let comArgs =
r.CompilerArguments
|> Array.map (fun line ->
if reg.IsMatch(line) then
if line.StartsWith("/reference") then "-r" + line.Substring(10)
else "--" + line.Substring(1)
else line)
match r.Properties.TryGetValue("OtherFlags") with
| false, _ -> comArgs
| true, otherFlags ->
let otherFlags = otherFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)
Array.append otherFlags comArgs)
finally
File.safeDelete csprojFile

let result =
csprojResult
|> Option.orElseWith (fun () -> projFile |> tryGetResult (fun r ->
// result.CompilerArguments doesn't seem to work well in Linux
System.Text.RegularExpressions.Regex.Split(r.Command, @"\r?\n")))
|> function
| Some result -> result
// TODO: Get Buildalyzer errors from the log
| None -> $"Cannot parse {projFile}" |> Fable.FableError |> raise
let projDir = IO.Path.GetDirectoryName(projFile)
let projOpts =
// result.CompilerArguments doesn't seem to work well in Linux
System.Text.RegularExpressions.Regex.Split(result.Command, @"\r?\n")
result.CompilerArguments
|> Array.skipWhile (fun line -> not(line.StartsWith("-")))
|> Array.map (compileFilesToAbsolutePath projDir)
projOpts, Seq.toArray result.ProjectReferences, result.Properties
Expand All @@ -437,37 +486,24 @@ let fullCrack (opts: CrackerOptions): CrackedFsproj =
Process.runSync (IO.Path.GetDirectoryName opts.ProjFile) "dotnet" [
"restore"
IO.Path.GetFileName opts.ProjFile
$"-p:TargetFramework={opts.TargetFramework}"
for constant in opts.FableOptions.Define do
$"-p:{constant}=true"
] |> ignore

let projOpts, projRefs, msbuildProps =
getProjectOptionsFromProjectFile opts opts.ProjFile

// let targetFramework =
// match Map.tryFind "TargetFramework" msbuildProps with
// | Some targetFramework -> targetFramework
// | None -> failwithf "Cannot find TargetFramework for project %s" projFile

let outputType = ReadOnlyDictionary.tryFind "OutputType" msbuildProps

getCrackedFsproj opts projOpts projRefs outputType
ReadOnlyDictionary.tryFind "OutputType" msbuildProps
|> getCrackedFsproj opts projOpts projRefs

/// For project references of main project, ignore dll and package references
let easyCrack (opts: CrackerOptions) dllRefs (projFile: string): CrackedFsproj =
let projOpts, projRefs, msbuildProps =
getProjectOptionsFromProjectFile opts projFile

let outputType = ReadOnlyDictionary.tryFind "OutputType" msbuildProps
let sourceFiles, otherOpts =
(projOpts, ([], []))
||> Array.foldBack (fun line (src, otherOpts) ->
if isUsefulOption line then
src, line::otherOpts
elif line.StartsWith("-") then
src, otherOpts
else
(Path.normalizeFullPath line)::src, otherOpts)
let sourceFiles, otherOpts = Array.foldBack extractUsefulOptionsAndSources projOpts ([], [])

{ ProjectFile = projFile
SourceFiles = sourceFiles
Expand Down Expand Up @@ -563,7 +599,6 @@ let getFableLibraryPath (opts: CrackerOptions) =
|> File.tryFindNonEmptyDirectoryUpwards {| matches = [buildDir; "build/" + buildDir]; exclude = ["src"] |}
|> Option.defaultWith (fun () -> Fable.FableError "Cannot find fable-library" |> raise)

Log.verbose(lazy ("fable-library: " + fableLibrarySource))
let fableLibraryTarget = IO.Path.Combine(opts.FableModulesDir, libDir)
// Always overwrite fable-library in case it has been updated, see #3208
copyDir false fableLibrarySource fableLibraryTarget
Expand Down Expand Up @@ -785,9 +820,12 @@ let getFullProjectOpts (opts: CrackerOptions) =
else Some("-r:" + r))
|> Seq.toArray

let outputType = mainProj.OutputType
let projRefs = projRefs |> List.map (fun p -> p.ProjectFile)
let otherOptions = Array.append otherOptions dllRefs
let outputType =
match mainProj.OutputType with
| Some "Library" -> OutputType.Library
| _ -> OutputType.Exe

let cacheInfo: CacheInfo =
{
Expand Down
17 changes: 14 additions & 3 deletions src/Fable.Cli/Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ module File =
let isDirectoryEmpty dir =
not(Directory.Exists(dir)) || Directory.EnumerateFileSystemEntries(dir) |> Seq.isEmpty

let safeDelete path =
try
File.Delete(path)
with _ -> ()

let withLock (dir: string) (action: unit -> 'T) =
let mutable fileCreated = false
let lockFile = Path.Join(dir, "fable.lock")
Expand Down Expand Up @@ -329,7 +334,7 @@ module Process =
IO.Path.GetFullPath(dir) + (if isWindows() then ";" else ":") + currentPath

// Adapted from https://github.com/enricosada/dotnet-proj-info/blob/1e6d0521f7f333df7eff3148465f7df6191e0201/src/dotnet-proj/Program.fs#L155
let private startProcess (envVars: (string * string) list) workingDir exePath (args: string list) =
let private startProcess redirectOutput (envVars: (string * string) list) workingDir exePath (args: string list) =
let exePath, args =
if isWindows() then "cmd", "/C"::exePath::args
else exePath, args
Expand All @@ -346,6 +351,7 @@ module Process =
psi.WorkingDirectory <- workingDir
psi.CreateNoWindow <- false
psi.UseShellExecute <- false
psi.RedirectStandardOutput <- redirectOutput

// TODO: Make this output no logs if we've set silent verbosity
Process.Start(psi)
Expand All @@ -372,7 +378,7 @@ module Process =
fun (workingDir: string) (exePath: string) (args: string list) ->
try
runningProcess |> Option.iter kill
let p = startProcess envVars workingDir exePath args
let p = startProcess false envVars workingDir exePath args
runningProcess <- Some p
with ex ->
Log.always("Cannot run: " + ex.Message)
Expand All @@ -382,7 +388,7 @@ module Process =

let runSyncWithEnv envVars (workingDir: string) (exePath: string) (args: string list) =
try
let p = startProcess envVars workingDir exePath args
let p = startProcess false envVars workingDir exePath args
p.WaitForExit()
p.ExitCode
with ex ->
Expand All @@ -393,6 +399,11 @@ module Process =
let runSync (workingDir: string) (exePath: string) (args: string list) =
runSyncWithEnv [] workingDir exePath args

let runSyncWithOutput workingDir exePath args =
let p = startProcess true [] workingDir exePath args
p.WaitForExit()
p.StandardOutput.ReadToEnd()

[<RequireQualifiedAccess>]
module Async =
let fold f (state: 'State) (xs: 'T seq) = async {
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Transforms/Fable.Transforms.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
<TargetFramework>netstandard2.0</TargetFramework>
<OtherFlags>$(OtherFlags) --nowarn:3536</OtherFlags>
</PropertyGroup>
<ItemGroup>
<Compile Include="Global/Babel.fs" />
Expand Down
2 changes: 0 additions & 2 deletions src/Fable.Transforms/Global/Compiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ type Severity =
type OutputType =
| Library
| Exe
| Module
| Winexe

open FSharp.Compiler.Symbols
open Fable.AST
Expand Down
Loading