PyProd 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- pyprod/__init__.py +0 -0
- pyprod/__main__.py +4 -0
- pyprod/main.py +118 -0
- pyprod/prod.py +532 -0
- pyprod/utils.py +17 -0
- pyprod/venv.py +47 -0
- pyprod-0.1.0.dist-info/METADATA +76 -0
- pyprod-0.1.0.dist-info/RECORD +11 -0
- pyprod-0.1.0.dist-info/WHEEL +4 -0
- pyprod-0.1.0.dist-info/entry_points.txt +2 -0
- pyprod-0.1.0.dist-info/licenses/LICENSE +21 -0
pyprod/__init__.py
ADDED
File without changes
|
pyprod/__main__.py
ADDED
pyprod/main.py
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
import argparse
|
2
|
+
import asyncio
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
import pyprod
|
9
|
+
import pyprod.prod
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
parser = argparse.ArgumentParser(
|
14
|
+
description="""PyProd - More makable than make""",
|
15
|
+
)
|
16
|
+
|
17
|
+
parser.add_argument(
|
18
|
+
"-f", "--file", help="Use FILE as the Prodfile (default: 'PRODFILE.py')"
|
19
|
+
)
|
20
|
+
|
21
|
+
parser.add_argument(
|
22
|
+
"-j",
|
23
|
+
"--job",
|
24
|
+
type=int,
|
25
|
+
default=1,
|
26
|
+
help="Allow up to N jobs to run simultaneously (default: 1)",
|
27
|
+
)
|
28
|
+
|
29
|
+
parser.add_argument(
|
30
|
+
"-C",
|
31
|
+
"--directory",
|
32
|
+
dest="directory",
|
33
|
+
help="Change to DIRECTORY before performing any operations",
|
34
|
+
)
|
35
|
+
|
36
|
+
parser.add_argument(
|
37
|
+
"-v",
|
38
|
+
dest="verbose",
|
39
|
+
action="count",
|
40
|
+
default=0,
|
41
|
+
help="Increase verbosity level (default: 0)",
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
parser.add_argument("targets", nargs="*", help="Build targets")
|
46
|
+
|
47
|
+
|
48
|
+
def print_exc(e):
|
49
|
+
match pyprod.args.verbose:
|
50
|
+
case 0:
|
51
|
+
logger.error("%s: %s", type(e).__name__, e)
|
52
|
+
case 1:
|
53
|
+
logger.error("%r", e)
|
54
|
+
case _:
|
55
|
+
logger.exception("Terminated by exception")
|
56
|
+
|
57
|
+
|
58
|
+
def main():
|
59
|
+
args = pyprod.args = parser.parse_args()
|
60
|
+
pyprod.verbose = args.verbose
|
61
|
+
chdir = args.directory
|
62
|
+
if chdir:
|
63
|
+
os.chdir(chdir)
|
64
|
+
|
65
|
+
if "" not in sys.path:
|
66
|
+
sys.path.insert(0, "")
|
67
|
+
|
68
|
+
match args.verbose:
|
69
|
+
case 0:
|
70
|
+
level = logging.ERROR
|
71
|
+
case 1:
|
72
|
+
level = logging.INFO
|
73
|
+
case _:
|
74
|
+
level = logging.DEBUG
|
75
|
+
|
76
|
+
logging.basicConfig(level=level, format="%(asctime)s: %(message)s")
|
77
|
+
|
78
|
+
if args.file:
|
79
|
+
pyprodfiles = [args.file]
|
80
|
+
else:
|
81
|
+
pyprodfiles = ["Prodfile.py"]
|
82
|
+
|
83
|
+
for mod in pyprodfiles:
|
84
|
+
mod = pyprod.modulefile = Path(mod)
|
85
|
+
if mod.is_file():
|
86
|
+
break
|
87
|
+
else:
|
88
|
+
sys.exit("No make module found")
|
89
|
+
|
90
|
+
params = {}
|
91
|
+
targets = []
|
92
|
+
for target in args.targets:
|
93
|
+
if "=" in target:
|
94
|
+
name, value = target.split("=", 1)
|
95
|
+
params[name] = value
|
96
|
+
else:
|
97
|
+
targets.append(target)
|
98
|
+
|
99
|
+
try:
|
100
|
+
# load module
|
101
|
+
prod = pyprod.prod.Prod(mod, args.job, params)
|
102
|
+
|
103
|
+
# select targets
|
104
|
+
if not targets:
|
105
|
+
target = prod.get_default_target()
|
106
|
+
if not target:
|
107
|
+
sys.exit("No default target")
|
108
|
+
targets = [target]
|
109
|
+
|
110
|
+
ret = asyncio.run(prod.start(targets))
|
111
|
+
if not ret:
|
112
|
+
print(f"Nothing to be done for {targets}")
|
113
|
+
|
114
|
+
except pyprod.prod.HandledExceptionError:
|
115
|
+
# ignored
|
116
|
+
pass
|
117
|
+
except Exception as e:
|
118
|
+
print_exc(e)
|
pyprod/prod.py
ADDED
@@ -0,0 +1,532 @@
|
|
1
|
+
import asyncio
|
2
|
+
import concurrent.futures
|
3
|
+
import datetime
|
4
|
+
import importlib
|
5
|
+
import importlib.machinery
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
import re
|
9
|
+
import shlex
|
10
|
+
import shutil
|
11
|
+
import subprocess
|
12
|
+
import sys
|
13
|
+
from collections import defaultdict
|
14
|
+
from collections.abc import Collection
|
15
|
+
from dataclasses import dataclass, field
|
16
|
+
from fnmatch import fnmatch, translate
|
17
|
+
from pathlib import Path
|
18
|
+
|
19
|
+
import pyprod
|
20
|
+
|
21
|
+
from .utils import flatten, unique_list
|
22
|
+
from .venv import pip
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
class CircularReferenceError(Exception):
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
31
|
+
class NoRuleToMakeTargetError(Exception):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
class InvalidRuleError(Exception):
|
36
|
+
pass
|
37
|
+
|
38
|
+
|
39
|
+
class HandledExceptionError(Exception):
|
40
|
+
pass
|
41
|
+
|
42
|
+
|
43
|
+
omit = object()
|
44
|
+
|
45
|
+
|
46
|
+
def run(
|
47
|
+
*args,
|
48
|
+
echo=True,
|
49
|
+
shell=None,
|
50
|
+
stdout=False,
|
51
|
+
cwd=None,
|
52
|
+
text=True,
|
53
|
+
check=True,
|
54
|
+
):
|
55
|
+
match args:
|
56
|
+
case [[*tokens]]:
|
57
|
+
if shell is None:
|
58
|
+
shell = False
|
59
|
+
args = [list(str(t) for t in flatten(tokens))]
|
60
|
+
sargs = str(tokens)
|
61
|
+
case _:
|
62
|
+
args = [" ".join(str(a) for a in flatten(args))]
|
63
|
+
sargs = args[0]
|
64
|
+
if shell is None:
|
65
|
+
shell = True
|
66
|
+
|
67
|
+
if stdout is True:
|
68
|
+
stdout = subprocess.PIPE
|
69
|
+
elif stdout is False:
|
70
|
+
stdout = None
|
71
|
+
|
72
|
+
if echo:
|
73
|
+
print("run: %s" % sargs, file=sys.stderr)
|
74
|
+
try:
|
75
|
+
ret = subprocess.run(
|
76
|
+
*args, cwd=cwd, shell=shell, stdout=stdout, text=text, check=check
|
77
|
+
)
|
78
|
+
except subprocess.CalledProcessError as e:
|
79
|
+
match pyprod.verbose:
|
80
|
+
case 0:
|
81
|
+
logger.debug("command failed: %s %s", str(e), sargs)
|
82
|
+
case _:
|
83
|
+
logger.warning("command failed: %s %s", str(e), sargs)
|
84
|
+
|
85
|
+
raise HandledExceptionError() from e
|
86
|
+
|
87
|
+
return ret
|
88
|
+
|
89
|
+
|
90
|
+
def capture(*args, echo=True, cwd=None, check=True, text=True, shell=None):
|
91
|
+
ret = run(
|
92
|
+
*args, echo=echo, cwd=cwd, check=check, text=text, stdout=True, shell=shell
|
93
|
+
)
|
94
|
+
ret = ret.stdout or ""
|
95
|
+
ret = ret.rstrip("\n")
|
96
|
+
return ret
|
97
|
+
|
98
|
+
|
99
|
+
def glob(path, dir="."):
|
100
|
+
ret = []
|
101
|
+
for c in Path(dir).glob(path):
|
102
|
+
# ignore dot files
|
103
|
+
if any(p.startswith(".") for p in c.parts):
|
104
|
+
continue
|
105
|
+
ret.append(c)
|
106
|
+
return ret
|
107
|
+
|
108
|
+
|
109
|
+
def rule_to_re(rule):
|
110
|
+
if not isinstance(rule, (str, Path)):
|
111
|
+
raise InvalidRuleError(rule)
|
112
|
+
|
113
|
+
srule = str(rule)
|
114
|
+
srule = translate(srule)
|
115
|
+
srule = replace_pattern(srule, "(?P<stem>.*)", maxreplace=1)
|
116
|
+
if isinstance(rule, Path):
|
117
|
+
return Path(srule)
|
118
|
+
return srule
|
119
|
+
|
120
|
+
|
121
|
+
def replace_pattern(rule, replaceto, *, maxreplace=None):
|
122
|
+
n = 0
|
123
|
+
s_rule = str(rule)
|
124
|
+
|
125
|
+
def f(m):
|
126
|
+
nonlocal n
|
127
|
+
if len(m[0]) == 2:
|
128
|
+
return "%"
|
129
|
+
else:
|
130
|
+
n += 1
|
131
|
+
if maxreplace is not None:
|
132
|
+
if n > maxreplace:
|
133
|
+
# contains multiple '%'
|
134
|
+
raise InvalidRuleError(s_rule)
|
135
|
+
return replaceto
|
136
|
+
|
137
|
+
s_rule = re.sub("%%|%", f, s_rule)
|
138
|
+
if isinstance(rule, Path):
|
139
|
+
return Path(s_rule)
|
140
|
+
return s_rule
|
141
|
+
|
142
|
+
|
143
|
+
@dataclass
|
144
|
+
class Rule:
|
145
|
+
targets: list
|
146
|
+
pattern: str | None
|
147
|
+
depends: list
|
148
|
+
uses: list | None
|
149
|
+
builder: str | None
|
150
|
+
|
151
|
+
orig_targets: list = field(init=False)
|
152
|
+
orig_depends: list = field(init=False)
|
153
|
+
orig_uses: list = field(init=False)
|
154
|
+
|
155
|
+
def __post_init__(self):
|
156
|
+
self.orig_targets = self.targets
|
157
|
+
self.orig_depends = self.depends
|
158
|
+
self.orig_uses = self.uses
|
159
|
+
|
160
|
+
self.targets = [rule_to_re(r) for r in flatten(self.targets)]
|
161
|
+
self.depends = list(flatten(self.depends))
|
162
|
+
if self.pattern:
|
163
|
+
self.pattern = rule_to_re(self.pattern)
|
164
|
+
if self.uses:
|
165
|
+
self.uses = list(flatten(self.uses))
|
166
|
+
|
167
|
+
def __call__(self, f):
|
168
|
+
self.builder = f
|
169
|
+
return f
|
170
|
+
|
171
|
+
|
172
|
+
class Rules:
|
173
|
+
def __init__(self):
|
174
|
+
self.rules = []
|
175
|
+
self.tree = defaultdict(set)
|
176
|
+
self._detect_loop = set()
|
177
|
+
self.frozen = False
|
178
|
+
|
179
|
+
def add_rule(self, targets, pattern, depends, uses, builder):
|
180
|
+
if self.frozen:
|
181
|
+
raise RuntimeError("No new rule can be added after initialization")
|
182
|
+
|
183
|
+
dep = Rule(
|
184
|
+
targets,
|
185
|
+
pattern,
|
186
|
+
depends,
|
187
|
+
uses,
|
188
|
+
builder,
|
189
|
+
)
|
190
|
+
self.rules.append(dep)
|
191
|
+
return dep
|
192
|
+
|
193
|
+
def rule(self, target, pattern=None, depends=(), uses=()):
|
194
|
+
if (not isinstance(depends, Collection)) or isinstance(depends, str):
|
195
|
+
depends = [depends]
|
196
|
+
if (not isinstance(uses, Collection)) or isinstance(uses, str):
|
197
|
+
uses = [uses]
|
198
|
+
dep = self.add_rule([target], pattern, depends, uses, None)
|
199
|
+
return dep
|
200
|
+
|
201
|
+
def iter_rule(self, name):
|
202
|
+
name = str(name)
|
203
|
+
for dep in self.rules:
|
204
|
+
for target in dep.targets:
|
205
|
+
m = re.fullmatch(str(target), name)
|
206
|
+
if m:
|
207
|
+
stem = None
|
208
|
+
d = m.groupdict().get("stem", None)
|
209
|
+
if d is not None:
|
210
|
+
stem = d
|
211
|
+
elif dep.pattern:
|
212
|
+
m = re.fullmatch(str(dep.pattern), name)
|
213
|
+
d = m.groupdict().get("stem", None)
|
214
|
+
if d is not None:
|
215
|
+
stem = d
|
216
|
+
|
217
|
+
depends = [replace_pattern(r, stem) for r in dep.depends]
|
218
|
+
uses = [replace_pattern(r, stem) for r in dep.uses]
|
219
|
+
|
220
|
+
yield depends, uses, dep
|
221
|
+
break
|
222
|
+
|
223
|
+
def get_dep_names(self, name):
|
224
|
+
assert name
|
225
|
+
ret_depends = []
|
226
|
+
ret_uses = []
|
227
|
+
|
228
|
+
for depends, uses, dep in self.iter_rule(name):
|
229
|
+
if dep.builder:
|
230
|
+
continue
|
231
|
+
|
232
|
+
ret_depends.extend(depends)
|
233
|
+
ret_uses.extend(uses)
|
234
|
+
|
235
|
+
return unique_list(ret_depends), unique_list(ret_uses)
|
236
|
+
|
237
|
+
def select_first_target(self):
|
238
|
+
for dep in self.rules:
|
239
|
+
for target in dep.orig_targets:
|
240
|
+
s_target = str(target)
|
241
|
+
# pattern?
|
242
|
+
matches = re.finditer(r"%%|%", s_target)
|
243
|
+
if any((len(m[0]) == 1) for m in matches):
|
244
|
+
# has %
|
245
|
+
continue
|
246
|
+
return target
|
247
|
+
|
248
|
+
def select_builder(self, name):
|
249
|
+
for depends, uses, dep in self.iter_rule(name):
|
250
|
+
if not dep.builder:
|
251
|
+
continue
|
252
|
+
return depends, uses, dep
|
253
|
+
|
254
|
+
def build_tree(self, name, lv=1):
|
255
|
+
self.frozen = True
|
256
|
+
|
257
|
+
name = str(name)
|
258
|
+
if name in self._detect_loop:
|
259
|
+
raise CircularReferenceError(name)
|
260
|
+
self._detect_loop.add(name)
|
261
|
+
try:
|
262
|
+
if name in self.tree:
|
263
|
+
return
|
264
|
+
deps, uses = self.get_dep_names(name)
|
265
|
+
depends = deps + uses
|
266
|
+
|
267
|
+
selected = self.select_builder(name)
|
268
|
+
if selected:
|
269
|
+
build_deps, build_uses, _ = selected
|
270
|
+
depends.extend(build_deps)
|
271
|
+
depends.extend(build_uses)
|
272
|
+
|
273
|
+
depends = unique_list(depends)
|
274
|
+
self.tree[name].update(str(d) for d in depends)
|
275
|
+
for dep in depends:
|
276
|
+
self.build_tree(dep, lv=lv + 1)
|
277
|
+
|
278
|
+
finally:
|
279
|
+
self._detect_loop.remove(name)
|
280
|
+
|
281
|
+
|
282
|
+
class Checkers:
|
283
|
+
def __init__(self):
|
284
|
+
self.checkers = []
|
285
|
+
|
286
|
+
def get_checker(self, name):
|
287
|
+
for targets, f in self.checkers:
|
288
|
+
for target in targets:
|
289
|
+
if fnmatch(name, str(target)):
|
290
|
+
return f
|
291
|
+
|
292
|
+
def add_check(self, target, f):
|
293
|
+
self.checkers.append((list(t for t in flatten(target)), f))
|
294
|
+
|
295
|
+
def check(self, target):
|
296
|
+
def deco(f):
|
297
|
+
self.add_check(target, f)
|
298
|
+
return f
|
299
|
+
|
300
|
+
return deco
|
301
|
+
|
302
|
+
|
303
|
+
MAX_TS = 1 << 63
|
304
|
+
|
305
|
+
|
306
|
+
def is_file_exists(name):
|
307
|
+
return os.path.getmtime(name)
|
308
|
+
|
309
|
+
|
310
|
+
class Exists:
|
311
|
+
def __init__(self, name, exists, ts=None):
|
312
|
+
self.name = name
|
313
|
+
self.exists = exists
|
314
|
+
self.ts = ts if exists else 0
|
315
|
+
|
316
|
+
def __repr__(self):
|
317
|
+
return f"Exists({self.name!r}, {self.exists!r}, {self.ts!r})"
|
318
|
+
|
319
|
+
|
320
|
+
class Params:
|
321
|
+
def __init__(self, params):
|
322
|
+
if params:
|
323
|
+
self.__dict__.update(params)
|
324
|
+
|
325
|
+
def __getattr__(self, name):
|
326
|
+
# never raise AttributeError
|
327
|
+
return ""
|
328
|
+
|
329
|
+
def get(self, name, default=None):
|
330
|
+
# hasattr cannot be used since __getattr__ never raise AttributeError
|
331
|
+
if name in self.__dict__:
|
332
|
+
return getattr(self, name)
|
333
|
+
else:
|
334
|
+
return default
|
335
|
+
|
336
|
+
|
337
|
+
class Envs:
|
338
|
+
def __getattr__(self, name):
|
339
|
+
return os.environ.get(name, "")
|
340
|
+
|
341
|
+
def __setattr__(self, name, value):
|
342
|
+
os.environ[name] = str(value)
|
343
|
+
|
344
|
+
def __getitem__(self, name):
|
345
|
+
return os.environ.get(name, "")
|
346
|
+
|
347
|
+
def __setitem__(self, name, value):
|
348
|
+
os.environ[name] = str(value)
|
349
|
+
|
350
|
+
def __delitem__(self, name):
|
351
|
+
if name in os.environ:
|
352
|
+
del os.environ[name]
|
353
|
+
|
354
|
+
def get(self, name, default=None):
|
355
|
+
return os.environ.get(name, default=default)
|
356
|
+
|
357
|
+
|
358
|
+
class Prod:
|
359
|
+
def __init__(self, modulefile, njobs=1, params=None):
|
360
|
+
self.modulefile = Path(modulefile)
|
361
|
+
self.rules = Rules()
|
362
|
+
self.checkers = Checkers()
|
363
|
+
if njobs > 1:
|
364
|
+
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=njobs)
|
365
|
+
else:
|
366
|
+
self.executor = None
|
367
|
+
self.params = Params(params)
|
368
|
+
self.buildings = {}
|
369
|
+
self.module = self.load_pyprodfile(self.modulefile)
|
370
|
+
self.built = 0 # number of build execused
|
371
|
+
|
372
|
+
def get_module_globals(self):
|
373
|
+
globals = {
|
374
|
+
"pip": pip,
|
375
|
+
"rule": self.rules.rule,
|
376
|
+
"check": self.checkers.check,
|
377
|
+
"Path": Path,
|
378
|
+
"run": run,
|
379
|
+
"capture": capture,
|
380
|
+
"glob": glob,
|
381
|
+
"MAX_TS": MAX_TS,
|
382
|
+
"environ": Envs(),
|
383
|
+
"shutil": shutil,
|
384
|
+
"quote": shlex.quote,
|
385
|
+
"params": self.params,
|
386
|
+
}
|
387
|
+
return globals
|
388
|
+
|
389
|
+
def load_pyprodfile(self, pyprodfile: Path) -> dict:
|
390
|
+
spath = os.fspath(pyprodfile)
|
391
|
+
loader = importlib.machinery.SourceFileLoader(pyprodfile.stem, spath)
|
392
|
+
spec = importlib.util.spec_from_file_location(
|
393
|
+
pyprodfile.stem, spath, loader=loader
|
394
|
+
)
|
395
|
+
mod = importlib.util.module_from_spec(spec)
|
396
|
+
|
397
|
+
# exec module
|
398
|
+
mod.__dict__.update(self.get_module_globals())
|
399
|
+
|
400
|
+
spec.loader.exec_module(mod)
|
401
|
+
return mod
|
402
|
+
|
403
|
+
async def run_in_executor(self, func, *args, **kwargs):
|
404
|
+
if self.executor:
|
405
|
+
loop = asyncio.get_running_loop()
|
406
|
+
return await loop.run_in_executor(
|
407
|
+
self.executor, lambda: func(*args, **kwargs)
|
408
|
+
)
|
409
|
+
else:
|
410
|
+
return func(*args, **kwargs)
|
411
|
+
|
412
|
+
def get_default_target(self):
|
413
|
+
return self.rules.select_first_target()
|
414
|
+
|
415
|
+
async def start(self, deps):
|
416
|
+
self.built = 0
|
417
|
+
names = []
|
418
|
+
for name in deps:
|
419
|
+
if isinstance(name, str):
|
420
|
+
value = getattr(self.module, name, None)
|
421
|
+
if value:
|
422
|
+
names.append(value)
|
423
|
+
else:
|
424
|
+
names.append(name)
|
425
|
+
else:
|
426
|
+
names.append(name)
|
427
|
+
|
428
|
+
builds = []
|
429
|
+
waitings = []
|
430
|
+
for obj in flatten(names):
|
431
|
+
if isinstance(obj, str | Path):
|
432
|
+
builds.append(obj)
|
433
|
+
elif isinstance(obj, Rule):
|
434
|
+
raise InvalidRuleError(obj)
|
435
|
+
elif callable(obj):
|
436
|
+
self.built += 1
|
437
|
+
task = asyncio.create_task(self.run_in_executor(obj))
|
438
|
+
waitings.append(task)
|
439
|
+
else:
|
440
|
+
raise InvalidRuleError(obj)
|
441
|
+
|
442
|
+
await self.build(builds)
|
443
|
+
await asyncio.gather(*waitings)
|
444
|
+
return self.built
|
445
|
+
|
446
|
+
async def build(self, deps):
|
447
|
+
tasks = []
|
448
|
+
waits = []
|
449
|
+
for dep in deps:
|
450
|
+
if dep not in self.buildings:
|
451
|
+
ev = asyncio.Event()
|
452
|
+
self.buildings[dep] = ev
|
453
|
+
task = self.run(dep)
|
454
|
+
tasks.append((dep, task))
|
455
|
+
waits.append(ev)
|
456
|
+
else:
|
457
|
+
obj = self.buildings[dep]
|
458
|
+
if isinstance(obj, asyncio.Event):
|
459
|
+
waits.append(obj)
|
460
|
+
|
461
|
+
for dep, task in tasks:
|
462
|
+
ev = self.buildings[dep]
|
463
|
+
try:
|
464
|
+
self.buildings[dep] = await task
|
465
|
+
finally:
|
466
|
+
ev.set()
|
467
|
+
|
468
|
+
events = [ev.wait() for ev in waits]
|
469
|
+
await asyncio.gather(*events)
|
470
|
+
|
471
|
+
ts = []
|
472
|
+
for dep in deps:
|
473
|
+
obj = self.buildings[dep]
|
474
|
+
if isinstance(obj, int | float):
|
475
|
+
ts.append(obj)
|
476
|
+
if ts:
|
477
|
+
return max(ts)
|
478
|
+
return 0
|
479
|
+
|
480
|
+
async def is_exists(self, name):
|
481
|
+
checker = self.checkers.get_checker(name)
|
482
|
+
try:
|
483
|
+
if checker:
|
484
|
+
ret = await self.run_in_executor(checker, name)
|
485
|
+
else:
|
486
|
+
ret = await self.run_in_executor(is_file_exists, name)
|
487
|
+
except FileNotFoundError:
|
488
|
+
ret = False
|
489
|
+
|
490
|
+
if not ret:
|
491
|
+
return Exists(name, False)
|
492
|
+
if isinstance(ret, datetime.datetime):
|
493
|
+
ret = ret.timestamp()
|
494
|
+
if ret < 0:
|
495
|
+
ret = MAX_TS
|
496
|
+
return Exists(name, True, ret)
|
497
|
+
|
498
|
+
async def run(self, name): # -> Any | int:
|
499
|
+
s_name = str(name)
|
500
|
+
|
501
|
+
self.rules.build_tree(s_name)
|
502
|
+
deps, uses = self.rules.get_dep_names(s_name)
|
503
|
+
selected = self.rules.select_builder(s_name)
|
504
|
+
if selected:
|
505
|
+
build_deps, build_uses, builder = selected
|
506
|
+
deps = deps + build_deps
|
507
|
+
uses = uses + build_uses
|
508
|
+
|
509
|
+
ts = 0
|
510
|
+
if deps:
|
511
|
+
ts = await self.build(deps)
|
512
|
+
if uses:
|
513
|
+
await self.build(uses)
|
514
|
+
|
515
|
+
exists = await self.is_exists(s_name)
|
516
|
+
|
517
|
+
if not exists.exists:
|
518
|
+
logger.debug("%r does not exists", str(s_name))
|
519
|
+
elif (ts >= MAX_TS) or (exists.ts < ts):
|
520
|
+
logger.debug("%r should be updated", str(s_name))
|
521
|
+
else:
|
522
|
+
logger.debug("%r already exists", str(s_name))
|
523
|
+
|
524
|
+
if not exists.exists and not selected:
|
525
|
+
raise NoRuleToMakeTargetError(s_name)
|
526
|
+
elif selected and ((not exists.exists) or (ts >= MAX_TS) or (exists.ts < ts)):
|
527
|
+
logger.warning("building: %r", s_name)
|
528
|
+
await self.run_in_executor(builder.builder, name, *build_deps)
|
529
|
+
self.built += 1
|
530
|
+
return MAX_TS
|
531
|
+
|
532
|
+
return max(ts, exists.ts)
|
pyprod/utils.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
from collections.abc import Iterable, Sequence
|
2
|
+
|
3
|
+
|
4
|
+
def flatten(seq):
|
5
|
+
if isinstance(seq, str) or (not isinstance(seq, Sequence)):
|
6
|
+
yield seq
|
7
|
+
return
|
8
|
+
|
9
|
+
for item in seq:
|
10
|
+
if isinstance(item, str) or (not isinstance(item, Iterable)):
|
11
|
+
yield item
|
12
|
+
else:
|
13
|
+
yield from flatten(item)
|
14
|
+
|
15
|
+
|
16
|
+
def unique_list(lst):
|
17
|
+
return list({e: None for e in lst}.keys())
|
pyprod/venv.py
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import sys
|
4
|
+
import sysconfig
|
5
|
+
import threading
|
6
|
+
import venv
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
import pyprod
|
10
|
+
|
11
|
+
from .utils import flatten
|
12
|
+
|
13
|
+
THREADID = threading.get_ident()
|
14
|
+
pyprodenv = ".pyprod"
|
15
|
+
venvdir = None
|
16
|
+
|
17
|
+
PYPRODVENV = ".pyprod"
|
18
|
+
|
19
|
+
|
20
|
+
def makevenv(conffile):
|
21
|
+
global venvdir
|
22
|
+
venvdir = Path(".") / f".{conffile.name}{PYPRODVENV}"
|
23
|
+
if not venvdir.is_dir():
|
24
|
+
venv.main([str(venvdir)])
|
25
|
+
|
26
|
+
purelib = sysconfig.get_path(
|
27
|
+
"purelib", scheme="venv", vars={"base": str(venvdir.resolve())}
|
28
|
+
)
|
29
|
+
sys.path.insert(0, purelib)
|
30
|
+
|
31
|
+
os.environ["VIRTUAL_ENV"] = str(venvdir)
|
32
|
+
os.environ["PATH"] = os.path.pathsep.join(
|
33
|
+
[str(venvdir / "bin"), os.environ["PATH"]]
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
def pip(*args):
|
38
|
+
if THREADID != threading.get_ident():
|
39
|
+
raise RuntimeError("pip() should not be called in workder thread")
|
40
|
+
|
41
|
+
if not venvdir:
|
42
|
+
makevenv(pyprod.modulefile)
|
43
|
+
args = flatten(args)
|
44
|
+
subprocess.run(
|
45
|
+
[venvdir / "bin/python", "-m", "pip", "--no-input", "install", *args],
|
46
|
+
check=True,
|
47
|
+
)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: PyProd
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: PyProd: More Makeable than Make
|
5
|
+
Project-URL: Homepage, https://github.com/atsuoishimoto/pyprod
|
6
|
+
Project-URL: Documentation, https://pyprod.readthedocs.io/en/latest/
|
7
|
+
License-File: LICENSE
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Environment :: Console
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
14
|
+
Requires-Python: >=3.10
|
15
|
+
Description-Content-Type: text/x-rst
|
16
|
+
|
17
|
+
PyProd - More Makeable than Make
|
18
|
+
=================================
|
19
|
+
|
20
|
+
PyProd is a Python script that can be used as an alternative to Makefile. By leveraging Python's versatility, it enables you to define build rules and dependencies programmatically, allowing for dynamic configurations, integration with existing Python libraries, and custom build logic not easily achievable with traditional Makefiles. For detailed documentation, please refer to the `official documentation <https://example.com/pyprod-docs>`_.
|
21
|
+
|
22
|
+
|
23
|
+
Features
|
24
|
+
--------
|
25
|
+
|
26
|
+
- Define build rules in Python: Use Python functions to create clear and concise build logic.
|
27
|
+
- Specify dependencies for each rule: Automatically track and resolve dependencies between files, such as source files and headers.
|
28
|
+
- Easily extendable with custom Python functions: Integrate custom logic for specialized tasks, like code linting or deployment.
|
29
|
+
- Manages virtual environments: Automatically create and manage virtual environments for each project, ensuring a clean and isolated build environment.
|
30
|
+
|
31
|
+
Installation
|
32
|
+
--------------
|
33
|
+
|
34
|
+
To install PyProd, simply use pip:
|
35
|
+
|
36
|
+
.. code-block:: sh
|
37
|
+
|
38
|
+
pip install pyprod
|
39
|
+
|
40
|
+
Usage
|
41
|
+
-----
|
42
|
+
|
43
|
+
In PyProd, a traditional Makefile for C can be expressed as a Python script like this:
|
44
|
+
|
45
|
+
|
46
|
+
.. code-block:: python
|
47
|
+
|
48
|
+
CC = "gcc"
|
49
|
+
CFLAGS = "-I."
|
50
|
+
DEPS = "hello.h"
|
51
|
+
OBJS = "hello.o main.o".split()
|
52
|
+
|
53
|
+
@rule("%.o", depends=("%.c", DEPS))
|
54
|
+
def compile(target, src, *deps):
|
55
|
+
run(CC, "-c -o", target, src, CFLAGS)
|
56
|
+
|
57
|
+
@rule("hello.exe", depends=OBJS)
|
58
|
+
def link(target, *objs):
|
59
|
+
run(CC, "-o", target, objs)
|
60
|
+
|
61
|
+
def clean():
|
62
|
+
run("rm -f", OBJS, "hello.exe")
|
63
|
+
|
64
|
+
all = "hello.exe"
|
65
|
+
|
66
|
+
To run the build script, simply execute:
|
67
|
+
|
68
|
+
.. code-block:: sh
|
69
|
+
|
70
|
+
$ cd project
|
71
|
+
$ pyprod
|
72
|
+
|
73
|
+
License
|
74
|
+
-------
|
75
|
+
|
76
|
+
PyProd is licensed under the MIT License. See the `LICENSE <LICENSE>`_ file for more details.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
pyprod/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
pyprod/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
|
3
|
+
pyprod/main.py,sha256=nS0mcLy3Ix7ydnX1Pu5g7ceLzhh6DndwJWO5sjO_qWg,2580
|
4
|
+
pyprod/prod.py,sha256=pVNAc_piwsMo0KaML9AChrm_TnXIeZK_cMT6MZ7abWo,14564
|
5
|
+
pyprod/utils.py,sha256=oiiUkSbeqTazbtJ6gz7ZKqG1OvAeV-nV9u_3Y0DCOOM,401
|
6
|
+
pyprod/venv.py,sha256=_riw56YQvUOSd55u_1m9ElsqPdjM5qVvIZP6dr9Fzt4,1051
|
7
|
+
pyprod-0.1.0.dist-info/METADATA,sha256=KfxGRpJW-_Vjs5O-r9IGOTjdTm29K4R8_eh-8TVMf98,2470
|
8
|
+
pyprod-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
+
pyprod-0.1.0.dist-info/entry_points.txt,sha256=zFycf8BYSMRDTiI0jftmcvtkf9XM4MZ4BL3JaIer_ZM,44
|
10
|
+
pyprod-0.1.0.dist-info/licenses/LICENSE,sha256=OtPgwnlLrsVEYPnTraun5AqftAT5vUv4rIan-qYj7nE,1071
|
11
|
+
pyprod-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Atsuo Ishimoto
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|