PyProd 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyprod-0.4.0 → pyprod-0.6.0}/.gitignore +3 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/PKG-INFO +2 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/commandline.rst +6 -3
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/conf.py +1 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/prodfile.rst +29 -17
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/releasenotes.rst +13 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/pyproject.toml +7 -2
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/build-c/Makefile +1 -1
- pyprod-0.4.0/samples/generate-doc/PRODFILE.py → pyprod-0.6.0/samples/generate-doc/Prodfile.py +1 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/md-to-pdf/Prodfile.py +1 -1
- pyprod-0.6.0/src/pyprod/__init__.py +1 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/src/pyprod/main.py +23 -2
- {pyprod-0.4.0 → pyprod-0.6.0}/src/pyprod/prod.py +101 -46
- {pyprod-0.4.0 → pyprod-0.6.0}/src/pyprod/utils.py +3 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/src/pyprod/venv.py +10 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/tests/conftest.py +1 -1
- {pyprod-0.4.0 → pyprod-0.6.0}/tests/test_prod.py +18 -24
- pyprod-0.4.0/.python-version +0 -1
- pyprod-0.4.0/.readthedocs.yaml +0 -20
- pyprod-0.4.0/samples/generate-doc/.gitignore +0 -2
- pyprod-0.4.0/tests/__init__.py +0 -0
- pyprod-0.4.0/uv.lock +0 -737
- {pyprod-0.4.0 → pyprod-0.6.0}/.github/workflows/publish.yml +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/.github/workflows/test.yml +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/LICENSE +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/README.rst +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/Makefile +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/index.rst +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/make.bat +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/pyprod2.png +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/quickstart.rst +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/docs/requirements.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/pyprod.webp +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/pyprod2.png +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/build-c/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/build-c/hello.c +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/build-c/hello.h +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/build-c/main.c +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/generate-doc/a.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/generate-doc/b.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/generate-doc/c.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/generate-doc/inc1.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/generate-doc/inc2.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/md-to-pdf/doc.md +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/md-to-pdf/md_to_html.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/md-to-pdf/template.html +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/s3files/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/s3files/S3TEST.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/tutorial-1/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/samples/tutorial-2/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/src/pyprod/__main__.py +0 -0
- {pyprod-0.4.0/src/pyprod → pyprod-0.6.0/tests}/__init__.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/tests/test_prodfuncs.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/tests/test_rule.py +0 -0
- {pyprod-0.4.0 → pyprod-0.6.0}/tests/utils.py +0 -0
@@ -1,3 +1,5 @@
|
|
1
|
+
.*
|
2
|
+
!.github
|
1
3
|
# Byte-compiled / optimized / DLL files
|
2
4
|
__pycache__/
|
3
5
|
*.py[cod]
|
@@ -98,7 +100,7 @@ ipython_config.py
|
|
98
100
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99
101
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100
102
|
# commonly ignored for libraries.
|
101
|
-
|
103
|
+
uv.lock
|
102
104
|
|
103
105
|
# poetry
|
104
106
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: PyProd
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: PyProd: More Makeable than Make
|
5
5
|
Project-URL: Homepage, https://github.com/atsuoishimoto/pyprod
|
6
6
|
Project-URL: Documentation, https://pyprod.readthedocs.io/en/latest/
|
@@ -12,6 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
13
13
|
Classifier: Topic :: Software Development :: Build Tools
|
14
14
|
Requires-Python: >=3.10
|
15
|
+
Requires-Dist: python-dateutil
|
15
16
|
Description-Content-Type: text/x-rst
|
16
17
|
|
17
18
|
PyProd - More Makeable than Make
|
@@ -5,16 +5,19 @@ Command line options
|
|
5
5
|
------------------------
|
6
6
|
|
7
7
|
|
8
|
-
usage: pyprod [-h] [-C DIRECTORY] [-f FILE] [-j JOB] [-r] [-v] [targets ...]
|
8
|
+
usage: pyprod [-h] [-C DIRECTORY] [-f FILE] [-j JOB] [-r] [-g] [-v] [targets ...]
|
9
|
+
|
10
|
+
PyProd - More makable than make
|
9
11
|
|
10
12
|
positional arguments:
|
11
|
-
targets Build targets
|
13
|
+
targets Build targets
|
12
14
|
|
13
15
|
options:
|
14
16
|
-h, --help show this help message and exit
|
15
17
|
-C, --directory DIRECTORY
|
16
18
|
Change to DIRECTORY before performing any operations
|
17
|
-
-f, --file FILE Use FILE as the Prodfile (default: '
|
19
|
+
-f, --file FILE Use FILE as the Prodfile (default: 'Prodfile.py')
|
18
20
|
-j, --job JOB Allow up to N jobs to run simultaneously (default: 1)
|
19
21
|
-r, --rebuild Rebuild all
|
22
|
+
-g, --use-git Get file timestamps from Git
|
20
23
|
-v Increase verbosity level (default: 0)
|
@@ -7,7 +7,7 @@
|
|
7
7
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
8
8
|
|
9
9
|
project = "PyProd"
|
10
|
-
copyright = "2024, Atsuo Ishimoto"
|
10
|
+
copyright = "2024-2025, Atsuo Ishimoto"
|
11
11
|
author = "Atsuo Ishimoto"
|
12
12
|
|
13
13
|
# -- General configuration ---------------------------------------------------
|
@@ -142,19 +142,17 @@ Task definition
|
|
142
142
|
|
143
143
|
A task is similar to a rule but does not have a target and is always executed when it is depended upon.
|
144
144
|
|
145
|
-
.. py:function:: @task(*, name=None,
|
145
|
+
.. py:function:: @task(*, name=None, uses=(), default=False)
|
146
146
|
|
147
147
|
Defines a task to be executed.
|
148
148
|
|
149
149
|
:param name: The name of the task. Defaults to the function name.
|
150
150
|
:type name: str
|
151
151
|
|
152
|
-
:param
|
153
|
-
:type depends: str | Path | list[str | Path]
|
154
|
-
|
155
|
-
:param uses: Specify the dependencies of the target file. Unline the ``depends`` parameter, ``uses`` are not passed to the task function.
|
152
|
+
:param uses: Specify the dependencies of the target file.
|
156
153
|
:type uses: str | Path | list[str | Path]
|
157
154
|
|
155
|
+
:param default: If True, this task will be executed when no target is specified in the command-line arguments.
|
158
156
|
|
159
157
|
.. code-block:: python
|
160
158
|
|
@@ -180,24 +178,32 @@ In addition to the ``@rule`` and ``@check`` decorators, PyProd provides several
|
|
180
178
|
|
181
179
|
The following built-ins are available:
|
182
180
|
|
183
|
-
|
181
|
+
|
182
|
+
.. py:function:: build(*deps)
|
184
183
|
|
185
184
|
Schedule dependencies. The specified deps are built sequentially after the current build completes.
|
186
185
|
|
187
|
-
:param
|
186
|
+
:param deps: name of dependencies to be built.
|
187
|
+
|
188
|
+
Example:
|
189
|
+
|
190
|
+
.. code-block:: python
|
191
|
+
|
192
|
+
@task
|
193
|
+
def rebuild():
|
194
|
+
build(clean, EXE)
|
188
195
|
|
189
196
|
.. py:function:: pip(*args)
|
190
197
|
|
191
198
|
Install Python packages. It creates a virtual environment if one does not already exist and installs the specified packages.
|
192
199
|
|
193
200
|
:param args: Arguments to pass to the pip install command.
|
194
|
-
:type target: str
|
195
201
|
|
196
|
-
Example:
|
202
|
+
Example:
|
197
203
|
|
198
|
-
.. code-block:: python
|
204
|
+
.. code-block:: python
|
199
205
|
|
200
|
-
|
206
|
+
pip("numpy", "pandas")
|
201
207
|
|
202
208
|
.. _run:
|
203
209
|
|
@@ -241,7 +247,7 @@ Example:
|
|
241
247
|
files = run("ls", stdout=True).stdout # Capture output
|
242
248
|
|
243
249
|
|
244
|
-
.. py:function::
|
250
|
+
.. py:function:: capture(*args, echo=True, cwd=None, check=True, text=True, shell=None)
|
245
251
|
|
246
252
|
Execute a command and capture the output. This function is a wrapper around
|
247
253
|
:ref:`run <run>`.
|
@@ -273,7 +279,7 @@ Example:
|
|
273
279
|
msg = capture("echo Hello, World!")
|
274
280
|
|
275
281
|
|
276
|
-
.. py:function:: read(filename)
|
282
|
+
.. py:function:: read(filename)
|
277
283
|
|
278
284
|
Read the contents of a file.
|
279
285
|
|
@@ -283,7 +289,7 @@ Example:
|
|
283
289
|
:return: The contents of the file.
|
284
290
|
:rtype: str
|
285
291
|
|
286
|
-
.. py:function:: write(filename, txt, append=False)
|
292
|
+
.. py:function:: write(filename, txt, append=False)
|
287
293
|
|
288
294
|
Write text to a file.
|
289
295
|
|
@@ -296,7 +302,7 @@ Example:
|
|
296
302
|
:param append: Append to the file instead of overwriting it (default ``False``).
|
297
303
|
:type append: bool
|
298
304
|
|
299
|
-
.. py:function:: makedirs(path)
|
305
|
+
.. py:function:: makedirs(path)
|
300
306
|
|
301
307
|
Create a directory along with any necessary parent directories if they do not already exist. This function wraps `os.makedirs() <https://docs.python.org/3/library/os.html#os.makedirs>`_ with the ``exists_ok`` parameter set to ``True``.
|
302
308
|
|
@@ -345,6 +351,13 @@ Example:
|
|
345
351
|
:return: The quoted string.
|
346
352
|
:rtype: str
|
347
353
|
|
354
|
+
.. py:function:: use_git(use)
|
355
|
+
|
356
|
+
Enable or disable git support. If enabled, PyProd retrieves the last modified time of the files from the git log.
|
357
|
+
|
358
|
+
:param use: Enable or disable git support.
|
359
|
+
:type bool: bool
|
360
|
+
|
348
361
|
.. py:class:: Path
|
349
362
|
|
350
363
|
A class representing file paths. This function is an alias for `pathlib.Path <https://docs.python.org/3/library/pathlib.html#pathlib.Path>`_.
|
@@ -354,8 +367,7 @@ Example:
|
|
354
367
|
|
355
368
|
.. py:data:: shutil
|
356
369
|
|
357
|
-
|
358
|
-
|
370
|
+
An alias for the Python Standard Library's `shutil <https://docs.python.org/3/library/shutil.html>`_ module. This module provides a higher-level interface for file operations than the built-in `os <https://docs.python.org/3/library/os.html>`_ module.
|
359
371
|
|
360
372
|
.. py:data:: env
|
361
373
|
|
@@ -1,12 +1,24 @@
|
|
1
1
|
Release Notes
|
2
2
|
================
|
3
3
|
|
4
|
+
0.6.0 (2025-4-6)
|
5
|
+
-----------------------------
|
6
|
+
|
7
|
+
- Ignore None or falsy values in the ``depends`` and ``uses`` in the rule.
|
8
|
+
|
9
|
+
0.5.0 (2025-2-12)
|
10
|
+
-----------------------------
|
11
|
+
|
12
|
+
- Added ``default`` to the ``@task``.
|
13
|
+
- Removed ``depends`` from thr ``@task``.
|
14
|
+
- Added --use-git commandline option.
|
15
|
+
|
4
16
|
0.4.0 (2025-1-17)
|
5
17
|
-------------------------
|
6
18
|
- Swapped the behavior of quote() and squote() to make their naming more intuitive.
|
7
19
|
- Add @task decorator.
|
8
20
|
- Change the parameter name target to targets.
|
9
|
-
-
|
21
|
+
- Added --rebuild option.
|
10
22
|
|
11
23
|
0.3.0 (2025-01-03)
|
12
24
|
------------------
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "PyProd"
|
3
|
-
|
3
|
+
dynamic = ["version"]
|
4
4
|
description = "PyProd: More Makeable than Make"
|
5
5
|
readme = "README.rst"
|
6
6
|
requires-python = ">=3.10"
|
@@ -12,7 +12,9 @@ classifiers = [
|
|
12
12
|
"Environment :: Console",
|
13
13
|
"Topic :: Software Development :: Build Tools",
|
14
14
|
]
|
15
|
-
dependencies = [
|
15
|
+
dependencies = [
|
16
|
+
"python-dateutil",
|
17
|
+
]
|
16
18
|
|
17
19
|
[project.urls]
|
18
20
|
Homepage = "https://github.com/atsuoishimoto/pyprod"
|
@@ -25,6 +27,9 @@ pyprod = "pyprod.main:main"
|
|
25
27
|
requires = ["hatchling"]
|
26
28
|
build-backend = "hatchling.build"
|
27
29
|
|
30
|
+
[tool.hatch.version]
|
31
|
+
path = "src/pyprod/__init__.py"
|
32
|
+
|
28
33
|
[dependency-groups]
|
29
34
|
dev = [
|
30
35
|
"pytest>=8.3.4",
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.6.0"
|
@@ -22,7 +22,7 @@ parser.add_argument(
|
|
22
22
|
)
|
23
23
|
|
24
24
|
parser.add_argument(
|
25
|
-
"-f", "--file", help="Use FILE as the Prodfile (default: '
|
25
|
+
"-f", "--file", help="Use FILE as the Prodfile (default: 'Prodfile.py')"
|
26
26
|
)
|
27
27
|
|
28
28
|
parser.add_argument(
|
@@ -36,6 +36,15 @@ parser.add_argument(
|
|
36
36
|
parser.add_argument(
|
37
37
|
"-r", "--rebuild", dest="rebuild", action="store_true", help="Rebuild all"
|
38
38
|
)
|
39
|
+
|
40
|
+
parser.add_argument(
|
41
|
+
"-g",
|
42
|
+
"--use-git",
|
43
|
+
dest="use_git",
|
44
|
+
action="store_true",
|
45
|
+
help="Get file timestamps from Git",
|
46
|
+
)
|
47
|
+
|
39
48
|
parser.add_argument(
|
40
49
|
"-v",
|
41
50
|
dest="verbose",
|
@@ -44,6 +53,15 @@ parser.add_argument(
|
|
44
53
|
help="Increase verbosity level (default: 0)",
|
45
54
|
)
|
46
55
|
|
56
|
+
parser.add_argument(
|
57
|
+
"-V",
|
58
|
+
"--version",
|
59
|
+
dest="version",
|
60
|
+
action="store_true",
|
61
|
+
default=0,
|
62
|
+
help="Show version",
|
63
|
+
)
|
64
|
+
|
47
65
|
|
48
66
|
parser.add_argument("targets", nargs="*", help="Build targets")
|
49
67
|
|
@@ -65,6 +83,10 @@ def init_args(args=None):
|
|
65
83
|
|
66
84
|
def main():
|
67
85
|
args = init_args()
|
86
|
+
if args.version:
|
87
|
+
print(f"PyProd {pyprod.__version__}")
|
88
|
+
sys.exit(0)
|
89
|
+
|
68
90
|
pyprod.verbose = args.verbose
|
69
91
|
chdir = args.directory
|
70
92
|
if chdir:
|
@@ -108,7 +130,6 @@ def main():
|
|
108
130
|
try:
|
109
131
|
# load module
|
110
132
|
prod = pyprod.prod.Prod(mod, args.job, params)
|
111
|
-
|
112
133
|
# select targets
|
113
134
|
if not targets:
|
114
135
|
target = prod.get_default_target()
|
@@ -15,8 +15,9 @@ from collections import defaultdict
|
|
15
15
|
from collections.abc import Collection
|
16
16
|
from dataclasses import dataclass, field
|
17
17
|
from fnmatch import fnmatch, translate
|
18
|
+
from functools import wraps
|
18
19
|
from pathlib import Path
|
19
|
-
|
20
|
+
import dateutil.parser
|
20
21
|
import pyprod
|
21
22
|
|
22
23
|
from .utils import flatten, unique_list
|
@@ -103,9 +104,10 @@ def capture(*args, echo=True, cwd=None, check=True, text=True, shell=None):
|
|
103
104
|
|
104
105
|
def glob(path, dir="."):
|
105
106
|
ret = []
|
106
|
-
|
107
|
+
root = Path(dir)
|
108
|
+
for c in root.glob(path):
|
107
109
|
# ignore dot files
|
108
|
-
if any(p.startswith(".") for p in c.parts):
|
110
|
+
if any((p not in (".", "..")) and p.startswith(".") for p in c.parts):
|
109
111
|
continue
|
110
112
|
ret.append(c)
|
111
113
|
return ret
|
@@ -175,6 +177,8 @@ def _name_to_str(name):
|
|
175
177
|
return str(name)
|
176
178
|
case str():
|
177
179
|
return name
|
180
|
+
case _:
|
181
|
+
raise ValueError(f"Invalid dependency name: {name}")
|
178
182
|
|
179
183
|
return name
|
180
184
|
|
@@ -182,6 +186,7 @@ def _name_to_str(name):
|
|
182
186
|
class Rule:
|
183
187
|
def __init__(self, targets, pattern=None, depends=(), uses=(), builder=None):
|
184
188
|
self.targets = []
|
189
|
+
self.default = False
|
185
190
|
self.first_target = None
|
186
191
|
if targets:
|
187
192
|
for target in flatten(targets):
|
@@ -211,6 +216,9 @@ class Rule:
|
|
211
216
|
|
212
217
|
self.depends = []
|
213
218
|
for depend in flatten(depends or ()):
|
219
|
+
if not depend:
|
220
|
+
continue
|
221
|
+
|
214
222
|
depend = _name_to_str(depend)
|
215
223
|
_check_pattern_count(depend)
|
216
224
|
_check_wildcard(depend)
|
@@ -218,6 +226,9 @@ class Rule:
|
|
218
226
|
|
219
227
|
self.uses = []
|
220
228
|
for use in flatten(uses or ()):
|
229
|
+
if not use:
|
230
|
+
continue
|
231
|
+
|
221
232
|
use = _name_to_str(use)
|
222
233
|
_check_pattern_count(use)
|
223
234
|
_check_wildcard(use)
|
@@ -239,18 +250,23 @@ class _TaskFunc:
|
|
239
250
|
return self.f(*args, **kwargs)
|
240
251
|
|
241
252
|
|
242
|
-
def default_builder(
|
253
|
+
def default_builder(*args, **kwargs):
|
243
254
|
# default builder
|
244
255
|
pass
|
245
256
|
|
246
257
|
|
247
258
|
class Task(Rule):
|
248
|
-
def __init__(self, name,
|
249
|
-
super().__init__((), pattern=None, depends=
|
250
|
-
self.name = _name_to_str(name)
|
259
|
+
def __init__(self, name, uses, default, func=None):
|
260
|
+
super().__init__((), pattern=None, depends=(), uses=uses, builder=func)
|
251
261
|
if name:
|
252
|
-
self.
|
253
|
-
|
262
|
+
self.name = _name_to_str(name)
|
263
|
+
if name:
|
264
|
+
self.targets = [name]
|
265
|
+
self.first_target = self.name
|
266
|
+
else:
|
267
|
+
self.name = None
|
268
|
+
|
269
|
+
self.default = default
|
254
270
|
if func:
|
255
271
|
self._set_funcname(func)
|
256
272
|
if not self.builder:
|
@@ -292,10 +308,10 @@ class Rules:
|
|
292
308
|
self.rules.append(dep)
|
293
309
|
return dep
|
294
310
|
|
295
|
-
def add_task(self, name=None,
|
311
|
+
def add_task(self, name=None, uses=(), default=False, func=None):
|
296
312
|
if self.frozen:
|
297
313
|
raise RuntimeError("No new rule can be added after initialization")
|
298
|
-
dep = Task(name,
|
314
|
+
dep = Task(name, uses, default, func)
|
299
315
|
self.rules.append(dep)
|
300
316
|
return dep
|
301
317
|
|
@@ -306,12 +322,12 @@ class Rules:
|
|
306
322
|
dep = self.add_rule([targets], pattern, depends, uses, None)
|
307
323
|
return dep
|
308
324
|
|
309
|
-
def task(self, func=None, *, name=None,
|
325
|
+
def task(self, func=None, *, name=None, uses=(), default=False):
|
310
326
|
if func:
|
311
327
|
if not callable(func):
|
312
328
|
raise ValueError(f"{func} is not callable")
|
313
329
|
|
314
|
-
dep = self.add_task(name,
|
330
|
+
dep = self.add_task(name, uses, default, func)
|
315
331
|
return dep
|
316
332
|
|
317
333
|
def iter_rule(self, name):
|
@@ -353,10 +369,16 @@ class Rules:
|
|
353
369
|
return unique_list(ret_depends), unique_list(ret_uses)
|
354
370
|
|
355
371
|
def select_first_target(self):
|
372
|
+
first = None
|
356
373
|
for dep in self.rules:
|
374
|
+
if dep.default and (not first):
|
375
|
+
first = dep.name
|
376
|
+
|
357
377
|
if dep.first_target:
|
358
378
|
return dep.first_target
|
359
379
|
|
380
|
+
return first
|
381
|
+
|
360
382
|
def select_builder(self, name):
|
361
383
|
for depends, uses, dep in self.iter_rule(name):
|
362
384
|
if not dep.builder:
|
@@ -419,10 +441,6 @@ class Checkers:
|
|
419
441
|
MAX_TS = 1 << 63
|
420
442
|
|
421
443
|
|
422
|
-
def is_file_exists(name):
|
423
|
-
return os.path.getmtime(name)
|
424
|
-
|
425
|
-
|
426
444
|
class Exists:
|
427
445
|
def __init__(self, name, exists, ts=None):
|
428
446
|
self.name = name
|
@@ -506,10 +524,12 @@ class Prod:
|
|
506
524
|
self.rules = Rules()
|
507
525
|
self.checkers = Checkers()
|
508
526
|
if njobs > 1:
|
509
|
-
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=
|
527
|
+
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=100)
|
510
528
|
else:
|
511
529
|
self.executor = None
|
512
530
|
self.params = Params(params)
|
531
|
+
self.use_git_timestamp = pyprod.args.use_git
|
532
|
+
|
513
533
|
self.buildings = {}
|
514
534
|
self.module = None
|
515
535
|
if self.modulefile:
|
@@ -536,6 +556,7 @@ class Prod:
|
|
536
556
|
"run": run,
|
537
557
|
"shutil": shutil,
|
538
558
|
"task": self.rules.task,
|
559
|
+
"use_git": self.use_git,
|
539
560
|
"write": write,
|
540
561
|
"MAX_TS": MAX_TS,
|
541
562
|
"Path": Path,
|
@@ -567,6 +588,52 @@ class Prod:
|
|
567
588
|
|
568
589
|
return ret
|
569
590
|
|
591
|
+
def get_file_mtime(self, name):
|
592
|
+
return os.path.getmtime(name)
|
593
|
+
|
594
|
+
def get_file_mtime_git(self, name):
|
595
|
+
ret = subprocess.check_output(
|
596
|
+
["git", "log", "-1", "--format=%ai", "--", name], text=True
|
597
|
+
).strip()
|
598
|
+
if not ret:
|
599
|
+
raise FileNotFoundError(f"{name} did not match any file in git")
|
600
|
+
|
601
|
+
# 2025-01-17 00:05:48 +0900
|
602
|
+
return dateutil.parser.parse(ret)
|
603
|
+
|
604
|
+
async def is_exists(self, name):
|
605
|
+
checker = self.checkers.get_checker(name)
|
606
|
+
try:
|
607
|
+
if checker:
|
608
|
+
ret = await self.run_in_executor(checker, name)
|
609
|
+
elif self.use_git_timestamp:
|
610
|
+
ret = await self.run_in_executor(self.get_file_mtime_git, name)
|
611
|
+
else:
|
612
|
+
ret = await self.run_in_executor(self.get_file_mtime, name)
|
613
|
+
except FileNotFoundError:
|
614
|
+
ret = False
|
615
|
+
|
616
|
+
if isinstance(ret, FileNotFoundError):
|
617
|
+
ret = False
|
618
|
+
|
619
|
+
if not ret:
|
620
|
+
return Exists(name, False)
|
621
|
+
if isinstance(ret, datetime.datetime):
|
622
|
+
ret = ret.timestamp()
|
623
|
+
if ret < 0:
|
624
|
+
ret = MAX_TS
|
625
|
+
return Exists(name, True, ret)
|
626
|
+
|
627
|
+
def build(self, *deps):
|
628
|
+
children = []
|
629
|
+
for elem in deps:
|
630
|
+
child = [_name_to_str(name) for name in flatten(elem)]
|
631
|
+
children.append(child)
|
632
|
+
self.deps[0:0] = children
|
633
|
+
|
634
|
+
def use_git(self, use):
|
635
|
+
self.use_git_timestamp = use
|
636
|
+
|
570
637
|
def get_default_target(self):
|
571
638
|
return self.rules.select_first_target()
|
572
639
|
|
@@ -581,24 +648,26 @@ class Prod:
|
|
581
648
|
return self.built
|
582
649
|
|
583
650
|
async def schedule(self, deps):
|
651
|
+
deps = list(flatten(deps))
|
584
652
|
tasks = []
|
585
653
|
waits = []
|
586
654
|
for dep in deps:
|
587
655
|
if dep not in self.buildings:
|
588
656
|
ev = asyncio.Event()
|
589
657
|
self.buildings[dep] = ev
|
590
|
-
|
591
|
-
tasks.append((dep,
|
658
|
+
coro = self.run(dep)
|
659
|
+
tasks.append((dep, coro))
|
592
660
|
waits.append(ev)
|
593
661
|
else:
|
594
662
|
obj = self.buildings[dep]
|
595
663
|
if isinstance(obj, asyncio.Event):
|
596
664
|
waits.append(obj)
|
597
665
|
|
598
|
-
for
|
666
|
+
results = await asyncio.gather(*(coro for _, coro in tasks))
|
667
|
+
for ret, (dep, _) in zip(results, tasks):
|
599
668
|
ev = self.buildings[dep]
|
600
669
|
try:
|
601
|
-
self.buildings[dep] =
|
670
|
+
self.buildings[dep] = ret
|
602
671
|
finally:
|
603
672
|
ev.set()
|
604
673
|
|
@@ -614,27 +683,6 @@ class Prod:
|
|
614
683
|
return max(ts)
|
615
684
|
return 0
|
616
685
|
|
617
|
-
async def is_exists(self, name):
|
618
|
-
checker = self.checkers.get_checker(name)
|
619
|
-
try:
|
620
|
-
if checker:
|
621
|
-
ret = await self.run_in_executor(checker, name)
|
622
|
-
else:
|
623
|
-
ret = await self.run_in_executor(is_file_exists, name)
|
624
|
-
except FileNotFoundError:
|
625
|
-
ret = False
|
626
|
-
|
627
|
-
if not ret:
|
628
|
-
return Exists(name, False)
|
629
|
-
if isinstance(ret, datetime.datetime):
|
630
|
-
ret = ret.timestamp()
|
631
|
-
if ret < 0:
|
632
|
-
ret = MAX_TS
|
633
|
-
return Exists(name, True, ret)
|
634
|
-
|
635
|
-
def build(self, *deps):
|
636
|
-
self.deps[0:0] = [_name_to_str(name) for name in flatten(deps)]
|
637
|
-
|
638
686
|
async def run(self, name): # -> Any | int:
|
639
687
|
name = _name_to_str(name)
|
640
688
|
self.rules.build_tree(name)
|
@@ -645,11 +693,18 @@ class Prod:
|
|
645
693
|
deps = deps + build_deps
|
646
694
|
uses = uses + build_uses
|
647
695
|
|
648
|
-
|
696
|
+
tasks = []
|
649
697
|
if deps:
|
650
|
-
|
698
|
+
deps_task = asyncio.create_task(self.schedule(deps))
|
699
|
+
tasks.append(deps_task)
|
651
700
|
if uses:
|
652
|
-
|
701
|
+
uses_task = self.schedule(uses)
|
702
|
+
tasks.append(uses_task)
|
703
|
+
|
704
|
+
await asyncio.gather(*tasks)
|
705
|
+
ts = 0
|
706
|
+
if deps:
|
707
|
+
ts = deps_task.result()
|
653
708
|
|
654
709
|
if selected and isinstance(builder, Task):
|
655
710
|
self.built += 1
|
@@ -1,13 +1,15 @@
|
|
1
1
|
from collections.abc import Iterable
|
2
2
|
|
3
3
|
|
4
|
-
def flatten(seq):
|
4
|
+
def flatten(seq, ignore_none=True):
|
5
5
|
if isinstance(seq, str) or (not isinstance(seq, Iterable)):
|
6
6
|
yield seq
|
7
7
|
return
|
8
8
|
|
9
9
|
for item in seq:
|
10
10
|
if isinstance(item, str) or (not isinstance(item, Iterable)):
|
11
|
+
if ignore_none and (item is None):
|
12
|
+
continue
|
11
13
|
yield item
|
12
14
|
else:
|
13
15
|
yield from flatten(item)
|
@@ -42,6 +42,15 @@ def pip(*args):
|
|
42
42
|
makevenv(pyprod.modulefile)
|
43
43
|
args = flatten(args)
|
44
44
|
subprocess.run(
|
45
|
-
[
|
45
|
+
[
|
46
|
+
venvdir / "bin/python",
|
47
|
+
"-m",
|
48
|
+
"pip",
|
49
|
+
"--disable-pip-version-check",
|
50
|
+
"--no-input",
|
51
|
+
"install",
|
52
|
+
"-q",
|
53
|
+
*args,
|
54
|
+
],
|
46
55
|
check=True,
|
47
56
|
)
|