chorelib 0.1.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.
- chorelib-0.1.0/LICENSE +21 -0
- chorelib-0.1.0/PKG-INFO +170 -0
- chorelib-0.1.0/README.md +145 -0
- chorelib-0.1.0/pyproject.toml +53 -0
- chorelib-0.1.0/src/chorelib/.ruff_cache/.gitignore +2 -0
- chorelib-0.1.0/src/chorelib/.ruff_cache/0.14.11/17628432600115339624 +0 -0
- chorelib-0.1.0/src/chorelib/.ruff_cache/CACHEDIR.TAG +1 -0
- chorelib-0.1.0/src/chorelib/__init__.py +48 -0
- chorelib-0.1.0/src/chorelib/depgraph.py +122 -0
- chorelib-0.1.0/src/chorelib/depmain.py +209 -0
- chorelib-0.1.0/src/chorelib/deprunner.py +279 -0
- chorelib-0.1.0/src/chorelib/errors.py +27 -0
- chorelib-0.1.0/src/chorelib/ruledef.py +641 -0
- chorelib-0.1.0/src/chorelib/utils.py +189 -0
chorelib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 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.
|
chorelib-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chorelib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python build automation framework with decorator-based DSL
|
|
5
|
+
Keywords: build,automation,make,async,task-runner
|
|
6
|
+
Author: Atsuo Ishimoto
|
|
7
|
+
Author-email: Atsuo Ishimoto <atsuoishimoto@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Project-URL: Documentation, https://chorelib.readthedocs.io/en/latest/
|
|
21
|
+
Project-URL: Homepage, https://github.com/atsuoishimoto/chorelib
|
|
22
|
+
Project-URL: Issues, https://github.com/atsuoishimoto/chorelib/issues
|
|
23
|
+
Project-URL: Repository, https://github.com/atsuoishimoto/chorelib
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# chorelib
|
|
27
|
+
|
|
28
|
+
A Python build automation framework — like Make, but in Python.
|
|
29
|
+
chorelib focuses on keeping Make-style speed and rebuild behavior while letting you write build logic in real Python.
|
|
30
|
+
|
|
31
|
+
chorelib uses a decorator-based DSL for defining build rules and tasks, with dependency management, parallel execution, and mtime-based rebuild detection.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Decorator-based DSL** — Define build rules with `@rule` and tasks with `@task`, using familiar Python syntax
|
|
36
|
+
- **Regex target patterns** — Match multiple targets with regex and use backreferences (`\1`) in dependency specifications
|
|
37
|
+
- **Async parallel execution** — Run independent build steps concurrently with configurable worker count
|
|
38
|
+
- **mtime-based rebuild** — Skip up-to-date targets automatically, just like Make
|
|
39
|
+
- **Custom mtime functions** — Override mtime checking per target pattern to manage non-file resources (databases, S3 objects, etc.)
|
|
40
|
+
- **Order-only prerequisites** — `needs` dependencies ensure build order without triggering rebuilds
|
|
41
|
+
- **Auto-generated help** — `-h` shows usage with target documentation from docstrings, `-l` lists all available targets
|
|
42
|
+
- **Zero dependencies** — Pure Python, no external packages required
|
|
43
|
+
|
|
44
|
+
**Best for:** developers who want Make-like rebuild behavior but need Python for complex logic or non-file resources.
|
|
45
|
+
It also works well as a simple task runner for everyday scripts.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install chorelib
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
Here is a Makefile and its chorelib equivalent side by side.
|
|
56
|
+
|
|
57
|
+
**Makefile:**
|
|
58
|
+
|
|
59
|
+
```makefile
|
|
60
|
+
CC=gcc
|
|
61
|
+
CFLAGS=-I.
|
|
62
|
+
DEPS = hello.h
|
|
63
|
+
OBJS = main.o hello.o
|
|
64
|
+
.PHONY: clean
|
|
65
|
+
|
|
66
|
+
%.o: %.c $(DEPS)
|
|
67
|
+
$(CC) -c -o $@ $< $(CFLAGS)
|
|
68
|
+
|
|
69
|
+
hello.exe: $(OBJS)
|
|
70
|
+
$(CC) -o $@ $^
|
|
71
|
+
|
|
72
|
+
clean:
|
|
73
|
+
rm -f $(OBJS) hello.exe
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**chorelib (`make.py`):**
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import re
|
|
80
|
+
from chorelib import Main, command, rule, task
|
|
81
|
+
|
|
82
|
+
APP = "hello.exe"
|
|
83
|
+
CC = "gcc"
|
|
84
|
+
CFLAGS = ["-c", "-I."]
|
|
85
|
+
DEPS = ["hello.h"]
|
|
86
|
+
OBJS = ["hello.o", "main.o"]
|
|
87
|
+
|
|
88
|
+
@rule(APP, depends=OBJS, default=True)
|
|
89
|
+
def link(target, deps, needs):
|
|
90
|
+
"""Build executable"""
|
|
91
|
+
command(CC, "-o", target, deps)
|
|
92
|
+
|
|
93
|
+
@rule(re.compile(r"(.+)\.o"), depends=(r"\1.c", DEPS))
|
|
94
|
+
def compile(target, deps, needs):
|
|
95
|
+
command(CC, "-o", target, deps[0], CFLAGS)
|
|
96
|
+
|
|
97
|
+
@task
|
|
98
|
+
def clean():
|
|
99
|
+
"""Remove the built files."""
|
|
100
|
+
command("rm", "-f", OBJS, APP)
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
Main().run()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv run python make.py # Build default target
|
|
108
|
+
uv run python make.py clean # Run the clean task
|
|
109
|
+
uv run python make.py -w 4 # Build with 4 parallel workers
|
|
110
|
+
uv run python make.py -r # Force rebuild all
|
|
111
|
+
uv run python make.py -h # Show help with target docs
|
|
112
|
+
uv run python make.py -l # List all available targets
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`-h` displays usage information along with target documentation extracted from docstrings:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
$ uv run python make.py -h
|
|
119
|
+
usage: make.py [-h] [-C DIRECTORY] [-w WORKERS] [-r] [-l] [-v] [-V] [targets ...]
|
|
120
|
+
|
|
121
|
+
Sample build script for building a C program using chorelib.
|
|
122
|
+
|
|
123
|
+
hello.exe [default]:
|
|
124
|
+
Build executable
|
|
125
|
+
|
|
126
|
+
clean:
|
|
127
|
+
Remove the built files.
|
|
128
|
+
|
|
129
|
+
rebuild:
|
|
130
|
+
Clean and rebuild all files.
|
|
131
|
+
|
|
132
|
+
execute:
|
|
133
|
+
Rebuild and execute the program.
|
|
134
|
+
...
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`-l` lists all registered targets with their types, dependencies, and builder functions:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
$ uv run python make.py -l
|
|
141
|
+
[rule] hello.exe:
|
|
142
|
+
depends: hello.o, main.o
|
|
143
|
+
needs:
|
|
144
|
+
function: link
|
|
145
|
+
|
|
146
|
+
[rule] re: (.+)\.o:
|
|
147
|
+
depends: \1.c, hello.h
|
|
148
|
+
needs:
|
|
149
|
+
function: compile
|
|
150
|
+
|
|
151
|
+
[task] clean:
|
|
152
|
+
needs:
|
|
153
|
+
builder: clean
|
|
154
|
+
...
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Why chorelib over Make?
|
|
158
|
+
|
|
159
|
+
| | Make | chorelib |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| Pattern rules | `%.o: %.c` | `r"^(.+)\.o"` with backreferences |
|
|
162
|
+
| Variables & logic | Limited macro language | Full Python |
|
|
163
|
+
| Parallel builds | `make -j4` | `python make.py -w 4` |
|
|
164
|
+
| Non-file targets | `.PHONY` | `@task` decorator |
|
|
165
|
+
| Custom mtime | Not supported | `@mtime` decorator for databases, remote objects, etc. |
|
|
166
|
+
| Dependencies | File-only | Files, functions, or any callable |
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
chorelib-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# chorelib
|
|
2
|
+
|
|
3
|
+
A Python build automation framework — like Make, but in Python.
|
|
4
|
+
chorelib focuses on keeping Make-style speed and rebuild behavior while letting you write build logic in real Python.
|
|
5
|
+
|
|
6
|
+
chorelib uses a decorator-based DSL for defining build rules and tasks, with dependency management, parallel execution, and mtime-based rebuild detection.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Decorator-based DSL** — Define build rules with `@rule` and tasks with `@task`, using familiar Python syntax
|
|
11
|
+
- **Regex target patterns** — Match multiple targets with regex and use backreferences (`\1`) in dependency specifications
|
|
12
|
+
- **Async parallel execution** — Run independent build steps concurrently with configurable worker count
|
|
13
|
+
- **mtime-based rebuild** — Skip up-to-date targets automatically, just like Make
|
|
14
|
+
- **Custom mtime functions** — Override mtime checking per target pattern to manage non-file resources (databases, S3 objects, etc.)
|
|
15
|
+
- **Order-only prerequisites** — `needs` dependencies ensure build order without triggering rebuilds
|
|
16
|
+
- **Auto-generated help** — `-h` shows usage with target documentation from docstrings, `-l` lists all available targets
|
|
17
|
+
- **Zero dependencies** — Pure Python, no external packages required
|
|
18
|
+
|
|
19
|
+
**Best for:** developers who want Make-like rebuild behavior but need Python for complex logic or non-file resources.
|
|
20
|
+
It also works well as a simple task runner for everyday scripts.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install chorelib
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Here is a Makefile and its chorelib equivalent side by side.
|
|
31
|
+
|
|
32
|
+
**Makefile:**
|
|
33
|
+
|
|
34
|
+
```makefile
|
|
35
|
+
CC=gcc
|
|
36
|
+
CFLAGS=-I.
|
|
37
|
+
DEPS = hello.h
|
|
38
|
+
OBJS = main.o hello.o
|
|
39
|
+
.PHONY: clean
|
|
40
|
+
|
|
41
|
+
%.o: %.c $(DEPS)
|
|
42
|
+
$(CC) -c -o $@ $< $(CFLAGS)
|
|
43
|
+
|
|
44
|
+
hello.exe: $(OBJS)
|
|
45
|
+
$(CC) -o $@ $^
|
|
46
|
+
|
|
47
|
+
clean:
|
|
48
|
+
rm -f $(OBJS) hello.exe
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**chorelib (`make.py`):**
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import re
|
|
55
|
+
from chorelib import Main, command, rule, task
|
|
56
|
+
|
|
57
|
+
APP = "hello.exe"
|
|
58
|
+
CC = "gcc"
|
|
59
|
+
CFLAGS = ["-c", "-I."]
|
|
60
|
+
DEPS = ["hello.h"]
|
|
61
|
+
OBJS = ["hello.o", "main.o"]
|
|
62
|
+
|
|
63
|
+
@rule(APP, depends=OBJS, default=True)
|
|
64
|
+
def link(target, deps, needs):
|
|
65
|
+
"""Build executable"""
|
|
66
|
+
command(CC, "-o", target, deps)
|
|
67
|
+
|
|
68
|
+
@rule(re.compile(r"(.+)\.o"), depends=(r"\1.c", DEPS))
|
|
69
|
+
def compile(target, deps, needs):
|
|
70
|
+
command(CC, "-o", target, deps[0], CFLAGS)
|
|
71
|
+
|
|
72
|
+
@task
|
|
73
|
+
def clean():
|
|
74
|
+
"""Remove the built files."""
|
|
75
|
+
command("rm", "-f", OBJS, APP)
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
Main().run()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run python make.py # Build default target
|
|
83
|
+
uv run python make.py clean # Run the clean task
|
|
84
|
+
uv run python make.py -w 4 # Build with 4 parallel workers
|
|
85
|
+
uv run python make.py -r # Force rebuild all
|
|
86
|
+
uv run python make.py -h # Show help with target docs
|
|
87
|
+
uv run python make.py -l # List all available targets
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`-h` displays usage information along with target documentation extracted from docstrings:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
$ uv run python make.py -h
|
|
94
|
+
usage: make.py [-h] [-C DIRECTORY] [-w WORKERS] [-r] [-l] [-v] [-V] [targets ...]
|
|
95
|
+
|
|
96
|
+
Sample build script for building a C program using chorelib.
|
|
97
|
+
|
|
98
|
+
hello.exe [default]:
|
|
99
|
+
Build executable
|
|
100
|
+
|
|
101
|
+
clean:
|
|
102
|
+
Remove the built files.
|
|
103
|
+
|
|
104
|
+
rebuild:
|
|
105
|
+
Clean and rebuild all files.
|
|
106
|
+
|
|
107
|
+
execute:
|
|
108
|
+
Rebuild and execute the program.
|
|
109
|
+
...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`-l` lists all registered targets with their types, dependencies, and builder functions:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
$ uv run python make.py -l
|
|
116
|
+
[rule] hello.exe:
|
|
117
|
+
depends: hello.o, main.o
|
|
118
|
+
needs:
|
|
119
|
+
function: link
|
|
120
|
+
|
|
121
|
+
[rule] re: (.+)\.o:
|
|
122
|
+
depends: \1.c, hello.h
|
|
123
|
+
needs:
|
|
124
|
+
function: compile
|
|
125
|
+
|
|
126
|
+
[task] clean:
|
|
127
|
+
needs:
|
|
128
|
+
builder: clean
|
|
129
|
+
...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Why chorelib over Make?
|
|
133
|
+
|
|
134
|
+
| | Make | chorelib |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| Pattern rules | `%.o: %.c` | `r"^(.+)\.o"` with backreferences |
|
|
137
|
+
| Variables & logic | Limited macro language | Full Python |
|
|
138
|
+
| Parallel builds | `make -j4` | `python make.py -w 4` |
|
|
139
|
+
| Non-file targets | `.PHONY` | `@task` decorator |
|
|
140
|
+
| Custom mtime | Not supported | `@mtime` decorator for databases, remote objects, etc. |
|
|
141
|
+
| Dependencies | File-only | Files, functions, or any callable |
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "chorelib"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python build automation framework with decorator-based DSL"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Atsuo Ishimoto", email = "atsuoishimoto@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["build", "automation", "make", "async", "task-runner"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Topic :: Software Development :: Build Tools",
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/atsuoishimoto/chorelib"
|
|
28
|
+
Repository = "https://github.com/atsuoishimoto/chorelib"
|
|
29
|
+
Documentation = "https://chorelib.readthedocs.io/en/latest/"
|
|
30
|
+
Issues = "https://github.com/atsuoishimoto/chorelib/issues"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.8.15,<0.9.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"mypy>=1.19.1",
|
|
39
|
+
"pytest>=9.0.2",
|
|
40
|
+
"ruff>=0.15.0",
|
|
41
|
+
"sphinx>=8.1.3",
|
|
42
|
+
"sphinx-rtd-theme>=3.1.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
lint.select = [
|
|
47
|
+
"E4",
|
|
48
|
+
"E7",
|
|
49
|
+
"E9",
|
|
50
|
+
"F",
|
|
51
|
+
"I"
|
|
52
|
+
]
|
|
53
|
+
line-length = 99
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Signature: 8a477f597d28d172789f06886806bc55
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Chorelib: An async-first Python build automation framework.
|
|
2
|
+
|
|
3
|
+
Provides a decorator-based DSL for defining build rules and tasks with
|
|
4
|
+
dependency management, parallel execution, and mtime-based rebuild detection.
|
|
5
|
+
|
|
6
|
+
Typical usage::
|
|
7
|
+
|
|
8
|
+
import chorelib
|
|
9
|
+
|
|
10
|
+
@chorelib.rule("output.txt", depends="input.txt")
|
|
11
|
+
def build_output(target, depends, needs):
|
|
12
|
+
chorelib.shell(f"cp {depends[0]} {target}")
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
chorelib.Main().run()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .depmain import Main
|
|
19
|
+
from .deprunner import schedule
|
|
20
|
+
from .ruledef import RuleSet
|
|
21
|
+
from .utils import command, message, shell
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
# Default RuleSet instance used by the module-level decorators.
|
|
26
|
+
# Users can use `@chorelib.rule(...)` and `@chorelib.task` directly
|
|
27
|
+
# without creating a RuleSet manually.
|
|
28
|
+
_default_rules = RuleSet()
|
|
29
|
+
rule = _default_rules.rule
|
|
30
|
+
task = _default_rules.task
|
|
31
|
+
mtime = _default_rules.mtime
|
|
32
|
+
|
|
33
|
+
# Global verbosity level controlling message output.
|
|
34
|
+
# 0 = normal, 1 = verbose, 2 = debug messages, 3+ = logging debug.
|
|
35
|
+
verbose: int = 0
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"RuleSet",
|
|
39
|
+
"rule",
|
|
40
|
+
"task",
|
|
41
|
+
"mtime",
|
|
42
|
+
"Main",
|
|
43
|
+
"schedule",
|
|
44
|
+
"verbose",
|
|
45
|
+
"command",
|
|
46
|
+
"message",
|
|
47
|
+
"shell",
|
|
48
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Dependency graph construction and cycle detection.
|
|
2
|
+
|
|
3
|
+
Builds a directed acyclic graph (DAG) of build targets and their
|
|
4
|
+
dependencies. Detects circular dependencies via depth-first search
|
|
5
|
+
before any build execution begins.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import RuleError
|
|
11
|
+
from .ruledef import RuleBase, RuleSet, Task
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BuildInfo:
|
|
15
|
+
"""A node in the dependency graph representing a single build target.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
target: The target name (file path or task name).
|
|
19
|
+
rule: The Rule or Task that produces this target.
|
|
20
|
+
depends: List of dependency target names that trigger rebuilds.
|
|
21
|
+
needs: List of order-only prerequisite target names.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, target: str, rule: RuleBase, depends: list[str], needs: list[str]) -> None:
|
|
25
|
+
self.rule = rule
|
|
26
|
+
self.target = target
|
|
27
|
+
self.depends = depends
|
|
28
|
+
self.needs = needs
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return f"<BuildInfo> {self.rule}:{self.target}:{self.depends}:{self.needs}"
|
|
32
|
+
|
|
33
|
+
def is_task(self) -> bool:
|
|
34
|
+
"""Return True if this node represents a Task (always-execute)."""
|
|
35
|
+
return isinstance(self.rule, Task)
|
|
36
|
+
|
|
37
|
+
def run_builder(self) -> Any:
|
|
38
|
+
"""Execute the builder function for this target."""
|
|
39
|
+
return self.rule.run_builder(target=self.target, depends=self.depends, needs=self.needs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DepGraph:
|
|
43
|
+
"""Dependency graph that manages BuildInfo nodes and detects cycles.
|
|
44
|
+
|
|
45
|
+
Nodes are registered by recursively resolving each target's dependencies
|
|
46
|
+
through the RuleSet. After registration, ``detectloop`` should be called
|
|
47
|
+
to verify the graph is acyclic.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._nodes: dict[str, BuildInfo] = {}
|
|
52
|
+
|
|
53
|
+
def addtarget(self, ruleset: RuleSet, target: str) -> bool:
|
|
54
|
+
"""Add a target and all its transitive dependencies to the graph.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if the target was newly registered, False if already present.
|
|
58
|
+
"""
|
|
59
|
+
return self._register(ruleset, target)
|
|
60
|
+
|
|
61
|
+
def get(self, target: str) -> BuildInfo | None:
|
|
62
|
+
"""Return the BuildInfo node for a target, or None if not registered."""
|
|
63
|
+
return self._nodes.get(target)
|
|
64
|
+
|
|
65
|
+
def _register(self, ruleset: RuleSet, target: str) -> bool:
|
|
66
|
+
"""Recursively register a target and its dependencies.
|
|
67
|
+
|
|
68
|
+
Looks up the matching rule in the ruleset, creates a BuildInfo node,
|
|
69
|
+
and recurses into each dependency and need.
|
|
70
|
+
"""
|
|
71
|
+
if target in self._nodes:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
ret = ruleset.select_rule(target)
|
|
75
|
+
if ret:
|
|
76
|
+
rule, deps, needs = ret
|
|
77
|
+
node = BuildInfo(target, rule, deps, needs)
|
|
78
|
+
self._nodes[target] = node
|
|
79
|
+
|
|
80
|
+
# Recursively register all dependencies
|
|
81
|
+
for dep in deps:
|
|
82
|
+
self._register(ruleset, dep)
|
|
83
|
+
|
|
84
|
+
# Recursively register all order-only prerequisites
|
|
85
|
+
for need in needs:
|
|
86
|
+
self._register(ruleset, need)
|
|
87
|
+
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def detectloop(self) -> None:
|
|
91
|
+
"""Detect circular dependencies in the graph using DFS.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
RuleError: If a dependency cycle is found.
|
|
95
|
+
"""
|
|
96
|
+
done: set[BuildInfo] = set()
|
|
97
|
+
|
|
98
|
+
def _walk(node: BuildInfo, seen: set[BuildInfo]) -> None:
|
|
99
|
+
"""DFS traversal tracking the current path in ``seen``."""
|
|
100
|
+
if node in seen:
|
|
101
|
+
raise RuleError(f"Dependency cycle detected: {node}")
|
|
102
|
+
|
|
103
|
+
if node in done:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
seen.add(node)
|
|
107
|
+
try:
|
|
108
|
+
for dep in node.depends:
|
|
109
|
+
depnode = self._nodes.get(dep)
|
|
110
|
+
if depnode:
|
|
111
|
+
_walk(depnode, seen)
|
|
112
|
+
for need in node.needs:
|
|
113
|
+
neednode = self._nodes.get(need)
|
|
114
|
+
if neednode:
|
|
115
|
+
_walk(neednode, seen)
|
|
116
|
+
finally:
|
|
117
|
+
seen.remove(node)
|
|
118
|
+
done.add(node)
|
|
119
|
+
|
|
120
|
+
for node in self._nodes.values():
|
|
121
|
+
seen: set[BuildInfo] = set()
|
|
122
|
+
_walk(node, seen)
|