fab-build 0.0.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # from . import caching
2
+ #
3
+ #
4
+ # __all__ = ["caching"]
fab/__main__.py ADDED
@@ -0,0 +1,2 @@
1
+ from . import main
2
+ main.main()
@@ -0,0 +1,5 @@
1
+ from .memory import MemoryCache
2
+ from .nullcache import NullCache
3
+ from .diskcache import DiskCache
4
+
5
+ __all__ = ["MemoryCache", "NullCache", "DiskCache"]
@@ -0,0 +1,73 @@
1
+ from typing import override
2
+ from pathlib import Path
3
+ from ..model import Cache
4
+
5
+ from ..util.string_slices import string_slices
6
+ from ..util.hash_path import hash_path
7
+
8
+ import blake3
9
+
10
+
11
+ class DiskCacheError(RuntimeError):
12
+ pass
13
+
14
+
15
+ class DiskCache(Cache):
16
+ def __init__(self, root: Path) -> None:
17
+
18
+ self.__ops = root / "v1/ops"
19
+ self.__ops.mkdir(parents=True, exist_ok=True)
20
+
21
+ self.__blobs = root / "v1/blobs"
22
+ self.__blobs.mkdir(parents=True, exist_ok=True)
23
+
24
+ def __get_op_path(self, op_key: bytes) -> Path:
25
+ key_parts = string_slices(op_key.hex(), 2, 2)
26
+ return self.__ops / key_parts[0] / key_parts[1] / op_key.hex()
27
+
28
+ def __get_blob_path(self, blob_key: bytes) -> Path:
29
+ key_parts = string_slices(blob_key.hex(), 2, 2)
30
+ return self.__blobs / key_parts[0] / key_parts[1] / blob_key.hex()
31
+
32
+ @override
33
+ def has(self, op_key: bytes) -> bool:
34
+ op_path = self.__get_op_path(op_key)
35
+ return op_path.exists()
36
+
37
+ @override
38
+ def get_path(self, op_key: bytes) -> Path:
39
+ op_path = self.__get_op_path(op_key)
40
+ if not op_path.is_dir():
41
+ raise DiskCacheError(f"{op_path} does not exist")
42
+
43
+ object_link = op_path / "object"
44
+ if not object_link.is_symlink():
45
+ raise DiskCacheError(f"Corrupt cache, {object_link} is not a symlink")
46
+
47
+ blob_path = object_link.resolve()
48
+
49
+ return blob_path
50
+
51
+ @override
52
+ def store_path(self, op_key: bytes, path: Path) -> Path:
53
+ if not path.exists():
54
+ raise DiskCacheError(f"Cannot store non-existent path {path} in cache")
55
+
56
+ blob_key = hash_path(blake3.blake3(), path)
57
+
58
+ blob_path = self.__get_blob_path(blob_key)
59
+ blob_path.mkdir(parents=True, exist_ok=True)
60
+ obj_path = blob_path / path.name
61
+ if not obj_path.exists():
62
+ path.rename(obj_path)
63
+
64
+ op_path = self.__get_op_path(op_key)
65
+ if op_path.exists():
66
+ raise DiskCacheError(
67
+ f"Operation with key '{op_key.hex()}' already exists in cache"
68
+ )
69
+
70
+ op_path.mkdir(parents=True, exist_ok=True)
71
+ (op_path / "object").symlink_to(obj_path.relative_to(op_path, walk_up=True))
72
+
73
+ return obj_path
fab/caching/memory.py ADDED
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+ from typing import override
3
+
4
+
5
+ from ..model import Cache
6
+
7
+ class MemoryCache(Cache):
8
+ def __init__(self):
9
+ self.__cached_paths: dict[bytes, Path] = {}
10
+
11
+ @override
12
+ def has(self, op_key: bytes) -> bool:
13
+ return op_key in self.__cached_paths
14
+
15
+ @override
16
+ def get_path(self, op_key: bytes) -> Path:
17
+ return self.__cached_paths[op_key]
18
+
19
+ @override
20
+ def store_path(self, op_key: bytes, path: Path) -> Path:
21
+ self.__cached_paths[op_key] = path
22
+ return path
23
+
24
+ def print_cache(self) -> None:
25
+ for key, value in self.__cached_paths.items():
26
+ print(f"{key.hex()}: {value}")
@@ -0,0 +1,16 @@
1
+ from pathlib import Path
2
+ from typing import override
3
+ from ..model import Cache
4
+
5
+ class NullCache(Cache):
6
+ @override
7
+ def has(self, op_key: bytes) -> bool:
8
+ return False
9
+
10
+ @override
11
+ def get_path(self, op_key: bytes) -> Path:
12
+ raise KeyError()
13
+
14
+ @override
15
+ def store_path(self, op_key: bytes, path: Path) -> Path:
16
+ raise NotImplementedError()
@@ -0,0 +1,52 @@
1
+ import hashlib
2
+ from pathlib import Path
3
+
4
+ from ..util.hash_path import hash_path
5
+ from ..util.temp_file_structure import TempFileStructure
6
+ from .diskcache import DiskCache
7
+
8
+
9
+ def path_key(path: Path) -> str:
10
+ hashfunc = hashlib.sha256()
11
+ return hash_path(hashfunc, path).hex()
12
+
13
+
14
+ def test_store_has_get_path():
15
+ with TempFileStructure(
16
+ [
17
+ ("objects/file1", "file1"),
18
+ ("cache_dir/", None),
19
+ ]
20
+ ) as root:
21
+ cache = DiskCache(root / "cache_dir")
22
+ file = root / "objects/file1"
23
+ op_key = bytes.fromhex("abcdef1234567890")
24
+ original_key = path_key(file)
25
+
26
+ assert not cache.has(op_key)
27
+ cached_file = cache.store_path(op_key, file)
28
+
29
+ assert path_key(cached_file) == original_key
30
+ assert cache.has(op_key)
31
+
32
+ retrieved_file = cache.get_path(op_key)
33
+ assert path_key(retrieved_file) == original_key
34
+
35
+
36
+ def test_store_again():
37
+ with TempFileStructure(
38
+ [
39
+ ("objects1/file", "file"),
40
+ ("objects2/file", "file"),
41
+ ("cache_dir/", None),
42
+ ]
43
+ ) as root:
44
+ cache = DiskCache(root / "cache_dir")
45
+ file1 = root / "objects1/file"
46
+ file2 = root / "objects2/file"
47
+ op1_key = bytes.fromhex("abcd01")
48
+ op2_key = bytes.fromhex("abcd02")
49
+ cached_file1 = cache.store_path(op1_key, file1)
50
+ cached_file2 = cache.store_path(op2_key, file2)
51
+
52
+ assert path_key(cached_file1) == path_key(cached_file2)
fab/lang/__init__.py ADDED
File without changes
fab/lang/astree.py ADDED
@@ -0,0 +1,109 @@
1
+ import abc
2
+ from math import exp
3
+ from typing import override
4
+
5
+
6
+ class AstNode(abc.ABC):
7
+ pass
8
+
9
+
10
+ class Name(AstNode):
11
+ name: str
12
+
13
+ def __init__(self, name: str) -> None:
14
+ self.name = name
15
+
16
+ @override
17
+ def __repr__(self) -> str:
18
+ return f"<Name {self.name}>"
19
+
20
+ @override
21
+ def __str__(self) -> str:
22
+ return self.name
23
+
24
+
25
+ class Expression(AstNode, abc.ABC):
26
+ pass
27
+
28
+
29
+ class LiteralString(Expression):
30
+ value: str
31
+
32
+ def __init__(self, value: str) -> None:
33
+ self.value = value
34
+
35
+ @override
36
+ def __repr__(self):
37
+ return f'<LiteralString "{self.value}">'
38
+
39
+
40
+ class Call(Expression):
41
+ target: Expression
42
+ pos_args: list[Expression]
43
+ key_args: dict[Name, Expression]
44
+
45
+ def __init__(
46
+ self,
47
+ target: Expression,
48
+ pos_args: list[Expression],
49
+ key_args: dict[Name, Expression],
50
+ ) -> None:
51
+ self.target = target
52
+ self.pos_args = pos_args
53
+ self.key_args = key_args
54
+
55
+ @override
56
+ def __repr__(self) -> str:
57
+ return (
58
+ f"<Call target={self.target}, args={self.pos_args}, kwargs={self.key_args}>"
59
+ )
60
+
61
+
62
+ class Variable(Expression):
63
+ name: Name
64
+
65
+ def __init__(self, name: Name) -> None:
66
+ self.name = name
67
+
68
+ @override
69
+ def __repr__(self) -> str:
70
+ return f"<Variable {self.name}>"
71
+
72
+
73
+ class List(Expression):
74
+ items: list[Expression]
75
+
76
+ def __init__(self, items: list[Expression]) -> None:
77
+ self.items = items
78
+
79
+ @override
80
+ def __repr__(self):
81
+ return f"<List {','.join((str(x) for x in self.items))}>"
82
+
83
+
84
+ class ListComprehension(Expression):
85
+ expression: Expression
86
+ target: Name
87
+ iterable: Expression
88
+
89
+ def __init__(self, expression: Expression, target: Name, iterable: Expression):
90
+ self.expression = expression
91
+ self.target = target
92
+ self.iterable = iterable
93
+
94
+ @override
95
+ def __repr__(self) -> str:
96
+ return f"<ListComprehension ...>"
97
+
98
+
99
+ class AttributeRef(Expression):
100
+ target: Expression
101
+ name: Name
102
+
103
+ def __init__(self, target: Expression, name: Name) -> None:
104
+ self.target = target
105
+ self.name = name
106
+
107
+ @override
108
+ def __repr__(self) -> str:
109
+ return f"<AttributeRef {self.target}.{self.name}>"
fab/lang/buildins.py ADDED
@@ -0,0 +1,140 @@
1
+ from copy import copy
2
+ from pathlib import Path
3
+ from typing import override
4
+
5
+ from fab.operations.gcc import GccCompile, GccLink, GccCollectCompileCommands
6
+
7
+ from ..operations import Extract, HttpArchive, HttpGet
8
+ from .data import EvaluationContext, Function, List, Object, PathObj, String
9
+ from .evaluator import EvaluationError, evaluate_context
10
+
11
+
12
+ class LoadFunc(Function):
13
+ @override
14
+ async def call(
15
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
16
+ ) -> Object:
17
+ assert isinstance(args[0], String)
18
+ filename = Path(args[0].value)
19
+
20
+ if filename.is_absolute():
21
+ raise EvaluationError("Cannot load absolute path")
22
+
23
+ new_context = copy(context)
24
+ new_context.apply_filename(filename)
25
+
26
+ return evaluate_context(new_context)
27
+
28
+
29
+ class PathFunc(Function):
30
+ @override
31
+ async def call(
32
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
33
+ ) -> Object:
34
+ assert isinstance(args[0], String)
35
+ path_str = args[0].value
36
+ working_dir = context.get_current_file().parent
37
+ path = working_dir / path_str
38
+ if not path.exists():
39
+ raise EvaluationError(f"No such path '{path_str}' ('{path}')")
40
+
41
+ return PathObj(path)
42
+
43
+
44
+ class LinkFunc(Function):
45
+ @override
46
+ async def call(
47
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
48
+ ) -> Object:
49
+ assert isinstance(args[0], PathObj)
50
+ target_path = args[0].path
51
+
52
+ destination = context.get_current_file().parent
53
+ link_path = destination / target_path.name
54
+
55
+ if link_path.exists():
56
+ if not link_path.is_symlink():
57
+ raise EvaluationError(
58
+ f"Link target already exists and is not a symlink '{link_path}'"
59
+ )
60
+
61
+ link_path.unlink()
62
+
63
+ link_path.symlink_to(target_path)
64
+
65
+ return PathObj(link_path)
66
+
67
+
68
+ class HttpGetFunc(Function):
69
+ @override
70
+ async def call(
71
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
72
+ ) -> Object:
73
+ assert isinstance(args[0], String)
74
+ url = args[0].value
75
+ op = HttpGet(url)
76
+ result = await context.execute_operation(op)
77
+ return PathObj(result)
78
+
79
+
80
+ class ExtractFunc(Function):
81
+ @override
82
+ async def call(
83
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
84
+ ) -> Object:
85
+ assert isinstance(args[0], PathObj)
86
+ archive = args[0].path
87
+ op = Extract(archive)
88
+ result = await context.execute_operation(op)
89
+ return PathObj(result)
90
+
91
+
92
+ class HttpArchiveFunc(Function):
93
+ @override
94
+ async def call(
95
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
96
+ ) -> Object:
97
+ assert isinstance(args[0], String)
98
+ url = args[0].value
99
+ op = HttpArchive(url)
100
+ result = await context.execute_operation(op)
101
+ return PathObj(result)
102
+
103
+
104
+ class GccCompileFunc(Function):
105
+ @override
106
+ async def call(
107
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
108
+ ) -> Object:
109
+ assert isinstance(args[0], PathObj)
110
+ assert isinstance(kwargs["includes"], List)
111
+ source = args[0].path
112
+ includes = [p.path for p in kwargs["includes"].items]
113
+ op = GccCompile(source, includes)
114
+ result = await context.execute_operation(op)
115
+ return PathObj(result)
116
+
117
+
118
+ class GccLinkFunc(Function):
119
+ @override
120
+ async def call(
121
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
122
+ ) -> Object:
123
+ assert isinstance(args[0], String)
124
+ assert isinstance(args[1], List)
125
+ outputname = args[0].value
126
+ objects = [item.path for item in args[1].items]
127
+
128
+ op = GccLink(outputname, objects)
129
+ result = await context.execute_operation(op)
130
+ return PathObj(result)
131
+
132
+
133
+ class GccCollectCompileCommandsFunc(Function):
134
+ @override
135
+ async def call(
136
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
137
+ ) -> Object:
138
+ op = GccCollectCompileCommands()
139
+ result = await context.execute_operation(op)
140
+ return PathObj(result)
fab/lang/data.py ADDED
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any, override
7
+
8
+ from ..model import Operation
9
+
10
+
11
+ class Object(ABC):
12
+ def has_attr(self, name: str) -> bool:
13
+ return False
14
+
15
+ def attrs(self) -> Sequence[str]:
16
+ return []
17
+
18
+ async def get_attr(self, name: str) -> Object:
19
+ raise KeyError
20
+
21
+ @override
22
+ def __eq__(self, value: object, /) -> bool:
23
+ raise NotImplementedError
24
+
25
+
26
+ class String(Object):
27
+ value: str
28
+
29
+ def __init__(self, value: str) -> None:
30
+ self.value = value
31
+
32
+ @override
33
+ def __eq__(self, other: object, /) -> bool:
34
+ if not isinstance(other, String):
35
+ return False
36
+ return self.value == other.value
37
+
38
+ @override
39
+ def __repr__(self) -> str:
40
+ return f'<String "{self.value}">'
41
+
42
+
43
+ class List(Object):
44
+ items: Sequence[Object]
45
+
46
+ def __init__(self, items: Sequence[Object]) -> None:
47
+ self.items = items
48
+
49
+ @override
50
+ def __eq__(self, other: object, /) -> bool:
51
+ if not isinstance(other, List):
52
+ return False
53
+ return self.items == other.items
54
+
55
+
56
+ class PathObj(Object):
57
+ path: pathlib.Path
58
+
59
+ def __init__(self, path: pathlib.Path) -> None:
60
+ self.path = path
61
+
62
+ @override
63
+ def __eq__(self, other: object, /) -> bool:
64
+ if not isinstance(other, PathObj):
65
+ return False
66
+ return self.path == other.path
67
+
68
+
69
+ class EvaluationContext(ABC):
70
+ @abstractmethod
71
+ def apply_filename(self, path: pathlib.Path) -> None: ...
72
+
73
+ @abstractmethod
74
+ def get_current_file(self) -> pathlib.Path: ...
75
+
76
+ @abstractmethod
77
+ async def execute_operation(self, operation: Operation) -> Any: ...
78
+
79
+ @property
80
+ @abstractmethod
81
+ def buildins(self) -> Mapping[str, Object]: ...
82
+
83
+
84
+ class Function(Object, ABC):
85
+ @abstractmethod
86
+ async def call(
87
+ self, context: EvaluationContext, args: list[Object], kwargs: dict[str, Object]
88
+ ) -> Object: ...
fab/lang/evaluator.py ADDED
@@ -0,0 +1,119 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from typing import override
3
+
4
+ from ..lang import astree
5
+ from .data import EvaluationContext, Function, List, Object, String
6
+ from .parser import parse
7
+
8
+
9
+ class EvaluationError(RuntimeError):
10
+ pass
11
+
12
+
13
+ def evaluate_context(context: EvaluationContext) -> Object:
14
+ source = context.get_current_file().read_text()
15
+
16
+ assignments = parse(source)
17
+
18
+ class LoadedObject(Object):
19
+ @override
20
+ def has_attr(self, name: str) -> bool:
21
+ return name in assignments
22
+
23
+ @override
24
+ async def get_attr(self, name: str) -> Object:
25
+ expr = assignments[name]
26
+ return await evaluate(expr, assignments, {}, context)
27
+
28
+ @override
29
+ def attrs(self) -> Sequence[str]:
30
+ return list(assignments.keys())
31
+
32
+ return LoadedObject()
33
+
34
+
35
+ async def evaluate(
36
+ expr: astree.Expression,
37
+ assignments: Mapping[str, astree.Expression],
38
+ locals: Mapping[str, Object],
39
+ context: EvaluationContext,
40
+ ) -> Object:
41
+
42
+ async def evaluate_variable(expr: astree.Variable) -> Object:
43
+ var_name = expr.name.name
44
+ if var_name in locals:
45
+ return locals[var_name]
46
+ if var_name in context.buildins:
47
+ return context.buildins[var_name]
48
+ if var_name not in assignments:
49
+ raise EvaluationError(f"No variable named '{var_name}'")
50
+ return await evaluate(assignments[var_name], assignments, locals, context)
51
+
52
+ async def evaluate_literal_string(expr: astree.LiteralString) -> Object:
53
+ return String(expr.value)
54
+
55
+ async def evaluate_list(expr: astree.List) -> Object:
56
+ evaluated_items = [
57
+ await evaluate(item, assignments, locals, context) for item in expr.items
58
+ ]
59
+ return List(evaluated_items)
60
+
61
+ async def evaluate_list_comprehension(expr: astree.ListComprehension) -> Object:
62
+
63
+ items = await evaluate(expr.iterable, assignments, locals, context)
64
+ assert isinstance(items, List)
65
+
66
+ evaluated_items = [
67
+ await evaluate(
68
+ expr.expression, assignments, {expr.target.name: item}, context
69
+ )
70
+ for item in items.items
71
+ ]
72
+
73
+ return List(evaluated_items)
74
+
75
+ async def evaluate_attribute_ref(expr: astree.AttributeRef) -> Object:
76
+ evaluated_target = await evaluate(expr.target, assignments, locals, context)
77
+ attr_name = expr.name.name
78
+ if not evaluated_target.has_attr(attr_name):
79
+ raise EvaluationError(f"No attribute named '{attr_name}'")
80
+ return await evaluated_target.get_attr(attr_name)
81
+
82
+ async def evaluate_call(expr: astree.Call) -> Object:
83
+ evaluated_target = await evaluate(expr.target, assignments, locals, context)
84
+ evaluated_args = [
85
+ await evaluate(arg, assignments, locals, context) for arg in expr.pos_args
86
+ ]
87
+ evaluated_kwargs = {
88
+ kw.name: await evaluate(arg, assignments, locals, context)
89
+ for kw, arg in expr.key_args.items()
90
+ }
91
+
92
+ if isinstance(evaluated_target, Function):
93
+ return await evaluate_function_call(
94
+ evaluated_target, evaluated_args, evaluated_kwargs
95
+ )
96
+ else:
97
+ raise EvaluationError("Target is not a callable")
98
+
99
+ async def evaluate_function_call(
100
+ target: Function,
101
+ args: list[Object],
102
+ kwargs: dict[str, Object],
103
+ ) -> Object:
104
+ return await target.call(context, args, kwargs)
105
+
106
+ if isinstance(expr, astree.Variable):
107
+ return await evaluate_variable(expr)
108
+ elif isinstance(expr, astree.LiteralString):
109
+ return await evaluate_literal_string(expr)
110
+ elif isinstance(expr, astree.List):
111
+ return await evaluate_list(expr)
112
+ elif isinstance(expr, astree.ListComprehension):
113
+ return await evaluate_list_comprehension(expr)
114
+ elif isinstance(expr, astree.AttributeRef):
115
+ return await evaluate_attribute_ref(expr)
116
+ elif isinstance(expr, astree.Call):
117
+ return await evaluate_call(expr)
118
+ else:
119
+ raise NotImplementedError(f"Handling of {type(expr)} is not implemented")
fab/lang/grammar.lark ADDED
@@ -0,0 +1,39 @@
1
+ %import common.CNAME
2
+ %import common.WS
3
+ %import common.ESCAPED_STRING
4
+
5
+ COMMENT: /#[^\n]*/
6
+
7
+ %ignore WS
8
+ %ignore COMMENT
9
+
10
+ listing: assignment*
11
+
12
+ string: ESCAPED_STRING
13
+ name: CNAME
14
+
15
+ assignment: name "=" expression
16
+
17
+
18
+ ?expression: string
19
+ | call
20
+ | variable
21
+ | list
22
+ | list_comprehension
23
+ | attributeref
24
+
25
+
26
+ variable: name
27
+
28
+ list: "[" [expression ("," expression)* [","] ] "]"
29
+
30
+ list_comprehension: "[" expression "for" name "in" expression "]"
31
+
32
+ attributeref: expression "." name
33
+
34
+ call: expression "(" [ call_args ] ")"
35
+ call_args: (arg ("," arg)* ["," kwarg ("," kwarg)* ] [","] )
36
+ | (kwarg ("," kwarg)* [","] )
37
+ ?arg: expression
38
+ kwarg: name "=" expression
39
+