PyProd 0.4.0__tar.gz → 0.5.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.5.0}/.gitignore +3 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/PKG-INFO +2 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/commandline.rst +6 -3
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/conf.py +1 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/prodfile.rst +29 -17
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/releasenotes.rst +8 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/pyproject.toml +7 -2
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/Makefile +1 -1
- pyprod-0.4.0/samples/generate-doc/PRODFILE.py → pyprod-0.5.0/samples/generate-doc/Prodfile.py +1 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/Prodfile.py +1 -1
- pyprod-0.5.0/src/pyprod/__init__.py +1 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/main.py +10 -2
- {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/prod.py +95 -46
- {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/utils.py +3 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/venv.py +10 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/tests/conftest.py +1 -1
- {pyprod-0.4.0 → pyprod-0.5.0}/tests/test_prod.py +17 -23
- 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.5.0}/.github/workflows/publish.yml +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/.github/workflows/test.yml +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/LICENSE +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/README.rst +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/Makefile +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/index.rst +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/make.bat +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/pyprod2.png +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/quickstart.rst +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/docs/requirements.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/pyprod.webp +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/pyprod2.png +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/hello.c +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/hello.h +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/build-c/main.c +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/a.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/b.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/c.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/inc1.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/generate-doc/inc2.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/doc.md +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/md_to_html.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/md-to-pdf/template.html +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/s3files/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/s3files/S3TEST.txt +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/tutorial-1/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/samples/tutorial-2/Prodfile.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/src/pyprod/__main__.py +0 -0
- {pyprod-0.4.0/src/pyprod → pyprod-0.5.0/tests}/__init__.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/tests/test_prodfuncs.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.0}/tests/test_rule.py +0 -0
- {pyprod-0.4.0 → pyprod-0.5.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.5.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,19 @@
|
|
1
1
|
Release Notes
|
2
2
|
================
|
3
3
|
|
4
|
+
0.5.0 (2025-2-12)
|
5
|
+
-----------------------------
|
6
|
+
|
7
|
+
- Added ``default`` to the ``@task``.
|
8
|
+
- Removed ``depends`` from thr ``@task``.
|
9
|
+
- Added --use-git commandline option.
|
10
|
+
|
4
11
|
0.4.0 (2025-1-17)
|
5
12
|
-------------------------
|
6
13
|
- Swapped the behavior of quote() and squote() to make their naming more intuitive.
|
7
14
|
- Add @task decorator.
|
8
15
|
- Change the parameter name target to targets.
|
9
|
-
-
|
16
|
+
- Added --rebuild option.
|
10
17
|
|
11
18
|
0.3.0 (2025-01-03)
|
12
19
|
------------------
|
@@ -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.5.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",
|
@@ -108,7 +117,6 @@ def main():
|
|
108
117
|
try:
|
109
118
|
# load module
|
110
119
|
prod = pyprod.prod.Prod(mod, args.job, params)
|
111
|
-
|
112
120
|
# select targets
|
113
121
|
if not targets:
|
114
122
|
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):
|
@@ -239,18 +244,23 @@ class _TaskFunc:
|
|
239
244
|
return self.f(*args, **kwargs)
|
240
245
|
|
241
246
|
|
242
|
-
def default_builder(
|
247
|
+
def default_builder(*args, **kwargs):
|
243
248
|
# default builder
|
244
249
|
pass
|
245
250
|
|
246
251
|
|
247
252
|
class Task(Rule):
|
248
|
-
def __init__(self, name,
|
249
|
-
super().__init__((), pattern=None, depends=
|
250
|
-
self.name = _name_to_str(name)
|
253
|
+
def __init__(self, name, uses, default, func=None):
|
254
|
+
super().__init__((), pattern=None, depends=(), uses=uses, builder=func)
|
251
255
|
if name:
|
252
|
-
self.
|
253
|
-
|
256
|
+
self.name = _name_to_str(name)
|
257
|
+
if name:
|
258
|
+
self.targets = [name]
|
259
|
+
self.first_target = self.name
|
260
|
+
else:
|
261
|
+
self.name = None
|
262
|
+
|
263
|
+
self.default = default
|
254
264
|
if func:
|
255
265
|
self._set_funcname(func)
|
256
266
|
if not self.builder:
|
@@ -292,10 +302,10 @@ class Rules:
|
|
292
302
|
self.rules.append(dep)
|
293
303
|
return dep
|
294
304
|
|
295
|
-
def add_task(self, name=None,
|
305
|
+
def add_task(self, name=None, uses=(), default=False, func=None):
|
296
306
|
if self.frozen:
|
297
307
|
raise RuntimeError("No new rule can be added after initialization")
|
298
|
-
dep = Task(name,
|
308
|
+
dep = Task(name, uses, default, func)
|
299
309
|
self.rules.append(dep)
|
300
310
|
return dep
|
301
311
|
|
@@ -306,12 +316,12 @@ class Rules:
|
|
306
316
|
dep = self.add_rule([targets], pattern, depends, uses, None)
|
307
317
|
return dep
|
308
318
|
|
309
|
-
def task(self, func=None, *, name=None,
|
319
|
+
def task(self, func=None, *, name=None, uses=(), default=False):
|
310
320
|
if func:
|
311
321
|
if not callable(func):
|
312
322
|
raise ValueError(f"{func} is not callable")
|
313
323
|
|
314
|
-
dep = self.add_task(name,
|
324
|
+
dep = self.add_task(name, uses, default, func)
|
315
325
|
return dep
|
316
326
|
|
317
327
|
def iter_rule(self, name):
|
@@ -353,10 +363,16 @@ class Rules:
|
|
353
363
|
return unique_list(ret_depends), unique_list(ret_uses)
|
354
364
|
|
355
365
|
def select_first_target(self):
|
366
|
+
first = None
|
356
367
|
for dep in self.rules:
|
368
|
+
if dep.default and (not first):
|
369
|
+
first = dep.name
|
370
|
+
|
357
371
|
if dep.first_target:
|
358
372
|
return dep.first_target
|
359
373
|
|
374
|
+
return first
|
375
|
+
|
360
376
|
def select_builder(self, name):
|
361
377
|
for depends, uses, dep in self.iter_rule(name):
|
362
378
|
if not dep.builder:
|
@@ -419,10 +435,6 @@ class Checkers:
|
|
419
435
|
MAX_TS = 1 << 63
|
420
436
|
|
421
437
|
|
422
|
-
def is_file_exists(name):
|
423
|
-
return os.path.getmtime(name)
|
424
|
-
|
425
|
-
|
426
438
|
class Exists:
|
427
439
|
def __init__(self, name, exists, ts=None):
|
428
440
|
self.name = name
|
@@ -506,10 +518,12 @@ class Prod:
|
|
506
518
|
self.rules = Rules()
|
507
519
|
self.checkers = Checkers()
|
508
520
|
if njobs > 1:
|
509
|
-
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=
|
521
|
+
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=100)
|
510
522
|
else:
|
511
523
|
self.executor = None
|
512
524
|
self.params = Params(params)
|
525
|
+
self.use_git_timestamp = pyprod.args.use_git
|
526
|
+
|
513
527
|
self.buildings = {}
|
514
528
|
self.module = None
|
515
529
|
if self.modulefile:
|
@@ -536,6 +550,7 @@ class Prod:
|
|
536
550
|
"run": run,
|
537
551
|
"shutil": shutil,
|
538
552
|
"task": self.rules.task,
|
553
|
+
"use_git": self.use_git,
|
539
554
|
"write": write,
|
540
555
|
"MAX_TS": MAX_TS,
|
541
556
|
"Path": Path,
|
@@ -567,6 +582,52 @@ class Prod:
|
|
567
582
|
|
568
583
|
return ret
|
569
584
|
|
585
|
+
def get_file_mtime(self, name):
|
586
|
+
return os.path.getmtime(name)
|
587
|
+
|
588
|
+
def get_file_mtime_git(self, name):
|
589
|
+
ret = subprocess.check_output(
|
590
|
+
["git", "log", "-1", "--format=%ai", "--", name], text=True
|
591
|
+
).strip()
|
592
|
+
if not ret:
|
593
|
+
raise FileNotFoundError(f"{name} did not match any file in git")
|
594
|
+
|
595
|
+
# 2025-01-17 00:05:48 +0900
|
596
|
+
return dateutil.parser.parse(ret)
|
597
|
+
|
598
|
+
async def is_exists(self, name):
|
599
|
+
checker = self.checkers.get_checker(name)
|
600
|
+
try:
|
601
|
+
if checker:
|
602
|
+
ret = await self.run_in_executor(checker, name)
|
603
|
+
elif self.use_git_timestamp:
|
604
|
+
ret = await self.run_in_executor(self.get_file_mtime_git, name)
|
605
|
+
else:
|
606
|
+
ret = await self.run_in_executor(self.get_file_mtime, name)
|
607
|
+
except FileNotFoundError:
|
608
|
+
ret = False
|
609
|
+
|
610
|
+
if isinstance(ret, FileNotFoundError):
|
611
|
+
ret = False
|
612
|
+
|
613
|
+
if not ret:
|
614
|
+
return Exists(name, False)
|
615
|
+
if isinstance(ret, datetime.datetime):
|
616
|
+
ret = ret.timestamp()
|
617
|
+
if ret < 0:
|
618
|
+
ret = MAX_TS
|
619
|
+
return Exists(name, True, ret)
|
620
|
+
|
621
|
+
def build(self, *deps):
|
622
|
+
children = []
|
623
|
+
for elem in deps:
|
624
|
+
child = [_name_to_str(name) for name in flatten(elem)]
|
625
|
+
children.append(child)
|
626
|
+
self.deps[0:0] = children
|
627
|
+
|
628
|
+
def use_git(self, use):
|
629
|
+
self.use_git_timestamp = use
|
630
|
+
|
570
631
|
def get_default_target(self):
|
571
632
|
return self.rules.select_first_target()
|
572
633
|
|
@@ -581,24 +642,26 @@ class Prod:
|
|
581
642
|
return self.built
|
582
643
|
|
583
644
|
async def schedule(self, deps):
|
645
|
+
deps = list(flatten(deps))
|
584
646
|
tasks = []
|
585
647
|
waits = []
|
586
648
|
for dep in deps:
|
587
649
|
if dep not in self.buildings:
|
588
650
|
ev = asyncio.Event()
|
589
651
|
self.buildings[dep] = ev
|
590
|
-
|
591
|
-
tasks.append((dep,
|
652
|
+
coro = self.run(dep)
|
653
|
+
tasks.append((dep, coro))
|
592
654
|
waits.append(ev)
|
593
655
|
else:
|
594
656
|
obj = self.buildings[dep]
|
595
657
|
if isinstance(obj, asyncio.Event):
|
596
658
|
waits.append(obj)
|
597
659
|
|
598
|
-
for
|
660
|
+
results = await asyncio.gather(*(coro for _, coro in tasks))
|
661
|
+
for ret, (dep, _) in zip(results, tasks):
|
599
662
|
ev = self.buildings[dep]
|
600
663
|
try:
|
601
|
-
self.buildings[dep] =
|
664
|
+
self.buildings[dep] = ret
|
602
665
|
finally:
|
603
666
|
ev.set()
|
604
667
|
|
@@ -614,27 +677,6 @@ class Prod:
|
|
614
677
|
return max(ts)
|
615
678
|
return 0
|
616
679
|
|
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
680
|
async def run(self, name): # -> Any | int:
|
639
681
|
name = _name_to_str(name)
|
640
682
|
self.rules.build_tree(name)
|
@@ -645,11 +687,18 @@ class Prod:
|
|
645
687
|
deps = deps + build_deps
|
646
688
|
uses = uses + build_uses
|
647
689
|
|
648
|
-
|
690
|
+
tasks = []
|
649
691
|
if deps:
|
650
|
-
|
692
|
+
deps_task = asyncio.create_task(self.schedule(deps))
|
693
|
+
tasks.append(deps_task)
|
651
694
|
if uses:
|
652
|
-
|
695
|
+
uses_task = self.schedule(uses)
|
696
|
+
tasks.append(uses_task)
|
697
|
+
|
698
|
+
await asyncio.gather(*tasks)
|
699
|
+
ts = 0
|
700
|
+
if deps:
|
701
|
+
ts = deps_task.result()
|
653
702
|
|
654
703
|
if selected and isinstance(builder, Task):
|
655
704
|
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
|
)
|