Skip to content
Closed
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
7 changes: 7 additions & 0 deletions Fable.sln
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Library.TypeScript",
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Quicktest.Rust", "src\quicktest-rust\Quicktest.Rust.fsproj", "{0818518D-5BAF-498E-B2E8-C3FA5EC3758D}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Spectre.Cli", "src\Fable.Spectre.Cli\Fable.Spectre.Cli.fsproj", "{4942CD4A-6006-4DB9-A944-5A9236E73313}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -186,6 +188,10 @@ Global
{0818518D-5BAF-498E-B2E8-C3FA5EC3758D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0818518D-5BAF-498E-B2E8-C3FA5EC3758D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0818518D-5BAF-498E-B2E8-C3FA5EC3758D}.Release|Any CPU.Build.0 = Release|Any CPU
{4942CD4A-6006-4DB9-A944-5A9236E73313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4942CD4A-6006-4DB9-A944-5A9236E73313}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4942CD4A-6006-4DB9-A944-5A9236E73313}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4942CD4A-6006-4DB9-A944-5A9236E73313}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -220,6 +226,7 @@ Global
{A68CF5C2-DAB1-451F-AA36-3C171C61D4F2} = {493A3959-758E-4D88-8E82-16680E70D282}
{C0AD4BF9-638A-43D0-AD46-AAC72C508B42} = {C8CB96CF-68A8-4083-A0F8-319275CF8097}
{0818518D-5BAF-498E-B2E8-C3FA5EC3758D} = {C8CB96CF-68A8-4083-A0F8-319275CF8097}
{4942CD4A-6006-4DB9-A944-5A9236E73313} = {C8CB96CF-68A8-4083-A0F8-319275CF8097}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {58DF9285-8523-4EAC-B598-BE5B02A76A00}
Expand Down
163 changes: 163 additions & 0 deletions src/Fable.Spectre.Cli/BuildalyzerCrackerResolver.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
namespace Fable.Cli

open System
open System.Xml.Linq
open System.Text.RegularExpressions
open Fable
open Fable.AST
open Fable.Compiler.Util
open Fable.Compiler.ProjectCracker
open Buildalyzer

/// Use Buildalyzer to invoke MSBuild and get F# compiler args from an .fsproj file.
/// As we'll merge this later with other projects we'll only take the sources and
/// the references, checking if some .dlls correspond to Fable libraries
type BuildalyzerCrackerResolver() =
let mutable manager = None

let tryGetResult (isMain: bool) (opts: CrackerOptions) (manager: AnalyzerManager) (maybeCsprojFile: string) =
if isMain && not opts.NoRestore then
Process.runSync
(IO.Path.GetDirectoryName opts.ProjFile)
"dotnet"
[
"restore"
IO.Path.GetFileName maybeCsprojFile
// $"-p:TargetFramework={opts.TargetFramework}"
for constant in opts.FableOptions.Define do
$"-p:{constant}=true"
]
|> ignore

let analyzer = manager.GetProject(maybeCsprojFile)

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)

interface ProjectCrackerResolver with
member x.GetProjectOptionsFromProjectFile(isMain, options: CrackerOptions, projectFile) =
let manager =
match manager with
| Some m -> m
| None ->
let log = new System.IO.StringWriter()
let amo = AnalyzerManagerOptions(LogWriter = log)
let m = AnalyzerManager(amo)
m.SetGlobalProperty("Configuration", options.Configuration)
// m.SetGlobalProperty("TargetFramework", opts.TargetFramework)
for define in options.FableOptions.Define do
m.SetGlobalProperty(define, "true")

manager <- Some m
m

// Because BuildAnalyzer 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 = projectFile.Replace(".fsproj", ".fable-temp.csproj")

if IO.File.Exists(csprojFile) then
None
else
try
IO.File.Copy(projectFile, csprojFile)

let xmlDocument = XDocument.Load(csprojFile)

let xmlComment =
XComment(
"""This is a temporary file used by Fable to restore dependencies.
If you see this file in your project, you can delete it safely"""
)

// An fsproj/csproj should always have a root element
// so it should be safe to add the comment as first child
// of the root element
xmlDocument.Root.AddFirst(xmlComment)
xmlDocument.Save(csprojFile)

tryGetResult isMain options manager csprojFile
|> Option.map (fun (r: IAnalyzerResult) ->
// Careful, options for .csproj start with / but so do root paths in unix
let reg = Regex(@"^\/[^\/]+?(:?:|$)")

let comArgs =
r.CompilerArguments
|> Array.map (fun line ->
if reg.IsMatch(line) then
if line.StartsWith("/reference", StringComparison.Ordinal) then
"-r" + line.Substring(10)
else
"--" + line.Substring(1)
else
line
)

let comArgs =
match r.Properties.TryGetValue("OtherFlags") with
| false, _ -> comArgs
| true, otherFlags ->
let otherFlags = otherFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)

Array.append otherFlags comArgs

comArgs, r
)
finally
File.safeDelete csprojFile
// Restore the original fsproj because when restoring/analyzing the
// csproj implicit references added by F# SDKs are not included
// See https://github.com/fable-compiler/Fable/issues/3719
// Restoring the F# project should restore the bin/obj folders
// to their expected state
let projDir = IO.Path.GetDirectoryName projectFile

Process.runSync projDir "dotnet" [ "restore"; projectFile ] |> ignore

let compilerArgs, result =
csprojResult
|> Option.orElseWith (fun () ->
tryGetResult isMain options manager projectFile
|> Option.map (fun r ->
// result.CompilerArguments doesn't seem to work well in Linux
let comArgs = Regex.Split(r.Command, @"\r?\n")
comArgs, r
)
)
|> function
| Some result -> result
// TODO: Get Buildalyzer errors from the log
| None -> $"Cannot parse {projectFile}" |> Fable.FableError |> raise

let projDir = IO.Path.GetDirectoryName(projectFile)

let projOpts =
compilerArgs
|> Array.skipWhile (fun line -> not (line.StartsWith('-')))
|> Array.map (fun f ->
if
f.EndsWith(".fs", StringComparison.Ordinal)
|| f.EndsWith(".fsi", StringComparison.Ordinal)
then
if Path.IsPathRooted f then
f
else
Path.Combine(projDir, f)
else
f
)

let outputType = ReadOnlyDictionary.tryFind "OutputType" result.Properties

{
ProjectOptions = projOpts
ProjectReferences = Seq.toArray result.ProjectReferences
OutputType = outputType
TargetFramework = Some result.TargetFramework
}
96 changes: 96 additions & 0 deletions src/Fable.Spectre.Cli/Commands/Clean.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module Fable.Spectre.Cli.Commands.Clean

open System
open Fable
open Fable.Spectre.Cli.Settings.Clean
open Spectre.Console
open Spectre.Console.Cli
open SpectreCoff

type CleanCommand() =
inherit Command<CleanSettings>()

override this.Execute(_, settings) =
let logAlways content = toConsole content

let logVerbose (content: Lazy<OutputPayload>) =
if settings.verbosity.IsVerbose then
logAlways content.Value

let ignoreDirs = set [ "bin"; "obj"; "node_modules" ]

let outDir =
if settings.cwd |> String.IsNullOrWhiteSpace then
None
else
Some settings.cwd

let fileExt = settings.extension

let cleanDir = outDir |> Option.defaultValue settings.cwd |> IO.Path.GetFullPath

// clean is a potentially destructive operation, we need a permission before proceeding
let payload = [ V "all"; E $"*{fileExt}[.map]"; V "files in"; E cleanDir ]

if not settings.yes then
Many
[
V "This will recursively"
MarkupCD(Color.DarkOrange, [ Decoration.Bold ], "delete")
yield! payload
]
|> toConsole

if confirm "Continue?" |> not then
V "Clean was cancelled." |> toConsole
exit 0
else
V "Deleting" :: payload |> Many |> toConsole

let mutable fileCount = 0
let mutable fableModulesDeleted = false

let asyncProcess (_context: StatusContext) =
async {
let rec recClean dir =
seq {
yield! IO.Directory.GetFiles(dir, "*" + fileExt)
yield! IO.Directory.GetFiles(dir, "*" + fileExt + ".map")
}
|> Seq.iter (fun file ->
async {
IO.File.Delete(file)
fileCount <- fileCount + 1
logVerbose (lazy ("Deleted " + file |> V))
}
|> Async.RunSynchronously
)

IO.Directory.GetDirectories(dir)
|> Array.filter (fun subdir -> ignoreDirs.Contains(IO.Path.GetFileName(subdir)) |> not)
|> Array.iter (fun subdir ->
if IO.Path.GetFileName(subdir) = Naming.fableModules then
IO.Directory.Delete(subdir, true)
fableModulesDeleted <- true

V $"Deleted {IO.Path.GetRelativePath(settings.cwd, subdir)}" |> logAlways
else
recClean subdir
)

recClean cleanDir
}

Status.start "Cleaning directories" asyncProcess |> Async.RunSynchronously


if fileCount = 0 && not fableModulesDeleted then
V
":orange_circle: No files have been deleted. If Fable output is in another directory, pass it as an argument."
|> logAlways
else
":check_mark: Clean completed! Files deleted: " + string<int> fileCount
|> V
|> logAlways

0
Loading
Loading