Thursday, January 5, 2017

Run build tasks and targets via F# in VsCode

For developing the client side of a web app, I've switched from the ultra heavy Visual Studio 2015 to Visual Studio Code. It's written in JavaScript and runs pretty smoothly. It has hooks for kicking off a build, and tests.

I hooked up the build kick off to FAKE (an F# build engine) tasks (Via an .ionide file at the project root).

so I just hit Ctrl+Shift+P -> Fake -> and pick the build.fsx target I want to run from the menu (via keyboard, mouse if desired)

Things I have hooked up to be kicked off from Fake


  • Setup/install node (Via installing Chocolatey first)
    • install npm packages
  • Transpile Coffeescripts
  • Transpile Babel (React preset)
  • Transpile Sass
  • List packages installed to this project
  • Kill any running instances of the app
  • Clean the app
  • MsBuild the app
  • Kick off Mocha tests



[Fake]
linuxPrefix = "mono"
command = "build.cmd"
build = "build.fsx"
view raw .ionide hosted with ❤ by GitHub
// include Fake lib
#r @"../packages/FAKE/tools/FakeLib.dll"
open System
open System.Diagnostics
open System.IO
open Fake
// Properties
let buildDir = "./bin/"
let targetScript =
let linqPadQueriesFolder = @"C:\projects\LinqPad\LinqPad\LINQPad Queries\"
Path.Combine(linqPadQueriesFolder,@"WIP\runPmWeb.linq")
let lprunPath = @"C:\ProgramData\LINQPad\Updates50\510\lprun.exe"
let configLpRun (pi: ProcessStartInfo) =
trace (sprintf "Target script:%s" targetScript)
pi.FileName <- lprunPath
pi.Arguments <- sprintf "\"%s\"" targetScript
// Helpers
let flip f y x = f x y
let warn msg = trace (sprintf "WARNING: %s" msg)
type System.String with
static member Delimit delimiter (items:string seq) =
String.Join(delimiter,items |> Array.ofSeq)
module Sec =
open System.Security.Principal
let getIsAdmin() =
WindowsIdentity.GetCurrent()
|> WindowsPrincipal
|> fun wp -> wp.IsInRole(WindowsBuiltInRole.Administrator)
let requireAdmin () =
let runningAsAdmin = getIsAdmin()
if not runningAsAdmin then
failwithf "Requested feature is not known to work without administrator permissions"
module Proc =
//let execCmd prog args timeout =
let findCmd cmd =
let processResult =
ExecProcessAndReturnMessages (fun psi ->
psi.FileName <- "where"
psi.Arguments <- quoteIfNeeded cmd
) (TimeSpan.FromSeconds 2.)
if processResult.OK then
// require the result not be a directory
let cmdPath =
processResult.Messages
|> Seq.filter (Directory.Exists >> not)
|> Seq.filter (File.Exists)
|> Seq.filter (fun x -> x.EndsWith ".bat" || x.EndsWith ".exe" || x.EndsWith ".cmd")
|> Seq.tryHead
if processResult.Messages.Count > 1 then
warn (sprintf "found multiple items matching '%s'" cmd)
trace (processResult.Messages |> String.Delimit ";")
match cmdPath with
| Some path ->
trace (sprintf "found %s at %s" cmd path)
Some path
| None ->
warn "where didn't return a valid file"
None
else None
let runWithOutput cmd args timeOut =
let cmd =
// consider: what if the cmd is in the current dir? where may find one elsewhere first?
if Path.IsPathRooted cmd then
cmd
else
match findCmd cmd with
| Some x -> x
| None ->
warn (sprintf "findCmd didn't find %s" cmd)
cmd
let result =
ExecProcessAndReturnMessages (fun f ->
//ExecProcessRedirected (fun f ->
//f.FileName <- @"gulp"
//f. Arguments <- "sass"
// why did 'where' with no full path work, but this fails?
f.FileName <- cmd
f.Arguments <- args
) (TimeSpan.FromMinutes 1.0)
result,cmd
let showInExplorer path =
Process.Start("explorer.exe",sprintf "/select, \"%s\"" path)
// wrapper for fake built-in in case we want the entire process results, not just the exitcode
let runElevated cmd args timeOut =
let tempFilePath = System.IO.Path.GetTempFileName()
// could also redirect error stream with 2> tempErrorFilePath
// see also http://www.robvanderwoude.com/battech_redirection.php
let resultCode = ExecProcessElevated "cmd" (sprintf "/c %s %s > %s" cmd args tempFilePath) timeOut
trace "reading output results of runElevated"
let outputResults = File.ReadAllLines tempFilePath
File.Delete tempFilePath
let processResult = ProcessResult.New resultCode (ResizeArray<_> outputResults) (ResizeArray<_>())
(String.Delimit "\r\n" outputResults)
|> trace
processResult
type FindOrInstallResult =
|Found
|InstalledThenFound
let findOrInstall cmd fInstall =
match findCmd cmd with
| Some x -> Some (x,Found)
| None ->
fInstall()
findCmd cmd
|> Option.map (fun x -> (x,InstalledThenFound))
module Node =
let npmPath = lazy(Proc.findCmd "npm")
// assumes the output is unimportant, just the result code
let npmInstall args =
let resultCode =
let filename, useShell =
match npmPath.Value with
| Some x -> x, false
// can't capture output with true
| None -> "npm", true
trace (sprintf "npm filename is %s" filename)
ExecProcess (fun psi ->
psi.FileName <- filename
psi.Arguments <- "install"
psi.UseShellExecute <- useShell
) (TimeSpan.FromMinutes 1.)
resultCode
// Targets
Target "SetupNode" (fun _ ->
// goal: install and setup everything required for any node dependencies this project has
// including nodejs, gulp, node-sass
// install Choco
let chocoPath =
let fInstall () =
let resultCode =
ExecProcessElevated
"@powershell"
"""-NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" """
(TimeSpan.FromMinutes 3.)
resultCode
|> sprintf "choco install script returned %i"
|> trace
if resultCode <> 0 then
failwithf "Task failed"
// choco is installled, we think
// probably won't work if it was just installed, the %path% variable given to/used by a process is immutable
match Proc.findOrInstall "choco" fInstall with
//| Some (x,Proc.FindOrInstallResult.Found) -> x
| Some (x,_) -> x
| None -> failwithf "choco was installed, in order for choco to be found or used, this has process has to be restarted"
// choco install nodeJs
let nodePath =
let fInstall () =
let results = Proc.runElevated "choco" "install nodejs -y" (TimeSpan.FromSeconds 3.)
trace (sprintf "%A" results)
match Proc.findOrInstall "node" fInstall with
| Some (x,_) -> x
| None -> failwithf "nodejs was installed, in order for node to be found or used, this process has to be restarted"
// node should have installed npm
// npm
let npmPath = Proc.findCmd "npm"
// install all packages that packages.json says this project needs
let resultCode =
let filename, useShell =
match npmPath with
| Some x -> x, false
// can't capture output with true
| None -> "npm", true
trace (sprintf "npm filename is %s" filename)
ExecProcess (fun psi ->
psi.FileName <- filename
psi.Arguments <- "install"
psi.UseShellExecute <- useShell
) (TimeSpan.FromMinutes 1.)
()
)
// run node tests and whatever else
Target "Test" (fun _ ->
let result, _ = Proc.runWithOutput "npm" "test" (TimeSpan.FromSeconds 4.)
result.Messages
|> Seq.iter (printfn "test-msg:%s")
if result.ExitCode <> 0 then
result.Errors
|> Seq.iter(printfn "test-err:%s")
failwithf "Task failed: %i" result.ExitCode
)
Target "Coffee" (fun _ ->
let coffees = [
"test/test.coffee"
]
let compileCoffee relPath =
let result,_ = Proc.runWithOutput "node" (sprintf "node_modules/coffee-script/bin/coffee -b -m --no-header -c %s" relPath) (TimeSpan.FromSeconds 2.)
if result.ExitCode <> 0 then
failwithf "Task failed: %A" result
coffees
|> Seq.iter compileCoffee
)
Target "Babel" (fun _ ->
// run jsx compilation
let babels = [
"Scripts/pm-era.jsx"
"Scripts/pm-era-remitdetail.jsx"
]
let babel relPath =
let targetPath =
let fullPath = Path.GetFullPath relPath
Path.Combine(fullPath |> Path.GetDirectoryName, fullPath |> Path.GetFileNameWithoutExtension |> flip (+) ".react.js")
let result,_ = Proc.runWithOutput "node" (sprintf "node_modules/babel-cli/bin/babel %s -o %s -s --presets react" relPath targetPath) (TimeSpan.FromSeconds 2.)
if result.ExitCode <> 0 then
result.Messages
|> Seq.iter (printfn "babel-msg:%s")
result.Errors
|> Seq.iter(printfn "babel-err:%s")
failwithf "Task failed: %i" result.ExitCode
else
result.Messages
|> Seq.iter (printfn "babel-msg:%s")
babels
|> Seq.iter babel
)
//node node_modules\coffee-script\bin\coffee -b -c test/test.coffee
Target "Clean" (fun _ ->
CleanDir buildDir
let files = Directory.GetFiles buildDir |> Seq.length
let directories = Directory.GetDirectories buildDir |> Seq.length
printfn "cleaned directory had %i item(s) remaining after clean" (files + directories)
)
Target "BuildApp" (fun _ ->
let output =
if isNullOrEmpty buildDir then ""
else
buildDir
|> FullName
|> trimSeparator
let setParams defaults =
{ defaults with
MSBuildParams.Targets= ["Build"]
//Verbosity = Some(MSBuildVerbosity.Diagnostic)
Properties =
[
"Configuration", "Debug"
"Platform", "AnyCPU"
"DebugSymbols", "True"
"OutputPath", buildDir
"SolutionDir", ".." |> FullName
]
}
//https://github.com/fsharp/FAKE/blob/master/src/app/FakeLib/MSBuildHelper.fs
build setParams "Pm.Web.fsproj"
if isNotNullOrEmpty output then !!(buildDir @@ "/**/*.*") |> Seq.toList
else []
|> Log "AppBuild-Output: "
)
Target "StartApp" (fun _ ->
let proc = new Process()
configLpRun proc.StartInfo
//proc.StartInfo.UseShellExecute <- false
// not using ProcessHelper.Start we don't want fake killing it
proc.Start() |> ignore
trace (sprintf "started app with pid:%i" proc.Id)
)
Target "Fire" (fun _ ->
ProcessHelper.fireAndForget configLpRun
)
Target "Run" (fun _ ->
//C:\projects\LinqPad\LinqPad\LINQPad Queries\WIP\runPmWeb.linq
//https://github.com/fsharp/FAKE/blob/master/src/app/FakeLib/ProcessHelper.fs
asyncShellExec {
Program = lprunPath
WorkingDirectory = null
CommandLine = sprintf "\"%s\"" targetScript
Args = list.Empty
}
|> Async.RunSynchronously
//Shell.Exec (,@"C:\projects\LinqPad\LinqPad\LINQPad Queries\",@"C:\projects\LinqPad\LinqPad\LINQPad Queries\WIP\")
|> sprintf "script returned %i"
|> trace
)
Target "Stop" (fun _ ->
killProcess "lprun"
)
Target "NodeTasks" (fun _ ->
trace "Node Tasks completed"
)
Target "Sass" (fun _ ->
let result,_ = Proc.runWithOutput "node-sass" "content/site.scss content/site.css" (TimeSpan.FromSeconds 4.)
trace "finished attempting sass"
printfn "sass:%A" result
//result.Messages
//|> Seq.iter (printfn "sass:%A")
if result.ExitCode <> 0 then
failwithf "Task failed"
)
Target "NodeList" (fun _ ->
let result = Proc.runWithOutput "npm" "list --depth=0" (TimeSpan.FromSeconds 2.0)
printfn "npm list -g --depth=0:\r\n%A" result
)
Target "Default" (fun _ ->
trace "Hello World from FAKE"
)
Target "AfterAll" (fun _ ->
trace (sprintf "%A" DateTime.Now)
)
// Dependencies
// this should be NodeTasks depends on Sass, Coffee, and jsx compilation
// ==> "Coffee"
"Sass"
==> "NodeTasks"
"Coffee"
==> "NodeTasks"
"Stop" ==> "Run"
For "NodeTasks" ["Sass";"Coffee";"Babel";"Test"]
// "Stop" ==> "Fire"
For "Fire" [ "Stop" ]
For "StartApp" [ "Stop" ]
For "StartApp" [ "Stop" ]
For "Test" [ "Coffee" ]
// default doesn't include starting the app
"Stop"
==> "Clean"
==> "BuildApp"
==> "StartApp"
==> "Default"
==> "AfterAll"
// make sure nodetasks happens before buildapp if nodetasks is executed
"NodeTasks" ?=> "BuildApp"
"Coffee" ?=> "Test"
RunTargetOrDefault "Default"
view raw build.fsx hosted with ❤ by GitHub
{
"name": "pm.web",
"version": "1.0.0",
"description": "Registry additions have been made in order to provide you the best web development experience. See http://bloggemdano.blogspot.com/2013/11/adding-new-items-to-pure-f-aspnet.html for more information.",
"main": "index.js",
"dependencies": {
"requirejs": "^2.3.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-preset-react": "^6.16.0",
"coffee-script": "github:jashkenas/coffeescript",
"mocha": "^3.2.0"
},
"scripts": {
"wastest": "echo \"Error: no test specified\" && exit 1",
"test": "mocha"
},
"author": "",
"license": "ISC"
}
view raw package.json hosted with ❤ by GitHub
Version information:

No comments:

Post a Comment