diff --git a/.gitignore b/.gitignore
index 8bbb6ff..931a514 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ _UpgradeReport*
packages
.nuget
.paket
+.ionide
# Ignore Visual Studio files
*.pdb
@@ -32,6 +33,8 @@ TestResult.*
.xake*
.fake
.vs/
+.vscode/
+.ionide/
samples/**/*.exe
samples/**/*.dll
samples/**/*.fsx.lock
diff --git a/.travis.yml b/.travis.yml
index d534405..d121ea2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,15 @@
language: csharp
-mono: latest
-dotnet: 2.1.300
+mono: none
+dotnet: 3.0
env: VER=$(if [[ "${TRAVIS_TAG:0:1}" == "v" ]]; then echo ${TRAVIS_TAG:1}.${TRAVIS_BUILD_NUMBER}; else echo 1.0.0.${TRAVIS_BUILD_NUMBER}; fi;)
install:
- - dotnet restore build.proj
+ - chmod +x ./fake.sh
script:
- - export FrameworkPathOverride=$(dirname $(which mono))/../lib/mono/4.5-api/
- - dotnet fake run build.fsx -- build test -ll Diag
+ - dotnet build src/core /p:Version=$VER -c Release /p:DocumentationFile=Xake.xml
+ - dotnet test src/tests -c Release -p:ParallelizeTestCollections=false
deploy:
- provider: script
- script: dotnet fake run build.fsx -- pack push -ll Diag
+ script: ./fake.sh run build.fsx -- pack push -ll Diag
skip_cleanup: true
on:
tags: true
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index e2dfd84..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-// Place your settings in this file to overwrite default and user settings.
-{
- "editor.wordWrap": "wordWrapColumn",
- "editor.wordWrapColumn": 120
-}
\ No newline at end of file
diff --git a/build.cmd b/build.cmd
index 3986414..5119630 100644
--- a/build.cmd
+++ b/build.cmd
@@ -1,3 +1,2 @@
@echo off
-dotnet restore build.proj
-dotnet fake run build.fsx -- build
\ No newline at end of file
+fake run build.fsx -- build
\ No newline at end of file
diff --git a/build.fsx b/build.fsx
index 0e27744..fecc750 100644
--- a/build.fsx
+++ b/build.fsx
@@ -1,5 +1,5 @@
#r "paket:
- nuget Xake ~> 1.1 prerelease //"
+ nuget Xake ~> 1.1.4 prerelease //"
#if !FAKE
#load ".fake/build.fsx/intellisense.fsx"
@@ -8,26 +8,23 @@
open Xake
open Xake.Tasks
-let frameworks = ["netstandard2.0"; "net46"]
+let frameworks = ["netstandard2.0"]
let libtargets =
[ for t in frameworks do
- for e in ["dll"; "xml"]
- -> sprintf "out/%s/Xake.%s" t e
- ]
+ for e in ["dll"; "xml"] -> sprintf "out/%s/Xake.%s" t e ]
let getVersion () = recipe {
- let! verVar = getVar("VER")
- let! verEnv = getEnv("VER")
+ let! verVar = getVar "VER"
+ let! verEnv = getEnv "VER"
let ver = verVar |> Option.defaultValue (verEnv |> Option.defaultValue "0.0.1")
let! verSuffix =
- getVar("SUFFIX")
- |> Recipe.map (
+ getVar "SUFFIX"
+ |> map (
function
| None -> "-beta"
| Some "" -> "" // this is release!
- | Some s -> "-" + s
- )
+ | Some s -> "-" + s )
return ver + verSuffix
}
@@ -37,8 +34,7 @@ let dotnet arglist = recipe {
do! shell {
cmd "dotnet"
args arglist
- failonerror
- } |> Recipe.Ignore
+ failonerror } |> Ignore
}
do xakeScript {
@@ -46,10 +42,7 @@ do xakeScript {
// consolelog Verbosity.Normal
rules [
- "main" => recipe {
- do! need ["build"]
- do! need ["test"]
- }
+ "main" <<< ["build"; "test"]
"build" <== libtargets
"clean" => rm {dir "out"}
@@ -58,13 +51,13 @@ do xakeScript {
do! alwaysRerun()
let! where =
- getVar("FILTER")
- |> Recipe.map (function |Some clause -> ["--filter"; sprintf "Name~\"%s\"" clause] | None -> [])
+ getVar "FILTER"
+ |> map (function |Some clause -> ["--filter"; sprintf "Name~\"%s\"" clause] | None -> [])
// in case of travis only run tests for standard runtime, eventually will add more
- let! limitFwk = getEnv("TRAVIS") |> Recipe.map (function | Some _ -> ["-f:netcoreapp2.0"] | _ -> [])
+ let! limitFwk = getEnv "TRAVIS" |> map (function | Some _ -> ["-f:netcoreapp2.0"] | _ -> [])
- do! dotnet <| ["test"; "src/tests"; "-c"; "Release"] @ where @ limitFwk
+ do! dotnet <| ["test"; "src/tests"; "-c"; "Release"; "-p:ParallelizeTestCollections=false"] @ where @ limitFwk
}
libtargets *..> recipe {
@@ -80,16 +73,14 @@ do xakeScript {
let! version = getVersion()
for framework in frameworks do
- do! dotnet
- [
- "build"
- "src/core"
- "/p:Version=" + version
- "--configuration"; "Release"
- "--framework"; framework
- "--output"; "../../out/" + framework
- "/p:DocumentationFile=Xake.xml"
- ]
+ do! dotnet [
+ "build"
+ "src/core"
+ "/p:Version=" + version
+ "--configuration"; "Release"
+ "--framework"; framework
+ "--output"; "../../out/" + framework
+ "/p:DocumentationFile=Xake.xml" ]
}
]
@@ -101,29 +92,25 @@ do xakeScript {
}
"out/Xake.(ver:*).nupkg" ..> recipe {
- let! ver = getRuleMatch("ver")
- do! dotnet
- [
- "pack"; "src/core"
- "-c"; "Release"
- "/p:Version=" + ver
- "--output"; "../../out/"
- "/p:DocumentationFile=Xake.xml"
- ]
+ let! ver = getRuleMatch "ver"
+ do! dotnet [
+ "pack"; "src/core"
+ "-c"; "Release"
+ "/p:Version=" + ver
+ "--output"; "../../out/"
+ "/p:DocumentationFile=Xake.xml" ]
}
// push need pack to be explicitly called in advance
"push" => recipe {
let! version = getVersion()
- let! nuget_key = getEnv("NUGET_KEY")
- do! dotnet
- [
- "nuget"; "push"
- "out" > makePackageName version
- "--source"; "https://www.nuget.org/api/v2/package"
- "--api-key"; nuget_key |> Option.defaultValue ""
- ]
+ let! nuget_key = getEnv "NUGET_KEY"
+ do! dotnet [
+ "nuget"; "push"
+ "out" > makePackageName version
+ "--source"; "https://www.nuget.org/api/v2/package"
+ "--api-key"; nuget_key |> Option.defaultValue "" ]
}
]
}
diff --git a/build.fsx.lock b/build.fsx.lock
index 5497458..c924b70 100644
--- a/build.fsx.lock
+++ b/build.fsx.lock
@@ -2,6 +2,6 @@ STORAGE: NONE
RESTRICTION: == netstandard2.0
NUGET
remote: https://api.nuget.org/v3/index.json
- FSharp.Core (4.3.4)
- Xake (1.1.0.413-alpha)
+ FSharp.Core (4.7)
+ Xake (1.1.4.427-beta)
FSharp.Core (>= 4.3.4)
diff --git a/build.proj b/build.proj
deleted file mode 100644
index 5483b72..0000000
--- a/build.proj
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- netstandard2.0
-
-
-
-
-
-
diff --git a/build.sh b/build.sh
index f19bb05..b93f96d 100755
--- a/build.sh
+++ b/build.sh
@@ -1,3 +1,2 @@
#!/bin/bash
-dotnet restore build.proj
-dotnet fake run build.fsx -- build
\ No newline at end of file
+fake run build.fsx -- build
\ No newline at end of file
diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md
new file mode 100644
index 0000000..1fed66a
--- /dev/null
+++ b/docs/cheatsheet.md
@@ -0,0 +1,9 @@
+## Operators ##
+
+### `<<<` - depends on targets (run sequentially)
+
+> `"main" <<< ["restore"; "build-debug"; "unit-test"]`
+
+### `<==` - depends on targets that are allowed to run in parallel
+> `"main" <<< ["build-debug"; "build-release"]`
+
diff --git a/fake.cmd b/fake.cmd
new file mode 100644
index 0000000..2f757d1
--- /dev/null
+++ b/fake.cmd
@@ -0,0 +1,7 @@
+SET TOOL_PATH=.fake
+
+IF NOT EXIST "%TOOL_PATH%\fake.exe" (
+ dotnet tool install fake-cli --tool-path ./%TOOL_PATH%
+)
+
+"%TOOL_PATH%/fake.exe" %*
\ No newline at end of file
diff --git a/fake.sh b/fake.sh
new file mode 100644
index 0000000..4bd92af
--- /dev/null
+++ b/fake.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -eu
+set -o pipefail
+
+# liberated from https://stackoverflow.com/a/18443300/433393
+realpath() {
+ OURPWD=$PWD
+ cd "$(dirname "$1")"
+ LINK=$(readlink "$(basename "$1")")
+ while [ "$LINK" ]; do
+ cd "$(dirname "$LINK")"
+ LINK=$(readlink "$(basename "$1")")
+ done
+ REALPATH="$PWD/$(basename "$1")"
+ cd "$OURPWD"
+ echo "$REALPATH"
+}
+
+TOOL_PATH=$(realpath .fake)
+FAKE="$TOOL_PATH"/fake
+
+if ! [ -e "$FAKE" ]
+then
+ dotnet tool install fake-cli --tool-path "$TOOL_PATH"
+fi
+"$FAKE" "$@"
diff --git a/readme.md b/readme.md
index be9cbd1..e1db639 100644
--- a/readme.md
+++ b/readme.md
@@ -1,6 +1,6 @@
Xake is a build utility that uses the full power of the F# programming language. Xake is inspired by [shake](https://github.com/ndmitchell/shake) build tool.
-[](https://travis-ci.org/xakebuild/Xake)
+[](https://travis-ci.org/xakebuild/Xake)
## Sample script
@@ -29,12 +29,13 @@ To run this script:
1. Clone the project:
- ```
+ ```bash
git clone http://github.com/xakebuild/xake
```
+
1. Run the "Hello world" build sctipt:
- ```
+ ```bash
cd samples
dotnet restore dotnet-fake.csproj
dotnet fake run gettingstarted.fsx
@@ -51,8 +52,7 @@ To run this script:
Once you cloned the repository you are ready to compile and test the binaries:
```
-dotnet restore build.proj
-dotnet fake run build.fsx -- build test
+fake run build.fsx -- build test
```
... or use `build.cmd` (`build.sh`) in the root folder
@@ -68,4 +68,3 @@ dotnet fake run build.fsx -- build test
Xake requires 'pkg-config' to locate mono runtime. Pkg-config utility is deployed with mono, but it's not included in
$PATH. The options available are described on [monobjc mailing list](http://www.mail-archive.com/users@lists.monobjc.net/msg00235.html)
-
diff --git a/samples/features.fsx b/samples/features.fsx
index b474bde..07fab0c 100644
--- a/samples/features.fsx
+++ b/samples/features.fsx
@@ -39,13 +39,19 @@ do xakeScript {
// this is shorter way to express the same. See also `<==` and '<<<' operators.
"main" => need ["tracetest"; "temp/a.exe"]
- // .NET build rules
- // build .net executable using full .net framework (or mono under unix)
-
- // define a "phony rule", which has goal to produce a file
- "clean" => rm {file "temp/a*"}
+ // "phony" rule that produces no file but just removes the files
+ // `rm` recipe (Xake.Tasks namespace) allow to remove files and folders
+ "clean" => recipe {
+ do! rm {file "paket-files/*.*"}
+ do! rm {dir "out"}
+ do! rm {files (fileset {
+ includes "samplefile*"
+ }); verbose
+ }
+ }
- // rule to build an a.exe executable by using c# compiler
+ // .NET build rules
+ // build .net executable from C# sources using full .net framework (or mono under unix)
// notice there's no "out" parameter: csc recipe will use the target file as an output
"temp/a.exe" ..> csc {src (!!"temp/a.cs" + "temp/AssemblyInfo.cs")}
@@ -107,17 +113,6 @@ do xakeScript {
return ()
}
- // "phony" rule that produces no file but just removes the files
- // `rm` recipe (Xake.Tasks namespace) allow to remove files and folders
- "clean" => recipe {
- do! rm {file "paket-files/*.*"}
- do! rm {dir "out"}
- do! rm {files (fileset {
- includes "samplefile*"
- }); verbose
- }
- }
-
"libs" => recipe {
// this command will copy all dlls to `lib` (flat files)
do! cp {file "packages/mylib/net46/*.dll"; todir "lib"}
diff --git a/src/core/BuildDatabase.fs b/src/core/BuildDatabase.fs
new file mode 100644
index 0000000..52ac726
--- /dev/null
+++ b/src/core/BuildDatabase.fs
@@ -0,0 +1,53 @@
+module Xake.BuildDatabase
+
+open Xake.Util
+
+module Picklers =
+
+ open Pickler
+ open ExecTypes
+
+ let file = wrap (File.make, fun a -> a.FullName) str
+
+ let target =
+ alt (function
+ | FileTarget _ -> 0
+ | PhonyAction _ -> 1)
+ [| wrap (File.make >> FileTarget, fun (FileTarget f | OtherwiseFail f) -> f.Name) str
+ wrap (PhonyAction, (fun (PhonyAction a | OtherwiseFail a) -> a)) str |]
+
+ let step =
+ wrap
+ ((fun (n, s, o, w) -> {StepInfo.Name = n; Start = s; OwnTime = o * 1; WaitTime = w * 1}),
+ fun ({StepInfo.Name = n; Start = s; OwnTime = o; WaitTime = w}) -> (n, s, o / 1, w / 1)) (quad str date int int)
+
+ // Fileset of FilesetOptions * FilesetElement list
+ let dependency =
+ alt (function
+ | ArtifactDep _ -> 0
+ | FileDep _ -> 1
+ | EnvVar _ -> 2
+ | Var _ -> 3
+ | AlwaysRerun _ -> 4
+ | GetFiles _ -> 5)
+ [| wrap (ArtifactDep, fun (ArtifactDep f | OtherwiseFail f) -> f) target
+ wrap (FileDep, fun (FileDep(f, ts) | OtherwiseFail (f, ts)) -> (f, ts)) (pair file date)
+ wrap (EnvVar, fun (EnvVar(n, v) | OtherwiseFail (n,v)) -> n, v) (pair str (option str))
+ wrap (Var, fun (Var(n, v)| OtherwiseFail (n,v)) -> n, v) (pair str (option str))
+ wrap0 AlwaysRerun
+ wrap (GetFiles, fun (GetFiles(fs, fi)| OtherwiseFail (fs,fi)) -> fs, fi) (pair filesetPickler filelistPickler) |]
+
+ let result =
+ wrap
+ ((fun (r, built, deps, steps) ->
+ { Targets = r
+ Built = built
+ Depends = deps
+ Steps = steps }),
+ fun r -> (r.Targets, r.Built, r.Depends, r.Steps))
+ (quad (list target) date (list dependency) (list step))
+
+type DatabaseApi = Database.DatabaseApi
+
+/// Opens the database
+let openDb path loggers = Database.openDb (Picklers.target, Picklers.result) path loggers
\ No newline at end of file
diff --git a/src/core/Common/Database.fs b/src/core/Common/Database.fs
new file mode 100644
index 0000000..3f34972
--- /dev/null
+++ b/src/core/Common/Database.fs
@@ -0,0 +1,133 @@
+module Xake.Database
+
+open Xake
+
+let XakeDbVersion = "0.5"
+
+type Database<'target,'result> when 'target: comparison
+ = { Status : Map<'target, 'result> }
+
+type DatabaseHeader =
+ { XakeSign : string
+ XakeVer : string
+ ScriptDate : System.DateTime }
+
+(* API *)
+
+/// Creates a new database
+let newDatabase() = { Database.Status = Map.empty }
+
+/// Adds result to a database
+let internal addResult db targets (result: 'result) =
+ { db with Status = targets |> List.fold (fun m i -> Map.add i result m) db.Status }
+
+module private impl =
+ open System.IO
+ open Pickler
+
+ let dbHeaderPu =
+ wrap
+ ((fun (sign, ver, scriptDate) ->
+ { DatabaseHeader.XakeSign = sign
+ XakeVer = ver
+ ScriptDate = scriptDate }),
+ fun h -> (h.XakeSign, h.XakeVer, h.ScriptDate))
+ (triple str str date)
+
+ let writeHeader w =
+ let h =
+ { DatabaseHeader.XakeSign = "XAKE"
+ XakeVer = XakeDbVersion
+ ScriptDate = System.DateTime.Now }
+ dbHeaderPu.pickle h w
+
+ let openDatabaseFile (targetPu: 'target PU, resultPU: 'result PU) dbpath (logger : ILogger) =
+ let log = logger.Log
+ let bkpath = dbpath + ".bak"
+ // if exists backup restore
+ if File.Exists(bkpath) then
+ log Level.Message "Backup file found ('%s'), restoring db"
+ bkpath
+ try
+ File.Delete(dbpath)
+ with _ -> ()
+ File.Move(bkpath, dbpath)
+ let db = ref (newDatabase())
+ let recordCount = ref 0
+
+ // read database
+ if File.Exists(dbpath) then
+ let targetListPu = list targetPu
+
+ try
+ use reader = new BinaryReader(File.OpenRead(dbpath))
+ let stream = reader.BaseStream
+ let header = dbHeaderPu.unpickle reader
+ if header.XakeVer < XakeDbVersion then
+ failwith "Database version is old."
+ while stream.Position < stream.Length do
+ let targets = targetListPu.unpickle reader
+ let result = resultPU.unpickle reader
+ db := addResult !db targets result
+ recordCount := !recordCount + 1
+ // if fails create new
+ with ex ->
+ log Level.Error
+ "Failed to read database, so recreating. Got \"%s\""
+ <| ex.ToString()
+ try
+ File.Delete(dbpath)
+ with _ -> ()
+ // check if we can cleanup db
+ if !recordCount > (!db).Status.Count * 5 then
+ log Level.Message "Compacting database"
+ File.Move(dbpath, bkpath)
+ use writer =
+ new BinaryWriter(File.Open(dbpath, FileMode.CreateNew))
+ writeHeader writer
+ (!db).Status
+ |> Map.toSeq
+ |> Seq.map snd
+ |> Seq.iter (fun r -> resultPU.pickle r writer)
+ File.Delete(bkpath)
+ let dbwriter =
+ new BinaryWriter(File.Open (dbpath, FileMode.Append, FileAccess.Write))
+ if dbwriter.BaseStream.Position = 0L then writeHeader dbwriter
+ db, dbwriter
+
+type DatabaseApi<'target,'result> =
+ | GetResult of 'target * AsyncReplyChannel<'result option>
+ | Store of 'target list * 'result
+ | Close
+ | CloseWait of AsyncReplyChannel
+
+/// Opens database.
+let openDb (targetPu: 'target Pickler.PU, resultPU: 'result Pickler.PU) dbPath (logger : ILogger) =
+ let db, dbWriter = impl.openDatabaseFile (targetPu, resultPU) dbPath logger
+ let targetListPu = Pickler.list targetPu
+
+ MailboxProcessor.Start(fun mbox ->
+ let rec loop (db) =
+ async {
+ let! msg = mbox.Receive()
+ match msg with
+ | GetResult(key, chan) ->
+ db.Status
+ |> Map.tryFind key
+ |> chan.Reply
+ return! loop (db)
+ | Store (targets, result) ->
+ targetListPu.pickle targets dbWriter
+ resultPU.pickle result dbWriter
+ return! loop (addResult db targets result)
+ | Close ->
+ logger.Log Info "Closing database"
+ dbWriter.Dispose()
+ return ()
+ | CloseWait ch ->
+ logger.Log Info "Closing database"
+ dbWriter.Dispose()
+ ch.Reply()
+ return ()
+ }
+ loop (!db))
diff --git a/src/core/Env.fs b/src/core/Common/Env.fs
similarity index 100%
rename from src/core/Env.fs
rename to src/core/Common/Env.fs
diff --git a/src/core/Common/Estimate.fs b/src/core/Common/Estimate.fs
new file mode 100644
index 0000000..e426895
--- /dev/null
+++ b/src/core/Common/Estimate.fs
@@ -0,0 +1,80 @@
+module Xake.Estimate
+
+open Xake.Util
+
+[] type Ms
+
+type CpuState = | BusyUntil of int
+type MachineState<'T when 'T:comparison> =
+ { Cpu: CpuState list; Tasks: Map<'T,int>}
+
+module internal Impl =
+ ///
+ /// Updates the first item matching the criteria and returns the updated value.
+ ///
+ ///
+ ///
+ let rec updateFirst predicate upd = function
+ | [] -> None,[]
+ | c::list when predicate c ->
+ let updated = upd c in
+ Some updated, updated :: list
+ | c::list ->
+ let result,list = (updateFirst predicate upd list) in
+ result, c::list
+
+ let ignoreFailures f a =
+ try
+ f a
+ with _ ->
+ ()
+
+///
+/// "Executes" one task
+///
+/// Initial "machine" state
+/// Provide a task information to exec function
+/// The task to execute
+let rec exec state getDurationDeps task =
+ // Gets the thread that will be freed after specific moment
+ let nearest after =
+ let ready (BusyUntil x) = if x <= after then 0 else x in
+ List.minBy ready
+
+ match state.Tasks |> Map.tryFind task with
+ | Some result -> state,result
+ | None ->
+ let duration,deps = task |> getDurationDeps
+ let readyAt (BusyUntil x)= x
+
+ let mstate, endTime = execMany state getDurationDeps deps
+ let slot = mstate.Cpu |> nearest endTime
+ let (Some (BusyUntil result)|OtherwiseFail result), newState =
+ mstate.Cpu |> Impl.updateFirst ((=) slot) (readyAt >> max endTime >> (+) duration >> BusyUntil)
+ {Cpu = newState; Tasks = mstate.Tasks |> Map.add task result}, result
+// |> fun r ->
+// printf "after %A" task
+// printf " %A\n\n" r
+// r
+
+/// Executes multiple targers simultaneously
+and execMany state getDurationDeps goals =
+ let machineState,endTime =
+ goals |> List.fold (
+ fun (prevState,prevTime) t ->
+ let newState,time = exec prevState getDurationDeps t in
+ (newState, max time prevTime)
+ ) (state, 0)
+
+ machineState, endTime
+
+/// Gets estimated execution time for several target groups. Each group start when previous group is completed and runs in parallel.
+let estimateEndTime getDurationDeps threadCount groups =
+ let machineState = {Cpu = BusyUntil 0 |> List.replicate threadCount; Tasks = Map.empty}
+
+ groups |> List.fold (fun (state, _) group ->
+ let newState, endTime = execMany state getDurationDeps group
+ let newState = {newState with Cpu = newState.Cpu |> List.map (fun _ -> BusyUntil endTime)}
+ newState, endTime
+ ) (machineState, 0)
+ |> snd
diff --git a/src/core/Logging.fs b/src/core/Common/Logging.fs
similarity index 99%
rename from src/core/Logging.fs
rename to src/core/Common/Logging.fs
index a8124df..0607cdb 100644
--- a/src/core/Logging.fs
+++ b/src/core/Common/Logging.fs
@@ -153,7 +153,7 @@ module private ConsoleSink =
| Some colors ->
// in case of CRLF in the string make sure we washed out the progress message
let rec writeLines = function
- | [] -> fun _ -> ()
+ | [] -> ignore
| (txt: string)::tail ->
function
| true ->
diff --git a/src/core/Common/Pickler.fs b/src/core/Common/Pickler.fs
new file mode 100644
index 0000000..1b3ecc2
--- /dev/null
+++ b/src/core/Common/Pickler.fs
@@ -0,0 +1,89 @@
+module Xake.Pickler
+
+open System
+
+/// Pickler Combinators implementation
+type OutState = IO.BinaryWriter
+type InState = IO.BinaryReader
+
+///
+/// Main pickler type.
+///
+type 'a PU = { pickle: 'a -> OutState -> unit; unpickle: InState -> 'a }
+
+///
+/// Unit pickler, does nothing.
+///
+let unit = {pickle = (fun () _ -> ()); unpickle = ignore}
+
+///
+/// Translates pickler of one type into another's
+///
+let wrap (d:'a -> 'b, r: 'b -> 'a) (pu: PU<'a>) = {pickle = r >> pu.pickle; unpickle = pu.unpickle >> d}
+
+///
+/// 'wrap' helper for argumentless variants
+///
+let wrap0 r = wrap ((fun () -> r), ignore) unit
+
+let byte = {pickle = (fun (b:byte) st -> st.Write(b)); unpickle = fun st -> st.ReadByte()}
+let int = {pickle = (fun (i:Int32) st -> st.Write(i)); unpickle = fun st -> st.ReadInt32()}
+let int64 = {pickle = (fun (i:Int64) st -> st.Write(i)); unpickle = fun st -> st.ReadInt64()}
+let str = {pickle = (fun (s:string) st -> st.Write(s)); unpickle = fun st -> st.ReadString()}
+let float = {pickle = (fun (s:single) st -> st.Write(s)); unpickle = fun st -> st.ReadSingle()}
+let double = {pickle = (fun (s:float) st -> st.Write(s)); unpickle = fun st -> st.ReadDouble()}
+
+let date = wrap (DateTime.FromBinary, fun (d:DateTime) -> d.Ticks) int64
+let bool = wrap ((<>) 0uy, function | true -> 1uy |false -> 0uy) byte
+
+/// Tuple picklers
+let pair pu1 pu2 = {
+ pickle = (fun (a,b) st -> (pu1.pickle a st : unit); (pu2.pickle b st))
+ unpickle = fun st -> pu1.unpickle st, pu2.unpickle st}
+let triple pu1 pu2 pu3 = {
+ pickle = (fun (a,b,c) st -> (pu1.pickle a st : unit); (pu2.pickle b st : unit); (pu3.pickle c st))
+ unpickle = fun st -> pu1.unpickle st, pu2.unpickle st, pu3.unpickle st}
+
+let quad pu1 pu2 pu3 pu4 =
+ wrap ((fun ((a,b),(c,d)) -> (a,b,c,d)), fun(a,b,c,d) -> (a,b),(c,d)) <| pair (pair pu1 pu2) (pair pu3 pu4)
+
+let private mux3 (a,b,c) x = (a x : unit); (b x : unit); (c x : unit)
+let private mux2 (a,b) x = (a x : unit); (b x : unit)
+
+///
+/// List pickler.
+///
+///
+let list pu =
+ let rec listP f = function | [] -> byte.pickle 0uy | h :: t -> mux3 (byte.pickle 1uy, f h, listP f t)
+ let rec listUim f acc st = match byte.unpickle st with | 0uy -> List.rev acc | 1uy -> listUim f (f st :: acc) st | n -> failwithf "listU: found number %d" n
+ {
+ pickle = listP pu.pickle
+ unpickle = listUim pu.unpickle []
+ }
+
+///
+/// Variant (discriminated union) pickler.
+///
+/// Maps type to index in array of picklers.
+/// Array of picklers for each type.
+let alt<'a> (ftag: 'a -> Core.int) (puu: PU<'a> array): PU<'a> =
+ {
+ pickle = fun (a:'a) ->
+ let tag = ftag a in
+ mux2 (tag |> Convert.ToByte |> byte.pickle, puu.[tag].pickle a)
+ unpickle = fun st ->
+ let tag = st |> byte.unpickle |> Convert.ToInt32 in
+ (puu.[tag].unpickle st)
+ }
+
+///
+/// Option type pickler.
+///
+let option pu =
+ alt
+ (function | None _ -> 0 | Some _ -> 1)
+ [|
+ wrap ((fun () -> None), ignore) unit
+ wrap (Some, Option.get) pu
+ |]
diff --git a/src/core/Progress.fs b/src/core/Common/Progress.fs
similarity index 62%
rename from src/core/Progress.fs
rename to src/core/Common/Progress.fs
index 0a5c5f8..338a0c5 100644
--- a/src/core/Progress.fs
+++ b/src/core/Common/Progress.fs
@@ -1,5 +1,16 @@
module Xake.Progress
+open Xake.Util
+open Xake.Estimate
+
+///
+/// A message to a progress reporter.
+///
+type ProgressMessage =
+ | Begin of System.TimeSpan
+ | Progress of System.TimeSpan * int
+ | End
+
module internal WindowsProgress =
open System
@@ -74,86 +85,17 @@ module internal WindowsProgress =
with _ ->
ignore
-module internal Impl =
- ///
- /// Updates the first item matching the criteria and returns the updated value.
- ///
- ///
- ///
- let rec updateFirst predicate upd = function
- | [] -> None,[]
- | c::list when predicate c ->
- let updated = upd c in
- Some updated, updated :: list
- | c::list ->
- let result,list = (updateFirst predicate upd list) in
- result, c::list
-
- let ignoreFailures f a =
- try
- f a
- with _ ->
- ()
-
-/// Estimate the task execution times
-module Estimate =
-
- type CpuState = | BusyUntil of int
- type MachineState<'T when 'T:comparison> =
- { Cpu: CpuState list; Tasks: Map<'T,int>}
-
- ///
- /// "Executes" one task
- ///
- /// Initial "machine" state
- /// Provide a task information to exec function
- /// The task to execute
- let rec exec state getDurationDeps task =
- // Gets the thread that will be freed after specific moment
- let nearest after =
- let ready (BusyUntil x) = if x <= after then 0 else x in
- List.minBy ready
-
- match state.Tasks |> Map.tryFind task with
- | Some result -> state,result
- | None ->
- let duration,deps = task |> getDurationDeps
- let readyAt (BusyUntil x)= x
-
- let mstate, endTime = execMany state getDurationDeps deps
- let slot = mstate.Cpu |> nearest endTime
- let (Some (BusyUntil result)|OtherwiseFail result), newState =
- mstate.Cpu |> Impl.updateFirst ((=) slot) (readyAt >> max endTime >> (+) duration >> BusyUntil)
- {Cpu = newState; Tasks = mstate.Tasks |> Map.add task result}, result
-// |> fun r ->
-// printf "after %A" task
-// printf " %A\n\n" r
-// r
-
- /// Executes multiple targers simultaneously
- and execMany state getDurationDeps goals =
- let machineState,endTime =
- goals |> List.fold (
- fun (prevState,prevTime) t ->
- let newState,time = exec prevState getDurationDeps t in
- (newState, max time prevTime)
- ) (state, 0)
-
- machineState, endTime
-
///
/// Interface for progress module.
///
-type ProgressReport =
- | TaskStart of Target
- | TaskSuspend of Target
- | TaskResume of Target
- | TaskComplete of Target
+type ProgressReport<'TKey> when 'TKey: comparison =
+ | TaskStart of 'TKey
+ | TaskSuspend of 'TKey
+ | TaskResume of 'TKey
+ | TaskComplete of 'TKey
| Refresh
| Finish
-open Estimate
-
///
/// Creates "null" progress reporter.
///
@@ -168,19 +110,6 @@ let emptyProgress () =
}
loop ())
-///
-/// Gets estimated execution time for several target groups. Each group start when previous group is completed and runs in parallel.
-///
-let estimateEndTime getDurationDeps threadCount groups =
- let machineState = {Cpu = BusyUntil 0 |> List.replicate threadCount; Tasks = Map.empty}
-
- groups |> List.fold (fun (state, _) group ->
- let newState, endTime = execMany state getDurationDeps group
- let newState = {newState with Cpu = newState.Cpu |> List.map (fun _ -> BusyUntil endTime)}
- newState, endTime
- ) (machineState, 0)
- |> snd
-
///
/// Creates windows taskbar progress reporter.
///
@@ -190,12 +119,12 @@ let estimateEndTime getDurationDeps threadCount groups =
let openProgress getDurationDeps threadCount goals toConsole =
let progressBar = WindowsProgress.createTaskbarIndicator() |> Impl.ignoreFailures
- let machineState = {Cpu = BusyUntil 0 |> List.replicate threadCount; Tasks = Map.empty}
+ let machineState = {Cpu = BusyUntil 0 |> List.replicate threadCount; Tasks = Map.empty}
let _,endTime = execMany machineState getDurationDeps goals
let startTime = System.DateTime.Now
- progressBar <| ProgressMessage.Begin (System.TimeSpan.FromMilliseconds (float endTime))
+ progressBar <| Begin (System.TimeSpan.FromMilliseconds (float endTime))
/// We track currently running tasks and subtract already passed time from task duration
let getDuration2 runningTasks t =
@@ -204,24 +133,24 @@ let openProgress getDurationDeps threadCount goals toConsole =
let originalDuration,deps = getDurationDeps t
//do printf "\nestimate %A: %A\n" t timeToComplete
in
- originalDuration - runningTime |> max 0, deps
+ originalDuration - runningTime |> max 0, deps
| _ ->
getDurationDeps t
let reportProgress (state, runningTasks) =
- let timePassed = 1 * int (System.DateTime.Now - startTime).TotalMilliseconds in
+ let timePassed = 1 * int (System.DateTime.Now - startTime).TotalMilliseconds in
let _,leftTime = execMany state (getDuration2 runningTasks) goals
//printf "progress %A to %A " timePassed endTime
let percentDone = timePassed * 100 / (timePassed + leftTime) |> int
- let progressData = System.TimeSpan.FromMilliseconds (leftTime/1 |> float), percentDone
- do ProgressMessage.Progress progressData |> progressBar
+ let progressData = System.TimeSpan.FromMilliseconds (leftTime/1 |> float), percentDone
+ do Progress progressData |> progressBar
if toConsole then
do WriteConsoleProgress progressData
let updTime = ref System.DateTime.Now
let advanceRunningTime rt =
let now = System.DateTime.Now
- let increment = 1 * int (now - !updTime).TotalMilliseconds
+ let increment = 1 * int (now - !updTime).TotalMilliseconds
updTime := now
rt |> Map.map (
fun _ (cpu,isRunning) ->
@@ -240,7 +169,7 @@ let openProgress getDurationDeps threadCount goals toConsole =
let runningTasks = runningTasks |> advanceRunningTime
match msg with
- | TaskStart target -> return! loop (state, runningTasks |> Map.add target (0, true))
+ | TaskStart target -> return! loop (state, runningTasks |> Map.add target (0, true))
| TaskSuspend target -> return! loop (state, runningTasks |> Map.map (suspend target))
| TaskResume target -> return! loop (state, runningTasks |> Map.map (resume target))
| Refresh _ ->
@@ -248,7 +177,7 @@ let openProgress getDurationDeps threadCount goals toConsole =
return! loop (state,runningTasks)
| TaskComplete target ->
- let newState = ({state with Tasks = state.Tasks |> Map.add target 0}, runningTasks |> Map.remove target)
+ let newState = ({state with Tasks = state.Tasks |> Map.add target 0}, runningTasks |> Map.remove target)
reportProgress newState
return! loop newState
diff --git a/src/core/RecipeBuilder.fs b/src/core/Common/Recipe.fs
similarity index 83%
rename from src/core/RecipeBuilder.fs
rename to src/core/Common/Recipe.fs
index e4ed98e..11457ce 100644
--- a/src/core/RecipeBuilder.fs
+++ b/src/core/Common/Recipe.fs
@@ -1,23 +1,26 @@
namespace Xake
-module internal A =
+// expression type
+type Recipe<'a,'b> = Recipe of ('a -> Async<'a * 'b>)
+
+module internal RecipeAlgebra =
let runAction (Recipe r) = r
- let returnF a = Recipe (fun (s,_) -> async {return (s,a)})
+ let returnF a = Recipe (fun s -> async {return (s,a)})
- let bindF m f = Recipe (fun (s, a) -> async {
- let! (s', b) = runAction m (s, a) in
- return! runAction (f b) (s', a)
+ let bindF m f = Recipe (fun s -> async {
+ let! (s', b) = runAction m s in
+ return! runAction (f b) s'
})
- let bindA m f = Recipe (fun (s, r) -> async {
+ let bindA m f = Recipe (fun s -> async {
let! a = m in
- return! runAction (f a) (s, r)
+ return! runAction (f a) s
})
let resultFromF m = m
let callF f a = bindF (returnF a) f
let delayF f = callF f ()
- let doneF = Recipe (fun (s,_) -> async {return (s,())})
+ let doneF = Recipe (fun s -> async {return (s,())})
let ignoreF p = bindF p (fun _ -> doneF)
let combineF f g = bindF f (fun _ -> g)
@@ -61,7 +64,7 @@ module internal A =
[]
module Builder =
- open A
+ open RecipeAlgebra
type RecipeBuilder() =
member this.Return(c) = returnF c
member this.Zero() = doneF
diff --git a/src/core/Common/RecipeFunctions.fs b/src/core/Common/RecipeFunctions.fs
new file mode 100644
index 0000000..9ac313f
--- /dev/null
+++ b/src/core/Common/RecipeFunctions.fs
@@ -0,0 +1,55 @@
+[]
+module Xake.Recipe
+
+open Xake
+
+///
+/// Ignores action result in case task returns the value but you don't need it.
+///
+///
+let Ignore act = act |> RecipeAlgebra.ignoreF
+
+///
+/// Translates the recipe result.
+///
+let map f (rc: Recipe<_,_>) = recipe {
+ let! r = rc
+ return f r
+}
+
+/// Gets action context.
+let getCtx() = Recipe (fun c -> async {return (c,c)})
+
+/// Updates the build result
+let setCtx ctx = Recipe (fun _ -> async {return (ctx,())})
+
+/// Consumes the task output and in case condition is met raises the error.
+let FailWhen cond err (act: Recipe<_,_>) =
+ recipe {
+ let! b = act
+ if cond b then failwith err
+ return b
+ }
+
+///
+/// Supplemental for FailWhen to verify errorlevel set by system command.
+///
+let Not0 = (<>) 0
+
+///
+/// Error handler verifying result of system command.
+///
+///
+let CheckErrorLevel rc = rc |> FailWhen Not0 "system command returned a non-zero result"
+
+///
+/// Wraps action so that exceptions occured while executing action are ignored.
+///
+///
+let WhenError handler (rc:Recipe<_,_>) =
+ recipe {
+ try
+ let! r = rc
+ return r
+ with e -> return handler e
+ }
diff --git a/src/core/Common/Util.fs b/src/core/Common/Util.fs
new file mode 100644
index 0000000..2495561
--- /dev/null
+++ b/src/core/Common/Util.fs
@@ -0,0 +1,41 @@
+module Xake.Util
+
+let (><) f a b = f b a
+let inline (|OtherwiseFail|) _ = failwith "no choice"
+let inline (|OtherwiseFailErr|) message _ = failwith message
+
+type 't Agent = 't MailboxProcessor
+
+type private CacheKey<'K> = K of 'K
+
+///
+/// Creates a memoized function with the same signature. Performs memoization by storing run results to a cache.
+///
+///
+let memoize f =
+ let cache = ref Map.empty
+ let lck = System.Object()
+ fun x ->
+ match !cache |> Map.tryFind (K x) with
+ | Some v -> v
+ | None ->
+ lock lck (fun () ->
+ match !cache |> Map.tryFind (K x) with
+ | Some v -> v
+ | None ->
+ let res = f x
+ cache := !cache |> Map.add (K x) res
+ res)
+
+
+/// Memoizes the recursive function. Memoized function is passed as first argument to f.
+/// * `f` - parameter of type `('a -> 'b) -> 'a -> 'b` The function to be memoized.
+let memoizeRec f =
+ let rec fn x = f fm x
+ and fm = fn |> memoize
+ in
+ fm
+
+/// Takes n first elements from a list.
+let take cnt =
+ if cnt > 0 then List.chunkBySize cnt >> List.head else fun _ -> List.empty
diff --git a/src/core/Common/WorkerPool.fs b/src/core/Common/WorkerPool.fs
new file mode 100644
index 0000000..0d4ae26
--- /dev/null
+++ b/src/core/Common/WorkerPool.fs
@@ -0,0 +1,45 @@
+module Xake.WorkerPool
+
+type ExecMessage<'target,'result> =
+ | Run of string * 'target list * Async<'result> * AsyncReplyChannel>
+
+open System.Threading
+open System.Threading.Tasks
+
+// execution context
+let create (logger:ILogger) maxThreads =
+ // controls how many threads are running in parallel
+ let throttler = new SemaphoreSlim (maxThreads)
+ let log = logger.Log
+
+ throttler, MailboxProcessor.Start(fun mbox ->
+ let rec loop(map) = async {
+ match! mbox.Receive() with
+ | Run(_, [], _, _) ->
+ log Error "Empty target list"
+ return! loop map
+ | Run(title, (artifact::_ as targets), action, chnl) ->
+ match map |> Map.tryFind artifact with
+ | Some (task:Task<'a>) ->
+ log Never "Task found for '%s'. Status %A" title task.Status
+ chnl.Reply <| Async.AwaitTask task
+ return! loop map
+
+ | None ->
+ do log Info "Task queued '%s'" title
+ do! throttler.WaitAsync(-1) |> Async.AwaitTask |> Async.Ignore
+
+ let task = Async.StartAsTask <| async {
+ try
+ let! buildResult = action
+ do log Info "Task done '%s'" title
+ return buildResult
+ finally
+ throttler.Release() |> ignore
+ }
+
+ chnl.Reply <| Async.AwaitTask task
+ let map' = List.fold (fun m t -> m |> Map.add t task) map targets
+ return! loop map'
+ }
+ loop(Map.empty) )
diff --git a/src/core/CommonLib.fs b/src/core/CommonLib.fs
deleted file mode 100644
index 0796fac..0000000
--- a/src/core/CommonLib.fs
+++ /dev/null
@@ -1,57 +0,0 @@
-namespace Xake
-
-[]
-module internal CommonLib =
-
- type private CacheKey<'K> = K of 'K
-
- ///
- /// Creates a memoized function with the same signature. Performs memoization by storing run results to a cache.
- ///
- ///
- let memoize f =
- let cache = ref Map.empty
- let lck = System.Object()
- fun x ->
- match !cache |> Map.tryFind (K x) with
- | Some v -> v
- | None ->
- lock lck (fun () ->
- match !cache |> Map.tryFind (K x) with
- | Some v -> v
- | None ->
- let res = f x
- cache := !cache |> Map.add (K x) res
- res)
-
-
- ///**Description**
- /// Memoizes the recursive function. Memoized function is passed as first argument to f.
- ///**Parameters**
- /// * `f` - parameter of type `('a -> 'b) -> 'a -> 'b` The function to be memoized.
- ///
- ///**Output Type**
- /// * `'a -> 'b`
- ///
- ///**Exceptions**
- ///
- let memoizeRec f =
- let rec fn x = f fm x
- and fm = fn |> memoize
- in
- fm
-
- ///
- /// Takes n first elements from a list.
- ///
- ///
- let rec take cnt = function |_ when cnt <= 0 -> [] |[] -> [] |a::rest -> a :: (take (cnt-1) rest)
-
- ///
- /// Returns a list of unique values for a specific list.
- ///
- ///
- let distinct ls =
- ls |>
- List.fold (fun map item -> if map |> Map.containsKey item then map else map |> Map.add item 1) Map.empty
- |> Map.toList |> List.map fst
diff --git a/src/core/Database.fs b/src/core/Database.fs
deleted file mode 100644
index d36dca3..0000000
--- a/src/core/Database.fs
+++ /dev/null
@@ -1,218 +0,0 @@
-namespace Xake
-
-module BuildLog =
- open Xake
- open System
-
- let XakeDbVersion = "0.4"
-
- type Database = { Status : Map }
-
- (* API *)
-
- /// Creates a new build result
- let makeResult target =
- { Targets = target
- Built = DateTime.Now
- Depends = []
- Steps = [] }
-
- /// Creates a new database
- let newDatabase() = { Database.Status = Map.empty }
-
- /// Adds result to a database
- let internal addResult db result =
- { db with Status = result.Targets |> List.fold (fun m i -> Map.add i result m) db.Status }
-
-type 't Agent = 't MailboxProcessor
-
-module Storage =
- open Xake
- open BuildLog
-
- module private Persist =
- open Pickler
-
- type DatabaseHeader =
- { XakeSign : string
- XakeVer : string
- ScriptDate : Timestamp }
-
- let file = wrap (File.make, fun a -> a.FullName) str
-
- let target =
- alt (function
- | FileTarget _ -> 0
- | PhonyAction _ -> 1)
- [| wrap (File.make >> FileTarget, fun (FileTarget f | OtherwiseFail f) -> f.Name) str
- wrap (PhonyAction, (fun (PhonyAction a | OtherwiseFail a) -> a)) str |]
-
- let step =
- wrap
- ((fun (n, s, o, w) -> {StepInfo.Name = n; Start = s; OwnTime = o * 1; WaitTime = w * 1}),
- fun ({StepInfo.Name = n; Start = s; OwnTime = o; WaitTime = w}) -> (n, s, o / 1, w / 1)) (quad str date int int)
-
- // Fileset of FilesetOptions * FilesetElement list
- let dependency =
- alt (function
- | ArtifactDep _ -> 0
- | FileDep _ -> 1
- | EnvVar _ -> 2
- | Var _ -> 3
- | AlwaysRerun _ -> 4
- | GetFiles _ -> 5)
- [| wrap (ArtifactDep, fun (ArtifactDep f | OtherwiseFail f) -> f) target
- wrap (FileDep, fun (FileDep(f, ts) | OtherwiseFail (f, ts)) -> (f, ts)) (pair file date)
- wrap (EnvVar, fun (EnvVar(n, v) | OtherwiseFail (n,v)) -> n, v) (pair str (option str))
- wrap (Var, fun (Var(n, v)| OtherwiseFail (n,v)) -> n, v) (pair str (option str))
- wrap0 AlwaysRerun
- wrap (GetFiles, fun (GetFiles(fs, fi)| OtherwiseFail (fs,fi)) -> fs, fi) (pair filesetPickler filelistPickler) |]
-
- let result =
- wrap
- ((fun (r, built, deps, steps) ->
- { Targets = r
- Built = built
- Depends = deps
- Steps = steps }),
- fun r -> (r.Targets, r.Built, r.Depends, r.Steps))
- (quad (list target) date (list dependency) (list step))
-
- let dbHeader =
- wrap
- ((fun (sign, ver, scriptDate) ->
- { DatabaseHeader.XakeSign = sign
- XakeVer = ver
- ScriptDate = scriptDate }),
- fun h -> (h.XakeSign, h.XakeVer, h.ScriptDate))
- (triple str str date)
-
- module private impl =
- open System.IO
- open Persist
-
- let writeHeader w =
- let h =
- { DatabaseHeader.XakeSign = "XAKE"
- XakeVer = XakeDbVersion
- ScriptDate = System.DateTime.Now }
- Persist.dbHeader.pickle h w
-
- let openDatabaseFile dbpath (logger : ILogger) =
- let log = logger.Log
- let resultPU = Persist.result
- let bkpath = dbpath <.> "bak"
- // if exists backup restore
- if File.Exists(bkpath) then
- log Level.Message "Backup file found ('%s'), restoring db"
- bkpath
- try
- File.Delete(dbpath)
- with _ -> ()
- File.Move(bkpath, dbpath)
- let db = ref (newDatabase())
- let recordCount = ref 0
- // read database
- if File.Exists(dbpath) then
- try
- use reader = new BinaryReader(File.OpenRead(dbpath))
- let stream = reader.BaseStream
- let header = Persist.dbHeader.unpickle reader
- if header.XakeVer < XakeDbVersion then
- failwith "Database version is old."
- while stream.Position < stream.Length do
- let result = resultPU.unpickle reader
- db := result |> addResult !db
- recordCount := !recordCount + 1
- // if fails create new
- with ex ->
- log Level.Error
- "Failed to read database, so recreating. Got \"%s\""
- <| ex.ToString()
- try
- File.Delete(dbpath)
- with _ -> ()
- // check if we can cleanup db
- if !recordCount > (!db).Status.Count * 5 then
- log Level.Message "Compacting database"
- File.Move(dbpath, bkpath)
- use writer =
- new BinaryWriter(File.Open(dbpath, FileMode.CreateNew))
- writeHeader writer
- (!db).Status
- |> Map.toSeq
- |> Seq.map snd
- |> Seq.iter (fun r -> resultPU.pickle r writer)
- File.Delete(bkpath)
- let dbwriter =
- new BinaryWriter(File.Open (dbpath, FileMode.Append, FileAccess.Write))
- if dbwriter.BaseStream.Position = 0L then writeHeader dbwriter
- db, dbwriter
-
- type DatabaseApi =
- | GetResult of Target * AsyncReplyChannel