1
+ import glob
1
2
import shlex
2
3
import sys
3
4
from argparse import Namespace
5
+ from dataclasses import dataclass
4
6
from functools import wraps
7
+ from pathlib import Path
5
8
from subprocess import Popen
6
9
from typing import Any
7
10
from typing import Callable
@@ -39,24 +42,78 @@ def set_exit_non_zero(self, val: bool) -> None:
39
42
self .exit_non_zero = val
40
43
41
44
45
+ @dataclass
46
+ class TargetDepends :
47
+ targets : list [str ]
48
+ files : list [str ]
49
+
50
+
51
+ @dataclass
52
+ class TargetCreates :
53
+ files : list [str ] | None
54
+ func : Callable [[str ], str ] | None
55
+
56
+
42
57
class TargetData (NamedTuple ):
43
58
name : str
44
- depends : list [str ] = []
59
+ depends : TargetDepends
60
+ creates : TargetCreates
45
61
46
62
63
+ TargetCreateType = list [str ] | Callable [[str ], str ]
47
64
TargetFuncType = Callable [[Namespace , TargetData ], None ]
48
65
WrapTargetFuncType = Callable [[Namespace ], None ]
49
66
50
- _targets = {}
67
+ _targets : dict [str , TargetData ] = {}
68
+
69
+
70
+ def _parse_dependencies (depends : list [str ]) -> TargetDepends :
71
+ ts = []
72
+ fs = []
73
+ for d in depends :
74
+ if d .startswith ('f:' ):
75
+ fs .extend (glob .glob (d [2 :]))
76
+ else :
77
+ ts .append (d )
78
+ return TargetDepends (targets = ts , files = fs )
79
+
80
+
81
+ def _parse_creates (creates : TargetCreateType ) -> TargetCreates :
82
+ if callable (creates ):
83
+ return TargetCreates (func = creates , files = None )
84
+
85
+ if isinstance (creates , list ):
86
+ files = []
87
+ for f in creates :
88
+ g = glob .glob (f )
89
+ files .extend (g if g else [f ]) # the might exist in the future
90
+ return TargetCreates (files = files , func = None )
91
+
92
+
93
+ def _gen_create_files (func : Callable [[str ], str ] | None , depends : list [str ]) -> list [str ]:
94
+ # if a function was provided to crate instead of list of file globs then
95
+ # the file name are generated by call the provided function on all the dependencies
96
+ if func :
97
+ return list (map (func , depends ))
98
+ return []
51
99
52
100
53
- def target (depends : list [str ] = []) -> Callable [[TargetFuncType ], WrapTargetFuncType ]:
101
+ def target (creates : TargetCreateType = [], depends : list [str ] = []) -> Callable [[TargetFuncType ], WrapTargetFuncType ]:
54
102
def target_dec (fn : TargetFuncType ) -> WrapTargetFuncType :
55
- _targets [fn .__name__ ] = TargetData (name = fn .__name__ , depends = depends )
103
+ _targets [fn .__name__ ] = TargetData (name = fn .__name__ ,
104
+ creates = _parse_creates (creates ), depends = _parse_dependencies (depends ))
105
+
106
+ if _targets [fn .__name__ ].creates .func is not None :
107
+ # gen the file names that target might create
108
+ _targets [fn .__name__ ].creates .files = _gen_create_files (
109
+ func = _targets [fn .__name__ ].creates .func ,
110
+ depends = _targets [fn .__name__ ].depends .files ,
111
+ )
56
112
57
113
@wraps (fn )
58
114
def target_wrap (opts : Namespace ) -> None :
59
115
fn (opts , _targets [fn .__name__ ])
116
+
60
117
return target_wrap
61
118
return target_dec
62
119
@@ -68,14 +125,46 @@ def _get_target_data(name: str) -> TargetData | None:
68
125
def _check_target_exists (name : str ) -> bool : return name in _targets
69
126
70
127
128
+ def _decide_target_exec (name : str ) -> bool :
129
+ # target executes if:
130
+ # creates.files is empty
131
+ # if ones of the fils in create does not exists
132
+ # max mtimes (creates) < min mtimes (depends)
133
+ t_data = _get_target_data (name )
134
+ if not t_data :
135
+ return False
136
+
137
+ if not t_data .creates .files :
138
+ return True
139
+
140
+ n_creates = [Path (i ) for i in t_data .creates .files ]
141
+ n_depends = [Path (i ) for i in t_data .depends .files ]
142
+
143
+ # check whether file exists
144
+ if not all (i .exists () for i in n_creates ):
145
+ return True
146
+
147
+ if max (i .stat ().st_mtime for i in n_creates ) < min (i .stat ().st_mtime for i in n_depends ):
148
+ return True
149
+
150
+ return False
151
+
152
+
71
153
def _exec_target (name : str , opts : Namespace , target_globals : dict [str , Any ]) -> None :
72
154
if name not in _targets :
73
155
print (f'unknown target: { name } ' , file = sys .stderr )
74
156
exit (1 )
75
157
158
+ if not _decide_target_exec (name ):
159
+ return
160
+
76
161
if callable (fn := target_globals [name ]):
77
162
t_data = _get_target_data (name )
78
163
if t_data :
79
- for d in t_data .depends :
80
- _exec_target (d , opts , target_globals )
164
+ for d in t_data .depends .targets :
165
+ if d not in _targets :
166
+ print (f'unknown target: { d } ' , file = sys .stderr )
167
+ exit (1 )
168
+ if _decide_target_exec (d ):
169
+ _exec_target (d , opts , target_globals )
81
170
fn (opts )
0 commit comments