1+ module internal Xake.DependencyAnalysis
2+
3+ open Xake
4+ open Storage
5+
6+ /// <summary >
7+ /// Dependency state.
8+ /// </summary >
9+ type ChangeReason =
10+ | NotChanged
11+ | Depends of Target
12+ | DependsMissingTarget of Target
13+ | Refs of string list
14+ | FilesChanged of string list
15+ | Other of string
16+
17+ let TimeCompareToleranceMs = 10.0
18+
19+ /// <summary >
20+ /// Gets target execution time in the last run
21+ /// </summary >
22+ /// <param name =" ctx " ></param >
23+ /// <param name =" target " ></param >
24+ let getExecTime ctx target =
25+ ( fun ch -> Storage.GetResult( target, ch)) |> ctx.Db.PostAndReply
26+ |> Option.fold ( fun _ r -> r.Steps |> List.sumBy ( fun s -> s.OwnTime)) 0 < ms>
27+
28+ let targetName = function
29+ | PhonyAction a -> a
30+ | FileTarget file -> file.Name
31+
32+ /// Gets single dependency state and reason of a change.
33+ let getDepState getVar getFileList ( getChangedDeps : Target -> ChangeReason list ) = function
34+ | FileDep ( a: File, wrtime) when not (( File.exists a) && abs(( File.getLastWriteTime a - wrtime) .TotalMilliseconds) < TimeCompareToleranceMs) ->
35+ let dbgInfo = File.exists a |> function
36+ | false -> " file does not exists"
37+ | _ -> sprintf " write time: %A vs %A " ( File.getLastWriteTime a) wrtime
38+ ChangeReason.FilesChanged [ a.Name], Some dbgInfo
39+
40+ | ArtifactDep ( FileTarget file) when not ( File.exists file) ->
41+ ChangeReason.DependsMissingTarget ( FileTarget file), None
42+
43+ | ArtifactDep dependeeTarget ->
44+ dependeeTarget |> getChangedDeps |> List.filter ((<>) ChangeReason.NotChanged)
45+ |> function
46+ | [] -> NotChanged, None
47+ | item::_ ->
48+ ChangeReason.Depends dependeeTarget, Some ( sprintf " E.g. %A ..." item)
49+
50+ | EnvVar ( name, value) when value <> Util.getEnvVar name ->
51+ ChangeReason.Other <| sprintf " Environment variable %s was changed from '%A ' to '%A '" name value ( Util.getEnvVar name), None
52+
53+ | Var ( name, value) when value <> getVar name ->
54+ ChangeReason.Other <| sprintf " Global script variable %s was changed '%A '->'%A '" name value ( getVar name), None
55+
56+ | AlwaysRerun ->
57+ ChangeReason.Other <| " AlwaysRerun rule" , Some " Rule indicating target has to be run regardless dependencies state"
58+
59+ | GetFiles ( fileset, files) ->
60+ let newfiles = getFileList fileset
61+ let diff = compareFileList files newfiles
62+
63+ if List.isEmpty diff then
64+ NotChanged, None
65+ else
66+ Other <| sprintf " File list is changed for fileset %A " fileset, Some ( sprintf " The diff list is %A " diff)
67+ | _ -> NotChanged, None
68+
69+
70+ /// <summary >
71+ /// Gets the list of reasons to rebuilt the target. Empty list means target is not changed.
72+ /// </summary >
73+ /// <param name =" ctx " ></param >
74+ /// <param name =" getTargetDeps " >gets state for nested dependency</param >
75+ /// <param name =" target " >The target to analyze</param >
76+ let getChangeReasons ctx getTargetDeps target =
77+
78+ // separates change reason into two lists and collabses FilesChanged all into one
79+ let collapseFilesChanged reasons =
80+ let files , other = reasons |> List.partition ( fst >> function | ChangeReason.FilesChanged _ -> true | _ -> false )
81+ let filesChangedDbg = files |> List.collect ( snd >> Option.toList)
82+ let filesChanged = files |> List.collect ( fst >> fun ( FilesChanged files ) -> files) |> function | [] -> [] | ls -> [ FilesChanged ls, Some ( sprintf " %A " filesChangedDbg)]
83+ in
84+ filesChanged @ other |> List.rev
85+
86+
87+ let lastBuild = ( fun ch -> GetResult( target, ch)) |> ctx.Db.PostAndReply
88+
89+ match lastBuild with
90+ | Some { BuildResult.Depends = []} ->
91+ [ ChangeReason.Other " No dependencies" , Some " It means target is not \" pure\" and depends on something beyond our control (oracle)" ]
92+
93+ | Some { BuildResult.Depends = depends; Result = result} ->
94+ let dep_state = getDepState ( Util.getVar ctx.Options) ( toFileList ctx.Options.ProjectRoot) getTargetDeps
95+
96+ depends
97+ |> List.map dep_ state
98+ |> List.filter ( fst >> (<>) ChangeReason.NotChanged)
99+ |> collapseFilesChanged
100+ |> function
101+ | [] ->
102+ match result with
103+ | FileTarget file when not ( File.exists file) ->
104+ [ ChangeReason.Other " target file does not exist" , Some " The file has to be rebuilt regardless all its dependencies were not changed" ]
105+ | _ -> []
106+ | ls -> ls
107+
108+ | _ ->
109+ [ ChangeReason.Other " Not built yet" , Some " Target was not built before or build results were cleaned so we don't know dependencies." ]
110+ |> List.map fst
111+
112+ // gets task duration and list of targets it depends on. No clue why one method does both.
113+ let getDurationDeps ctx getDeps t =
114+ let collectTargets = List.collect ( function | Depends t | DependsMissingTarget t -> [ t] | _ -> [])
115+ getExecTime ctx t, getDeps t |> collectTargets
116+
117+ /// Dumps all dependencies for particular target
118+ let dumpDeps ( ctx : ExecContext ) ( target : Target list ) =
119+
120+ let rec getDeps = getChangeReasons ctx ( fun x -> getDeps x) |> memoize
121+ let doneTargets = new System.Collections.Hashtable()
122+ let indent i = String.replicate i " "
123+
124+ let rec displayNestedDeps ii =
125+ function
126+ | ArtifactDep dependeeTarget ->
127+ printfn " %s Artifact: %A " ( indent ii) dependeeTarget.FullName
128+ showTargetStatus ( ii+ 1 ) dependeeTarget
129+ | _ -> ()
130+ and showDepStatus ii ( d : Dependency ) =
131+ match d with
132+ | AlwaysRerun ->
133+ printfn " %s Always Rerun" ( indent ii)
134+ | FileDep ( a: File, wrtime) ->
135+ let changed = File.exists a |> function
136+ | true when abs(( File.getLastWriteTime a - wrtime) .TotalMilliseconds) >= TimeCompareToleranceMs ->
137+ sprintf " CHANGED (%A <> %A )" wrtime ( File.getLastWriteTime a)
138+ | false ->
139+ " NOT EXISTS"
140+ | _ -> " "
141+ printfn " %s File '%s ' %A %s " ( indent ii) a.Name wrtime changed
142+
143+ | EnvVar ( name, value) ->
144+ let newValue = Util.getEnvVar name
145+ let changed = if value <> newValue then sprintf " CHANGED %A => %A " value newValue else " "
146+ printfn " %s ENV Var: '%s ' = %A %s " ( indent ii) name value changed
147+
148+ | Var ( name, value) ->
149+ printfn " %s Script var: '%s ' = %A " ( indent ii) name value
150+
151+ | GetFiles ( fileset, files) ->
152+ printfn " %s GetFiles: %A " ( indent ii) fileset
153+ | _ ->
154+ ()
155+ and showTargetStatus ii ( target : Target ) =
156+ if not <| doneTargets.ContainsKey( target) then
157+ doneTargets.Add( target, 1 )
158+
159+ printfn " %s Target %A " ( indent ii) target.ShortName
160+
161+ let lastResult = ( fun ch -> GetResult( target, ch)) |> ctx.Db.PostAndReply
162+ match lastResult with
163+ | Some { BuildResult.Depends = []} ->
164+ printfn " %s no dependencies" ( indent ii)
165+ | Some { BuildResult.Depends = deps} ->
166+ deps |> List.iter ( showDepStatus ( ii+ 1 ))
167+ deps |> List.iter ( displayNestedDeps ( ii+ 1 ))
168+ | None ->
169+ printfn " %s no built yet (no stats)" ( indent ii)
170+
171+ target |> List.iter ( showTargetStatus 0 )
0 commit comments