vest-build 0.0.1__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.
- vest_build-0.0.1/PKG-INFO +68 -0
- vest_build-0.0.1/README.md +57 -0
- vest_build-0.0.1/pyproject.toml +23 -0
- vest_build-0.0.1/src/vest/__init__.py +55 -0
- vest_build-0.0.1/src/vest/__main__.py +221 -0
- vest_build-0.0.1/src/vest/common/collections.py +83 -0
- vest_build-0.0.1/src/vest/common/fs.py +28 -0
- vest_build-0.0.1/src/vest/common/log.py +145 -0
- vest_build-0.0.1/src/vest/common/tooling.py +26 -0
- vest_build-0.0.1/src/vest/spec/caching.py +108 -0
- vest_build-0.0.1/src/vest/spec/core.py +408 -0
- vest_build-0.0.1/src/vest/spec/externals.py +69 -0
- vest_build-0.0.1/src/vest/spec/fs.py +133 -0
- vest_build-0.0.1/src/vest/spec/host.py +312 -0
- vest_build-0.0.1/src/vest/spec/types.py +108 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: vest-build
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Python-based build system for bespoke use
|
|
5
|
+
Author: ascpixi
|
|
6
|
+
Author-email: ascpixi <ascpixi@proton.me>
|
|
7
|
+
Requires-Dist: packaging>=26.0
|
|
8
|
+
Requires-Dist: rich>=15.0.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# 🦺 Vest
|
|
13
|
+
[](https://pypi.python.org/pypi/vest-build)
|
|
14
|
+
[](https://github.com/ascpixi/vest/blob/main/LICENSE)
|
|
15
|
+
[](https://pypi.python.org/pypi/vest-build)
|
|
16
|
+
[](https://github.com/ascpixi/vest/actions)
|
|
17
|
+
|
|
18
|
+
Vest is a Python-based build system for bespoke use.
|
|
19
|
+
|
|
20
|
+
## Getting started
|
|
21
|
+
First, install Vest from PyPI:
|
|
22
|
+
```
|
|
23
|
+
pip install vest-build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then, create a `build.vest` file! What this file will do depends on your project. Let's say you're building a C project. In this case, your `.vest` file might look like this:
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
from vest import *
|
|
30
|
+
|
|
31
|
+
component(
|
|
32
|
+
name = "my-c-project"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@task
|
|
36
|
+
def build():
|
|
37
|
+
# Let's turn each C file to an object file...
|
|
38
|
+
for (src, dst) in build_list(
|
|
39
|
+
src = "./src/**/*.c",
|
|
40
|
+
dst = "./obj",
|
|
41
|
+
ext = ".o",
|
|
42
|
+
flatten = True
|
|
43
|
+
):
|
|
44
|
+
run("gcc", ["-c", src, "-o", dst])
|
|
45
|
+
|
|
46
|
+
# ...then link em' all!
|
|
47
|
+
run("gcc", [
|
|
48
|
+
"-o", "./bin/final",
|
|
49
|
+
*glob("./obj/*.o")
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
# Tasks should return all of their final artifacts. This can be a string, list of strings, or a dictionary of strings.
|
|
53
|
+
return "./bin/final"
|
|
54
|
+
|
|
55
|
+
@task
|
|
56
|
+
def run():
|
|
57
|
+
# "run" needs a build to execute!
|
|
58
|
+
artifact = self_dependency(build)
|
|
59
|
+
|
|
60
|
+
# Let's run what "build" gave us.
|
|
61
|
+
run(artifact)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
In the directory you've created the `vest.build` file in, run `vest -t build`.
|
|
65
|
+
|
|
66
|
+
## Extensions
|
|
67
|
+
Vest doesn't assume what languages nor frameworks you're using it for. However, there's a couple of ready PyPI packages for known use-cases:
|
|
68
|
+
- [`vest-build-dotnet`](https://github.com/ascpixi/vest-dotnet): support for the .NET SDK.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 🦺 Vest
|
|
2
|
+
[](https://pypi.python.org/pypi/vest-build)
|
|
3
|
+
[](https://github.com/ascpixi/vest/blob/main/LICENSE)
|
|
4
|
+
[](https://pypi.python.org/pypi/vest-build)
|
|
5
|
+
[](https://github.com/ascpixi/vest/actions)
|
|
6
|
+
|
|
7
|
+
Vest is a Python-based build system for bespoke use.
|
|
8
|
+
|
|
9
|
+
## Getting started
|
|
10
|
+
First, install Vest from PyPI:
|
|
11
|
+
```
|
|
12
|
+
pip install vest-build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then, create a `build.vest` file! What this file will do depends on your project. Let's say you're building a C project. In this case, your `.vest` file might look like this:
|
|
16
|
+
|
|
17
|
+
```py
|
|
18
|
+
from vest import *
|
|
19
|
+
|
|
20
|
+
component(
|
|
21
|
+
name = "my-c-project"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@task
|
|
25
|
+
def build():
|
|
26
|
+
# Let's turn each C file to an object file...
|
|
27
|
+
for (src, dst) in build_list(
|
|
28
|
+
src = "./src/**/*.c",
|
|
29
|
+
dst = "./obj",
|
|
30
|
+
ext = ".o",
|
|
31
|
+
flatten = True
|
|
32
|
+
):
|
|
33
|
+
run("gcc", ["-c", src, "-o", dst])
|
|
34
|
+
|
|
35
|
+
# ...then link em' all!
|
|
36
|
+
run("gcc", [
|
|
37
|
+
"-o", "./bin/final",
|
|
38
|
+
*glob("./obj/*.o")
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
# Tasks should return all of their final artifacts. This can be a string, list of strings, or a dictionary of strings.
|
|
42
|
+
return "./bin/final"
|
|
43
|
+
|
|
44
|
+
@task
|
|
45
|
+
def run():
|
|
46
|
+
# "run" needs a build to execute!
|
|
47
|
+
artifact = self_dependency(build)
|
|
48
|
+
|
|
49
|
+
# Let's run what "build" gave us.
|
|
50
|
+
run(artifact)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
In the directory you've created the `vest.build` file in, run `vest -t build`.
|
|
54
|
+
|
|
55
|
+
## Extensions
|
|
56
|
+
Vest doesn't assume what languages nor frameworks you're using it for. However, there's a couple of ready PyPI packages for known use-cases:
|
|
57
|
+
- [`vest-build-dotnet`](https://github.com/ascpixi/vest-dotnet): support for the .NET SDK.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vest-build"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A Python-based build system for bespoke use"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "ascpixi", email = "ascpixi@proton.me" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"packaging>=26.0",
|
|
12
|
+
"rich>=15.0.0"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
vest = "vest.__main__:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
module-name = "vest"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines parts of the component system specification that are recommended to be used in all Vest components.
|
|
3
|
+
This module should only be imported from components, via `from vest import *`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pkgutil import extend_path
|
|
7
|
+
__path__ = extend_path(__path__, __name__)
|
|
8
|
+
|
|
9
|
+
from vest.spec.core import (
|
|
10
|
+
artifact_dir,
|
|
11
|
+
artifact_dir_path_of,
|
|
12
|
+
clean_artifact_dir,
|
|
13
|
+
artifact_root,
|
|
14
|
+
artifact_root_path_of,
|
|
15
|
+
clean_artifact_root,
|
|
16
|
+
component,
|
|
17
|
+
dependency,
|
|
18
|
+
dependent_on,
|
|
19
|
+
host,
|
|
20
|
+
require_task,
|
|
21
|
+
self_dependency,
|
|
22
|
+
task,
|
|
23
|
+
run,
|
|
24
|
+
run_tool,
|
|
25
|
+
parameter
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from vest.spec.externals import (
|
|
29
|
+
git_repo,
|
|
30
|
+
GitRepoResolver
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from vest.spec.types import (
|
|
34
|
+
Component,
|
|
35
|
+
ComponentTask,
|
|
36
|
+
BuildError,
|
|
37
|
+
ExternalRepoResolver
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from vest.spec.fs import (
|
|
41
|
+
build_list,
|
|
42
|
+
copy,
|
|
43
|
+
copy_many,
|
|
44
|
+
expand_path,
|
|
45
|
+
foreign_file,
|
|
46
|
+
glob,
|
|
47
|
+
make_unique_dir
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from vest.common.collections import (
|
|
51
|
+
find,
|
|
52
|
+
maybe,
|
|
53
|
+
single,
|
|
54
|
+
flatten
|
|
55
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import traceback
|
|
4
|
+
import subprocess
|
|
5
|
+
from rich import print
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
import rich.traceback
|
|
8
|
+
|
|
9
|
+
from vest.common.log import is_agent, log_warn, log_error_hdr, log_success_hdr
|
|
10
|
+
from vest.spec.host import StandaloneHost
|
|
11
|
+
from vest.spec.types import BuildError, UsageError
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
console = Console()
|
|
15
|
+
rich.traceback.install()
|
|
16
|
+
|
|
17
|
+
parser = argparse.ArgumentParser()
|
|
18
|
+
parser.add_argument("-t", "--task", help = "The task to run.", required = True)
|
|
19
|
+
parser.add_argument("-p", "--path", default = "./build.vest", help = "The path to the target component.")
|
|
20
|
+
parser.add_argument("-r", "--run", action = "store_true", help = "If set, the 'run_script' of the given task will be invoked.")
|
|
21
|
+
parser.add_argument("--no-cache", action = "store_true", help = "If set, the results of the previous build will be ignored.")
|
|
22
|
+
args, extra_args = parser.parse_known_args()
|
|
23
|
+
|
|
24
|
+
def parse_extra_args():
|
|
25
|
+
transformed: dict[str, str] = {}
|
|
26
|
+
|
|
27
|
+
prev_arg: str | None = None
|
|
28
|
+
expecting_key = True
|
|
29
|
+
|
|
30
|
+
for arg in extra_args:
|
|
31
|
+
is_key = arg.startswith("-")
|
|
32
|
+
if is_key != expecting_key:
|
|
33
|
+
if expecting_key:
|
|
34
|
+
# Two keys right next to each other (e.g --type --arch)
|
|
35
|
+
if prev_arg is not None:
|
|
36
|
+
log_error_hdr(f"Two or more values specified for parameter {prev_arg}.")
|
|
37
|
+
exit(1)
|
|
38
|
+
else:
|
|
39
|
+
log_error_hdr(f"Stray argument {arg} found.")
|
|
40
|
+
exit(1)
|
|
41
|
+
else:
|
|
42
|
+
# Two values right next to each other (e.g. --arch amd64 release)
|
|
43
|
+
log_error_hdr(f"No value found for parameter {prev_arg}.")
|
|
44
|
+
exit(1)
|
|
45
|
+
|
|
46
|
+
if not expecting_key:
|
|
47
|
+
# There isn't really a possibility where we don't have a previous arg here. We're looking for a value, thus we went over the first iteration
|
|
48
|
+
# (which was looking for a key).
|
|
49
|
+
assert prev_arg is not None
|
|
50
|
+
|
|
51
|
+
# This is value! "prev_arg" holds the key, while "arg" holds the value.
|
|
52
|
+
transformed[prev_arg] = arg
|
|
53
|
+
|
|
54
|
+
prev_arg = arg
|
|
55
|
+
expecting_key = not expecting_key
|
|
56
|
+
|
|
57
|
+
return transformed
|
|
58
|
+
|
|
59
|
+
host = StandaloneHost(
|
|
60
|
+
parameters = parse_extra_args(),
|
|
61
|
+
load_cache = not args.no_cache
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
if os.path.isdir(args.path):
|
|
66
|
+
args.path = os.path.join(args.path, "build.vest")
|
|
67
|
+
|
|
68
|
+
component = host.eval_component(args.path)
|
|
69
|
+
except Exception as ex:
|
|
70
|
+
log_error_hdr("Could not evaluate the component")
|
|
71
|
+
print(f"[red] ╰─> {ex}[/]")
|
|
72
|
+
print()
|
|
73
|
+
|
|
74
|
+
console.print_exception()
|
|
75
|
+
exit(1)
|
|
76
|
+
|
|
77
|
+
if not any(t.name == args.task for t in component.tasks):
|
|
78
|
+
log_error_hdr(f"No task named '{args.task}'.")
|
|
79
|
+
print("[red] ╰─> The defined tasks are:[/]")
|
|
80
|
+
|
|
81
|
+
for task in component.tasks:
|
|
82
|
+
if task.description is not None:
|
|
83
|
+
print(f"[red] - {task.name}: {task.description}[/]")
|
|
84
|
+
else:
|
|
85
|
+
print(f"[red] - {task.name}[/]")
|
|
86
|
+
|
|
87
|
+
exit(1)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
task = next(x for x in component.tasks if x.name == args.task)
|
|
91
|
+
artifacts = host.run_task(task)
|
|
92
|
+
except Exception as ex:
|
|
93
|
+
if type(ex) is UsageError:
|
|
94
|
+
# A UsageError indicates that neither Vest or the components in question are at fault - the user might've e.g. not specified a
|
|
95
|
+
# required parameter. In this case, we don't print the stacktrace at all.
|
|
96
|
+
print(" │")
|
|
97
|
+
log_error_hdr(f"Could not build [bold]{component.name}:{args.task}[/]")
|
|
98
|
+
print(f"[red] ╰─> {ex}[/]")
|
|
99
|
+
print()
|
|
100
|
+
exit(1)
|
|
101
|
+
|
|
102
|
+
# https://github.com/Textualize/rich/blob/master/rich/traceback.py#L234
|
|
103
|
+
rich.traceback.Traceback.LEXERS[".vest"] = "python"
|
|
104
|
+
|
|
105
|
+
# Skip the first two frames; which are this script and the component host
|
|
106
|
+
# respectively. We only want to show the faulting components.
|
|
107
|
+
if ex.__traceback__ is not None:
|
|
108
|
+
first = ex.__traceback__
|
|
109
|
+
for i in range(2):
|
|
110
|
+
if ex.__traceback__.tb_next is None:
|
|
111
|
+
ex.__traceback__ = first # revert back to the previous state
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
ex.__traceback__ = ex.__traceback__.tb_next
|
|
115
|
+
|
|
116
|
+
# A BuildError indicates that the cause of the error is incorrect usage of the
|
|
117
|
+
# component system (as opposed to e.g. a bug). In this case, we don't need to
|
|
118
|
+
# show the frames that belong to the component system.
|
|
119
|
+
if type(ex) is BuildError:
|
|
120
|
+
# Filter out stacktraces that are not .vest files (unless it's the last one)
|
|
121
|
+
prev = ex.__traceback__
|
|
122
|
+
current = prev.tb_next
|
|
123
|
+
while current is not None and current.tb_next is not None:
|
|
124
|
+
if not current.tb_frame.f_code.co_filename.endswith(".vest"):
|
|
125
|
+
prev.tb_next = current.tb_next
|
|
126
|
+
current = prev.tb_next
|
|
127
|
+
else:
|
|
128
|
+
prev = current
|
|
129
|
+
current = current.tb_next
|
|
130
|
+
|
|
131
|
+
# Don't show the last frame
|
|
132
|
+
current = ex.__traceback__
|
|
133
|
+
prev = None
|
|
134
|
+
|
|
135
|
+
while current.tb_next is not None:
|
|
136
|
+
prev = current
|
|
137
|
+
current = current.tb_next
|
|
138
|
+
|
|
139
|
+
if prev is not None:
|
|
140
|
+
prev.tb_next = None
|
|
141
|
+
|
|
142
|
+
print(" │")
|
|
143
|
+
log_error_hdr(f"Could not build [bold]{component.name}:{args.task}[/]")
|
|
144
|
+
print(f"[red] ╰─> {ex}[/]")
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
if type(ex) is subprocess.CalledProcessError:
|
|
148
|
+
stdout = ex.stdout.decode("utf-8") if type(ex.stdout) is bytes else str(ex.stdout)
|
|
149
|
+
stderr = ex.stderr.decode("utf-8") if type(ex.stderr) is bytes else str(ex.stderr)
|
|
150
|
+
|
|
151
|
+
stdout = stdout.strip()
|
|
152
|
+
stderr = stderr.strip()
|
|
153
|
+
|
|
154
|
+
if stdout:
|
|
155
|
+
print(f"[red]Standard output:[/]")
|
|
156
|
+
print(stdout)
|
|
157
|
+
print()
|
|
158
|
+
|
|
159
|
+
if stderr:
|
|
160
|
+
print(f"[red]Standard error:[/]")
|
|
161
|
+
print(stderr)
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
if not is_agent():
|
|
165
|
+
console.print_exception()
|
|
166
|
+
else:
|
|
167
|
+
traceback.print_exc()
|
|
168
|
+
|
|
169
|
+
print()
|
|
170
|
+
exit(1)
|
|
171
|
+
|
|
172
|
+
print(" │")
|
|
173
|
+
log_success_hdr(f"Component [bold]{component.name}:{args.task}[/] built successfully")
|
|
174
|
+
|
|
175
|
+
if not args.run:
|
|
176
|
+
if type(artifacts) is str:
|
|
177
|
+
print(f" ╰─> You may find the resulting artifact in [bold]{artifacts}[/].")
|
|
178
|
+
elif type(artifacts) is list:
|
|
179
|
+
print(f" ╰─> The following artifacts have been created:")
|
|
180
|
+
|
|
181
|
+
for entry in artifacts:
|
|
182
|
+
print(f" - {entry}")
|
|
183
|
+
elif type(artifacts) is dict:
|
|
184
|
+
print(f" ╰─> The following named artifacts have been created:")
|
|
185
|
+
|
|
186
|
+
for (k, v) in artifacts.items():
|
|
187
|
+
print(f" - [bold]{k}[/]: {v}")
|
|
188
|
+
else:
|
|
189
|
+
print(artifacts) # this should never happen...
|
|
190
|
+
|
|
191
|
+
print()
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
host.write_cache()
|
|
195
|
+
except:
|
|
196
|
+
print()
|
|
197
|
+
print(" │")
|
|
198
|
+
log_warn(f"Could not write the build cache")
|
|
199
|
+
print(f"[yellow] ╰─> An exception occured while storing the build cache file.[/]")
|
|
200
|
+
print(f"[yellow] This might result in anomalous behavior in future build system invocations.[/]")
|
|
201
|
+
print(f"[yellow] You may delete the 'artifacts' directory if this is the case.[/]")
|
|
202
|
+
print()
|
|
203
|
+
console.print_exception()
|
|
204
|
+
|
|
205
|
+
if args.run:
|
|
206
|
+
if task.run_script == None:
|
|
207
|
+
log_error_hdr(f"Cannot run/debug the artifacts of [bold]{component.name}:{task.name}[/]")
|
|
208
|
+
print(f"[red] ╰─> The component does not define a 'run_script' to invoke.[/]")
|
|
209
|
+
exit(1)
|
|
210
|
+
|
|
211
|
+
run_args = task.run_script(artifacts)
|
|
212
|
+
assert len(run_args) > 0
|
|
213
|
+
|
|
214
|
+
console.show_cursor()
|
|
215
|
+
|
|
216
|
+
# Hand off control to the script, so that we don't mess stuff up with any
|
|
217
|
+
# e.g. signal handlers we may have installed
|
|
218
|
+
os.execvp(run_args[0], run_args)
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Callable, Iterable, TypeVar, overload
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
TKey = TypeVar("TKey")
|
|
5
|
+
TValue = TypeVar("TValue")
|
|
6
|
+
|
|
7
|
+
def find(collection: list[T], predicate: Callable[[T], bool]) -> T | None:
|
|
8
|
+
"Finds the first item in the given collection that meets the specified predicate."
|
|
9
|
+
for x in collection:
|
|
10
|
+
if predicate(x):
|
|
11
|
+
return x
|
|
12
|
+
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
def single(collection: list[T], predicate: Callable[[T], bool]) -> T:
|
|
16
|
+
"""
|
|
17
|
+
Picks the first item in the given collection that meets the specified predicate.
|
|
18
|
+
If no item meets the predicate, a `LookupError` is raised.
|
|
19
|
+
"""
|
|
20
|
+
for x in collection:
|
|
21
|
+
if predicate(x):
|
|
22
|
+
return x
|
|
23
|
+
|
|
24
|
+
raise LookupError("No value in the given collection matches the predicate.")
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def maybe(value: dict[TKey, TValue], when: bool) -> dict[TKey, TValue]:
|
|
28
|
+
"""
|
|
29
|
+
Returns the given `value` if `condition` is `True` - otherwise, returns an empty dictionary.
|
|
30
|
+
|
|
31
|
+
This function is useful when defining conditional collection elements:
|
|
32
|
+
```
|
|
33
|
+
example = {
|
|
34
|
+
"key1": "always present",
|
|
35
|
+
*maybe({ "key2": "sometimes present" }, when = some_condition)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
@overload
|
|
42
|
+
def maybe(value: T | list[T], when: bool) -> list[T]:
|
|
43
|
+
"""
|
|
44
|
+
Returns a 1-element list of the given `value` if `condition` is `True` - otherwise,
|
|
45
|
+
returns an empty list. If `value` is a `list`, it is returned instead.
|
|
46
|
+
|
|
47
|
+
This function is useful when defining conditional collection elements:
|
|
48
|
+
```
|
|
49
|
+
example = [
|
|
50
|
+
"always present",
|
|
51
|
+
*maybe("sometimes present", when = some_condition)
|
|
52
|
+
]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
...as opposed to:
|
|
56
|
+
```
|
|
57
|
+
example = [
|
|
58
|
+
"always present",
|
|
59
|
+
*(["sometimes present"] if some_condition else [])
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def maybe(value: T | list[T] | dict[TKey, TValue], when: bool) -> list[T] | dict[TKey, TValue]:
|
|
66
|
+
if isinstance(value, dict):
|
|
67
|
+
return value if when else {}
|
|
68
|
+
|
|
69
|
+
if not isinstance(value, list):
|
|
70
|
+
return [value] if when else []
|
|
71
|
+
else:
|
|
72
|
+
return value if when else []
|
|
73
|
+
|
|
74
|
+
def flatten(value: Iterable[list[T]]) -> list[T]:
|
|
75
|
+
"""
|
|
76
|
+
Flattens a collection of other, nested lists. For example, the sequence `[[1, 2], [3, 4], [5, 6]]`
|
|
77
|
+
becomes `[1, 2, 3, 4, 5, 6]`.
|
|
78
|
+
"""
|
|
79
|
+
result: list[T] = []
|
|
80
|
+
for subarr in value:
|
|
81
|
+
result.extend(subarr)
|
|
82
|
+
|
|
83
|
+
return result
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
def walk_down(start_path: str, end_path: str):
|
|
4
|
+
"""
|
|
5
|
+
Returns the paths to traverse when walking down a directory tree, starting at
|
|
6
|
+
`start_path`, and ending with `end_path`.
|
|
7
|
+
|
|
8
|
+
@start_path: The path to being traversal in. Must be a child of `end_path`.
|
|
9
|
+
@end_path: The path to end traversing directories on. Must be a parent of `start_path`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
start_path = os.path.abspath(start_path)
|
|
13
|
+
end_path = os.path.abspath(end_path)
|
|
14
|
+
|
|
15
|
+
if not start_path.startswith(end_path):
|
|
16
|
+
raise ValueError(f"End path '{end_path}' is not a parent of start path '{start_path}'")
|
|
17
|
+
|
|
18
|
+
current_path = start_path
|
|
19
|
+
paths = [current_path]
|
|
20
|
+
|
|
21
|
+
while current_path != end_path:
|
|
22
|
+
if current_path == os.path.dirname(current_path): # We've reached the root
|
|
23
|
+
raise ValueError(f"Reached root directory before finding end path '{end_path}'")
|
|
24
|
+
|
|
25
|
+
current_path = os.path.dirname(current_path)
|
|
26
|
+
paths.append(current_path)
|
|
27
|
+
|
|
28
|
+
return paths
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import atexit
|
|
3
|
+
import shutil
|
|
4
|
+
import rich
|
|
5
|
+
from rich import print
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
_current_log_indent = 0
|
|
11
|
+
_task_stack: list[str] = []
|
|
12
|
+
|
|
13
|
+
def is_github_actions():
|
|
14
|
+
return "GITHUB_RUN_ID" in os.environ
|
|
15
|
+
|
|
16
|
+
def is_agent():
|
|
17
|
+
return "CLAUDECODE" in os.environ or "GEMINI_CLI" in os.environ or "CURSOR_AGENT" in os.environ
|
|
18
|
+
|
|
19
|
+
def is_non_interactive():
|
|
20
|
+
return is_github_actions() or is_agent()
|
|
21
|
+
|
|
22
|
+
def log_dedent(delta: int = 2):
|
|
23
|
+
global _current_log_indent
|
|
24
|
+
_current_log_indent -= delta
|
|
25
|
+
_task_stack.pop()
|
|
26
|
+
|
|
27
|
+
def log_reset_terminal():
|
|
28
|
+
rich.reconfigure()
|
|
29
|
+
rich.console.Console().show_cursor()
|
|
30
|
+
|
|
31
|
+
def log_deinit():
|
|
32
|
+
"""Performs on-exit de-initialization of the logging sub-system."""
|
|
33
|
+
if not is_non_interactive():
|
|
34
|
+
log_reset_terminal()
|
|
35
|
+
|
|
36
|
+
def log_get_effective_width():
|
|
37
|
+
return shutil.get_terminal_size().columns - _current_log_indent
|
|
38
|
+
|
|
39
|
+
def log_dependency(full_name: str, fulfilled = False):
|
|
40
|
+
global _current_log_indent
|
|
41
|
+
|
|
42
|
+
if not fulfilled:
|
|
43
|
+
_current_log_indent += 2
|
|
44
|
+
_task_stack.append(full_name)
|
|
45
|
+
|
|
46
|
+
if is_agent():
|
|
47
|
+
print(f"{full_name} was built before - skipping because of cache." if fulfilled else f"Building {full_name}...")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
formatted = (
|
|
51
|
+
f"◆ Dependency on [bold]{full_name}[/] [bright_black](fullfilled previously)[/]" if fulfilled else
|
|
52
|
+
f"◇ Building [bold]{full_name}[/]..."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if _current_log_indent == 0:
|
|
56
|
+
print(f"[white] {formatted}[/]")
|
|
57
|
+
else:
|
|
58
|
+
print(f"[white] {'├'.ljust(_current_log_indent, '─')} {formatted}[/]")
|
|
59
|
+
|
|
60
|
+
def log(prefix: str, msg: str | Text):
|
|
61
|
+
"""
|
|
62
|
+
Logs a message to the console. All arguments can be formatted using Rich markup.
|
|
63
|
+
If executing in a CI environment, no special formatting will be applied.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if not isinstance(msg, Text):
|
|
67
|
+
msg = Text.from_markup(msg)
|
|
68
|
+
|
|
69
|
+
if is_non_interactive():
|
|
70
|
+
print(prefix, msg)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def create_table(border: Text):
|
|
74
|
+
grid = Table.grid()
|
|
75
|
+
grid.add_column()
|
|
76
|
+
grid.add_column()
|
|
77
|
+
grid.add_column()
|
|
78
|
+
grid.add_column()
|
|
79
|
+
|
|
80
|
+
grid.add_row(
|
|
81
|
+
border,
|
|
82
|
+
Text.from_markup(prefix),
|
|
83
|
+
" ",
|
|
84
|
+
msg
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return grid
|
|
88
|
+
|
|
89
|
+
console = Console()
|
|
90
|
+
with console.capture() as capture:
|
|
91
|
+
console.print(create_table(Text(" │".ljust(_current_log_indent))))
|
|
92
|
+
|
|
93
|
+
simulated = capture.get()
|
|
94
|
+
print(create_table(
|
|
95
|
+
Text((
|
|
96
|
+
# Make sure the first row, which holds the border, is as large as the table.
|
|
97
|
+
(" │".ljust(_current_log_indent + 2) + "\n") * simulated.count("\n")
|
|
98
|
+
).rstrip("\n"))
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
def _get_agent_label(label: str):
|
|
102
|
+
if len(_task_stack) == 0:
|
|
103
|
+
return f"({label}) "
|
|
104
|
+
|
|
105
|
+
return f"({label}@{_task_stack[-1]}) "
|
|
106
|
+
|
|
107
|
+
def log_info(msg: str | Text):
|
|
108
|
+
"""Logs an informational message to the console."""
|
|
109
|
+
log("[white bold]info[/]" if not is_non_interactive() else "(info) " if is_github_actions() else _get_agent_label("info"), msg)
|
|
110
|
+
|
|
111
|
+
def log_warn(msg: str | Text):
|
|
112
|
+
"""Logs a warning message to the console."""
|
|
113
|
+
if isinstance(msg, Text):
|
|
114
|
+
msg.stylize("#ffffaf")
|
|
115
|
+
else:
|
|
116
|
+
msg = f"[#ffffaf]{msg}[/]"
|
|
117
|
+
|
|
118
|
+
log("[yellow bold]warn[/]" if not is_non_interactive() else "::warning::" if is_github_actions() else _get_agent_label("warning"), msg)
|
|
119
|
+
|
|
120
|
+
def log_error(msg: str | Text):
|
|
121
|
+
"""Logs an error message to the console."""
|
|
122
|
+
if isinstance(msg, Text):
|
|
123
|
+
msg.stylize("#ffcbd7")
|
|
124
|
+
else:
|
|
125
|
+
msg = f"[#ffcbd7]{msg}[/]"
|
|
126
|
+
|
|
127
|
+
log("[red bold]error[/]" if not is_non_interactive() else "::error::" if is_github_actions() else _get_agent_label("error"), msg)
|
|
128
|
+
|
|
129
|
+
def log_error_hdr(msg: str):
|
|
130
|
+
"Prints an error header to the console."
|
|
131
|
+
if is_agent():
|
|
132
|
+
print(f"Failure: {msg}")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
print(f"[black on red] × ERROR [/] [red]{msg}[/]")
|
|
136
|
+
|
|
137
|
+
def log_success_hdr(msg: str):
|
|
138
|
+
"Prints a success header to the console."
|
|
139
|
+
if is_agent():
|
|
140
|
+
print(f"Success: {msg}")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
print(f"[black on green] ❯ SUCCESS [/] [green]{msg}[/]")
|
|
144
|
+
|
|
145
|
+
atexit.register(log_deinit)
|