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 ADDED
File without changes
pyprod/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyprod = pyprod.main:main
@@ -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.