-
Notifications
You must be signed in to change notification settings - Fork 0
/
smelt3.py
401 lines (336 loc) · 9.89 KB
/
smelt3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
import sys, os, json, hashlib, inspect, atexit, shutil, subprocess
HELP = """
No targets, settings or options provided.
Options:
--help -> display this info screen
--list -> show a list of all tasks
--all -> perform all named tasks
--clean -> clean cache and force remaking
Setting syntax: NAME=VALUE
"""
# Config
CACHE_FILENAME = ".smelt"
CACHE_SYNC_PERIODIC = False
CACHE_SYNC_ATEXIT = True
SETTING_RC_FILENAME = "smeltrc.cfg"
# Globals
tasklist = {}
cache = {}
is_cache_changed = False
settings = {}
# Core
class TaskNode:
def __init__(self, fun, id, pubname=None, pubdesc=None):
self.fun = fun
self.id = id
self.pubname = pubname
self.pubdesc = pubdesc
self.skip = False
self.setting_tracker = Token({})
self.setting_tracker.set_used()
self.srcs = [self.setting_tracker] # First token will hold used settings
def find_my_taskid():
stack = inspect.stack()
for finfo in stack:
frame = finfo.frame
if "__secret" in frame.f_locals:
return frame.f_locals["__secret"]
elif finfo[3] in tasklist:
return finfo[3]
raise BaseException("Invalid context, no associated task found")
def find_my_tasknode():
return tasklist[find_my_taskid()]
## Setting management
def create_setting(name, default_value):
global settings
settings[name] = default_value
def sett(setting):
tn = find_my_tasknode()
if not (setting in tn.setting_tracker.token):
tn.setting_tracker.token[setting] = settings[setting]
return settings[setting]
def update_setting(line):
(setting, value) = line.split("=")
if setting not in settings:
print(f"Unknown setting '{setting}'")
sys.exit()
settings[setting] = value
def load_setting_rc():
global settings
if not os.path.exists(SETTING_RC_FILENAME):
return
print(f"Loading settings from {SETTING_RC_FILENAME}...")
for line in SETTING_RC_FILENAME:
if "=" in line:
update_setting(line)
## FS utils
def rf(name):
with open(name, 'r') as f:
return f.read()
def rfb(name):
with open(name, 'rb') as f:
return f.read()
def af(name, data):
with open(name, 'a') as f:
return f.write(data)
def wf(name, data):
with open(name, 'w') as f:
return f.write(data)
## Caching
def cache_spawn():
global cache
if not os.path.exists(CACHE_FILENAME):
wf(CACHE_FILENAME, "")
return
for line in rf(CACHE_FILENAME).split('\n'):
try:
(id, sign) = line.split(' ')
cache[id] = sign
except: pass
def cache_sync():
if not is_cache_changed:
return
body = ""
for id in cache:
body += f"{id} {cache[id]}\n"
wf(CACHE_FILENAME, body)
print(f"[SYNC] Cache {CACHE_FILENAME}")
atexit.register(lambda: cache_sync() if CACHE_SYNC_ATEXIT else None )
def cache_get(taskid):
global cache
cache_spawn()
if taskid not in cache:
return None
return cache[taskid]
def cache_set(taskid, sign):
global cache, is_cache_changed
cache[taskid] = sign
is_cache_changed = True
if CACHE_SYNC_PERIODIC:
cache_sync()
"""
def cache_get(taskid):
cache_spawn()
for line in rf(CACHE_FILENAME).split('\n'):
try:
(id, sign) = line.split(' ')
if id == taskid:
return sign
except: pass
return None
def cache_set(taskid, sign):
cache_spawn()
body = ""
present = False
for line in rf(CACHE_FILENAME).split('\n'):
try:
(id, csign) = line.split(' ')
except:
continue
if id == taskid:
present = True
body += f"{taskid} {sign}\n"
else:
body += f"{id} {csign}\n"
if not present:
body += f"{taskid} {sign}\n"
wf(CACHE_FILENAME, body)
"""
## Interaction
def cli():
if len(sys.argv) == 1:
print(HELP)
return
load_setting_rc()
for arg in sys.argv[1:]:
if arg[0:2] == "--":
if arg == "--help":
print(HELP)
return
elif arg == "--list":
print("Available tasks:")
for tn in tasklist.values():
if tn.pubname is not None:
print(f"--> {tn.pubname}", end='')
if tn.pubdesc is not None:
print(" -", tn.pubdesc)
else:
print('')
print("Avaiable settings:")
for s in settings:
print(f"{s}={settings[s]}")
return
elif arg == "--all":
for task in tasklist.values():
if task.pubname is not None:
do_task(task.pubname)
elif arg == "--clean":
wf(CACHE_FILENAME, "")
else:
print(f"No option '{arg}' found")
elif "=" in arg:
update_setting(arg)
else:
do_task(arg)
def do_task(name):
print(f"[GOAL] {name}")
validpubname = False
for tn in tasklist.values():
if tn.pubname == name:
final = tn.fun()
if final == None:
validpubname = True
break
if type(final) != type([]):
final = [final]
print("[DONE] ", end="")
for a in final:
a.set_used()
print(a.display(), end=" ")
print()
validpubname = True
if not validpubname:
print(f"Could not find task with public name '{name}'")
def use(art):
# Flatten lists and dicts
if type(art) == type([]):
for a in art:
use(a)
return art
elif type(art) == type({}):
for val in art.values():
use(val)
return art
find_my_tasknode().srcs.append(art)
art.set_used()
return art
## Artifacts
class Artifact:
## system
def __init__(self):
self.used = False
def set_used(self):
self.used = True
def is_used(self):
return self.used
def __del__(self):
if not self.is_used():
print("[WARN] Dropped an unused artifact")
# redefinable
def identify(self) -> str:
raise BaseException('undefined artifact feature')
def exists(self) -> bool:
raise BaseException('undefined artifact feature')
def display(self) -> str:
raise BaseException('undefined artifact feature')
class Token(Artifact):
def __init__(self, token):
super().__init__()
self.token = token
def identify(self):
return str(self.token)
def exists(self):
return True
def display(self) -> str:
return f"Token({self.token})"
class File(Artifact):
def __init__(self, fname, mtime=False, hash=True, size=True):
if type(fname) == type(self):
self = fname
return
super().__init__()
self.fname = fname
self.content = None
self.mtime = None
self.size = None
if os.path.exists(fname):
if hash:
self.content = rfb(fname).decode('latin-1')
if mtime:
self.mtime = os.path.getmtime(fname)
if size:
self.size = os.path.getsize(fname)
def exists(self):
return os.path.exists(self.fname)
def identify(self):
return f"{self.fname}{self.content}{self.mtime}{self.size}"
def display(self) -> str:
return f"File({self.fname})"
def __str__(self):
return self.fname
# Tools
def file_tree(directory: str):
result = {}
for e in os.listdir(directory):
if os.path.isfile(directory + "/" + e):
result[e] = File(directory + "/" + e)
elif os.path.isdir(directory + "/" + e):
result[e] = file_tree(directory + "/" + e)
return result
## Minimalization
def grok_sign(arr):
sign = ""
for src in arr:
if not src.exists():
print("[ERROR] Missing artifact:", src.display())
sys.exit()
sha256 = hashlib.sha256()
txt = json.dumps(src.identify()).encode('utf-8')
sha256.update(txt)
sign += sha256.hexdigest() + "+"
return sign
def check4skip():
tnode = find_my_tasknode()
# Dont recompute if we know nothing has changed. This assumes that sources are not modified during builds.
if tnode.skip:
return True
sign = grok_sign(tnode.srcs)
cached_sign = cache_get(tnode.id)
if cached_sign == sign:
tnode.skip = True
print(f'[SKIP] {tnode.id} ({tnode.pubname})')
return True
print(f'[EXEC] {tnode.id} ({tnode.pubname})')
return False
## Actions
def shell(cmd):
if check4skip():
return
print(cmd)
ret = subprocess.call(cmd, shell=True)
if ret != 0:
print(f"[ERROR] While shell processing: {cmd}")
sys.exit(ret)
def copy(src, dest):
if check4skip():
return
print(f"> copy {src} to {dest}")
shutil.copy(src, dest)
def delete(fname, ignore_error=False):
if check4skip():
return
print("> delete", fname)
if ignore_error:
try:
os.remove(fname)
except: pass
else:
os.remove(fname)
## Tasks
def task(name=None, desc=None):
def real_decorator(f):
def inner(__secret=f.__name__, **kwargs):
tnode = find_my_tasknode()
scriptdep = File(sys.argv[0])
scriptdep.set_used()
tnode.srcs.append(scriptdep)
result = f(**kwargs)
if not tnode.skip:
cache_set(tnode.id, grok_sign(tnode.srcs))
# clearing sources
tnode.srcs = []
return result
id = f.__name__
tasklist[id] = TaskNode(inner, id, name, desc)
return inner
return real_decorator