fab-build 0.0.2__tar.gz → 0.0.2.dev1__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.
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/PKG-INFO +2 -1
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/pyproject.toml +9 -1
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/pyproject.toml.orig +9 -13
- fab_build-0.0.2.dev1/src/fab/caching/diskcache.py +248 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/caching/hit_counter.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/caching/nullcache.py +2 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/caching/test_diskcache.py +10 -4
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/caching/test_diskcache_log.py +5 -8
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/cli.py +44 -17
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/executors/simple_executor.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/buildins.py +6 -5
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/data.py +1 -1
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/evaluation_context.py +9 -11
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/parser.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/test_parser.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/util.py +3 -2
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/__init__.py +4 -4
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/cache.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/oci.py +2 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/operation.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/outputchunks.py +1 -2
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/oci/podman.py +1 -3
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/__init__.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/containerized_gcc.py +5 -6
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/extract.py +1 -2
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/gcc.py +2 -5
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/test_containerized_gcc.py +6 -7
- fab_build-0.0.2.dev1/src/fab/util/bytesizestr.py +42 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/hash_objects.py +1 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/string_slices.py +2 -2
- fab_build-0.0.2.dev1/src/fab/util/test_bytesizestr.py +5 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/test_hash_path.py +7 -3
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/test_string_slices.py +2 -1
- fab_build-0.0.2/src/fab/caching/diskcache.py +0 -165
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/.pytest_cache/.gitignore +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/.pytest_cache/CACHEDIR.TAG +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/.pytest_cache/README.md +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/.pytest_cache/v/cache/nodeids +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/__init__.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/caching/__init__.py +2 -2
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/caching/memory.py +1 -1
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/.pytest_cache/.gitignore +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/.pytest_cache/CACHEDIR.TAG +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/.pytest_cache/README.md +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/.pytest_cache/v/cache/lastfailed +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/.pytest_cache/v/cache/nodeids +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/__init__.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/astree.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/evaluator.py +1 -1
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/grammar.lark +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/test_buildins.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/lang/test_evaluator.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/errors.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/model/executor.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/oci/__init__.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/oci/container_admin.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/oci/test_podman.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/http_archive.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/http_get.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/operations/test_http_get.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/__init__.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/flatten_list.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/hash_path.py +1 -1
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/temp_file_structure.py +0 -0
- {fab_build-0.0.2 → fab_build-0.0.2.dev1}/src/fab/util/test_flatten_list.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fab-build
|
|
3
|
-
Version: 0.0.2
|
|
3
|
+
Version: 0.0.2.dev1
|
|
4
4
|
Requires-Dist: docopt
|
|
5
5
|
Requires-Dist: blake3
|
|
6
6
|
Requires-Dist: lark
|
|
7
7
|
Requires-Dist: aiohttp
|
|
8
8
|
Requires-Dist: yarl
|
|
9
9
|
Requires-Dist: xdg-base-dirs
|
|
10
|
+
Requires-Dist: lark-rust>=0.2.1
|
|
10
11
|
Requires-Python: >=3.12
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fab-build"
|
|
3
|
-
version = "0.0.2"
|
|
3
|
+
version = "0.0.2.dev1"
|
|
4
4
|
requires-python = ">=3.12"
|
|
5
5
|
dependencies = [
|
|
6
6
|
"docopt",
|
|
@@ -9,6 +9,7 @@ dependencies = [
|
|
|
9
9
|
"aiohttp",
|
|
10
10
|
"yarl",
|
|
11
11
|
"xdg-base-dirs",
|
|
12
|
+
"lark-rust>=0.2.1",
|
|
12
13
|
]
|
|
13
14
|
|
|
14
15
|
[project.scripts]
|
|
@@ -20,3 +21,10 @@ module-name = "fab"
|
|
|
20
21
|
[build-system]
|
|
21
22
|
requires = ["uv_build>=0.13.3,<0.14.0"]
|
|
22
23
|
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=9.1.1",
|
|
28
|
+
"pytest-aiohttp>=1.1.1",
|
|
29
|
+
"pytest-asyncio>=1.4.0",
|
|
30
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fab-build"
|
|
3
|
-
version = "0.0.2"
|
|
3
|
+
version = "0.0.2.dev1"
|
|
4
4
|
requires-python = ">=3.12"
|
|
5
5
|
dependencies = [
|
|
6
6
|
"docopt",
|
|
@@ -9,20 +9,9 @@ dependencies = [
|
|
|
9
9
|
"aiohttp",
|
|
10
10
|
"yarl",
|
|
11
11
|
"xdg-base-dirs",
|
|
12
|
+
"lark-rust>=0.2.1",
|
|
12
13
|
]
|
|
13
14
|
|
|
14
|
-
# packages = [
|
|
15
|
-
# {include = "fab"}
|
|
16
|
-
#
|
|
17
|
-
# ]
|
|
18
|
-
|
|
19
|
-
# [tool.setuptools.packages.find]
|
|
20
|
-
# where = ["src"]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# [tool.setuptools.package-dir]
|
|
24
|
-
# fab = "src/fab"
|
|
25
|
-
|
|
26
15
|
[tool.uv.build-backend]
|
|
27
16
|
module-name = "fab"
|
|
28
17
|
|
|
@@ -33,3 +22,10 @@ fab = "fab:cli.main"
|
|
|
33
22
|
[build-system]
|
|
34
23
|
requires = ["uv_build>=0.13.3,<0.14.0"]
|
|
35
24
|
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.1.1",
|
|
29
|
+
"pytest-aiohttp>=1.1.1",
|
|
30
|
+
"pytest-asyncio>=1.4.0",
|
|
31
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import override
|
|
10
|
+
|
|
11
|
+
import blake3
|
|
12
|
+
|
|
13
|
+
from ..model import Cache
|
|
14
|
+
from ..util.hash_path import hash_path
|
|
15
|
+
from ..util.string_slices import string_slices
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DiskCacheError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Log:
|
|
23
|
+
MAX_KEY_LENGTH = 32 # bytes
|
|
24
|
+
RECORD_LENGTH = len("YYYY-MM-DDTHH:MM:SS " + (MAX_KEY_LENGTH) * "00" + "\n")
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: Path):
|
|
27
|
+
self.__path = path
|
|
28
|
+
self.__fd = self.__open(self.__path)
|
|
29
|
+
|
|
30
|
+
def __open(self, path: Path) -> int:
|
|
31
|
+
fd = os.open(path, os.O_APPEND | os.O_RDWR | os.O_CREAT)
|
|
32
|
+
os.lseek(fd, 0, os.SEEK_END)
|
|
33
|
+
return fd
|
|
34
|
+
|
|
35
|
+
def write(self, timestamp: datetime, op_key: bytes):
|
|
36
|
+
assert len(op_key) <= self.MAX_KEY_LENGTH
|
|
37
|
+
time_str = timestamp.isoformat(timespec="seconds")
|
|
38
|
+
os.write(
|
|
39
|
+
self.__fd,
|
|
40
|
+
f"{time_str} {op_key.hex().ljust(self.MAX_KEY_LENGTH * 2)}\n".encode(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def read_new_to_old(self) -> Generator[tuple[datetime, bytes]]:
|
|
44
|
+
position = os.lseek(self.__fd, 0, os.SEEK_CUR) - self.RECORD_LENGTH
|
|
45
|
+
while position >= 0:
|
|
46
|
+
record = self.__read_record(position)
|
|
47
|
+
if record is None:
|
|
48
|
+
break
|
|
49
|
+
yield record
|
|
50
|
+
position -= self.RECORD_LENGTH
|
|
51
|
+
|
|
52
|
+
def oldest_entry(self) -> tuple[datetime, bytes] | None:
|
|
53
|
+
"""Return the oldest entry from the log"""
|
|
54
|
+
return self.__read_record(0)
|
|
55
|
+
|
|
56
|
+
def newest_entry(self) -> tuple[datetime, bytes] | None:
|
|
57
|
+
"""Return the newest entry from the log"""
|
|
58
|
+
position = os.lseek(self.__fd, 0, os.SEEK_CUR) - self.RECORD_LENGTH
|
|
59
|
+
if position < 0:
|
|
60
|
+
return None
|
|
61
|
+
return self.__read_record(position)
|
|
62
|
+
|
|
63
|
+
def record_count(self) -> int:
|
|
64
|
+
end = os.lseek(self.__fd, 0, os.SEEK_CUR)
|
|
65
|
+
return end // self.RECORD_LENGTH
|
|
66
|
+
|
|
67
|
+
def replace_with(self, other: Log):
|
|
68
|
+
os.close(self.__fd)
|
|
69
|
+
os.rename(other.__path, self.__path)
|
|
70
|
+
self.__fd = self.__open(self.__path)
|
|
71
|
+
|
|
72
|
+
def __read_record(self, position) -> tuple[datetime, bytes] | None:
|
|
73
|
+
record = os.pread(self.__fd, self.RECORD_LENGTH, position).decode()
|
|
74
|
+
if len(record) != self.RECORD_LENGTH:
|
|
75
|
+
return None
|
|
76
|
+
time_str, _, key_str = record.partition(" ")
|
|
77
|
+
timestamp = datetime.fromisoformat(time_str)
|
|
78
|
+
op_key = bytes.fromhex(key_str)
|
|
79
|
+
return timestamp, op_key
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclasses.dataclass
|
|
83
|
+
class DiskCacheInfo:
|
|
84
|
+
path: Path
|
|
85
|
+
n_ops: int
|
|
86
|
+
n_blobs: int
|
|
87
|
+
size_blobs: int
|
|
88
|
+
oldest_op: datetime | None
|
|
89
|
+
newest_op: datetime | None
|
|
90
|
+
log_records: int
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DiskCache(Cache):
|
|
94
|
+
def __init__(self, root: Path) -> None:
|
|
95
|
+
self.__root = root
|
|
96
|
+
self.__ops = root / "v1/ops"
|
|
97
|
+
self.__ops.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
self.__blobs = root / "v1/blobs"
|
|
100
|
+
self.__blobs.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
self.__log_path = root / "v1/log"
|
|
103
|
+
self.__log = Log(self.__log_path)
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
async def has(self, op_key: bytes) -> bool:
|
|
107
|
+
op_path = self.__get_op_path(op_key)
|
|
108
|
+
result = op_path.exists()
|
|
109
|
+
if result:
|
|
110
|
+
self.__log.write(datetime.now(), op_key)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
async def get_path(self, op_key: bytes) -> Path:
|
|
115
|
+
return self.__get_path(op_key)
|
|
116
|
+
|
|
117
|
+
def __get_path(self, op_key: bytes) -> Path:
|
|
118
|
+
op_path = self.__get_op_path(op_key)
|
|
119
|
+
if not op_path.is_dir():
|
|
120
|
+
raise DiskCacheError(f"{op_path} does not exist")
|
|
121
|
+
|
|
122
|
+
object_link = op_path / "object"
|
|
123
|
+
if not object_link.is_symlink():
|
|
124
|
+
raise DiskCacheError(f"Corrupt cache, {object_link} is not a symlink")
|
|
125
|
+
|
|
126
|
+
blob_path = object_link.resolve()
|
|
127
|
+
|
|
128
|
+
return blob_path
|
|
129
|
+
|
|
130
|
+
@override
|
|
131
|
+
async def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
132
|
+
if not path.exists():
|
|
133
|
+
raise DiskCacheError(f"Cannot store non-existent path {path} in cache")
|
|
134
|
+
|
|
135
|
+
blob_key = hash_path(blake3.blake3(), path)
|
|
136
|
+
|
|
137
|
+
blob_path = self.__get_blob_path(blob_key)
|
|
138
|
+
blob_path.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
obj_path = blob_path / path.name
|
|
140
|
+
if not obj_path.exists():
|
|
141
|
+
path.rename(obj_path)
|
|
142
|
+
|
|
143
|
+
op_path = self.__get_op_path(op_key)
|
|
144
|
+
if op_path.exists():
|
|
145
|
+
raise DiskCacheError(
|
|
146
|
+
f"Operation with key '{op_key.hex()}' already exists in cache"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
op_path.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
(op_path / "object").symlink_to(obj_path.relative_to(op_path, walk_up=True))
|
|
151
|
+
|
|
152
|
+
self.__log.write(datetime.now(), op_key)
|
|
153
|
+
return obj_path
|
|
154
|
+
|
|
155
|
+
def prune_to_size(self, size: int):
|
|
156
|
+
|
|
157
|
+
kept_ops: dict[bytes, datetime] = dict()
|
|
158
|
+
kept_blobs: set[bytes] = set()
|
|
159
|
+
kept_blobs_size: int = 0
|
|
160
|
+
discarded_ops: set[bytes] = set()
|
|
161
|
+
discarded_blobs: set[bytes] = set()
|
|
162
|
+
|
|
163
|
+
for timestamp, op_key in self.__log.read_new_to_old():
|
|
164
|
+
blob_path = self.__get_path(op_key)
|
|
165
|
+
blob_size = blob_path.stat().st_size
|
|
166
|
+
blob_key = bytes.fromhex(blob_path.parent.name)
|
|
167
|
+
|
|
168
|
+
if blob_key not in kept_blobs and kept_blobs_size + blob_size <= size:
|
|
169
|
+
kept_blobs.add(blob_key)
|
|
170
|
+
kept_blobs_size += blob_size
|
|
171
|
+
|
|
172
|
+
if blob_key in kept_blobs:
|
|
173
|
+
if op_key not in kept_ops:
|
|
174
|
+
kept_ops[op_key] = timestamp
|
|
175
|
+
else:
|
|
176
|
+
discarded_ops.add(op_key)
|
|
177
|
+
discarded_blobs.add(blob_key)
|
|
178
|
+
|
|
179
|
+
for op_key in discarded_ops:
|
|
180
|
+
self.__discard_op(op_key)
|
|
181
|
+
|
|
182
|
+
for blob_key in discarded_blobs:
|
|
183
|
+
self.__discard_blob(blob_key)
|
|
184
|
+
|
|
185
|
+
new_log = Log(self.__log_path.with_suffix(".new"))
|
|
186
|
+
for op_key, timestamp in reversed(kept_ops.items()):
|
|
187
|
+
new_log.write(timestamp, op_key)
|
|
188
|
+
|
|
189
|
+
self.__log.replace_with(new_log)
|
|
190
|
+
|
|
191
|
+
def prune_to_age(self, timestamp: datetime):
|
|
192
|
+
"""Remove all entries that have not been used after timestamp."""
|
|
193
|
+
# TODO, sometime...
|
|
194
|
+
raise NotImplementedError()
|
|
195
|
+
|
|
196
|
+
def get_info(self) -> DiskCacheInfo:
|
|
197
|
+
|
|
198
|
+
n_ops = 0
|
|
199
|
+
blob_keys: set[bytes] = set()
|
|
200
|
+
size_blobs = 0
|
|
201
|
+
|
|
202
|
+
for op_path in self.__scan_ops():
|
|
203
|
+
n_ops += 1
|
|
204
|
+
object_path = op_path / "object"
|
|
205
|
+
blob_key = bytes.fromhex(object_path.parent.name)
|
|
206
|
+
|
|
207
|
+
if blob_key in blob_keys:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
blob_keys.add(blob_key)
|
|
211
|
+
blob_path = object_path.resolve()
|
|
212
|
+
size_blobs += blob_path.stat().st_size
|
|
213
|
+
|
|
214
|
+
oldest_op = self.__log.oldest_entry()
|
|
215
|
+
newest_op = self.__log.newest_entry()
|
|
216
|
+
|
|
217
|
+
return DiskCacheInfo(
|
|
218
|
+
path=self.__root,
|
|
219
|
+
n_ops=n_ops,
|
|
220
|
+
n_blobs=len(blob_keys),
|
|
221
|
+
size_blobs=size_blobs,
|
|
222
|
+
oldest_op=oldest_op[0] if oldest_op else None,
|
|
223
|
+
newest_op=newest_op[0] if newest_op else None,
|
|
224
|
+
log_records=self.__log.record_count(),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def __get_op_path(self, op_key: bytes) -> Path:
|
|
228
|
+
key_parts = string_slices(op_key.hex(), 2, 2)
|
|
229
|
+
return self.__ops / key_parts[0] / key_parts[1] / op_key.hex()
|
|
230
|
+
|
|
231
|
+
def __get_blob_path(self, blob_key: bytes) -> Path:
|
|
232
|
+
key_parts = string_slices(blob_key.hex(), 2, 2)
|
|
233
|
+
return self.__blobs / key_parts[0] / key_parts[1] / blob_key.hex()
|
|
234
|
+
|
|
235
|
+
def __discard_op(self, op_key: bytes):
|
|
236
|
+
op_path = self.__get_op_path(op_key)
|
|
237
|
+
os.unlink(op_path / "object")
|
|
238
|
+
os.rmdir(op_path)
|
|
239
|
+
|
|
240
|
+
def __discard_blob(self, blob_key: bytes):
|
|
241
|
+
blob_path = self.__get_blob_path(blob_key)
|
|
242
|
+
shutil.rmtree(blob_path)
|
|
243
|
+
|
|
244
|
+
def __scan_ops(self) -> Generator[Path]:
|
|
245
|
+
for dir1 in os.scandir(self.__ops):
|
|
246
|
+
for dir2 in os.scandir(dir1):
|
|
247
|
+
for entry in os.scandir(dir2):
|
|
248
|
+
yield Path(entry.path)
|
|
@@ -68,10 +68,16 @@ async def test_prune_to_size():
|
|
|
68
68
|
cache = DiskCache(root / "cache_dir")
|
|
69
69
|
op1_key = bytes.fromhex("abcd01")
|
|
70
70
|
op2_key = bytes.fromhex("abcd02")
|
|
71
|
-
file1 = await cache.store_path(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
cache.
|
|
71
|
+
file1 = await cache.store_path(
|
|
72
|
+
op1_key, create_file(root / "file1", "aa")
|
|
73
|
+
) # 2 bytes
|
|
74
|
+
file2 = await cache.store_path(
|
|
75
|
+
op2_key, create_file(root / "file2", "bb")
|
|
76
|
+
) # 2 bytes
|
|
77
|
+
|
|
78
|
+
assert cache.get_info().log_records == 2
|
|
79
|
+
cache.prune_to_size(3) # 3 bytes
|
|
80
|
+
assert cache.get_info().log_records == 1
|
|
75
81
|
|
|
76
82
|
assert not await cache.has(op1_key)
|
|
77
83
|
assert not file1.exists()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import random
|
|
1
2
|
from datetime import datetime, timedelta
|
|
2
3
|
from pathlib import Path
|
|
3
|
-
|
|
4
|
+
|
|
4
5
|
from .diskcache import Log
|
|
5
6
|
|
|
6
7
|
ONE_HOUR = timedelta(hours=1)
|
|
@@ -11,11 +12,8 @@ def random_key() -> bytes:
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def test_empty(tmp_path: Path):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
log2 = Log(tmp_path / "log.log")
|
|
18
|
-
assert list(log2.read_entries()) == []
|
|
15
|
+
log = Log(tmp_path / "log.log")
|
|
16
|
+
assert list(log.read_new_to_old()) == []
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
def test_read(tmp_path: Path):
|
|
@@ -32,11 +30,10 @@ def test_read(tmp_path: Path):
|
|
|
32
30
|
log1.write(t0, k0)
|
|
33
31
|
log1.write(t1, k1)
|
|
34
32
|
log1.write(t2, k2)
|
|
35
|
-
log1.flush()
|
|
36
33
|
|
|
37
34
|
log2 = Log(tmp_path / "log.log")
|
|
38
35
|
|
|
39
|
-
gen = log2.
|
|
36
|
+
gen = log2.read_new_to_old()
|
|
40
37
|
assert next(gen) == (t2, k2)
|
|
41
38
|
assert next(gen) == (t1, k1)
|
|
42
39
|
assert next(gen) == (t0, k0)
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
Usage:
|
|
4
4
|
fab [options] (do|run|ls|tree) <target> [--file FILE] [--no-cache] [--no-remote-cache]
|
|
5
5
|
fab [options] list-targets [--file FILE]
|
|
6
|
-
fab [options] clean
|
|
7
|
-
fab [options]
|
|
8
|
-
fab [options] cache
|
|
6
|
+
fab [options] containers clean
|
|
7
|
+
fab [options] cache info
|
|
8
|
+
fab [options] cache prune <size>
|
|
9
9
|
fab --version
|
|
10
10
|
|
|
11
11
|
Options:
|
|
@@ -19,30 +19,29 @@ Options:
|
|
|
19
19
|
|
|
20
20
|
import asyncio
|
|
21
21
|
import subprocess
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
22
24
|
from typing import Any, override
|
|
25
|
+
|
|
23
26
|
from docopt import docopt
|
|
24
|
-
from pathlib import Path
|
|
25
27
|
from xdg_base_dirs import xdg_data_home
|
|
26
|
-
import tempfile
|
|
27
28
|
|
|
28
29
|
from fab.oci.container_admin import ContainerAdmin
|
|
29
30
|
from fab.oci.podman import PodmanClient
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
from .lang.data import List, Object, PathObj
|
|
33
|
-
from .caching import DiskCache, CacheHitCounter
|
|
32
|
+
from .caching import CacheHitCounter, DiskCache
|
|
34
33
|
from .executors.simple_executor import SimpleOperationExecutor
|
|
34
|
+
from .lang.data import List, Object, PathObj
|
|
35
35
|
from .lang.evaluation_context import DefaultEvaluationContext
|
|
36
36
|
from .lang.evaluator import EvaluationError, evaluate_context
|
|
37
37
|
from .model import (
|
|
38
38
|
Cache,
|
|
39
39
|
CommandFailedError,
|
|
40
|
-
OperationContext,
|
|
41
40
|
OciContainer,
|
|
42
|
-
|
|
43
|
-
StreamType,
|
|
41
|
+
OperationContext,
|
|
44
42
|
)
|
|
45
43
|
from .model.executor import OperationContextFactory
|
|
44
|
+
from .util.bytesizestr import bytesize2str, str2bytesize
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
class MainOperationContextFactory(OperationContextFactory):
|
|
@@ -168,7 +167,8 @@ async def cli():
|
|
|
168
167
|
# disk_cache = HitCounter(DiskCache(...))
|
|
169
168
|
# remote_cache = HitCounter(RemoteCache(...))
|
|
170
169
|
# combi_cache = ChainedCache(disk_cache, remote_cache)
|
|
171
|
-
|
|
170
|
+
disk_cache = DiskCache(data_home / "cache")
|
|
171
|
+
cache = CacheHitCounter(disk_cache)
|
|
172
172
|
|
|
173
173
|
# ui = Tui(disk_cache_counter, remote_cache_counter)
|
|
174
174
|
|
|
@@ -197,6 +197,7 @@ async def cli():
|
|
|
197
197
|
try:
|
|
198
198
|
eval_result = evaluate_context(evaluation_context)
|
|
199
199
|
result = await eval_result.get_attr(target)
|
|
200
|
+
print("Done")
|
|
200
201
|
print(f"Cache stats: {cache.hits} hits, {cache.misses} misses")
|
|
201
202
|
except EvaluationError as e:
|
|
202
203
|
print(format_evaluation_error(e))
|
|
@@ -208,13 +209,19 @@ async def cli():
|
|
|
208
209
|
return result
|
|
209
210
|
|
|
210
211
|
async def do():
|
|
212
|
+
await evaluate_target()
|
|
213
|
+
|
|
214
|
+
async def ls():
|
|
211
215
|
result = await evaluate_target()
|
|
212
|
-
|
|
216
|
+
if not isinstance(result, PathObj):
|
|
217
|
+
print(f"Target '{target}' does not evaluate to a Path")
|
|
218
|
+
return
|
|
219
|
+
print(result.path)
|
|
213
220
|
|
|
214
221
|
async def run():
|
|
215
222
|
result = await evaluate_target()
|
|
216
223
|
if not result:
|
|
217
|
-
return
|
|
224
|
+
return
|
|
218
225
|
if not isinstance(result, PathObj):
|
|
219
226
|
print(f"Target '{target}' does not evaluate to a Path")
|
|
220
227
|
return
|
|
@@ -226,18 +233,38 @@ async def cli():
|
|
|
226
233
|
for name in build_object.attrs():
|
|
227
234
|
print(name)
|
|
228
235
|
|
|
229
|
-
async def
|
|
236
|
+
async def containers_clean():
|
|
230
237
|
count = await container_admin.clean_containers()
|
|
231
238
|
print(f"Removed {count} container(s).")
|
|
232
239
|
|
|
240
|
+
def cache_info():
|
|
241
|
+
info = disk_cache.get_info()
|
|
242
|
+
print(f"root: {info.path}")
|
|
243
|
+
print(f"count: {info.n_ops} ops / {info.n_blobs} blobs")
|
|
244
|
+
print(f"size: {bytesize2str(info.size_blobs)}")
|
|
245
|
+
print(f"oldest: {info.oldest_op or 'n/a'}")
|
|
246
|
+
print(f"newest: {info.newest_op or 'n/a'}")
|
|
247
|
+
print(f"records: {info.log_records}")
|
|
248
|
+
|
|
249
|
+
def cache_prune():
|
|
250
|
+
size = str2bytesize(args["<size>"])
|
|
251
|
+
print(f"Pruning to {bytesize2str(size)}")
|
|
252
|
+
disk_cache.prune_to_size(size)
|
|
253
|
+
|
|
233
254
|
if args["list-targets"]:
|
|
234
255
|
list_targets()
|
|
235
|
-
elif args["
|
|
236
|
-
await
|
|
256
|
+
elif args["containers"] and args["clean"]:
|
|
257
|
+
await containers_clean()
|
|
237
258
|
elif args["do"]:
|
|
238
259
|
await do()
|
|
260
|
+
elif args["ls"]:
|
|
261
|
+
await ls()
|
|
239
262
|
elif args["run"]:
|
|
240
263
|
await run()
|
|
264
|
+
elif args["cache"] and args["info"]:
|
|
265
|
+
cache_info()
|
|
266
|
+
elif args["cache"] and args["prune"]:
|
|
267
|
+
cache_prune()
|
|
241
268
|
else:
|
|
242
269
|
print(args)
|
|
243
270
|
|
|
@@ -3,23 +3,24 @@ from pathlib import Path
|
|
|
3
3
|
from typing import override
|
|
4
4
|
|
|
5
5
|
from fab.lang.util import DictObject
|
|
6
|
-
from ..model import OperationError
|
|
7
6
|
|
|
8
|
-
from ..
|
|
7
|
+
from ..model import OperationError
|
|
8
|
+
from ..operations import Extract, HttpArchive, HttpGet
|
|
9
9
|
from ..operations.containerized_gcc import (
|
|
10
10
|
GccCompile as ContainerizedGccCompile,
|
|
11
|
+
)
|
|
12
|
+
from ..operations.containerized_gcc import (
|
|
11
13
|
GccLink as ContainerizedGccLink,
|
|
12
14
|
)
|
|
13
|
-
|
|
14
|
-
from ..operations import Extract, HttpArchive, HttpGet
|
|
15
|
+
from ..operations.gcc import GccCollectCompileCommands, GccCompile, GccLink
|
|
15
16
|
from .data import (
|
|
16
17
|
EvaluationContext,
|
|
17
18
|
Function,
|
|
19
|
+
FunctionCallError,
|
|
18
20
|
List,
|
|
19
21
|
Object,
|
|
20
22
|
PathObj,
|
|
21
23
|
String,
|
|
22
|
-
FunctionCallError,
|
|
23
24
|
)
|
|
24
25
|
from .evaluator import evaluate_context
|
|
25
26
|
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
from collections.abc import Mapping
|
|
2
1
|
import pathlib
|
|
2
|
+
from collections.abc import Mapping
|
|
3
3
|
from typing import Any, override
|
|
4
4
|
|
|
5
|
-
from ..lang.data import EvaluationContext, Object
|
|
6
|
-
|
|
7
|
-
from ..model import Operation, OperationExecutor
|
|
8
|
-
|
|
9
5
|
from ..lang.buildins import (
|
|
6
|
+
ContainerizedGcc,
|
|
7
|
+
ExtractFunc,
|
|
8
|
+
GccCollectCompileCommandsFunc,
|
|
10
9
|
GccCompileFunc,
|
|
11
10
|
GccLinkFunc,
|
|
12
|
-
|
|
11
|
+
HttpArchiveFunc,
|
|
12
|
+
HttpGetFunc,
|
|
13
|
+
LinkFunc,
|
|
13
14
|
LoadFunc,
|
|
14
15
|
PathFunc,
|
|
15
|
-
LinkFunc,
|
|
16
|
-
HttpGetFunc,
|
|
17
|
-
ExtractFunc,
|
|
18
|
-
HttpArchiveFunc,
|
|
19
|
-
ContainerizedGcc,
|
|
20
16
|
)
|
|
17
|
+
from ..lang.data import EvaluationContext, Object
|
|
18
|
+
from ..model import Operation, OperationExecutor
|
|
21
19
|
|
|
22
20
|
buildins = {
|
|
23
21
|
"load": LoadFunc(),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import pathlib
|
|
1
2
|
from abc import ABC
|
|
2
3
|
from collections.abc import Mapping, Sequence
|
|
3
|
-
import pathlib
|
|
4
4
|
from typing import override
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from .data import EvaluationContext, Function, Object
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class DictObject(Object):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from .cache import Cache
|
|
2
|
-
from .operation import Operation, OperationContext, OperationError
|
|
3
|
-
from .executor import OperationExecutor
|
|
4
|
-
from .oci import OciContainer, OciClient, OciProcess
|
|
5
|
-
from .outputchunks import StreamType, OutputChunk
|
|
6
2
|
from .errors import CommandFailedError, Error
|
|
3
|
+
from .executor import OperationExecutor
|
|
4
|
+
from .oci import OciClient, OciContainer, OciProcess
|
|
5
|
+
from .operation import Operation, OperationContext, OperationError
|
|
6
|
+
from .outputchunks import OutputChunk, StreamType
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
9
|
"Cache",
|
|
@@ -14,9 +14,7 @@ The multiplexed exec stream follows the Docker framing protocol:
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import abc
|
|
18
17
|
import asyncio
|
|
19
|
-
import enum
|
|
20
18
|
import json
|
|
21
19
|
import os
|
|
22
20
|
import struct
|
|
@@ -27,6 +25,7 @@ import aiohttp
|
|
|
27
25
|
|
|
28
26
|
from fab.model import OutputChunk
|
|
29
27
|
|
|
28
|
+
from ..model import StreamType
|
|
30
29
|
from ..model.oci import (
|
|
31
30
|
ImageNotFoundError,
|
|
32
31
|
OciClient,
|
|
@@ -37,7 +36,6 @@ from ..model.oci import (
|
|
|
37
36
|
PullError,
|
|
38
37
|
PullEvent,
|
|
39
38
|
)
|
|
40
|
-
from ..model import StreamType
|
|
41
39
|
|
|
42
40
|
# ---------------------------------------------------------------------------
|
|
43
41
|
# Helpers
|
|
@@ -4,8 +4,7 @@ from typing import override
|
|
|
4
4
|
|
|
5
5
|
from blake3 import blake3
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
from ..model import Operation, OperationContext, CommandFailedError
|
|
7
|
+
from ..model import CommandFailedError, Operation, OperationContext
|
|
9
8
|
from ..util.flatten_list import flatten
|
|
10
9
|
from ..util.hash_objects import hash_objects
|
|
11
10
|
from .gcc import CompileCommandsCollector, CompileObject
|
|
@@ -125,7 +124,7 @@ class GccCompile(Operation):
|
|
|
125
124
|
)
|
|
126
125
|
)
|
|
127
126
|
|
|
128
|
-
# First see if we can find the dependencies of this file. If this file has
|
|
127
|
+
# First see if we can find the dependencies of this file. If this file has
|
|
129
128
|
# been compiled before its deps are stored under the following key.
|
|
130
129
|
deps_key = hash_objects(
|
|
131
130
|
blake3(),
|
|
@@ -139,9 +138,9 @@ class GccCompile(Operation):
|
|
|
139
138
|
deps_cached = await context.cache_check(deps_key)
|
|
140
139
|
|
|
141
140
|
if deps_cached:
|
|
142
|
-
# Deps file is found. Next step is to see if we can reuse the previous build
|
|
141
|
+
# Deps file is found. Next step is to see if we can reuse the previous build
|
|
143
142
|
# result, ea: none of the dependencies have changed.
|
|
144
|
-
# We do that by calculating the key for this source file combined with
|
|
143
|
+
# We do that by calculating the key for this source file combined with
|
|
145
144
|
# all the dependencies.
|
|
146
145
|
|
|
147
146
|
deps_file = await context.cache_load_path(deps_key)
|
|
@@ -157,7 +156,7 @@ class GccCompile(Operation):
|
|
|
157
156
|
)
|
|
158
157
|
|
|
159
158
|
if await context.cache_check(key):
|
|
160
|
-
# Dependencies have not changed! Lets load the build result from cache
|
|
159
|
+
# Dependencies have not changed! Lets load the build result from cache
|
|
161
160
|
# and return
|
|
162
161
|
return await context.cache_load_path(key)
|
|
163
162
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import shutil
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Any, override
|
|
3
4
|
|
|
@@ -6,8 +7,6 @@ from blake3 import blake3
|
|
|
6
7
|
from ..model import Operation, OperationContext
|
|
7
8
|
from ..util.hash_objects import hash_objects
|
|
8
9
|
|
|
9
|
-
import shutil
|
|
10
|
-
|
|
11
10
|
|
|
12
11
|
class Extract(Operation):
|
|
13
12
|
def __init__(self, archive: Path) -> None:
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, override
|
|
7
7
|
|
|
8
8
|
from blake3 import blake3
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from ..model import Operation, OperationContext
|
|
10
|
+
from ..model import Operation, OperationContext
|
|
14
11
|
from ..util.hash_objects import hash_objects
|
|
15
12
|
|
|
16
13
|
|
|
@@ -9,9 +9,11 @@ from .containerized_gcc import GccLink, _parse_make_deps
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def test_parse_make_deps():
|
|
12
|
-
assert list(
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
assert list(
|
|
13
|
+
_parse_make_deps(
|
|
14
|
+
"main.o: main.cpp lib1.h lib2.h \\\n lib3.h lib4.h \\\n lib5.h path\\ with\\ spaces.h"
|
|
15
|
+
)
|
|
16
|
+
) == [
|
|
15
17
|
Path("lib1.h"),
|
|
16
18
|
Path("lib2.h"),
|
|
17
19
|
Path("lib3.h"),
|
|
@@ -21,7 +23,6 @@ def test_parse_make_deps():
|
|
|
21
23
|
]
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
|
|
25
26
|
async def _empty_chunks() -> AsyncIterator[OutputChunk]:
|
|
26
27
|
for _ in ():
|
|
27
28
|
yield OutputChunk(StreamType.STDOUT, b"")
|
|
@@ -126,9 +127,7 @@ async def test_link_options_appended(tmp_path: Path):
|
|
|
126
127
|
context = RecordingContext(tmp_path)
|
|
127
128
|
obj = tmp_path / "obj.o"
|
|
128
129
|
obj.write_bytes(b"content")
|
|
129
|
-
op = GccLink(
|
|
130
|
-
"img", None, "out", [obj], options=["-lm", "-Wl,--as-needed"]
|
|
131
|
-
)
|
|
130
|
+
op = GccLink("img", None, "out", [obj], options=["-lm", "-Wl,--as-needed"])
|
|
132
131
|
await op.execute(context)
|
|
133
132
|
assert context.container is not None
|
|
134
133
|
cmd = context.container.cmd
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
__steps = [
|
|
4
|
+
(1024**0, "B"),
|
|
5
|
+
(1024**1, "KiB"),
|
|
6
|
+
(1024**2, "MiB"),
|
|
7
|
+
(1024**3, "GiB"),
|
|
8
|
+
(1024**4, "TiB"),
|
|
9
|
+
(1024**5, "PiB"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def bytesize2str(size: int) -> str:
|
|
14
|
+
for step_size, suffix in __steps:
|
|
15
|
+
limit = step_size * 1024
|
|
16
|
+
if size < limit:
|
|
17
|
+
return f"{size / step_size:.1f} {suffix}"
|
|
18
|
+
return "huge!"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__suffixes = {
|
|
22
|
+
"": 1,
|
|
23
|
+
"K": 1024**1,
|
|
24
|
+
"M": 1024**2,
|
|
25
|
+
"G": 1024**3,
|
|
26
|
+
"T": 1024**4,
|
|
27
|
+
"P": 1024**5,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def str2bytesize(size: str) -> int:
|
|
32
|
+
match = re.match(r"(\d+(?:\.\d+)?)(\w?)", size)
|
|
33
|
+
if not match:
|
|
34
|
+
raise ValueError(f"Could not convert '{size}' to bytes")
|
|
35
|
+
|
|
36
|
+
value = float(match.group(1))
|
|
37
|
+
suffix = match.group(2).upper()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
return int(value * __suffixes[suffix])
|
|
41
|
+
except KeyError:
|
|
42
|
+
raise ValueError(f"Unknown suffix '{suffix}'")
|
|
@@ -11,8 +11,8 @@ def string_slices(inp: str, *slices: int) -> Sequence[str]:
|
|
|
11
11
|
for size in slices:
|
|
12
12
|
if current_pos + size > len(inp):
|
|
13
13
|
raise ValueError("Slice sizes exceed input string length.")
|
|
14
|
-
|
|
15
|
-
result.append(inp[current_pos:current_pos + size])
|
|
14
|
+
|
|
15
|
+
result.append(inp[current_pos : current_pos + size])
|
|
16
16
|
current_pos += size
|
|
17
17
|
|
|
18
18
|
return result
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
from .temp_file_structure import TempFileStructure
|
|
2
|
-
from .hash_path import hash_path
|
|
3
|
-
from pathlib import Path
|
|
4
1
|
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
5
4
|
import pytest
|
|
6
5
|
|
|
6
|
+
from .hash_path import hash_path
|
|
7
|
+
from .temp_file_structure import TempFileStructure
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
def path_key(path: Path) -> str:
|
|
8
11
|
hashfunc = hashlib.sha256()
|
|
9
12
|
return hash_path(hashfunc, path).hex()
|
|
10
13
|
|
|
14
|
+
|
|
11
15
|
def test_path():
|
|
12
16
|
with TempFileStructure(
|
|
13
17
|
[
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
from collections.abc import Generator
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
import os
|
|
4
|
-
from typing import override
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from ..model import Cache
|
|
8
|
-
|
|
9
|
-
from ..util.string_slices import string_slices
|
|
10
|
-
from ..util.hash_path import hash_path
|
|
11
|
-
|
|
12
|
-
import blake3
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DiskCacheError(RuntimeError):
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Log:
|
|
20
|
-
def __init__(self, path: Path) -> None:
|
|
21
|
-
self.__path = path
|
|
22
|
-
self.__path.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
self.__writer = open(self.__path, "a")
|
|
24
|
-
|
|
25
|
-
def __del__(self):
|
|
26
|
-
self.flush()
|
|
27
|
-
|
|
28
|
-
def write(self, timestamp: datetime, key: bytes):
|
|
29
|
-
time_str = timestamp.isoformat()
|
|
30
|
-
self.__writer.write(f"{time_str} {key.hex()}\n")
|
|
31
|
-
|
|
32
|
-
def flush(self):
|
|
33
|
-
self.__writer.flush()
|
|
34
|
-
|
|
35
|
-
def clear(self):
|
|
36
|
-
self.clear()
|
|
37
|
-
# TODO
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
|
-
def read_entries(self) -> Generator[tuple[datetime, bytes]]:
|
|
41
|
-
"""Read entries from log, most recent first."""
|
|
42
|
-
|
|
43
|
-
self.flush()
|
|
44
|
-
|
|
45
|
-
# NOTE: This approach is very inefficient for large files, will need refactoring at some point!
|
|
46
|
-
for line in reversed(self.__path.read_text().splitlines()):
|
|
47
|
-
parts = line.split(" ", 1)
|
|
48
|
-
timestamp = datetime.fromisoformat(parts[0])
|
|
49
|
-
key = bytes.fromhex(parts[1])
|
|
50
|
-
yield timestamp, key
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class DiskCache(Cache):
|
|
54
|
-
def __init__(self, root: Path) -> None:
|
|
55
|
-
|
|
56
|
-
self.__ops = root / "v1/ops"
|
|
57
|
-
self.__ops.mkdir(parents=True, exist_ok=True)
|
|
58
|
-
|
|
59
|
-
self.__blobs = root / "v1/blobs"
|
|
60
|
-
self.__blobs.mkdir(parents=True, exist_ok=True)
|
|
61
|
-
|
|
62
|
-
self.__log = Log(root / "v1/hits")
|
|
63
|
-
|
|
64
|
-
def __get_op_path(self, op_key: bytes) -> Path:
|
|
65
|
-
key_parts = string_slices(op_key.hex(), 2, 2)
|
|
66
|
-
return self.__ops / key_parts[0] / key_parts[1] / op_key.hex()
|
|
67
|
-
|
|
68
|
-
def __get_blob_path(self, blob_key: bytes) -> Path:
|
|
69
|
-
key_parts = string_slices(blob_key.hex(), 2, 2)
|
|
70
|
-
return self.__blobs / key_parts[0] / key_parts[1] / blob_key.hex()
|
|
71
|
-
|
|
72
|
-
def prune_to_size(self, size: int):
|
|
73
|
-
"""Remove longest unused entries until the combined size of the
|
|
74
|
-
remaining entries is <= size."""
|
|
75
|
-
|
|
76
|
-
blobs_to_keep: set[bytes] = set()
|
|
77
|
-
kept_blobs_size: int = 0
|
|
78
|
-
|
|
79
|
-
for _, key in self.__log.read_entries():
|
|
80
|
-
path = self.__get_path(key)
|
|
81
|
-
blob_size = path.stat().st_size
|
|
82
|
-
|
|
83
|
-
if kept_blobs_size + blob_size > size:
|
|
84
|
-
break
|
|
85
|
-
|
|
86
|
-
blobs_to_keep.add(key)
|
|
87
|
-
kept_blobs_size += blob_size
|
|
88
|
-
|
|
89
|
-
self.__prune(blobs_to_keep)
|
|
90
|
-
|
|
91
|
-
def prune_to_age(self, timestamp: datetime):
|
|
92
|
-
"""Remove all entries that have not been used after timestamp."""
|
|
93
|
-
pass
|
|
94
|
-
# TODO
|
|
95
|
-
|
|
96
|
-
def __prune(self, blobs_to_keep: set[bytes]):
|
|
97
|
-
|
|
98
|
-
for op_path in self.__scan_ops():
|
|
99
|
-
object_path = op_path / "object"
|
|
100
|
-
blob_key = bytes.fromhex(object_path.parent.name)
|
|
101
|
-
|
|
102
|
-
if blob_key in blobs_to_keep:
|
|
103
|
-
continue
|
|
104
|
-
|
|
105
|
-
blob_path = object_path.resolve().parent
|
|
106
|
-
object_path.resolve().unlink()
|
|
107
|
-
object_path.unlink()
|
|
108
|
-
op_path.rmdir()
|
|
109
|
-
blob_path.rmdir()
|
|
110
|
-
|
|
111
|
-
def __scan_ops(self) -> Generator[Path]:
|
|
112
|
-
for dir1 in os.scandir(self.__ops):
|
|
113
|
-
for dir2 in os.scandir(dir1):
|
|
114
|
-
for entry in os.scandir(dir2):
|
|
115
|
-
yield Path(entry.path)
|
|
116
|
-
|
|
117
|
-
@override
|
|
118
|
-
async def has(self, op_key: bytes) -> bool:
|
|
119
|
-
op_path = self.__get_op_path(op_key)
|
|
120
|
-
result = op_path.exists()
|
|
121
|
-
if result:
|
|
122
|
-
self.__log.write(datetime.now(), op_key)
|
|
123
|
-
return result
|
|
124
|
-
|
|
125
|
-
@override
|
|
126
|
-
async def get_path(self, op_key: bytes) -> Path:
|
|
127
|
-
return self.__get_path(op_key)
|
|
128
|
-
|
|
129
|
-
def __get_path(self, op_key: bytes) -> Path:
|
|
130
|
-
op_path = self.__get_op_path(op_key)
|
|
131
|
-
if not op_path.is_dir():
|
|
132
|
-
raise DiskCacheError(f"{op_path} does not exist")
|
|
133
|
-
|
|
134
|
-
object_link = op_path / "object"
|
|
135
|
-
if not object_link.is_symlink():
|
|
136
|
-
raise DiskCacheError(f"Corrupt cache, {object_link} is not a symlink")
|
|
137
|
-
|
|
138
|
-
blob_path = object_link.resolve()
|
|
139
|
-
|
|
140
|
-
return blob_path
|
|
141
|
-
|
|
142
|
-
@override
|
|
143
|
-
async def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
144
|
-
if not path.exists():
|
|
145
|
-
raise DiskCacheError(f"Cannot store non-existent path {path} in cache")
|
|
146
|
-
|
|
147
|
-
blob_key = hash_path(blake3.blake3(), path)
|
|
148
|
-
|
|
149
|
-
blob_path = self.__get_blob_path(blob_key)
|
|
150
|
-
blob_path.mkdir(parents=True, exist_ok=True)
|
|
151
|
-
obj_path = blob_path / path.name
|
|
152
|
-
if not obj_path.exists():
|
|
153
|
-
path.rename(obj_path)
|
|
154
|
-
|
|
155
|
-
op_path = self.__get_op_path(op_key)
|
|
156
|
-
if op_path.exists():
|
|
157
|
-
raise DiskCacheError(
|
|
158
|
-
f"Operation with key '{op_key.hex()}' already exists in cache"
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
op_path.mkdir(parents=True, exist_ok=True)
|
|
162
|
-
(op_path / "object").symlink_to(obj_path.relative_to(op_path, walk_up=True))
|
|
163
|
-
|
|
164
|
-
self.__log.write(datetime.now(), op_key)
|
|
165
|
-
return obj_path
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from .memory import MemoryCache
|
|
2
|
-
from .nullcache import NullCache
|
|
3
1
|
from .diskcache import DiskCache
|
|
4
2
|
from .hit_counter import CacheHitCounter
|
|
3
|
+
from .memory import MemoryCache
|
|
4
|
+
from .nullcache import NullCache
|
|
5
5
|
|
|
6
6
|
__all__ = ["MemoryCache", "NullCache", "DiskCache", "CacheHitCounter"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -2,9 +2,9 @@ from collections.abc import Mapping, Sequence
|
|
|
2
2
|
from typing import override
|
|
3
3
|
|
|
4
4
|
from ..lang import astree
|
|
5
|
+
from ..model import Error
|
|
5
6
|
from .data import EvaluationContext, Function, List, Object, String
|
|
6
7
|
from .parser import parse
|
|
7
|
-
from ..model import Error
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class EvaluationError(RuntimeError):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|